1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { createHash
} from
'node:crypto'
4 import { EventEmitter
} from
'node:events'
5 import { type FSWatcher
, existsSync
, mkdirSync
, readFileSync
, writeFileSync
} from
'node:fs'
6 import { dirname
, join
, parse
} from
'node:path'
7 import { URL
} from
'node:url'
8 import { parentPort
} from
'node:worker_threads'
10 import { millisecondsToSeconds
, secondsToMilliseconds
} from
'date-fns'
11 import merge from
'just-merge'
12 import { type RawData
, WebSocket
} from
'ws'
14 import { AutomaticTransactionGenerator
} from
'./AutomaticTransactionGenerator.js'
15 import { ChargingStationWorkerBroadcastChannel
} from
'./broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
18 deleteConfigurationKey
,
20 setConfigurationKeyValue
21 } from
'./ConfigurationKeyUtils.js'
26 checkConnectorsConfiguration
,
27 checkStationInfoConnectorStatus
,
29 createBootNotificationRequest
,
31 getAmperageLimitationUnitDivider
,
32 getBootConnectorStatus
,
33 getChargingStationConnectorChargingProfilesPowerLimit
,
39 getNumberOfReservableConnectors
,
40 getPhaseRotationValue
,
42 hasReservationExpired
,
43 initializeConnectorsMapStatus
,
44 propagateSerialNumber
,
45 stationTemplateToStationInfo
,
46 warnTemplateKeysDeprecation
48 import { IdTagsCache
} from
'./IdTagsCache.js'
50 OCPP16IncomingRequestService
,
52 OCPP16ResponseService
,
53 OCPP20IncomingRequestService
,
55 OCPP20ResponseService
,
56 type OCPPIncomingRequestService
,
57 type OCPPRequestService
,
59 buildTransactionEndMeterValue
,
61 sendAndSetConnectorStatus
62 } from
'./ocpp/index.js'
63 import { SharedLRUCache
} from
'./SharedLRUCache.js'
64 import { BaseError
, OCPPError
} from
'../exception/index.js'
65 import { PerformanceStatistics
} from
'../performance/index.js'
67 type AutomaticTransactionGeneratorConfiguration
,
69 type BootNotificationRequest
,
70 type BootNotificationResponse
,
72 type ChargingStationConfiguration
,
73 ChargingStationEvents
,
74 type ChargingStationInfo
,
75 type ChargingStationOcppConfiguration
,
76 type ChargingStationTemplate
,
84 type EvseStatusConfiguration
,
87 type FirmwareStatusNotificationRequest
,
88 type FirmwareStatusNotificationResponse
,
90 type HeartbeatRequest
,
91 type HeartbeatResponse
,
93 type IncomingRequestCommand
,
96 type MeterValuesRequest
,
97 type MeterValuesResponse
,
101 RegistrationStatusEnumType
,
105 ReservationTerminationReason
,
107 StandardParametersKey
,
109 type StopTransactionReason
,
110 type StopTransactionRequest
,
111 type StopTransactionResponse
,
112 SupervisionUrlDistribution
,
113 SupportedFeatureProfiles
,
116 WebSocketCloseEventStatusCode
,
118 } from
'../types/index.js'
127 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
128 buildConnectorsStatus
,
138 formatDurationMilliSeconds
,
139 formatDurationSeconds
,
141 getWebSocketCloseEventStatusString
,
153 } from
'../utils/index.js'
155 export class ChargingStation
extends EventEmitter
{
156 public readonly index
: number
157 public readonly templateFile
: string
158 public stationInfo
?: ChargingStationInfo
159 public started
: boolean
160 public starting
: boolean
161 public idTagsCache
: IdTagsCache
162 public automaticTransactionGenerator
!: AutomaticTransactionGenerator
| undefined
163 public ocppConfiguration
!: ChargingStationOcppConfiguration
| undefined
164 public wsConnection
: WebSocket
| null
165 public readonly connectors
: Map
<number, ConnectorStatus
>
166 public readonly evses
: Map
<number, EvseStatus
>
167 public readonly requests
: Map
<string, CachedRequest
>
168 public performanceStatistics
!: PerformanceStatistics
| undefined
169 public heartbeatSetInterval
?: NodeJS
.Timeout
170 public ocppRequestService
!: OCPPRequestService
171 public bootNotificationRequest
?: BootNotificationRequest
172 public bootNotificationResponse
?: BootNotificationResponse
173 public powerDivider
?: number
174 private stopping
: boolean
175 private configurationFile
!: string
176 private configurationFileHash
!: string
177 private connectorsConfigurationHash
!: string
178 private evsesConfigurationHash
!: string
179 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
180 private ocppIncomingRequestService
!: OCPPIncomingRequestService
181 private readonly messageBuffer
: Set
<string>
182 private configuredSupervisionUrl
!: URL
183 private wsConnectionRetried
: boolean
184 private wsConnectionRetryCount
: number
185 private templateFileWatcher
!: FSWatcher
| undefined
186 private templateFileHash
!: string
187 private readonly sharedLRUCache
: SharedLRUCache
188 private wsPingSetInterval
?: NodeJS
.Timeout
189 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
190 private flushMessageBufferSetInterval
?: NodeJS
.Timeout
192 constructor (index
: number, templateFile
: string) {
195 this.starting
= false
196 this.stopping
= false
197 this.wsConnection
= null
198 this.wsConnectionRetried
= false
199 this.wsConnectionRetryCount
= 0
201 this.templateFile
= templateFile
202 this.connectors
= new Map
<number, ConnectorStatus
>()
203 this.evses
= new Map
<number, EvseStatus
>()
204 this.requests
= new Map
<string, CachedRequest
>()
205 this.messageBuffer
= new Set
<string>()
206 this.sharedLRUCache
= SharedLRUCache
.getInstance()
207 this.idTagsCache
= IdTagsCache
.getInstance()
208 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this)
210 this.on(ChargingStationEvents
.added
, () => {
211 parentPort
?.postMessage(buildAddedMessage(this))
213 this.on(ChargingStationEvents
.started
, () => {
214 parentPort
?.postMessage(buildStartedMessage(this))
216 this.on(ChargingStationEvents
.stopped
, () => {
217 parentPort
?.postMessage(buildStoppedMessage(this))
219 this.on(ChargingStationEvents
.updated
, () => {
220 parentPort
?.postMessage(buildUpdatedMessage(this))
222 this.on(ChargingStationEvents
.accepted
, () => {
223 this.startMessageSequence(
224 this.wsConnectionRetried
226 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
228 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
230 this.wsConnectionRetried
= false
232 this.on(ChargingStationEvents
.rejected
, () => {
233 this.wsConnectionRetried
= false
235 this.on(ChargingStationEvents
.disconnected
, () => {
237 this.internalStopMessageSequence()
240 `${this.logPrefix()} Error while stopping the internal message sequence:`,
250 this.stationInfo
?.autoStart
=== true && this.start()
253 public get
hasEvses (): boolean {
254 return this.connectors
.size
=== 0 && this.evses
.size
> 0
257 private get
wsConnectionUrl (): URL
{
260 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
261 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
262 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
263 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
264 : this.configuredSupervisionUrl.href
265 }/${this.stationInfo?.chargingStationId}`
269 public logPrefix
= (): string => {
271 this instanceof ChargingStation
&&
272 this.stationInfo
!= null &&
273 isNotEmptyString(this.stationInfo
.chargingStationId
)
275 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
277 let stationTemplate
: ChargingStationTemplate
| undefined
279 stationTemplate
= JSON
.parse(
280 readFileSync(this.templateFile
, 'utf8')
281 ) as ChargingStationTemplate
283 stationTemplate
= undefined
285 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
288 public hasIdTags (): boolean {
289 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
290 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
293 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
295 const localStationInfo
= stationInfo
?? this.stationInfo
!
296 switch (this.getCurrentOutType(stationInfo
)) {
298 return localStationInfo
.numberOfPhases
?? 3
304 public isWebSocketConnectionOpened (): boolean {
305 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
308 public inUnknownState (): boolean {
309 return this.bootNotificationResponse
?.status == null
312 public inPendingState (): boolean {
313 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
316 public inAcceptedState (): boolean {
317 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
320 public inRejectedState (): boolean {
321 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
324 public isRegistered (): boolean {
325 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
328 public isChargingStationAvailable (): boolean {
329 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
332 public hasConnector (connectorId
: number): boolean {
334 for (const evseStatus
of this.evses
.values()) {
335 if (evseStatus
.connectors
.has(connectorId
)) {
341 return this.connectors
.has(connectorId
)
344 public isConnectorAvailable (connectorId
: number): boolean {
347 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
351 public getNumberOfConnectors (): number {
353 let numberOfConnectors
= 0
354 for (const [evseId
, evseStatus
] of this.evses
) {
356 numberOfConnectors
+= evseStatus
.connectors
.size
359 return numberOfConnectors
361 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
364 public getNumberOfEvses (): number {
365 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
368 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
370 for (const evseStatus
of this.evses
.values()) {
371 if (evseStatus
.connectors
.has(connectorId
)) {
372 return evseStatus
.connectors
.get(connectorId
)
377 return this.connectors
.get(connectorId
)
380 public getConnectorMaximumAvailablePower (connectorId
: number): number {
381 let connectorAmperageLimitationPowerLimit
: number | undefined
382 const amperageLimitation
= this.getAmperageLimitation()
384 amperageLimitation
!= null &&
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
388 connectorAmperageLimitationPowerLimit
=
389 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
390 ? ACElectricUtils
.powerTotal(
391 this.getNumberOfPhases(),
392 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
393 this.stationInfo
.voltageOut
!,
395 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
397 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
398 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
404 const connectorChargingProfilesPowerLimit
=
405 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
407 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 isNaN(connectorAmperageLimitationPowerLimit
!)
411 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
412 connectorAmperageLimitationPowerLimit
!,
413 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414 isNaN(connectorChargingProfilesPowerLimit
!) ? Infinity : connectorChargingProfilesPowerLimit
!
418 public getTransactionIdTag (transactionId
: number): string | undefined {
420 for (const evseStatus
of this.evses
.values()) {
421 for (const connectorStatus
of evseStatus
.connectors
.values()) {
422 if (connectorStatus
.transactionId
=== transactionId
) {
423 return connectorStatus
.transactionIdTag
428 for (const connectorId
of this.connectors
.keys()) {
429 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
430 return this.getConnectorStatus(connectorId
)?.transactionIdTag
436 public getNumberOfRunningTransactions (): number {
437 let numberOfRunningTransactions
= 0
439 for (const [evseId
, evseStatus
] of this.evses
) {
443 for (const connectorStatus
of evseStatus
.connectors
.values()) {
444 if (connectorStatus
.transactionStarted
=== true) {
445 ++numberOfRunningTransactions
450 for (const connectorId
of this.connectors
.keys()) {
451 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
452 ++numberOfRunningTransactions
456 return numberOfRunningTransactions
459 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
460 if (transactionId
== null) {
462 } else if (this.hasEvses
) {
463 for (const evseStatus
of this.evses
.values()) {
464 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
465 if (connectorStatus
.transactionId
=== transactionId
) {
471 for (const connectorId
of this.connectors
.keys()) {
472 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
479 public getEnergyActiveImportRegisterByTransactionId (
480 transactionId
: number | undefined,
483 return this.getEnergyActiveImportRegister(
484 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
485 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
490 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
491 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
494 public getAuthorizeRemoteTxRequests (): boolean {
495 const authorizeRemoteTxRequests
= getConfigurationKey(
497 StandardParametersKey
.AuthorizeRemoteTxRequests
499 return authorizeRemoteTxRequests
!= null
500 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
504 public getLocalAuthListEnabled (): boolean {
505 const localAuthListEnabled
= getConfigurationKey(
507 StandardParametersKey
.LocalAuthListEnabled
509 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
512 public getHeartbeatInterval (): number {
513 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
514 if (HeartbeatInterval
!= null) {
515 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
517 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
518 if (HeartBeatInterval
!= null) {
519 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
521 this.stationInfo
?.autoRegister
=== false &&
523 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
524 Constants.DEFAULT_HEARTBEAT_INTERVAL
527 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
530 public setSupervisionUrl (url
: string): void {
532 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
533 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
535 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
537 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
538 this.stationInfo
!.supervisionUrls
= url
539 this.saveStationInfo()
540 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
544 public startHeartbeat (): void {
545 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval
== null) {
546 this.heartbeatSetInterval
= setInterval(() => {
547 this.ocppRequestService
548 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
551 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
555 }, this.getHeartbeatInterval())
557 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
558 this.getHeartbeatInterval()
561 } else if (this.heartbeatSetInterval
!= null) {
563 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
564 this.getHeartbeatInterval()
569 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
574 public restartHeartbeat (): void {
578 this.startHeartbeat()
581 public restartWebSocketPing (): void {
582 // Stop WebSocket ping
583 this.stopWebSocketPing()
584 // Start WebSocket ping
585 this.startWebSocketPing()
588 public startMeterValues (connectorId
: number, interval
: number): void {
589 if (connectorId
=== 0) {
590 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
593 const connectorStatus
= this.getConnectorStatus(connectorId
)
594 if (connectorStatus
== null) {
596 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
601 if (connectorStatus
.transactionStarted
=== false) {
603 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
607 connectorStatus
.transactionStarted
=== true &&
608 connectorStatus
.transactionId
== null
611 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
616 connectorStatus
.transactionSetInterval
= setInterval(() => {
617 const meterValue
= buildMeterValue(
620 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
621 connectorStatus
.transactionId
!,
624 this.ocppRequestService
625 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
627 RequestCommand
.METER_VALUES
,
630 transactionId
: connectorStatus
.transactionId
,
631 meterValue
: [meterValue
]
636 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
643 `${this.logPrefix()} Charging station ${
644 StandardParametersKey.MeterValueSampleInterval
645 } configuration set to ${interval}, not sending MeterValues`
650 public stopMeterValues (connectorId
: number): void {
651 const connectorStatus
= this.getConnectorStatus(connectorId
)
652 if (connectorStatus
?.transactionSetInterval
!= null) {
653 clearInterval(connectorStatus
.transactionSetInterval
)
657 private add (): void {
658 this.emit(ChargingStationEvents
.added
)
661 public start (): void {
663 if (!this.starting
) {
665 if (this.stationInfo
?.enableStatistics
=== true) {
666 this.performanceStatistics
?.start()
668 this.openWSConnection()
669 // Monitor charging station template file
670 this.templateFileWatcher
= watchJsonFile(
672 FileType
.ChargingStationTemplate
,
675 (event
, filename
): void => {
676 if (isNotEmptyString(filename
) && event
=== 'change') {
679 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
681 } file have changed, reload`
683 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
686 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
687 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
689 const ATGStarted
= this.automaticTransactionGenerator
?.started
690 if (ATGStarted
=== true) {
691 this.stopAutomaticTransactionGenerator()
693 delete this.automaticTransactionGeneratorConfiguration
695 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
698 this.startAutomaticTransactionGenerator(undefined, true)
700 if (this.stationInfo
?.enableStatistics
=== true) {
701 this.performanceStatistics
?.restart()
703 this.performanceStatistics
?.stop()
705 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
708 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
716 this.emit(ChargingStationEvents
.started
)
717 this.starting
= false
719 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
722 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
727 reason
?: StopTransactionReason
,
728 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
731 if (!this.stopping
) {
733 await this.stopMessageSequence(reason
, stopTransactions
)
734 this.closeWSConnection()
735 if (this.stationInfo
?.enableStatistics
=== true) {
736 this.performanceStatistics
?.stop()
738 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
739 this.templateFileWatcher
?.close()
740 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
741 delete this.bootNotificationResponse
743 this.saveConfiguration()
744 this.emit(ChargingStationEvents
.stopped
)
745 this.stopping
= false
747 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
750 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
754 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
755 await this.stop(reason
)
756 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
757 await sleep(this.stationInfo
!.resetTime
!)
762 public saveOcppConfiguration (): void {
763 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
764 this.saveConfiguration()
768 public bufferMessage (message
: string): void {
769 this.messageBuffer
.add(message
)
770 this.setIntervalFlushMessageBuffer()
773 public openWSConnection (
775 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
778 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
779 ...this.stationInfo
?.wsOptions
,
782 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
783 if (!checkChargingStation(this, this.logPrefix())) {
786 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
787 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
789 if (params
.closeOpened
=== true) {
790 this.closeWSConnection()
792 if (params
.terminateOpened
=== true) {
793 this.terminateWSConnection()
796 if (this.isWebSocketConnectionOpened()) {
798 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
804 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
807 this.wsConnection
= new WebSocket(
808 this.wsConnectionUrl
,
809 `ocpp${this.stationInfo?.ocppVersion}`,
813 // Handle WebSocket message
814 this.wsConnection
.on('message', data
=> {
815 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
817 // Handle WebSocket error
818 this.wsConnection
.on('error', this.onError
.bind(this))
819 // Handle WebSocket close
820 this.wsConnection
.on('close', this.onClose
.bind(this))
821 // Handle WebSocket open
822 this.wsConnection
.on('open', () => {
823 this.onOpen().catch(error
=>
824 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
827 // Handle WebSocket ping
828 this.wsConnection
.on('ping', this.onPing
.bind(this))
829 // Handle WebSocket pong
830 this.wsConnection
.on('pong', this.onPong
.bind(this))
833 public closeWSConnection (): void {
834 if (this.isWebSocketConnectionOpened()) {
835 this.wsConnection
?.close()
836 this.wsConnection
= null
840 public getAutomaticTransactionGeneratorConfiguration ():
841 | AutomaticTransactionGeneratorConfiguration
843 if (this.automaticTransactionGeneratorConfiguration
== null) {
844 let automaticTransactionGeneratorConfiguration
:
845 | AutomaticTransactionGeneratorConfiguration
847 const stationTemplate
= this.getTemplateFromFile()
848 const stationConfiguration
= this.getConfigurationFromFile()
850 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
851 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
852 stationConfiguration
?.automaticTransactionGenerator
!= null
854 automaticTransactionGeneratorConfiguration
=
855 stationConfiguration
.automaticTransactionGenerator
857 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
859 this.automaticTransactionGeneratorConfiguration
= {
860 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
861 ...automaticTransactionGeneratorConfiguration
864 return this.automaticTransactionGeneratorConfiguration
867 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
868 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
871 public startAutomaticTransactionGenerator (
872 connectorIds
?: number[],
873 stopAbsoluteDuration
?: boolean
875 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
876 if (isNotEmptyArray(connectorIds
)) {
877 for (const connectorId
of connectorIds
) {
878 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
881 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
883 this.saveAutomaticTransactionGeneratorConfiguration()
884 this.emit(ChargingStationEvents
.updated
)
887 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
888 if (isNotEmptyArray(connectorIds
)) {
889 for (const connectorId
of connectorIds
) {
890 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
893 this.automaticTransactionGenerator
?.stop()
895 this.saveAutomaticTransactionGeneratorConfiguration()
896 this.emit(ChargingStationEvents
.updated
)
899 public async stopTransactionOnConnector (
901 reason
?: StopTransactionReason
902 ): Promise
<StopTransactionResponse
> {
903 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
905 this.stationInfo
?.beginEndMeterValues
=== true &&
906 this.stationInfo
.ocppStrictCompliance
=== true &&
907 this.stationInfo
.outOfOrderEndMeterValues
=== false
909 const transactionEndMeterValue
= buildTransactionEndMeterValue(
912 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
914 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
916 RequestCommand
.METER_VALUES
,
920 meterValue
: [transactionEndMeterValue
]
924 return await this.ocppRequestService
.requestHandler
<
925 StopTransactionRequest
,
926 StopTransactionResponse
927 >(this, RequestCommand
.STOP_TRANSACTION
, {
929 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
930 ...(reason
!= null && { reason
})
934 public getReserveConnectorZeroSupported (): boolean {
935 return convertToBoolean(
936 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
937 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
941 public async addReservation (reservation
: Reservation
): Promise
<void> {
942 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
943 if (reservationFound
!= null) {
944 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
946 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
947 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
948 await sendAndSetConnectorStatus(
950 reservation
.connectorId
,
951 ConnectorStatusEnum
.Reserved
,
953 { send
: reservation
.connectorId
!== 0 }
957 public async removeReservation (
958 reservation
: Reservation
,
959 reason
: ReservationTerminationReason
961 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
962 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
964 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
965 case ReservationTerminationReason
.TRANSACTION_STARTED
:
966 delete connector
.reservation
968 case ReservationTerminationReason
.RESERVATION_CANCELED
:
969 case ReservationTerminationReason
.REPLACE_EXISTING
:
970 case ReservationTerminationReason
.EXPIRED
:
971 await sendAndSetConnectorStatus(
973 reservation
.connectorId
,
974 ConnectorStatusEnum
.Available
,
976 { send
: reservation
.connectorId
!== 0 }
978 delete connector
.reservation
981 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
982 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
986 public getReservationBy (
987 filterKey
: ReservationKey
,
988 value
: number | string
989 ): Reservation
| undefined {
991 for (const evseStatus
of this.evses
.values()) {
992 for (const connectorStatus
of evseStatus
.connectors
.values()) {
993 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
994 return connectorStatus
.reservation
999 for (const connectorStatus
of this.connectors
.values()) {
1000 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1001 return connectorStatus
.reservation
1007 public isConnectorReservable (
1008 reservationId
: number,
1010 connectorId
?: number
1012 const reservation
= this.getReservationBy('reservationId', reservationId
)
1013 const reservationExists
= reservation
!== undefined && !hasReservationExpired(reservation
)
1014 if (arguments.length
=== 1) {
1015 return !reservationExists
1016 } else if (arguments.length
> 1) {
1017 const userReservation
=
1018 idTag
!== undefined ? this.getReservationBy('idTag', idTag
) : undefined
1019 const userReservationExists
=
1020 userReservation
!== undefined && !hasReservationExpired(userReservation
)
1021 const notConnectorZero
= connectorId
=== undefined ? true : connectorId
> 0
1022 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1024 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1030 private setIntervalFlushMessageBuffer (): void {
1031 if (this.flushMessageBufferSetInterval
== null) {
1032 this.flushMessageBufferSetInterval
= setInterval(() => {
1033 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1034 this.flushMessageBuffer()
1036 if (this.messageBuffer
.size
=== 0) {
1037 this.clearIntervalFlushMessageBuffer()
1039 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1043 private clearIntervalFlushMessageBuffer (): void {
1044 if (this.flushMessageBufferSetInterval
!= null) {
1045 clearInterval(this.flushMessageBufferSetInterval
)
1046 delete this.flushMessageBufferSetInterval
1050 private getNumberOfReservableConnectors (): number {
1051 let numberOfReservableConnectors
= 0
1052 if (this.hasEvses
) {
1053 for (const evseStatus
of this.evses
.values()) {
1054 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1057 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1059 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1062 private getNumberOfReservationsOnConnectorZero (): number {
1064 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1065 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1072 private flushMessageBuffer (): void {
1073 if (this.messageBuffer
.size
> 0) {
1074 for (const message
of this.messageBuffer
.values()) {
1075 let beginId
: string | undefined
1076 let commandName
: RequestCommand
| undefined
1077 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1078 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1080 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1081 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1083 this.wsConnection
?.send(message
, (error
?: Error) => {
1084 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1085 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1086 if (error
== null) {
1088 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1090 )} OCPP message sent '${JSON.stringify(message)}'`
1092 this.messageBuffer
.delete(message
)
1095 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1097 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1106 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1107 let template
: ChargingStationTemplate
| undefined
1109 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1110 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1112 const measureId
= `${FileType.ChargingStationTemplate} read`
1113 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1114 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1115 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1116 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1117 .update(JSON
.stringify(template
))
1119 this.sharedLRUCache
.setChargingStationTemplate(template
)
1120 this.templateFileHash
= template
.templateHash
1123 handleFileException(
1125 FileType
.ChargingStationTemplate
,
1126 error
as NodeJS
.ErrnoException
,
1133 private getStationInfoFromTemplate (): ChargingStationInfo
{
1134 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1135 const stationTemplate
= this.getTemplateFromFile()!
1136 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1137 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
, this)
1138 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1139 if (stationTemplate
.Connectors
!= null) {
1140 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1142 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1143 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1144 stationInfo
.autoStart
= stationTemplate
.autoStart
?? true
1145 stationInfo
.templateName
= parse(this.templateFile
).name
1146 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1147 stationInfo
.ocppVersion
= stationTemplate
.ocppVersion
?? OCPPVersion
.VERSION_16
1148 createSerialNumber(stationTemplate
, stationInfo
)
1149 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1150 if (isNotEmptyArray(stationTemplate
.power
)) {
1151 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1152 stationInfo
.maximumPower
=
1153 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1154 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1155 : stationTemplate
.power
[powerArrayRandomIndex
]
1157 stationInfo
.maximumPower
=
1158 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1159 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1160 stationTemplate
.power
! * 1000
1161 : stationTemplate
.power
1163 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1164 stationInfo
.firmwareVersionPattern
=
1165 stationTemplate
.firmwareVersionPattern
?? Constants
.SEMVER_PATTERN
1167 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1168 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1171 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1173 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1176 stationInfo
.firmwareUpgrade
= merge
<FirmwareUpgrade
>(
1183 stationTemplate
.firmwareUpgrade
?? {}
1185 stationInfo
.resetTime
=
1186 stationTemplate
.resetTime
!= null
1187 ? secondsToMilliseconds(stationTemplate
.resetTime
)
1188 : Constants
.DEFAULT_CHARGING_STATION_RESET_TIME
1192 private getStationInfoFromFile (
1193 stationInfoPersistentConfiguration
= true
1194 ): ChargingStationInfo
| undefined {
1195 let stationInfo
: ChargingStationInfo
| undefined
1196 if (stationInfoPersistentConfiguration
) {
1197 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1198 if (stationInfo
!= null) {
1199 delete stationInfo
.infoHash
1200 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1201 if (stationInfo
.templateName
== null) {
1202 stationInfo
.templateName
= parse(this.templateFile
).name
1204 if (stationInfo
.autoStart
== null) {
1205 stationInfo
.autoStart
= true
1212 private getStationInfo (): ChargingStationInfo
{
1213 const defaultStationInfo
= Constants
.DEFAULT_STATION_INFO
1214 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1215 const stationInfoFromFile
= this.getStationInfoFromFile(
1216 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1219 // 1. charging station info from template
1220 // 2. charging station info from configuration file
1222 stationInfoFromFile
!= null &&
1223 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1225 return { ...defaultStationInfo
, ...stationInfoFromFile
}
1227 stationInfoFromFile
!= null &&
1228 propagateSerialNumber(
1229 this.getTemplateFromFile(),
1230 stationInfoFromFile
,
1231 stationInfoFromTemplate
1233 return { ...defaultStationInfo
, ...stationInfoFromTemplate
}
1236 private saveStationInfo (): void {
1237 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1238 this.saveConfiguration()
1242 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1243 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1244 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1245 throw new BaseError(errorMsg
)
1248 private initialize (): void {
1249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1250 const stationTemplate
= this.getTemplateFromFile()!
1251 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1252 this.configurationFile
= join(
1253 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1254 `${getHashId(this.index, stationTemplate)}.json`
1256 const stationConfiguration
= this.getConfigurationFromFile()
1258 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1259 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1261 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1262 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1264 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1266 this.stationInfo
= this.getStationInfo()
1268 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1269 isNotEmptyString(this.stationInfo
.firmwareVersion
) &&
1270 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
)
1272 const patternGroup
=
1273 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1274 this.stationInfo
.firmwareVersion
.split('.').length
1275 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1276 .exec(this.stationInfo
.firmwareVersion
)
1277 ?.slice(1, patternGroup
+ 1)
1278 if (match
!= null) {
1279 const patchLevelIndex
= match
.length
- 1
1280 match
[patchLevelIndex
] = (
1281 convertToInt(match
[patchLevelIndex
]) +
1282 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1283 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1285 this.stationInfo
.firmwareVersion
= match
.join('.')
1288 this.saveStationInfo()
1289 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1290 if (this.stationInfo
.enableStatistics
=== true) {
1291 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1292 this.stationInfo
.hashId
,
1293 this.stationInfo
.chargingStationId
,
1294 this.configuredSupervisionUrl
1297 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1298 if (bootNotificationRequest
== null) {
1299 const errorMsg
= 'Error while creating boot notification request'
1300 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1301 throw new BaseError(errorMsg
)
1303 this.bootNotificationRequest
= bootNotificationRequest
1304 this.powerDivider
= this.getPowerDivider()
1305 // OCPP configuration
1306 this.ocppConfiguration
= this.getOcppConfiguration()
1307 this.initializeOcppConfiguration()
1308 this.initializeOcppServices()
1309 if (this.stationInfo
.autoRegister
=== true) {
1310 this.bootNotificationResponse
= {
1311 currentTime
: new Date(),
1312 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1313 status: RegistrationStatusEnumType
.ACCEPTED
1318 private initializeOcppServices (): void {
1319 const ocppVersion
= this.stationInfo
?.ocppVersion
1320 switch (ocppVersion
) {
1321 case OCPPVersion
.VERSION_16
:
1322 this.ocppIncomingRequestService
=
1323 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1324 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1325 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1328 case OCPPVersion
.VERSION_20
:
1329 case OCPPVersion
.VERSION_201
:
1330 this.ocppIncomingRequestService
=
1331 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1332 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1333 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1337 this.handleUnsupportedVersion(ocppVersion
)
1342 private initializeOcppConfiguration (): void {
1343 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1344 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1346 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1347 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', { visible
: false })
1350 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1351 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1352 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1354 addConfigurationKey(
1356 this.stationInfo
.supervisionUrlOcppKey
,
1357 this.configuredSupervisionUrl
.href
,
1361 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1362 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1363 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1365 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, { save
: false })
1368 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1369 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1371 addConfigurationKey(
1373 this.stationInfo
.amperageLimitationOcppKey
,
1375 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1376 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1379 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1380 addConfigurationKey(
1382 StandardParametersKey
.SupportedFeatureProfiles
,
1383 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1386 addConfigurationKey(
1388 StandardParametersKey
.NumberOfConnectors
,
1389 this.getNumberOfConnectors().toString(),
1393 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1394 addConfigurationKey(
1396 StandardParametersKey
.MeterValuesSampledData
,
1397 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1400 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1401 const connectorsPhaseRotation
: string[] = []
1402 if (this.hasEvses
) {
1403 for (const evseStatus
of this.evses
.values()) {
1404 for (const connectorId
of evseStatus
.connectors
.keys()) {
1405 connectorsPhaseRotation
.push(
1406 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1407 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1412 for (const connectorId
of this.connectors
.keys()) {
1413 connectorsPhaseRotation
.push(
1414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1415 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1419 addConfigurationKey(
1421 StandardParametersKey
.ConnectorPhaseRotation
,
1422 connectorsPhaseRotation
.toString()
1425 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1426 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1429 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1430 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1432 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1434 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1435 addConfigurationKey(
1437 StandardParametersKey
.ConnectionTimeOut
,
1438 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1441 this.saveOcppConfiguration()
1444 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1445 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1446 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1447 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1449 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1450 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1451 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1452 delete evseStatus
.connectorsStatus
1453 this.evses
.set(evseId
, {
1454 ...(evseStatus
as EvseStatus
),
1455 connectors
: new Map
<number, ConnectorStatus
>(
1456 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1457 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1464 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1465 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1466 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1467 throw new BaseError(errorMsg
)
1469 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1470 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1471 throw new BaseError(errorMsg
)
1475 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1476 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1477 this.initializeConnectorsFromTemplate(stationTemplate
)
1478 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1479 this.initializeEvsesFromTemplate(stationTemplate
)
1480 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1481 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1482 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1483 throw new BaseError(errorMsg
)
1485 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1486 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1487 throw new BaseError(errorMsg
)
1491 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1492 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1493 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1494 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1495 throw new BaseError(errorMsg
)
1497 if (stationTemplate
.Connectors
?.[0] == null) {
1499 `${this.logPrefix()} Charging station information from template ${
1501 } with no connector id 0 configuration`
1504 if (stationTemplate
.Connectors
!= null) {
1505 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1506 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1507 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1509 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1512 const connectorsConfigChanged
=
1513 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1514 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1515 connectorsConfigChanged
&& this.connectors
.clear()
1516 this.connectorsConfigurationHash
= connectorsConfigHash
1517 if (templateMaxConnectors
> 0) {
1518 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1520 connectorId
=== 0 &&
1521 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1522 (stationTemplate
.Connectors
[connectorId
] == null ||
1523 !this.getUseConnectorId0(stationTemplate
))
1527 const templateConnectorId
=
1528 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1529 ? getRandomInteger(templateMaxAvailableConnectors
, 1)
1531 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1532 checkStationInfoConnectorStatus(
1533 templateConnectorId
,
1538 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1540 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1541 this.saveConnectorsStatus()
1544 `${this.logPrefix()} Charging station information from template ${
1546 } with no connectors configuration defined, cannot create connectors`
1552 `${this.logPrefix()} Charging station information from template ${
1554 } with no connectors configuration defined, using already defined connectors`
1559 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1560 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1561 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1562 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1563 throw new BaseError(errorMsg
)
1565 if (stationTemplate
.Evses
?.[0] == null) {
1567 `${this.logPrefix()} Charging station information from template ${
1569 } with no evse id 0 configuration`
1572 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1574 `${this.logPrefix()} Charging station information from template ${
1576 } with evse id 0 with no connector id 0 configuration`
1579 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1581 `${this.logPrefix()} Charging station information from template ${
1583 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1586 if (stationTemplate
.Evses
!= null) {
1587 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1588 .update(JSON
.stringify(stationTemplate
.Evses
))
1590 const evsesConfigChanged
=
1591 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1592 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1593 evsesConfigChanged
&& this.evses
.clear()
1594 this.evsesConfigurationHash
= evsesConfigHash
1595 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1596 if (templateMaxEvses
> 0) {
1597 for (const evseKey
in stationTemplate
.Evses
) {
1598 const evseId
= convertToInt(evseKey
)
1599 this.evses
.set(evseId
, {
1600 connectors
: buildConnectorsMap(
1601 stationTemplate
.Evses
[evseKey
].Connectors
,
1605 availability
: AvailabilityType
.Operative
1607 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1608 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1610 this.saveEvsesStatus()
1613 `${this.logPrefix()} Charging station information from template ${
1615 } with no evses configuration defined, cannot create evses`
1621 `${this.logPrefix()} Charging station information from template ${
1623 } with no evses configuration defined, using already defined evses`
1628 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1629 let configuration
: ChargingStationConfiguration
| undefined
1630 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1632 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1633 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1634 this.configurationFileHash
1637 const measureId
= `${FileType.ChargingStationConfiguration} read`
1638 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1639 configuration
= JSON
.parse(
1640 readFileSync(this.configurationFile
, 'utf8')
1641 ) as ChargingStationConfiguration
1642 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1643 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1644 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1645 this.configurationFileHash
= configuration
.configurationHash
!
1648 handleFileException(
1649 this.configurationFile
,
1650 FileType
.ChargingStationConfiguration
,
1651 error
as NodeJS
.ErrnoException
,
1656 return configuration
1659 private saveAutomaticTransactionGeneratorConfiguration (): void {
1660 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1661 this.saveConfiguration()
1665 private saveConnectorsStatus (): void {
1666 this.saveConfiguration()
1669 private saveEvsesStatus (): void {
1670 this.saveConfiguration()
1673 private saveConfiguration (): void {
1674 if (isNotEmptyString(this.configurationFile
)) {
1676 if (!existsSync(dirname(this.configurationFile
))) {
1677 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1679 const configurationFromFile
= this.getConfigurationFromFile()
1680 let configurationData
: ChargingStationConfiguration
=
1681 configurationFromFile
!= null
1682 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1684 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1685 configurationData
.stationInfo
= this.stationInfo
1687 delete configurationData
.stationInfo
1690 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1691 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1693 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1695 delete configurationData
.configurationKey
1697 configurationData
= merge
<ChargingStationConfiguration
>(
1699 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1702 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== false ||
1703 this.getAutomaticTransactionGeneratorConfiguration() == null
1705 delete configurationData
.automaticTransactionGenerator
1707 if (this.connectors
.size
> 0) {
1708 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1710 delete configurationData
.connectorsStatus
1712 if (this.evses
.size
> 0) {
1713 configurationData
.evsesStatus
= buildEvsesStatus(this)
1715 delete configurationData
.evsesStatus
1717 delete configurationData
.configurationHash
1718 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1721 stationInfo
: configurationData
.stationInfo
,
1722 configurationKey
: configurationData
.configurationKey
,
1723 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1724 ...(this.connectors
.size
> 0 && {
1725 connectorsStatus
: configurationData
.connectorsStatus
1727 ...(this.evses
.size
> 0 && { evsesStatus
: configurationData
.evsesStatus
})
1728 } satisfies ChargingStationConfiguration
)
1731 if (this.configurationFileHash
!== configurationHash
) {
1732 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1733 configurationData
.configurationHash
= configurationHash
1734 const measureId
= `${FileType.ChargingStationConfiguration} write`
1735 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1737 this.configurationFile
,
1738 JSON
.stringify(configurationData
, undefined, 2),
1741 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1742 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1743 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1744 this.configurationFileHash
= configurationHash
1746 handleFileException(
1747 this.configurationFile
,
1748 FileType
.ChargingStationConfiguration
,
1749 error
as NodeJS
.ErrnoException
,
1755 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1756 this.configurationFile
1761 handleFileException(
1762 this.configurationFile
,
1763 FileType
.ChargingStationConfiguration
,
1764 error
as NodeJS
.ErrnoException
,
1770 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1775 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1776 return this.getTemplateFromFile()?.Configuration
1779 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration
| undefined {
1780 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1781 if (this.stationInfo
?.ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1782 return { configurationKey
}
1787 private getOcppConfiguration (): ChargingStationOcppConfiguration
| undefined {
1788 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1789 this.getOcppConfigurationFromFile()
1790 if (ocppConfiguration
== null) {
1791 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1793 return ocppConfiguration
1796 private async onOpen (): Promise
<void> {
1797 if (this.isWebSocketConnectionOpened()) {
1799 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1801 let registrationRetryCount
= 0
1802 if (!this.isRegistered()) {
1803 // Send BootNotification
1805 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1806 BootNotificationRequest
,
1807 BootNotificationResponse
1808 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1809 skipBufferingOnError
: true
1811 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1812 if (this.bootNotificationResponse
?.currentTime
!= null) {
1813 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1814 this.bootNotificationResponse
.currentTime
= convertToDate(
1815 this.bootNotificationResponse
.currentTime
1818 if (!this.isRegistered()) {
1819 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1821 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1822 this.bootNotificationResponse
?.interval
!= null
1823 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1824 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1828 !this.isRegistered() &&
1829 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1830 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1831 this.stationInfo
?.registrationMaxRetries
=== -1)
1834 if (this.isRegistered()) {
1835 this.emit(ChargingStationEvents
.registered
)
1836 if (this.inAcceptedState()) {
1837 this.emit(ChargingStationEvents
.accepted
)
1840 if (this.inRejectedState()) {
1841 this.emit(ChargingStationEvents
.rejected
)
1844 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1845 this.stationInfo?.registrationMaxRetries
1849 this.wsConnectionRetryCount
= 0
1850 this.emit(ChargingStationEvents
.updated
)
1853 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1858 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1859 this.emit(ChargingStationEvents
.disconnected
)
1862 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1863 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1865 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1867 )}' and reason '${reason.toString()}'`
1869 this.wsConnectionRetryCount
= 0
1874 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1876 )}' and reason '${reason.toString()}'`
1879 this.reconnect().catch(error
=>
1880 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1884 this.emit(ChargingStationEvents
.updated
)
1887 private getCachedRequest (
1888 messageType
: MessageType
| undefined,
1890 ): CachedRequest
| undefined {
1891 const cachedRequest
= this.requests
.get(messageId
)
1892 if (Array.isArray(cachedRequest
)) {
1893 return cachedRequest
1895 throw new OCPPError(
1896 ErrorType
.PROTOCOL_ERROR
,
1897 `Cached request for message id ${messageId} ${getMessageTypeString(
1899 )} is not an array`,
1905 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1906 const [messageType
, messageId
, commandName
, commandPayload
] = request
1907 if (this.stationInfo
?.enableStatistics
=== true) {
1908 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1911 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1915 // Process the message
1916 await this.ocppIncomingRequestService
.incomingRequestHandler(
1922 this.emit(ChargingStationEvents
.updated
)
1925 private handleResponseMessage (response
: Response
): void {
1926 const [messageType
, messageId
, commandPayload
] = response
1927 if (!this.requests
.has(messageId
)) {
1929 throw new OCPPError(
1930 ErrorType
.INTERNAL_ERROR
,
1931 `Response for unknown message id ${messageId}`,
1937 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1938 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1943 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1947 responseCallback(commandPayload
, requestPayload
)
1950 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
1951 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
1952 if (!this.requests
.has(messageId
)) {
1954 throw new OCPPError(
1955 ErrorType
.INTERNAL_ERROR
,
1956 `Error response for unknown message id ${messageId}`,
1958 { errorType
, errorMessage
, errorDetails
}
1961 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1962 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
1964 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1968 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
1971 private async onMessage (data
: RawData
): Promise
<void> {
1972 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
1973 let messageType
: MessageType
| undefined
1974 let errorMsg
: string
1976 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1977 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
1978 if (Array.isArray(request
)) {
1979 [messageType
] = request
1980 // Check the type of message
1981 switch (messageType
) {
1983 case MessageType
.CALL_MESSAGE
:
1984 await this.handleIncomingMessage(request
as IncomingRequest
)
1987 case MessageType
.CALL_RESULT_MESSAGE
:
1988 this.handleResponseMessage(request
as Response
)
1991 case MessageType
.CALL_ERROR_MESSAGE
:
1992 this.handleErrorMessage(request
as ErrorResponse
)
1996 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1997 errorMsg
= `Wrong message type ${messageType}`
1998 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1999 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2002 throw new OCPPError(
2003 ErrorType
.PROTOCOL_ERROR
,
2004 'Incoming message is not an array',
2012 if (!Array.isArray(request
)) {
2013 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2016 let commandName
: IncomingRequestCommand
| undefined
2017 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2018 let errorCallback
: ErrorCallback
2019 const [, messageId
] = request
2020 switch (messageType
) {
2021 case MessageType
.CALL_MESSAGE
:
2022 [, , commandName
] = request
as IncomingRequest
2024 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2026 case MessageType
.CALL_RESULT_MESSAGE
:
2027 case MessageType
.CALL_ERROR_MESSAGE
:
2028 if (this.requests
.has(messageId
)) {
2029 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2030 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2031 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2032 errorCallback(error
as OCPPError
, false)
2034 // Remove the request from the cache in case of error at response handling
2035 this.requests
.delete(messageId
)
2039 if (!(error
instanceof OCPPError
)) {
2041 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2042 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2043 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2044 }' message '${data.toString()}' handling is not an OCPPError:`,
2049 `${this.logPrefix()} Incoming OCPP command '${
2050 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2051 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2052 }' message '${data.toString()}'${
2053 this.requests.has(messageId)
2054 ? ` matching cached request
'${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
2056 } processing error:`,
2062 private onPing (): void {
2063 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2066 private onPong (): void {
2067 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2070 private onError (error
: WSError
): void {
2071 this.closeWSConnection()
2072 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2075 private getEnergyActiveImportRegister (
2076 connectorStatus
: ConnectorStatus
| undefined,
2079 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2082 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2083 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2085 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2090 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2091 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2093 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2097 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2098 return stationTemplate
?.useConnectorId0
?? true
2101 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2102 if (this.hasEvses
) {
2103 for (const [evseId
, evseStatus
] of this.evses
) {
2107 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2108 if (connectorStatus
.transactionStarted
=== true) {
2109 await this.stopTransactionOnConnector(connectorId
, reason
)
2114 for (const connectorId
of this.connectors
.keys()) {
2115 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2116 await this.stopTransactionOnConnector(connectorId
, reason
)
2123 private getConnectionTimeout (): number {
2124 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2125 return convertToInt(
2126 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2127 Constants
.DEFAULT_CONNECTION_TIMEOUT
2130 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2133 private getPowerDivider (): number {
2134 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2135 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2136 powerDivider
= this.getNumberOfRunningTransactions()
2141 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2143 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2144 switch (this.getCurrentOutType(stationInfo
)) {
2145 case CurrentType
.AC
:
2146 return ACElectricUtils
.amperagePerPhaseFromPower(
2147 this.getNumberOfPhases(stationInfo
),
2148 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2149 this.getVoltageOut(stationInfo
)
2151 case CurrentType
.DC
:
2152 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2156 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2157 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2158 return (stationInfo
?? this.stationInfo
!).currentOutType
?? CurrentType
.AC
2161 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2164 (stationInfo
?? this.stationInfo
!).voltageOut
??
2165 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2169 private getAmperageLimitation (): number | undefined {
2171 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2172 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2175 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2176 getAmperageLimitationUnitDivider(this.stationInfo
)
2181 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2182 if (this.stationInfo
?.autoRegister
=== true) {
2183 await this.ocppRequestService
.requestHandler
<
2184 BootNotificationRequest
,
2185 BootNotificationResponse
2186 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2187 skipBufferingOnError
: true
2190 // Start WebSocket ping
2191 this.startWebSocketPing()
2193 this.startHeartbeat()
2194 // Initialize connectors status
2195 if (this.hasEvses
) {
2196 for (const [evseId
, evseStatus
] of this.evses
) {
2198 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2199 const connectorBootStatus
= getBootConnectorStatus(this, connectorId
, connectorStatus
)
2200 await sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
, evseId
)
2205 for (const connectorId
of this.connectors
.keys()) {
2206 if (connectorId
> 0) {
2207 const connectorBootStatus
= getBootConnectorStatus(
2210 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2211 this.getConnectorStatus(connectorId
)!
2213 await sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
)
2217 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2218 await this.ocppRequestService
.requestHandler
<
2219 FirmwareStatusNotificationRequest
,
2220 FirmwareStatusNotificationResponse
2221 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2222 status: FirmwareStatus
.Installed
2224 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2228 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2229 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2231 this.flushMessageBuffer()
2234 private internalStopMessageSequence (): void {
2235 // Stop WebSocket ping
2236 this.stopWebSocketPing()
2238 this.stopHeartbeat()
2240 if (this.automaticTransactionGenerator
?.started
=== true) {
2241 this.stopAutomaticTransactionGenerator()
2245 private async stopMessageSequence (
2246 reason
?: StopTransactionReason
,
2247 stopTransactions
?: boolean
2249 this.internalStopMessageSequence()
2250 // Stop ongoing transactions
2251 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2252 if (this.hasEvses
) {
2253 for (const [evseId
, evseStatus
] of this.evses
) {
2255 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2256 await sendAndSetConnectorStatus(
2259 ConnectorStatusEnum
.Unavailable
,
2262 delete connectorStatus
.status
2267 for (const connectorId
of this.connectors
.keys()) {
2268 if (connectorId
> 0) {
2269 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2270 delete this.getConnectorStatus(connectorId
)?.status
2276 private startWebSocketPing (): void {
2277 const webSocketPingInterval
=
2278 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2280 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
2283 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2284 this.wsPingSetInterval
= setInterval(() => {
2285 if (this.isWebSocketConnectionOpened()) {
2286 this.wsConnection
?.ping()
2288 }, secondsToMilliseconds(webSocketPingInterval
))
2290 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2291 webSocketPingInterval
2294 } else if (this.wsPingSetInterval
!= null) {
2296 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2297 webSocketPingInterval
2302 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2307 private stopWebSocketPing (): void {
2308 if (this.wsPingSetInterval
!= null) {
2309 clearInterval(this.wsPingSetInterval
)
2310 delete this.wsPingSetInterval
2314 private getConfiguredSupervisionUrl (): URL
{
2315 let configuredSupervisionUrl
: string
2316 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2317 if (isNotEmptyArray(supervisionUrls
)) {
2318 let configuredSupervisionUrlIndex
: number
2319 switch (Configuration
.getSupervisionUrlDistribution()) {
2320 case SupervisionUrlDistribution
.RANDOM
:
2321 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2323 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2324 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2326 !Object.values(SupervisionUrlDistribution
).includes(
2327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2328 Configuration
.getSupervisionUrlDistribution()!
2331 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2332 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2333 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2336 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2339 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2341 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2342 configuredSupervisionUrl
= supervisionUrls
!
2344 if (isNotEmptyString(configuredSupervisionUrl
)) {
2345 return new URL(configuredSupervisionUrl
)
2347 const errorMsg
= 'No supervision url(s) configured'
2348 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2349 throw new BaseError(errorMsg
)
2352 private stopHeartbeat (): void {
2353 if (this.heartbeatSetInterval
!= null) {
2354 clearInterval(this.heartbeatSetInterval
)
2355 delete this.heartbeatSetInterval
2359 private terminateWSConnection (): void {
2360 if (this.isWebSocketConnectionOpened()) {
2361 this.wsConnection
?.terminate()
2362 this.wsConnection
= null
2366 private async reconnect (): Promise
<void> {
2368 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2369 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2370 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2372 this.wsConnectionRetried
= true
2373 ++this.wsConnectionRetryCount
2374 const reconnectDelay
=
2375 this.stationInfo
?.reconnectExponentialDelay
=== true
2376 ? exponentialDelay(this.wsConnectionRetryCount
)
2377 : secondsToMilliseconds(this.getConnectionTimeout())
2378 const reconnectDelayWithdraw
= 1000
2379 const reconnectTimeout
=
2380 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2382 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2385 )}ms, timeout ${reconnectTimeout}ms`
2387 await sleep(reconnectDelay
)
2389 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2391 this.openWSConnection(
2393 handshakeTimeout
: reconnectTimeout
2395 { closeOpened
: true }
2397 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2399 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2400 this.wsConnectionRetryCount
2401 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`