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
, rmSync
, 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 ChargingStationOptions
,
77 type ChargingStationTemplate
,
85 type EvseStatusConfiguration
,
88 type FirmwareStatusNotificationRequest
,
89 type FirmwareStatusNotificationResponse
,
91 type HeartbeatRequest
,
92 type HeartbeatResponse
,
94 type IncomingRequestCommand
,
97 type MeterValuesRequest
,
98 type MeterValuesResponse
,
100 type OutgoingRequest
,
102 RegistrationStatusEnumType
,
106 ReservationTerminationReason
,
108 StandardParametersKey
,
110 type StopTransactionReason
,
111 type StopTransactionRequest
,
112 type StopTransactionResponse
,
113 SupervisionUrlDistribution
,
114 SupportedFeatureProfiles
,
117 WebSocketCloseEventStatusCode
,
119 } from
'../types/index.js'
128 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
129 buildConnectorsStatus
,
140 formatDurationMilliSeconds
,
141 formatDurationSeconds
,
143 getWebSocketCloseEventStatusString
,
155 } from
'../utils/index.js'
157 export class ChargingStation
extends EventEmitter
{
158 public readonly index
: number
159 public readonly templateFile
: string
160 public stationInfo
?: ChargingStationInfo
161 public started
: boolean
162 public starting
: boolean
163 public idTagsCache
: IdTagsCache
164 public automaticTransactionGenerator
!: AutomaticTransactionGenerator
| undefined
165 public ocppConfiguration
!: ChargingStationOcppConfiguration
| undefined
166 public wsConnection
: WebSocket
| null
167 public readonly connectors
: Map
<number, ConnectorStatus
>
168 public readonly evses
: Map
<number, EvseStatus
>
169 public readonly requests
: Map
<string, CachedRequest
>
170 public performanceStatistics
: PerformanceStatistics
| undefined
171 public heartbeatSetInterval
?: NodeJS
.Timeout
172 public ocppRequestService
!: OCPPRequestService
173 public bootNotificationRequest
?: BootNotificationRequest
174 public bootNotificationResponse
?: BootNotificationResponse
175 public powerDivider
?: number
176 private stopping
: boolean
177 private configurationFile
!: string
178 private configurationFileHash
!: string
179 private connectorsConfigurationHash
!: string
180 private evsesConfigurationHash
!: string
181 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
182 private ocppIncomingRequestService
!: OCPPIncomingRequestService
183 private readonly messageBuffer
: Set
<string>
184 private configuredSupervisionUrl
!: URL
185 private wsConnectionRetried
: boolean
186 private wsConnectionRetryCount
: number
187 private templateFileWatcher
: FSWatcher
| undefined
188 private templateFileHash
!: string
189 private readonly sharedLRUCache
: SharedLRUCache
190 private wsPingSetInterval
?: NodeJS
.Timeout
191 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
192 private flushMessageBufferSetInterval
?: NodeJS
.Timeout
194 constructor (index
: number, templateFile
: string, options
?: ChargingStationOptions
) {
197 this.starting
= false
198 this.stopping
= false
199 this.wsConnection
= null
200 this.wsConnectionRetried
= false
201 this.wsConnectionRetryCount
= 0
203 this.templateFile
= templateFile
204 this.connectors
= new Map
<number, ConnectorStatus
>()
205 this.evses
= new Map
<number, EvseStatus
>()
206 this.requests
= new Map
<string, CachedRequest
>()
207 this.messageBuffer
= new Set
<string>()
208 this.sharedLRUCache
= SharedLRUCache
.getInstance()
209 this.idTagsCache
= IdTagsCache
.getInstance()
210 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this)
212 this.on(ChargingStationEvents
.added
, () => {
213 parentPort
?.postMessage(buildAddedMessage(this))
215 this.on(ChargingStationEvents
.deleted
, () => {
216 parentPort
?.postMessage(buildDeletedMessage(this))
218 this.on(ChargingStationEvents
.started
, () => {
219 parentPort
?.postMessage(buildStartedMessage(this))
221 this.on(ChargingStationEvents
.stopped
, () => {
222 parentPort
?.postMessage(buildStoppedMessage(this))
224 this.on(ChargingStationEvents
.updated
, () => {
225 parentPort
?.postMessage(buildUpdatedMessage(this))
227 this.on(ChargingStationEvents
.accepted
, () => {
228 this.startMessageSequence(
229 this.wsConnectionRetried
231 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
233 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
235 this.wsConnectionRetried
= false
237 this.on(ChargingStationEvents
.rejected
, () => {
238 this.wsConnectionRetried
= false
240 this.on(ChargingStationEvents
.disconnected
, () => {
242 this.internalStopMessageSequence()
245 `${this.logPrefix()} Error while stopping the internal message sequence:`,
251 this.initialize(options
)
255 if (options
?.autoStart
!= null) {
256 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
257 this.stationInfo
!.autoStart
= options
.autoStart
260 if (this.stationInfo
?.autoStart
=== true) {
265 public get
hasEvses (): boolean {
266 return this.connectors
.size
=== 0 && this.evses
.size
> 0
269 public get
wsConnectionUrl (): URL
{
272 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
273 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
274 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
275 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
276 : this.configuredSupervisionUrl.href
277 }/${this.stationInfo?.chargingStationId}`
281 public logPrefix
= (): string => {
283 this instanceof ChargingStation
&&
284 this.stationInfo
!= null &&
285 isNotEmptyString(this.stationInfo
.chargingStationId
)
287 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
289 let stationTemplate
: ChargingStationTemplate
| undefined
291 stationTemplate
= JSON
.parse(
292 readFileSync(this.templateFile
, 'utf8')
293 ) as ChargingStationTemplate
295 stationTemplate
= undefined
297 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
300 public hasIdTags (): boolean {
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
305 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307 const localStationInfo
= stationInfo
?? this.stationInfo
!
308 switch (this.getCurrentOutType(stationInfo
)) {
310 return localStationInfo
.numberOfPhases
?? 3
316 public isWebSocketConnectionOpened (): boolean {
317 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
320 public inUnknownState (): boolean {
321 return this.bootNotificationResponse
?.status == null
324 public inPendingState (): boolean {
325 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
328 public inAcceptedState (): boolean {
329 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
332 public inRejectedState (): boolean {
333 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
336 public isRegistered (): boolean {
337 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
340 public isChargingStationAvailable (): boolean {
341 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
344 public hasConnector (connectorId
: number): boolean {
346 for (const evseStatus
of this.evses
.values()) {
347 if (evseStatus
.connectors
.has(connectorId
)) {
353 return this.connectors
.has(connectorId
)
356 public isConnectorAvailable (connectorId
: number): boolean {
359 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
363 public getNumberOfConnectors (): number {
365 let numberOfConnectors
= 0
366 for (const [evseId
, evseStatus
] of this.evses
) {
368 numberOfConnectors
+= evseStatus
.connectors
.size
371 return numberOfConnectors
373 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
376 public getNumberOfEvses (): number {
377 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
380 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
382 for (const evseStatus
of this.evses
.values()) {
383 if (evseStatus
.connectors
.has(connectorId
)) {
384 return evseStatus
.connectors
.get(connectorId
)
389 return this.connectors
.get(connectorId
)
392 public getConnectorMaximumAvailablePower (connectorId
: number): number {
393 let connectorAmperageLimitationPowerLimit
: number | undefined
394 const amperageLimitation
= this.getAmperageLimitation()
396 amperageLimitation
!= null &&
397 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
398 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
400 connectorAmperageLimitationPowerLimit
=
401 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
402 ? ACElectricUtils
.powerTotal(
403 this.getNumberOfPhases(),
404 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
405 this.stationInfo
.voltageOut
!,
407 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
409 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
410 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
411 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
416 const connectorChargingProfilesPowerLimit
=
417 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
419 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
420 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
421 isNaN(connectorAmperageLimitationPowerLimit
!)
423 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
424 connectorAmperageLimitationPowerLimit
!,
425 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426 isNaN(connectorChargingProfilesPowerLimit
!) ? Infinity : connectorChargingProfilesPowerLimit
!
430 public getTransactionIdTag (transactionId
: number): string | undefined {
432 for (const evseStatus
of this.evses
.values()) {
433 for (const connectorStatus
of evseStatus
.connectors
.values()) {
434 if (connectorStatus
.transactionId
=== transactionId
) {
435 return connectorStatus
.transactionIdTag
440 for (const connectorId
of this.connectors
.keys()) {
441 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
442 return this.getConnectorStatus(connectorId
)?.transactionIdTag
448 public getNumberOfRunningTransactions (): number {
449 let numberOfRunningTransactions
= 0
451 for (const [evseId
, evseStatus
] of this.evses
) {
455 for (const connectorStatus
of evseStatus
.connectors
.values()) {
456 if (connectorStatus
.transactionStarted
=== true) {
457 ++numberOfRunningTransactions
462 for (const connectorId
of this.connectors
.keys()) {
463 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
464 ++numberOfRunningTransactions
468 return numberOfRunningTransactions
471 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
472 if (transactionId
== null) {
474 } else if (this.hasEvses
) {
475 for (const evseStatus
of this.evses
.values()) {
476 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
477 if (connectorStatus
.transactionId
=== transactionId
) {
483 for (const connectorId
of this.connectors
.keys()) {
484 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
491 public getEnergyActiveImportRegisterByTransactionId (
492 transactionId
: number | undefined,
495 return this.getEnergyActiveImportRegister(
496 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
497 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
502 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
503 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
506 public getAuthorizeRemoteTxRequests (): boolean {
507 const authorizeRemoteTxRequests
= getConfigurationKey(
509 StandardParametersKey
.AuthorizeRemoteTxRequests
511 return authorizeRemoteTxRequests
!= null
512 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
516 public getLocalAuthListEnabled (): boolean {
517 const localAuthListEnabled
= getConfigurationKey(
519 StandardParametersKey
.LocalAuthListEnabled
521 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
524 public getHeartbeatInterval (): number {
525 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
526 if (HeartbeatInterval
!= null) {
527 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
529 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
530 if (HeartBeatInterval
!= null) {
531 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
533 this.stationInfo
?.autoRegister
=== false &&
535 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
536 Constants.DEFAULT_HEARTBEAT_INTERVAL
539 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
542 public setSupervisionUrl (url
: string): void {
544 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
545 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
547 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
549 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
550 this.stationInfo
!.supervisionUrls
= url
551 this.saveStationInfo()
552 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
556 public startHeartbeat (): void {
557 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval
== null) {
558 this.heartbeatSetInterval
= setInterval(() => {
559 this.ocppRequestService
560 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
563 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
567 }, this.getHeartbeatInterval())
569 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
570 this.getHeartbeatInterval()
573 } else if (this.heartbeatSetInterval
!= null) {
575 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
576 this.getHeartbeatInterval()
581 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
586 public restartHeartbeat (): void {
590 this.startHeartbeat()
593 public restartWebSocketPing (): void {
594 // Stop WebSocket ping
595 this.stopWebSocketPing()
596 // Start WebSocket ping
597 this.startWebSocketPing()
600 public startMeterValues (connectorId
: number, interval
: number): void {
601 if (connectorId
=== 0) {
602 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
605 const connectorStatus
= this.getConnectorStatus(connectorId
)
606 if (connectorStatus
== null) {
608 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
613 if (connectorStatus
.transactionStarted
=== false) {
615 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
619 connectorStatus
.transactionStarted
=== true &&
620 connectorStatus
.transactionId
== null
623 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
628 connectorStatus
.transactionSetInterval
= setInterval(() => {
629 const meterValue
= buildMeterValue(
632 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
633 connectorStatus
.transactionId
!,
636 this.ocppRequestService
637 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
639 RequestCommand
.METER_VALUES
,
642 transactionId
: connectorStatus
.transactionId
,
643 meterValue
: [meterValue
]
648 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
655 `${this.logPrefix()} Charging station ${
656 StandardParametersKey.MeterValueSampleInterval
657 } configuration set to ${interval}, not sending MeterValues`
662 public stopMeterValues (connectorId
: number): void {
663 const connectorStatus
= this.getConnectorStatus(connectorId
)
664 if (connectorStatus
?.transactionSetInterval
!= null) {
665 clearInterval(connectorStatus
.transactionSetInterval
)
669 private add (): void {
670 this.emit(ChargingStationEvents
.added
)
673 public async delete (deleteConfiguration
= true): Promise
<void> {
677 AutomaticTransactionGenerator
.deleteInstance(this)
678 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
679 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
680 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
681 this.requests
.clear()
682 this.connectors
.clear()
684 this.templateFileWatcher
?.unref()
685 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
686 this.chargingStationWorkerBroadcastChannel
.unref()
687 this.emit(ChargingStationEvents
.deleted
)
690 public start (): void {
692 if (!this.starting
) {
694 if (this.stationInfo
?.enableStatistics
=== true) {
695 this.performanceStatistics
?.start()
697 this.openWSConnection()
698 // Monitor charging station template file
699 this.templateFileWatcher
= watchJsonFile(
701 FileType
.ChargingStationTemplate
,
704 (event
, filename
): void => {
705 if (isNotEmptyString(filename
) && event
=== 'change') {
708 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
710 } file have changed, reload`
712 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
713 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
714 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
718 const ATGStarted
= this.automaticTransactionGenerator
?.started
719 if (ATGStarted
=== true) {
720 this.stopAutomaticTransactionGenerator()
722 delete this.automaticTransactionGeneratorConfiguration
724 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
727 this.startAutomaticTransactionGenerator(undefined, true)
729 if (this.stationInfo
?.enableStatistics
=== true) {
730 this.performanceStatistics
?.restart()
732 this.performanceStatistics
?.stop()
734 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
737 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
745 this.emit(ChargingStationEvents
.started
)
746 this.starting
= false
748 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
751 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
756 reason
?: StopTransactionReason
,
757 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
760 if (!this.stopping
) {
762 await this.stopMessageSequence(reason
, stopTransactions
)
763 this.closeWSConnection()
764 if (this.stationInfo
?.enableStatistics
=== true) {
765 this.performanceStatistics
?.stop()
767 this.templateFileWatcher
?.close()
768 delete this.bootNotificationResponse
770 this.saveConfiguration()
771 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
772 this.emit(ChargingStationEvents
.stopped
)
773 this.stopping
= false
775 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
778 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
782 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
783 await this.stop(reason
)
784 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
785 await sleep(this.stationInfo
!.resetTime
!)
790 public saveOcppConfiguration (): void {
791 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
792 this.saveConfiguration()
796 public bufferMessage (message
: string): void {
797 this.messageBuffer
.add(message
)
798 this.setIntervalFlushMessageBuffer()
801 public openWSConnection (
803 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
806 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
807 ...this.stationInfo
?.wsOptions
,
810 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
811 if (!checkChargingStation(this, this.logPrefix())) {
814 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
815 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
817 if (params
.closeOpened
=== true) {
818 this.closeWSConnection()
820 if (params
.terminateOpened
=== true) {
821 this.terminateWSConnection()
824 if (this.isWebSocketConnectionOpened()) {
826 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
831 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
833 this.wsConnection
= new WebSocket(
834 this.wsConnectionUrl
,
835 `ocpp${this.stationInfo?.ocppVersion}`,
839 // Handle WebSocket message
840 this.wsConnection
.on('message', data
=> {
841 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
843 // Handle WebSocket error
844 this.wsConnection
.on('error', this.onError
.bind(this))
845 // Handle WebSocket close
846 this.wsConnection
.on('close', this.onClose
.bind(this))
847 // Handle WebSocket open
848 this.wsConnection
.on('open', () => {
849 this.onOpen().catch(error
=>
850 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
853 // Handle WebSocket ping
854 this.wsConnection
.on('ping', this.onPing
.bind(this))
855 // Handle WebSocket pong
856 this.wsConnection
.on('pong', this.onPong
.bind(this))
859 public closeWSConnection (): void {
860 if (this.isWebSocketConnectionOpened()) {
861 this.wsConnection
?.close()
862 this.wsConnection
= null
866 public getAutomaticTransactionGeneratorConfiguration ():
867 | AutomaticTransactionGeneratorConfiguration
869 if (this.automaticTransactionGeneratorConfiguration
== null) {
870 let automaticTransactionGeneratorConfiguration
:
871 | AutomaticTransactionGeneratorConfiguration
873 const stationTemplate
= this.getTemplateFromFile()
874 const stationConfiguration
= this.getConfigurationFromFile()
876 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
877 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
878 stationConfiguration
?.automaticTransactionGenerator
!= null
880 automaticTransactionGeneratorConfiguration
=
881 stationConfiguration
.automaticTransactionGenerator
883 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
885 this.automaticTransactionGeneratorConfiguration
= {
886 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
887 ...automaticTransactionGeneratorConfiguration
890 return this.automaticTransactionGeneratorConfiguration
893 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
894 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
897 public startAutomaticTransactionGenerator (
898 connectorIds
?: number[],
899 stopAbsoluteDuration
?: boolean
901 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
902 if (isNotEmptyArray(connectorIds
)) {
903 for (const connectorId
of connectorIds
) {
904 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
907 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
909 this.saveAutomaticTransactionGeneratorConfiguration()
910 this.emit(ChargingStationEvents
.updated
)
913 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
914 if (isNotEmptyArray(connectorIds
)) {
915 for (const connectorId
of connectorIds
) {
916 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
919 this.automaticTransactionGenerator
?.stop()
921 this.saveAutomaticTransactionGeneratorConfiguration()
922 this.emit(ChargingStationEvents
.updated
)
925 public async stopTransactionOnConnector (
927 reason
?: StopTransactionReason
928 ): Promise
<StopTransactionResponse
> {
929 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
931 this.stationInfo
?.beginEndMeterValues
=== true &&
932 this.stationInfo
.ocppStrictCompliance
=== true &&
933 this.stationInfo
.outOfOrderEndMeterValues
=== false
935 const transactionEndMeterValue
= buildTransactionEndMeterValue(
938 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
940 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
942 RequestCommand
.METER_VALUES
,
946 meterValue
: [transactionEndMeterValue
]
950 return await this.ocppRequestService
.requestHandler
<
951 StopTransactionRequest
,
952 StopTransactionResponse
953 >(this, RequestCommand
.STOP_TRANSACTION
, {
955 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
956 ...(reason
!= null && { reason
})
960 public getReserveConnectorZeroSupported (): boolean {
961 return convertToBoolean(
962 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
963 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
967 public async addReservation (reservation
: Reservation
): Promise
<void> {
968 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
969 if (reservationFound
!= null) {
970 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
972 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
973 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
974 await sendAndSetConnectorStatus(
976 reservation
.connectorId
,
977 ConnectorStatusEnum
.Reserved
,
979 { send
: reservation
.connectorId
!== 0 }
983 public async removeReservation (
984 reservation
: Reservation
,
985 reason
: ReservationTerminationReason
987 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
988 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
990 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
991 case ReservationTerminationReason
.TRANSACTION_STARTED
:
992 delete connector
.reservation
994 case ReservationTerminationReason
.RESERVATION_CANCELED
:
995 case ReservationTerminationReason
.REPLACE_EXISTING
:
996 case ReservationTerminationReason
.EXPIRED
:
997 await sendAndSetConnectorStatus(
999 reservation
.connectorId
,
1000 ConnectorStatusEnum
.Available
,
1002 { send
: reservation
.connectorId
!== 0 }
1004 delete connector
.reservation
1007 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1008 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1012 public getReservationBy (
1013 filterKey
: ReservationKey
,
1014 value
: number | string
1015 ): Reservation
| undefined {
1016 if (this.hasEvses
) {
1017 for (const evseStatus
of this.evses
.values()) {
1018 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1019 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1020 return connectorStatus
.reservation
1025 for (const connectorStatus
of this.connectors
.values()) {
1026 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1027 return connectorStatus
.reservation
1033 public isConnectorReservable (
1034 reservationId
: number,
1036 connectorId
?: number
1038 const reservation
= this.getReservationBy('reservationId', reservationId
)
1039 const reservationExists
= reservation
!== undefined && !hasReservationExpired(reservation
)
1040 if (arguments.length
=== 1) {
1041 return !reservationExists
1042 } else if (arguments.length
> 1) {
1043 const userReservation
=
1044 idTag
!== undefined ? this.getReservationBy('idTag', idTag
) : undefined
1045 const userReservationExists
=
1046 userReservation
!== undefined && !hasReservationExpired(userReservation
)
1047 const notConnectorZero
= connectorId
=== undefined ? true : connectorId
> 0
1048 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1050 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1056 private setIntervalFlushMessageBuffer (): void {
1057 if (this.flushMessageBufferSetInterval
== null) {
1058 this.flushMessageBufferSetInterval
= setInterval(() => {
1059 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1060 this.flushMessageBuffer()
1062 if (this.messageBuffer
.size
=== 0) {
1063 this.clearIntervalFlushMessageBuffer()
1065 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1069 private clearIntervalFlushMessageBuffer (): void {
1070 if (this.flushMessageBufferSetInterval
!= null) {
1071 clearInterval(this.flushMessageBufferSetInterval
)
1072 delete this.flushMessageBufferSetInterval
1076 private getNumberOfReservableConnectors (): number {
1077 let numberOfReservableConnectors
= 0
1078 if (this.hasEvses
) {
1079 for (const evseStatus
of this.evses
.values()) {
1080 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1083 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1085 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1088 private getNumberOfReservationsOnConnectorZero (): number {
1090 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1091 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1098 private flushMessageBuffer (): void {
1099 if (this.messageBuffer
.size
> 0) {
1100 for (const message
of this.messageBuffer
.values()) {
1101 let beginId
: string | undefined
1102 let commandName
: RequestCommand
| undefined
1103 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1104 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1106 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1107 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1109 this.wsConnection
?.send(message
, (error
?: Error) => {
1110 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1111 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1112 if (error
== null) {
1114 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1116 )} OCPP message sent '${JSON.stringify(message)}'`
1118 this.messageBuffer
.delete(message
)
1121 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1123 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1132 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1133 let template
: ChargingStationTemplate
| undefined
1135 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1136 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1138 const measureId
= `${FileType.ChargingStationTemplate} read`
1139 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1140 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1141 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1142 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1143 .update(JSON
.stringify(template
))
1145 this.sharedLRUCache
.setChargingStationTemplate(template
)
1146 this.templateFileHash
= template
.templateHash
1149 handleFileException(
1151 FileType
.ChargingStationTemplate
,
1152 error
as NodeJS
.ErrnoException
,
1159 private getStationInfoFromTemplate (): ChargingStationInfo
{
1160 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1161 const stationTemplate
= this.getTemplateFromFile()!
1162 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1163 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
, this)
1164 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1165 if (stationTemplate
.Connectors
!= null) {
1166 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1168 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1169 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1170 stationInfo
.templateName
= parse(this.templateFile
).name
1171 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1172 createSerialNumber(stationTemplate
, stationInfo
)
1173 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1174 if (isNotEmptyArray(stationTemplate
.power
)) {
1175 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1176 stationInfo
.maximumPower
=
1177 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1178 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1179 : stationTemplate
.power
[powerArrayRandomIndex
]
1181 stationInfo
.maximumPower
=
1182 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1183 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1184 stationTemplate
.power
! * 1000
1185 : stationTemplate
.power
1187 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1189 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1190 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1191 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1194 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1196 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1199 stationInfo
.firmwareUpgrade
= merge
<FirmwareUpgrade
>(
1206 stationTemplate
.firmwareUpgrade
?? {}
1208 if (stationTemplate
.resetTime
!= null) {
1209 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1214 private getStationInfoFromFile (
1215 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1216 .stationInfoPersistentConfiguration
1217 ): ChargingStationInfo
| undefined {
1218 let stationInfo
: ChargingStationInfo
| undefined
1219 if (stationInfoPersistentConfiguration
=== true) {
1220 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1221 if (stationInfo
!= null) {
1222 delete stationInfo
.infoHash
1223 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1224 if (stationInfo
.templateName
== null) {
1225 stationInfo
.templateName
= parse(this.templateFile
).name
1232 private getStationInfo (stationInfoPersistentConfiguration
?: boolean): ChargingStationInfo
{
1233 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1234 stationInfoPersistentConfiguration
!= null &&
1235 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
=
1236 stationInfoPersistentConfiguration
)
1237 const stationInfoFromFile
= this.getStationInfoFromFile(
1238 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1241 // 1. charging station info from template
1242 // 2. charging station info from configuration file
1244 stationInfoFromFile
!= null &&
1245 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1247 return { ...Constants
.DEFAULT_STATION_INFO
, ...stationInfoFromFile
}
1249 stationInfoFromFile
!= null &&
1250 propagateSerialNumber(
1251 this.getTemplateFromFile(),
1252 stationInfoFromFile
,
1253 stationInfoFromTemplate
1255 return { ...Constants
.DEFAULT_STATION_INFO
, ...stationInfoFromTemplate
}
1258 private saveStationInfo (): void {
1259 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1260 this.saveConfiguration()
1264 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1265 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1266 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1267 throw new BaseError(errorMsg
)
1270 private initialize (options
?: ChargingStationOptions
): void {
1271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1272 const stationTemplate
= this.getTemplateFromFile()!
1273 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1274 this.configurationFile
= join(
1275 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1276 `${getHashId(this.index, stationTemplate)}.json`
1278 const stationConfiguration
= this.getConfigurationFromFile()
1280 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1281 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1283 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1284 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1286 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1288 this.stationInfo
= this.getStationInfo(options
?.persistentConfiguration
)
1289 if (options
?.persistentConfiguration
!= null) {
1290 this.stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
1291 this.stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
1292 options
.persistentConfiguration
1294 if (options
?.autoRegister
!= null) {
1295 this.stationInfo
.autoRegister
= options
.autoRegister
1297 if (options
?.enableStatistics
!= null) {
1298 this.stationInfo
.enableStatistics
= options
.enableStatistics
1300 if (options
?.ocppStrictCompliance
!= null) {
1301 this.stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
1303 if (options
?.stopTransactionsOnStopped
!= null) {
1304 this.stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
1307 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1308 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1309 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1311 const patternGroup
=
1312 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1313 this.stationInfo
.firmwareVersion
.split('.').length
1314 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1315 .exec(this.stationInfo
.firmwareVersion
)
1316 ?.slice(1, patternGroup
+ 1)
1317 if (match
!= null) {
1318 const patchLevelIndex
= match
.length
- 1
1319 match
[patchLevelIndex
] = (
1320 convertToInt(match
[patchLevelIndex
]) +
1321 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1322 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1324 this.stationInfo
.firmwareVersion
= match
.join('.')
1327 this.saveStationInfo()
1328 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1329 if (this.stationInfo
.enableStatistics
=== true) {
1330 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1331 this.stationInfo
.hashId
,
1332 this.stationInfo
.chargingStationId
,
1333 this.configuredSupervisionUrl
1336 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1337 if (bootNotificationRequest
== null) {
1338 const errorMsg
= 'Error while creating boot notification request'
1339 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1340 throw new BaseError(errorMsg
)
1342 this.bootNotificationRequest
= bootNotificationRequest
1343 this.powerDivider
= this.getPowerDivider()
1344 // OCPP configuration
1345 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1346 this.initializeOcppConfiguration()
1347 this.initializeOcppServices()
1348 if (this.stationInfo
.autoRegister
=== true) {
1349 this.bootNotificationResponse
= {
1350 currentTime
: new Date(),
1351 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1352 status: RegistrationStatusEnumType
.ACCEPTED
1357 private initializeOcppServices (): void {
1358 const ocppVersion
= this.stationInfo
?.ocppVersion
1359 switch (ocppVersion
) {
1360 case OCPPVersion
.VERSION_16
:
1361 this.ocppIncomingRequestService
=
1362 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1363 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1364 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1367 case OCPPVersion
.VERSION_20
:
1368 case OCPPVersion
.VERSION_201
:
1369 this.ocppIncomingRequestService
=
1370 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1371 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1372 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1376 this.handleUnsupportedVersion(ocppVersion
)
1381 private initializeOcppConfiguration (): void {
1382 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1383 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1385 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1386 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', { visible
: false })
1389 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1390 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1391 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1393 addConfigurationKey(
1395 this.stationInfo
.supervisionUrlOcppKey
,
1396 this.configuredSupervisionUrl
.href
,
1400 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1401 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1402 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1404 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, { save
: false })
1407 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1408 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1410 addConfigurationKey(
1412 this.stationInfo
.amperageLimitationOcppKey
,
1414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1415 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1418 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1419 addConfigurationKey(
1421 StandardParametersKey
.SupportedFeatureProfiles
,
1422 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1425 addConfigurationKey(
1427 StandardParametersKey
.NumberOfConnectors
,
1428 this.getNumberOfConnectors().toString(),
1432 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1433 addConfigurationKey(
1435 StandardParametersKey
.MeterValuesSampledData
,
1436 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1439 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1440 const connectorsPhaseRotation
: string[] = []
1441 if (this.hasEvses
) {
1442 for (const evseStatus
of this.evses
.values()) {
1443 for (const connectorId
of evseStatus
.connectors
.keys()) {
1444 connectorsPhaseRotation
.push(
1445 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1446 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1451 for (const connectorId
of this.connectors
.keys()) {
1452 connectorsPhaseRotation
.push(
1453 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1454 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1458 addConfigurationKey(
1460 StandardParametersKey
.ConnectorPhaseRotation
,
1461 connectorsPhaseRotation
.toString()
1464 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1465 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1468 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1469 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1471 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1473 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1474 addConfigurationKey(
1476 StandardParametersKey
.ConnectionTimeOut
,
1477 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1480 this.saveOcppConfiguration()
1483 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1484 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1485 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1486 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1488 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1489 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1490 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1491 delete evseStatus
.connectorsStatus
1492 this.evses
.set(evseId
, {
1493 ...(evseStatus
as EvseStatus
),
1494 connectors
: new Map
<number, ConnectorStatus
>(
1495 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1496 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1503 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1504 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1505 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1506 throw new BaseError(errorMsg
)
1508 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1509 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1510 throw new BaseError(errorMsg
)
1514 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1515 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1516 this.initializeConnectorsFromTemplate(stationTemplate
)
1517 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1518 this.initializeEvsesFromTemplate(stationTemplate
)
1519 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1520 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1521 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1522 throw new BaseError(errorMsg
)
1524 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1525 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1526 throw new BaseError(errorMsg
)
1530 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1531 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1532 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1533 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1534 throw new BaseError(errorMsg
)
1536 if (stationTemplate
.Connectors
?.[0] == null) {
1538 `${this.logPrefix()} Charging station information from template ${
1540 } with no connector id 0 configuration`
1543 if (stationTemplate
.Connectors
!= null) {
1544 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1545 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1546 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1548 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1551 const connectorsConfigChanged
=
1552 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1553 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1554 connectorsConfigChanged
&& this.connectors
.clear()
1555 this.connectorsConfigurationHash
= connectorsConfigHash
1556 if (templateMaxConnectors
> 0) {
1557 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1559 connectorId
=== 0 &&
1560 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1561 (stationTemplate
.Connectors
[connectorId
] == null ||
1562 !this.getUseConnectorId0(stationTemplate
))
1566 const templateConnectorId
=
1567 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1568 ? getRandomInteger(templateMaxAvailableConnectors
, 1)
1570 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1571 checkStationInfoConnectorStatus(
1572 templateConnectorId
,
1577 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1579 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1580 this.saveConnectorsStatus()
1583 `${this.logPrefix()} Charging station information from template ${
1585 } with no connectors configuration defined, cannot create connectors`
1591 `${this.logPrefix()} Charging station information from template ${
1593 } with no connectors configuration defined, using already defined connectors`
1598 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1599 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1600 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1601 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1602 throw new BaseError(errorMsg
)
1604 if (stationTemplate
.Evses
?.[0] == null) {
1606 `${this.logPrefix()} Charging station information from template ${
1608 } with no evse id 0 configuration`
1611 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1613 `${this.logPrefix()} Charging station information from template ${
1615 } with evse id 0 with no connector id 0 configuration`
1618 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1620 `${this.logPrefix()} Charging station information from template ${
1622 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1625 if (stationTemplate
.Evses
!= null) {
1626 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1627 .update(JSON
.stringify(stationTemplate
.Evses
))
1629 const evsesConfigChanged
=
1630 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1631 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1632 evsesConfigChanged
&& this.evses
.clear()
1633 this.evsesConfigurationHash
= evsesConfigHash
1634 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1635 if (templateMaxEvses
> 0) {
1636 for (const evseKey
in stationTemplate
.Evses
) {
1637 const evseId
= convertToInt(evseKey
)
1638 this.evses
.set(evseId
, {
1639 connectors
: buildConnectorsMap(
1640 stationTemplate
.Evses
[evseKey
].Connectors
,
1644 availability
: AvailabilityType
.Operative
1646 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1647 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1649 this.saveEvsesStatus()
1652 `${this.logPrefix()} Charging station information from template ${
1654 } with no evses configuration defined, cannot create evses`
1660 `${this.logPrefix()} Charging station information from template ${
1662 } with no evses configuration defined, using already defined evses`
1667 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1668 let configuration
: ChargingStationConfiguration
| undefined
1669 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1671 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1672 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1673 this.configurationFileHash
1676 const measureId
= `${FileType.ChargingStationConfiguration} read`
1677 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1678 configuration
= JSON
.parse(
1679 readFileSync(this.configurationFile
, 'utf8')
1680 ) as ChargingStationConfiguration
1681 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1682 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1683 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1684 this.configurationFileHash
= configuration
.configurationHash
!
1687 handleFileException(
1688 this.configurationFile
,
1689 FileType
.ChargingStationConfiguration
,
1690 error
as NodeJS
.ErrnoException
,
1695 return configuration
1698 private saveAutomaticTransactionGeneratorConfiguration (): void {
1699 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1700 this.saveConfiguration()
1704 private saveConnectorsStatus (): void {
1705 this.saveConfiguration()
1708 private saveEvsesStatus (): void {
1709 this.saveConfiguration()
1712 private saveConfiguration (): void {
1713 if (isNotEmptyString(this.configurationFile
)) {
1715 if (!existsSync(dirname(this.configurationFile
))) {
1716 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1718 const configurationFromFile
= this.getConfigurationFromFile()
1719 let configurationData
: ChargingStationConfiguration
=
1720 configurationFromFile
!= null
1721 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1723 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1724 configurationData
.stationInfo
= this.stationInfo
1726 delete configurationData
.stationInfo
1729 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1730 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1732 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1734 delete configurationData
.configurationKey
1736 configurationData
= merge
<ChargingStationConfiguration
>(
1738 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1740 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1741 delete configurationData
.automaticTransactionGenerator
1743 if (this.connectors
.size
> 0) {
1744 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1746 delete configurationData
.connectorsStatus
1748 if (this.evses
.size
> 0) {
1749 configurationData
.evsesStatus
= buildEvsesStatus(this)
1751 delete configurationData
.evsesStatus
1753 delete configurationData
.configurationHash
1754 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1757 stationInfo
: configurationData
.stationInfo
,
1758 configurationKey
: configurationData
.configurationKey
,
1759 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1760 ...(this.connectors
.size
> 0 && {
1761 connectorsStatus
: configurationData
.connectorsStatus
1763 ...(this.evses
.size
> 0 && { evsesStatus
: configurationData
.evsesStatus
})
1764 } satisfies ChargingStationConfiguration
)
1767 if (this.configurationFileHash
!== configurationHash
) {
1768 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1769 configurationData
.configurationHash
= configurationHash
1770 const measureId
= `${FileType.ChargingStationConfiguration} write`
1771 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1773 this.configurationFile
,
1774 JSON
.stringify(configurationData
, undefined, 2),
1777 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1778 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1779 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1780 this.configurationFileHash
= configurationHash
1782 handleFileException(
1783 this.configurationFile
,
1784 FileType
.ChargingStationConfiguration
,
1785 error
as NodeJS
.ErrnoException
,
1791 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1792 this.configurationFile
1797 handleFileException(
1798 this.configurationFile
,
1799 FileType
.ChargingStationConfiguration
,
1800 error
as NodeJS
.ErrnoException
,
1806 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1811 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1812 return this.getTemplateFromFile()?.Configuration
1815 private getOcppConfigurationFromFile (
1816 ocppPersistentConfiguration
?: boolean
1817 ): ChargingStationOcppConfiguration
| undefined {
1818 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1819 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1820 return { configurationKey
}
1825 private getOcppConfiguration (
1826 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1827 ): ChargingStationOcppConfiguration
| undefined {
1828 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1829 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1830 if (ocppConfiguration
== null) {
1831 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1833 return ocppConfiguration
1836 private async onOpen (): Promise
<void> {
1837 if (this.isWebSocketConnectionOpened()) {
1839 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
1841 let registrationRetryCount
= 0
1842 if (!this.isRegistered()) {
1843 // Send BootNotification
1845 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1846 BootNotificationRequest
,
1847 BootNotificationResponse
1848 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1849 skipBufferingOnError
: true
1851 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1852 if (this.bootNotificationResponse
?.currentTime
!= null) {
1853 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1854 this.bootNotificationResponse
.currentTime
= convertToDate(
1855 this.bootNotificationResponse
.currentTime
1858 if (!this.isRegistered()) {
1859 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1861 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1862 this.bootNotificationResponse
?.interval
!= null
1863 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1864 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1868 !this.isRegistered() &&
1869 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1870 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1871 this.stationInfo
?.registrationMaxRetries
=== -1)
1874 if (this.isRegistered()) {
1875 this.emit(ChargingStationEvents
.registered
)
1876 if (this.inAcceptedState()) {
1877 this.emit(ChargingStationEvents
.accepted
)
1880 if (this.inRejectedState()) {
1881 this.emit(ChargingStationEvents
.rejected
)
1884 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1885 this.stationInfo?.registrationMaxRetries
1889 this.wsConnectionRetryCount
= 0
1890 this.emit(ChargingStationEvents
.updated
)
1893 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1898 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1899 this.emit(ChargingStationEvents
.disconnected
)
1902 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1903 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1905 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1907 )}' and reason '${reason.toString()}'`
1909 this.wsConnectionRetryCount
= 0
1914 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1916 )}' and reason '${reason.toString()}'`
1919 this.reconnect().catch(error
=>
1920 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1924 this.emit(ChargingStationEvents
.updated
)
1927 private getCachedRequest (
1928 messageType
: MessageType
| undefined,
1930 ): CachedRequest
| undefined {
1931 const cachedRequest
= this.requests
.get(messageId
)
1932 if (Array.isArray(cachedRequest
)) {
1933 return cachedRequest
1935 throw new OCPPError(
1936 ErrorType
.PROTOCOL_ERROR
,
1937 `Cached request for message id ${messageId} ${getMessageTypeString(
1939 )} is not an array`,
1945 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1946 const [messageType
, messageId
, commandName
, commandPayload
] = request
1947 if (this.stationInfo
?.enableStatistics
=== true) {
1948 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1951 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1955 // Process the message
1956 await this.ocppIncomingRequestService
.incomingRequestHandler(
1962 this.emit(ChargingStationEvents
.updated
)
1965 private handleResponseMessage (response
: Response
): void {
1966 const [messageType
, messageId
, commandPayload
] = response
1967 if (!this.requests
.has(messageId
)) {
1969 throw new OCPPError(
1970 ErrorType
.INTERNAL_ERROR
,
1971 `Response for unknown message id ${messageId}`,
1977 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1978 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1983 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1987 responseCallback(commandPayload
, requestPayload
)
1990 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
1991 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
1992 if (!this.requests
.has(messageId
)) {
1994 throw new OCPPError(
1995 ErrorType
.INTERNAL_ERROR
,
1996 `Error response for unknown message id ${messageId}`,
1998 { errorType
, errorMessage
, errorDetails
}
2001 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2002 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2004 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
2008 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
2011 private async onMessage (data
: RawData
): Promise
<void> {
2012 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
2013 let messageType
: MessageType
| undefined
2014 let errorMsg
: string
2016 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2017 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
2018 if (Array.isArray(request
)) {
2019 [messageType
] = request
2020 // Check the type of message
2021 switch (messageType
) {
2023 case MessageType
.CALL_MESSAGE
:
2024 await this.handleIncomingMessage(request
as IncomingRequest
)
2027 case MessageType
.CALL_RESULT_MESSAGE
:
2028 this.handleResponseMessage(request
as Response
)
2031 case MessageType
.CALL_ERROR_MESSAGE
:
2032 this.handleErrorMessage(request
as ErrorResponse
)
2036 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2037 errorMsg
= `Wrong message type ${messageType}`
2038 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2039 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2042 throw new OCPPError(
2043 ErrorType
.PROTOCOL_ERROR
,
2044 'Incoming message is not an array',
2052 if (!Array.isArray(request
)) {
2053 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2056 let commandName
: IncomingRequestCommand
| undefined
2057 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2058 let errorCallback
: ErrorCallback
2059 const [, messageId
] = request
2060 switch (messageType
) {
2061 case MessageType
.CALL_MESSAGE
:
2062 [, , commandName
] = request
as IncomingRequest
2064 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2066 case MessageType
.CALL_RESULT_MESSAGE
:
2067 case MessageType
.CALL_ERROR_MESSAGE
:
2068 if (this.requests
.has(messageId
)) {
2069 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2070 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2071 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2072 errorCallback(error
as OCPPError
, false)
2074 // Remove the request from the cache in case of error at response handling
2075 this.requests
.delete(messageId
)
2079 if (!(error
instanceof OCPPError
)) {
2081 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2082 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2083 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2084 }' message '${data.toString()}' handling is not an OCPPError:`,
2089 `${this.logPrefix()} Incoming OCPP command '${
2090 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2091 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2092 }' message '${data.toString()}'${
2093 this.requests.has(messageId)
2094 ? ` matching cached request
'${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
2096 } processing error:`,
2102 private onPing (): void {
2103 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2106 private onPong (): void {
2107 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2110 private onError (error
: WSError
): void {
2111 this.closeWSConnection()
2112 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2115 private getEnergyActiveImportRegister (
2116 connectorStatus
: ConnectorStatus
| undefined,
2119 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2122 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2123 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2125 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2130 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2131 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2133 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2137 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2138 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2139 return stationTemplate
?.useConnectorId0
?? Constants
.DEFAULT_STATION_INFO
.useConnectorId0
!
2142 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2143 if (this.hasEvses
) {
2144 for (const [evseId
, evseStatus
] of this.evses
) {
2148 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2149 if (connectorStatus
.transactionStarted
=== true) {
2150 await this.stopTransactionOnConnector(connectorId
, reason
)
2155 for (const connectorId
of this.connectors
.keys()) {
2156 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2157 await this.stopTransactionOnConnector(connectorId
, reason
)
2164 private getConnectionTimeout (): number {
2165 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2166 return convertToInt(
2167 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2168 Constants
.DEFAULT_CONNECTION_TIMEOUT
2171 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2174 private getPowerDivider (): number {
2175 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2176 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2177 powerDivider
= this.getNumberOfRunningTransactions()
2182 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2183 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2184 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2185 switch (this.getCurrentOutType(stationInfo
)) {
2186 case CurrentType
.AC
:
2187 return ACElectricUtils
.amperagePerPhaseFromPower(
2188 this.getNumberOfPhases(stationInfo
),
2189 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2190 this.getVoltageOut(stationInfo
)
2192 case CurrentType
.DC
:
2193 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2197 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2199 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2200 (stationInfo
?? this.stationInfo
!).currentOutType
??
2201 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2202 Constants
.DEFAULT_STATION_INFO
.currentOutType
!
2206 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2208 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2209 (stationInfo
?? this.stationInfo
!).voltageOut
??
2210 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2214 private getAmperageLimitation (): number | undefined {
2216 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2217 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2220 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2221 getAmperageLimitationUnitDivider(this.stationInfo
)
2226 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2227 if (this.stationInfo
?.autoRegister
=== true) {
2228 await this.ocppRequestService
.requestHandler
<
2229 BootNotificationRequest
,
2230 BootNotificationResponse
2231 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2232 skipBufferingOnError
: true
2235 // Start WebSocket ping
2236 this.startWebSocketPing()
2238 this.startHeartbeat()
2239 // Initialize connectors status
2240 if (this.hasEvses
) {
2241 for (const [evseId
, evseStatus
] of this.evses
) {
2243 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2244 const connectorBootStatus
= getBootConnectorStatus(this, connectorId
, connectorStatus
)
2245 await sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
, evseId
)
2250 for (const connectorId
of this.connectors
.keys()) {
2251 if (connectorId
> 0) {
2252 const connectorBootStatus
= getBootConnectorStatus(
2255 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2256 this.getConnectorStatus(connectorId
)!
2258 await sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
)
2262 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2263 await this.ocppRequestService
.requestHandler
<
2264 FirmwareStatusNotificationRequest
,
2265 FirmwareStatusNotificationResponse
2266 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2267 status: FirmwareStatus
.Installed
2269 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2273 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2274 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2276 this.flushMessageBuffer()
2279 private internalStopMessageSequence (): void {
2280 // Stop WebSocket ping
2281 this.stopWebSocketPing()
2283 this.stopHeartbeat()
2285 if (this.automaticTransactionGenerator
?.started
=== true) {
2286 this.stopAutomaticTransactionGenerator()
2290 private async stopMessageSequence (
2291 reason
?: StopTransactionReason
,
2292 stopTransactions
?: boolean
2294 this.internalStopMessageSequence()
2295 // Stop ongoing transactions
2296 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2297 if (this.hasEvses
) {
2298 for (const [evseId
, evseStatus
] of this.evses
) {
2300 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2301 await sendAndSetConnectorStatus(
2304 ConnectorStatusEnum
.Unavailable
,
2307 delete connectorStatus
.status
2312 for (const connectorId
of this.connectors
.keys()) {
2313 if (connectorId
> 0) {
2314 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2315 delete this.getConnectorStatus(connectorId
)?.status
2321 private startWebSocketPing (): void {
2322 const webSocketPingInterval
=
2323 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2325 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
2328 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2329 this.wsPingSetInterval
= setInterval(() => {
2330 if (this.isWebSocketConnectionOpened()) {
2331 this.wsConnection
?.ping()
2333 }, secondsToMilliseconds(webSocketPingInterval
))
2335 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2336 webSocketPingInterval
2339 } else if (this.wsPingSetInterval
!= null) {
2341 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2342 webSocketPingInterval
2347 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2352 private stopWebSocketPing (): void {
2353 if (this.wsPingSetInterval
!= null) {
2354 clearInterval(this.wsPingSetInterval
)
2355 delete this.wsPingSetInterval
2359 private getConfiguredSupervisionUrl (): URL
{
2360 let configuredSupervisionUrl
: string
2361 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2362 if (isNotEmptyArray(supervisionUrls
)) {
2363 let configuredSupervisionUrlIndex
: number
2364 switch (Configuration
.getSupervisionUrlDistribution()) {
2365 case SupervisionUrlDistribution
.RANDOM
:
2366 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2368 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2369 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2371 !Object.values(SupervisionUrlDistribution
).includes(
2372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2373 Configuration
.getSupervisionUrlDistribution()!
2376 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2377 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2378 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2381 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2384 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2386 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2387 configuredSupervisionUrl
= supervisionUrls
!
2389 if (isNotEmptyString(configuredSupervisionUrl
)) {
2390 return new URL(configuredSupervisionUrl
)
2392 const errorMsg
= 'No supervision url(s) configured'
2393 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2394 throw new BaseError(errorMsg
)
2397 private stopHeartbeat (): void {
2398 if (this.heartbeatSetInterval
!= null) {
2399 clearInterval(this.heartbeatSetInterval
)
2400 delete this.heartbeatSetInterval
2404 private terminateWSConnection (): void {
2405 if (this.isWebSocketConnectionOpened()) {
2406 this.wsConnection
?.terminate()
2407 this.wsConnection
= null
2411 private async reconnect (): Promise
<void> {
2413 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2414 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2415 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2417 this.wsConnectionRetried
= true
2418 ++this.wsConnectionRetryCount
2419 const reconnectDelay
=
2420 this.stationInfo
?.reconnectExponentialDelay
=== true
2421 ? exponentialDelay(this.wsConnectionRetryCount
)
2422 : secondsToMilliseconds(this.getConnectionTimeout())
2423 const reconnectDelayWithdraw
= 1000
2424 const reconnectTimeout
=
2425 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2427 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2430 )}ms, timeout ${reconnectTimeout}ms`
2432 await sleep(reconnectDelay
)
2434 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2436 this.openWSConnection(
2438 handshakeTimeout
: reconnectTimeout
2440 { closeOpened
: true }
2442 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2444 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2445 this.wsConnectionRetryCount
2446 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`