1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { createHash
, randomInt
} from
'node:crypto'
4 import { EventEmitter
} from
'node:events'
5 import { existsSync
, type FSWatcher
, mkdirSync
, readFileSync
, rmSync
, writeFileSync
} from
'node:fs'
6 import { dirname
, join
} 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 { mergeDeepRight
, once
} from
'rambda'
12 import { type RawData
, WebSocket
} from
'ws'
14 import { BaseError
, OCPPError
} from
'../exception/index.js'
15 import { PerformanceStatistics
} from
'../performance/index.js'
17 type AutomaticTransactionGeneratorConfiguration
,
19 type BootNotificationRequest
,
20 type BootNotificationResponse
,
22 type ChargingStationConfiguration
,
23 ChargingStationEvents
,
24 type ChargingStationInfo
,
25 type ChargingStationOcppConfiguration
,
26 type ChargingStationOptions
,
27 type ChargingStationTemplate
,
35 type EvseStatusConfiguration
,
38 type FirmwareStatusNotificationRequest
,
39 type FirmwareStatusNotificationResponse
,
40 type HeartbeatRequest
,
41 type HeartbeatResponse
,
43 type IncomingRequestCommand
,
46 type MeterValuesRequest
,
47 type MeterValuesResponse
,
51 RegistrationStatusEnumType
,
55 ReservationTerminationReason
,
57 StandardParametersKey
,
59 type StopTransactionReason
,
60 type StopTransactionRequest
,
61 type StopTransactionResponse
,
62 SupervisionUrlDistribution
,
63 SupportedFeatureProfiles
,
65 WebSocketCloseEventStatusCode
,
68 } from
'../types/index.js'
74 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
75 buildConnectorsStatus
,
89 formatDurationMilliSeconds
,
90 formatDurationSeconds
,
91 getWebSocketCloseEventStatusString
,
102 } from
'../utils/index.js'
103 import { AutomaticTransactionGenerator
} from
'./AutomaticTransactionGenerator.js'
104 import { ChargingStationWorkerBroadcastChannel
} from
'./broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
107 deleteConfigurationKey
,
109 setConfigurationKeyValue
110 } from
'./ConfigurationKeyUtils.js'
114 checkChargingStation
,
116 checkConnectorsConfiguration
,
117 checkStationInfoConnectorStatus
,
119 createBootNotificationRequest
,
121 getAmperageLimitationUnitDivider
,
122 getBootConnectorStatus
,
123 getChargingStationConnectorChargingProfilesPowerLimit
,
124 getChargingStationId
,
125 getDefaultVoltageOut
,
129 getNumberOfReservableConnectors
,
130 getPhaseRotationValue
,
132 hasReservationExpired
,
133 initializeConnectorsMapStatus
,
134 propagateSerialNumber
,
135 setChargingStationOptions
,
136 stationTemplateToStationInfo
,
137 warnTemplateKeysDeprecation
138 } from
'./Helpers.js'
139 import { IdTagsCache
} from
'./IdTagsCache.js'
142 buildTransactionEndMeterValue
,
143 getMessageTypeString
,
144 OCPP16IncomingRequestService
,
145 OCPP16RequestService
,
146 OCPP16ResponseService
,
147 OCPP20IncomingRequestService
,
148 OCPP20RequestService
,
149 OCPP20ResponseService
,
150 type OCPPIncomingRequestService
,
151 type OCPPRequestService
,
152 sendAndSetConnectorStatus
153 } from
'./ocpp/index.js'
154 import { SharedLRUCache
} from
'./SharedLRUCache.js'
156 export class ChargingStation
extends EventEmitter
{
157 public readonly index
: number
158 public readonly templateFile
: string
159 public stationInfo
?: ChargingStationInfo
160 public started
: boolean
161 public starting
: boolean
162 public idTagsCache
: IdTagsCache
163 public automaticTransactionGenerator
?: AutomaticTransactionGenerator
164 public ocppConfiguration
?: ChargingStationOcppConfiguration
165 public wsConnection
: WebSocket
| null
166 public readonly connectors
: Map
<number, ConnectorStatus
>
167 public readonly evses
: Map
<number, EvseStatus
>
168 public readonly requests
: Map
<string, CachedRequest
>
169 public performanceStatistics
?: PerformanceStatistics
170 public heartbeatSetInterval
?: NodeJS
.Timeout
171 public ocppRequestService
!: OCPPRequestService
172 public bootNotificationRequest
?: BootNotificationRequest
173 public bootNotificationResponse
?: BootNotificationResponse
174 public powerDivider
?: number
175 private stopping
: boolean
176 private configurationFile
!: string
177 private configurationFileHash
!: string
178 private connectorsConfigurationHash
!: string
179 private evsesConfigurationHash
!: string
180 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
181 private ocppIncomingRequestService
!: OCPPIncomingRequestService
182 private readonly messageBuffer
: Set
<string>
183 private configuredSupervisionUrl
!: URL
184 private wsConnectionRetried
: boolean
185 private wsConnectionRetryCount
: number
186 private templateFileWatcher
?: FSWatcher
187 private templateFileHash
!: string
188 private readonly sharedLRUCache
: SharedLRUCache
189 private wsPingSetInterval
?: NodeJS
.Timeout
190 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
191 private flushMessageBufferSetInterval
?: NodeJS
.Timeout
193 constructor (index
: number, templateFile
: string, options
?: ChargingStationOptions
) {
196 this.starting
= false
197 this.stopping
= false
198 this.wsConnection
= null
199 this.wsConnectionRetried
= false
200 this.wsConnectionRetryCount
= 0
202 this.templateFile
= templateFile
203 this.connectors
= new Map
<number, ConnectorStatus
>()
204 this.evses
= new Map
<number, EvseStatus
>()
205 this.requests
= new Map
<string, CachedRequest
>()
206 this.messageBuffer
= new Set
<string>()
207 this.sharedLRUCache
= SharedLRUCache
.getInstance()
208 this.idTagsCache
= IdTagsCache
.getInstance()
209 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this)
211 this.on(ChargingStationEvents
.added
, () => {
212 parentPort
?.postMessage(buildAddedMessage(this))
214 this.on(ChargingStationEvents
.deleted
, () => {
215 parentPort
?.postMessage(buildDeletedMessage(this))
217 this.on(ChargingStationEvents
.started
, () => {
218 parentPort
?.postMessage(buildStartedMessage(this))
220 this.on(ChargingStationEvents
.stopped
, () => {
221 parentPort
?.postMessage(buildStoppedMessage(this))
223 this.on(ChargingStationEvents
.updated
, () => {
224 parentPort
?.postMessage(buildUpdatedMessage(this))
226 this.on(ChargingStationEvents
.accepted
, () => {
227 this.startMessageSequence(
228 this.wsConnectionRetried
230 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
231 ).catch((error
: unknown
) => {
232 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
234 this.wsConnectionRetried
= false
236 this.on(ChargingStationEvents
.rejected
, () => {
237 this.wsConnectionRetried
= false
239 this.on(ChargingStationEvents
.connected
, () => {
240 if (this.wsPingSetInterval
== null) {
241 this.startWebSocketPing()
244 this.on(ChargingStationEvents
.disconnected
, () => {
246 this.internalStopMessageSequence()
249 `${this.logPrefix()} Error while stopping the internal message sequence:`,
255 this.initialize(options
)
259 if (this.stationInfo
?.autoStart
=== true) {
264 public get
hasEvses (): boolean {
265 return this.connectors
.size
=== 0 && this.evses
.size
> 0
268 public get
wsConnectionUrl (): URL
{
269 const wsConnectionBaseUrlStr
= `${
270 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
271 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
272 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
273 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
274 : this.configuredSupervisionUrl.href
277 `${wsConnectionBaseUrlStr}${
278 !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
279 }${this.stationInfo?.chargingStationId}`
283 public logPrefix
= (): string => {
285 this instanceof ChargingStation
&&
286 this.stationInfo
!= null &&
287 isNotEmptyString(this.stationInfo
.chargingStationId
)
289 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
291 let stationTemplate
: ChargingStationTemplate
| undefined
293 stationTemplate
= JSON
.parse(
294 readFileSync(this.templateFile
, 'utf8')
295 ) as ChargingStationTemplate
299 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
302 public hasIdTags (): boolean {
303 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
307 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
308 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
309 const localStationInfo
= stationInfo
?? this.stationInfo
!
310 switch (this.getCurrentOutType(stationInfo
)) {
312 return localStationInfo
.numberOfPhases
?? 3
318 public isWebSocketConnectionOpened (): boolean {
319 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
322 public inUnknownState (): boolean {
323 return this.bootNotificationResponse
?.status == null
326 public inPendingState (): boolean {
327 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
330 public inAcceptedState (): boolean {
331 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
334 public inRejectedState (): boolean {
335 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
338 public isRegistered (): boolean {
339 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
342 public isChargingStationAvailable (): boolean {
343 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
346 public hasConnector (connectorId
: number): boolean {
348 for (const evseStatus
of this.evses
.values()) {
349 if (evseStatus
.connectors
.has(connectorId
)) {
355 return this.connectors
.has(connectorId
)
358 public isConnectorAvailable (connectorId
: number): boolean {
361 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
365 public getNumberOfConnectors (): number {
367 let numberOfConnectors
= 0
368 for (const [evseId
, evseStatus
] of this.evses
) {
370 numberOfConnectors
+= evseStatus
.connectors
.size
373 return numberOfConnectors
375 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
378 public getNumberOfEvses (): number {
379 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
382 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
384 for (const evseStatus
of this.evses
.values()) {
385 if (evseStatus
.connectors
.has(connectorId
)) {
386 return evseStatus
.connectors
.get(connectorId
)
391 return this.connectors
.get(connectorId
)
394 public getConnectorMaximumAvailablePower (connectorId
: number): number {
395 let connectorAmperageLimitationPowerLimit
: number | undefined
396 const amperageLimitation
= this.getAmperageLimitation()
398 amperageLimitation
!= null &&
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
402 connectorAmperageLimitationPowerLimit
=
403 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
404 ? ACElectricUtils
.powerTotal(
405 this.getNumberOfPhases(),
406 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
407 this.stationInfo
.voltageOut
!,
409 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
411 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
412 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
413 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
417 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
418 const connectorChargingProfilesPowerLimit
=
419 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
421 isNaN(connectorMaximumPower
) ? Number.POSITIVE_INFINITY
: connectorMaximumPower
,
422 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
423 isNaN(connectorAmperageLimitationPowerLimit
!)
424 ? Number.POSITIVE_INFINITY
425 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426 connectorAmperageLimitationPowerLimit
!,
427 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
428 isNaN(connectorChargingProfilesPowerLimit
!)
429 ? Number.POSITIVE_INFINITY
430 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
431 connectorChargingProfilesPowerLimit
!
435 public getTransactionIdTag (transactionId
: number): string | undefined {
437 for (const evseStatus
of this.evses
.values()) {
438 for (const connectorStatus
of evseStatus
.connectors
.values()) {
439 if (connectorStatus
.transactionId
=== transactionId
) {
440 return connectorStatus
.transactionIdTag
445 for (const connectorId
of this.connectors
.keys()) {
446 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
447 return this.getConnectorStatus(connectorId
)?.transactionIdTag
453 public getNumberOfRunningTransactions (): number {
454 let numberOfRunningTransactions
= 0
456 for (const [evseId
, evseStatus
] of this.evses
) {
460 for (const connectorStatus
of evseStatus
.connectors
.values()) {
461 if (connectorStatus
.transactionStarted
=== true) {
462 ++numberOfRunningTransactions
467 for (const connectorId
of this.connectors
.keys()) {
468 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
469 ++numberOfRunningTransactions
473 return numberOfRunningTransactions
476 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
477 if (transactionId
== null) {
479 } else if (this.hasEvses
) {
480 for (const evseStatus
of this.evses
.values()) {
481 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
482 if (connectorStatus
.transactionId
=== transactionId
) {
488 for (const connectorId
of this.connectors
.keys()) {
489 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
496 public getEnergyActiveImportRegisterByTransactionId (
497 transactionId
: number | undefined,
500 return this.getEnergyActiveImportRegister(
501 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
502 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
507 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
508 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
511 public getAuthorizeRemoteTxRequests (): boolean {
512 const authorizeRemoteTxRequests
= getConfigurationKey(
514 StandardParametersKey
.AuthorizeRemoteTxRequests
516 return authorizeRemoteTxRequests
!= null
517 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
521 public getLocalAuthListEnabled (): boolean {
522 const localAuthListEnabled
= getConfigurationKey(
524 StandardParametersKey
.LocalAuthListEnabled
526 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
529 public getHeartbeatInterval (): number {
530 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
531 if (HeartbeatInterval
!= null) {
532 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
534 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
535 if (HeartBeatInterval
!= null) {
536 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
538 this.stationInfo
?.autoRegister
=== false &&
540 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
541 Constants.DEFAULT_HEARTBEAT_INTERVAL
544 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
547 public setSupervisionUrl (url
: string): void {
549 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
550 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
552 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
554 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
555 this.stationInfo
!.supervisionUrls
= url
556 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
557 this.saveStationInfo()
561 public startHeartbeat (): void {
562 const heartbeatInterval
= this.getHeartbeatInterval()
563 if (heartbeatInterval
> 0 && this.heartbeatSetInterval
== null) {
564 this.heartbeatSetInterval
= setInterval(() => {
565 this.ocppRequestService
566 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
567 .catch((error
: unknown
) => {
569 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
573 }, heartbeatInterval
)
575 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
579 } else if (this.heartbeatSetInterval
!= null) {
581 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
587 `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat`
592 public restartHeartbeat (): void {
596 this.startHeartbeat()
599 public restartWebSocketPing (): void {
600 // Stop WebSocket ping
601 this.stopWebSocketPing()
602 // Start WebSocket ping
603 this.startWebSocketPing()
606 public startMeterValues (connectorId
: number, interval
: number): void {
607 if (connectorId
=== 0) {
608 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
611 const connectorStatus
= this.getConnectorStatus(connectorId
)
612 if (connectorStatus
== null) {
614 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
619 if (connectorStatus
.transactionStarted
=== false) {
621 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
625 connectorStatus
.transactionStarted
=== true &&
626 connectorStatus
.transactionId
== null
629 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
634 connectorStatus
.transactionSetInterval
= setInterval(() => {
635 const meterValue
= buildMeterValue(
638 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
639 connectorStatus
.transactionId
!,
642 this.ocppRequestService
643 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
645 RequestCommand
.METER_VALUES
,
648 transactionId
: connectorStatus
.transactionId
,
649 meterValue
: [meterValue
]
652 .catch((error
: unknown
) => {
654 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
661 `${this.logPrefix()} Charging station ${
662 StandardParametersKey.MeterValueSampleInterval
663 } configuration set to ${interval}, not sending MeterValues`
668 public stopMeterValues (connectorId
: number): void {
669 const connectorStatus
= this.getConnectorStatus(connectorId
)
670 if (connectorStatus
?.transactionSetInterval
!= null) {
671 clearInterval(connectorStatus
.transactionSetInterval
)
675 private add (): void {
676 this.emit(ChargingStationEvents
.added
)
679 public async delete (deleteConfiguration
= true): Promise
<void> {
683 AutomaticTransactionGenerator
.deleteInstance(this)
684 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
685 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
686 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
687 this.requests
.clear()
688 this.connectors
.clear()
690 this.templateFileWatcher
?.unref()
691 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
692 this.chargingStationWorkerBroadcastChannel
.unref()
693 this.emit(ChargingStationEvents
.deleted
)
694 this.removeAllListeners()
697 public start (): void {
699 if (!this.starting
) {
701 if (this.stationInfo
?.enableStatistics
=== true) {
702 this.performanceStatistics
?.start()
704 this.openWSConnection()
705 // Monitor charging station template file
706 this.templateFileWatcher
= watchJsonFile(
708 FileType
.ChargingStationTemplate
,
711 (event
, filename
): void => {
712 if (isNotEmptyString(filename
) && event
=== 'change') {
715 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
717 } file have changed, reload`
719 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
720 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
721 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
725 const ATGStarted
= this.automaticTransactionGenerator
?.started
726 if (ATGStarted
=== true) {
727 this.stopAutomaticTransactionGenerator()
729 delete this.automaticTransactionGeneratorConfiguration
731 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
734 this.startAutomaticTransactionGenerator(undefined, true)
736 if (this.stationInfo
?.enableStatistics
=== true) {
737 this.performanceStatistics
?.restart()
739 this.performanceStatistics
?.stop()
741 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
744 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
752 this.emit(ChargingStationEvents
.started
)
753 this.starting
= false
755 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
758 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
763 reason
?: StopTransactionReason
,
764 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
767 if (!this.stopping
) {
769 await this.stopMessageSequence(reason
, stopTransactions
)
770 this.closeWSConnection()
771 if (this.stationInfo
?.enableStatistics
=== true) {
772 this.performanceStatistics
?.stop()
774 this.templateFileWatcher
?.close()
775 delete this.bootNotificationResponse
777 this.saveConfiguration()
778 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
779 this.emit(ChargingStationEvents
.stopped
)
780 this.stopping
= false
782 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
785 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
789 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
790 await this.stop(reason
)
791 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
792 await sleep(this.stationInfo
!.resetTime
!)
797 public saveOcppConfiguration (): void {
798 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
799 this.saveConfiguration()
803 public bufferMessage (message
: string): void {
804 this.messageBuffer
.add(message
)
805 this.setIntervalFlushMessageBuffer()
808 public openWSConnection (
810 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
813 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
814 ...this.stationInfo
?.wsOptions
,
817 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
818 if (!checkChargingStation(this, this.logPrefix())) {
821 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
822 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
824 if (params
.closeOpened
=== true) {
825 this.closeWSConnection()
827 if (params
.terminateOpened
=== true) {
828 this.terminateWSConnection()
831 if (this.isWebSocketConnectionOpened()) {
833 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
838 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
840 this.wsConnection
= new WebSocket(
841 this.wsConnectionUrl
,
842 `ocpp${this.stationInfo?.ocppVersion}`,
846 // Handle WebSocket message
847 this.wsConnection
.on('message', data
=> {
848 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
850 // Handle WebSocket error
851 this.wsConnection
.on('error', this.onError
.bind(this))
852 // Handle WebSocket close
853 this.wsConnection
.on('close', this.onClose
.bind(this))
854 // Handle WebSocket open
855 this.wsConnection
.on('open', () => {
856 this.onOpen().catch((error
: unknown
) =>
857 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
860 // Handle WebSocket ping
861 this.wsConnection
.on('ping', this.onPing
.bind(this))
862 // Handle WebSocket pong
863 this.wsConnection
.on('pong', this.onPong
.bind(this))
866 public closeWSConnection (): void {
867 if (this.isWebSocketConnectionOpened()) {
868 this.wsConnection
?.close()
869 this.wsConnection
= null
873 public getAutomaticTransactionGeneratorConfiguration ():
874 | AutomaticTransactionGeneratorConfiguration
876 if (this.automaticTransactionGeneratorConfiguration
== null) {
877 let automaticTransactionGeneratorConfiguration
:
878 | AutomaticTransactionGeneratorConfiguration
880 const stationTemplate
= this.getTemplateFromFile()
881 const stationConfiguration
= this.getConfigurationFromFile()
883 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
884 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
885 stationConfiguration
?.automaticTransactionGenerator
!= null
887 automaticTransactionGeneratorConfiguration
=
888 stationConfiguration
.automaticTransactionGenerator
890 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
892 this.automaticTransactionGeneratorConfiguration
= {
893 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
894 ...automaticTransactionGeneratorConfiguration
897 return this.automaticTransactionGeneratorConfiguration
900 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
901 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
904 public startAutomaticTransactionGenerator (
905 connectorIds
?: number[],
906 stopAbsoluteDuration
?: boolean
908 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
909 if (isNotEmptyArray(connectorIds
)) {
910 for (const connectorId
of connectorIds
) {
911 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
914 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
916 this.saveAutomaticTransactionGeneratorConfiguration()
917 this.emit(ChargingStationEvents
.updated
)
920 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
921 if (isNotEmptyArray(connectorIds
)) {
922 for (const connectorId
of connectorIds
) {
923 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
926 this.automaticTransactionGenerator
?.stop()
928 this.saveAutomaticTransactionGeneratorConfiguration()
929 this.emit(ChargingStationEvents
.updated
)
932 public async stopTransactionOnConnector (
934 reason
?: StopTransactionReason
935 ): Promise
<StopTransactionResponse
> {
936 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
938 this.stationInfo
?.beginEndMeterValues
=== true &&
939 this.stationInfo
.ocppStrictCompliance
=== true &&
940 this.stationInfo
.outOfOrderEndMeterValues
=== false
942 const transactionEndMeterValue
= buildTransactionEndMeterValue(
945 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
947 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
949 RequestCommand
.METER_VALUES
,
953 meterValue
: [transactionEndMeterValue
]
957 return await this.ocppRequestService
.requestHandler
<
958 Partial
<StopTransactionRequest
>,
959 StopTransactionResponse
960 >(this, RequestCommand
.STOP_TRANSACTION
, {
962 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
963 ...(reason
!= null && { reason
})
967 public getReserveConnectorZeroSupported (): boolean {
968 return convertToBoolean(
969 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
970 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
974 public async addReservation (reservation
: Reservation
): Promise
<void> {
975 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
976 if (reservationFound
!= null) {
977 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
979 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
980 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
981 await sendAndSetConnectorStatus(
983 reservation
.connectorId
,
984 ConnectorStatusEnum
.Reserved
,
986 { send
: reservation
.connectorId
!== 0 }
990 public async removeReservation (
991 reservation
: Reservation
,
992 reason
: ReservationTerminationReason
994 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
995 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
997 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
998 case ReservationTerminationReason
.TRANSACTION_STARTED
:
999 delete connector
.reservation
1001 case ReservationTerminationReason
.RESERVATION_CANCELED
:
1002 case ReservationTerminationReason
.REPLACE_EXISTING
:
1003 case ReservationTerminationReason
.EXPIRED
:
1004 await sendAndSetConnectorStatus(
1006 reservation
.connectorId
,
1007 ConnectorStatusEnum
.Available
,
1009 { send
: reservation
.connectorId
!== 0 }
1011 delete connector
.reservation
1014 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1015 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1019 public getReservationBy (
1020 filterKey
: ReservationKey
,
1021 value
: number | string
1022 ): Reservation
| undefined {
1023 if (this.hasEvses
) {
1024 for (const evseStatus
of this.evses
.values()) {
1025 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1026 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1027 return connectorStatus
.reservation
1032 for (const connectorStatus
of this.connectors
.values()) {
1033 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1034 return connectorStatus
.reservation
1040 public isConnectorReservable (
1041 reservationId
: number,
1043 connectorId
?: number
1045 const reservation
= this.getReservationBy('reservationId', reservationId
)
1046 const reservationExists
= reservation
!= null && !hasReservationExpired(reservation
)
1047 if (arguments.length
=== 1) {
1048 return !reservationExists
1049 } else if (arguments.length
> 1) {
1050 const userReservation
= idTag
!= null ? this.getReservationBy('idTag', idTag
) : undefined
1051 const userReservationExists
=
1052 userReservation
!= null && !hasReservationExpired(userReservation
)
1053 const notConnectorZero
= connectorId
== null ? true : connectorId
> 0
1054 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1056 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1062 private setIntervalFlushMessageBuffer (): void {
1063 if (this.flushMessageBufferSetInterval
== null) {
1064 this.flushMessageBufferSetInterval
= setInterval(() => {
1065 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1066 this.flushMessageBuffer()
1068 if (this.messageBuffer
.size
=== 0) {
1069 this.clearIntervalFlushMessageBuffer()
1071 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1075 private clearIntervalFlushMessageBuffer (): void {
1076 if (this.flushMessageBufferSetInterval
!= null) {
1077 clearInterval(this.flushMessageBufferSetInterval
)
1078 delete this.flushMessageBufferSetInterval
1082 private getNumberOfReservableConnectors (): number {
1083 let numberOfReservableConnectors
= 0
1084 if (this.hasEvses
) {
1085 for (const evseStatus
of this.evses
.values()) {
1086 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1089 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1091 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1094 private getNumberOfReservationsOnConnectorZero (): number {
1096 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1097 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1104 private flushMessageBuffer (): void {
1105 if (this.messageBuffer
.size
> 0) {
1106 for (const message
of this.messageBuffer
.values()) {
1107 let beginId
: string | undefined
1108 let commandName
: RequestCommand
| undefined
1109 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1110 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1112 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1113 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1115 this.wsConnection
?.send(message
, (error
?: Error) => {
1116 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1117 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1118 if (error
== null) {
1120 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1122 )} OCPP message sent '${JSON.stringify(message)}'`
1124 this.messageBuffer
.delete(message
)
1127 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1129 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1138 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1139 let template
: ChargingStationTemplate
| undefined
1141 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1142 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1144 const measureId
= `${FileType.ChargingStationTemplate} read`
1145 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1146 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1147 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1148 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1149 .update(JSON
.stringify(template
))
1151 this.sharedLRUCache
.setChargingStationTemplate(template
)
1152 this.templateFileHash
= template
.templateHash
1155 handleFileException(
1157 FileType
.ChargingStationTemplate
,
1158 error
as NodeJS
.ErrnoException
,
1165 private getStationInfoFromTemplate (): ChargingStationInfo
{
1166 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1167 const stationTemplate
= this.getTemplateFromFile()!
1168 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1169 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
)
1170 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1171 if (stationTemplate
.Connectors
!= null) {
1172 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1174 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1175 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1176 stationInfo
.templateIndex
= this.index
1177 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1178 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1179 createSerialNumber(stationTemplate
, stationInfo
)
1180 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1181 if (isNotEmptyArray(stationTemplate
.power
)) {
1182 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1183 stationInfo
.maximumPower
=
1184 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1185 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1186 : stationTemplate
.power
[powerArrayRandomIndex
]
1188 stationInfo
.maximumPower
=
1189 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1190 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1191 stationTemplate
.power
! * 1000
1192 : stationTemplate
.power
1194 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1196 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1197 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1198 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1201 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1203 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1206 if (stationTemplate
.resetTime
!= null) {
1207 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1212 private getStationInfoFromFile (
1213 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1214 .stationInfoPersistentConfiguration
1215 ): ChargingStationInfo
| undefined {
1216 let stationInfo
: ChargingStationInfo
| undefined
1217 if (stationInfoPersistentConfiguration
=== true) {
1218 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1219 if (stationInfo
!= null) {
1220 delete stationInfo
.infoHash
1221 delete (stationInfo
as ChargingStationTemplate
).numberOfConnectors
1222 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1223 if (stationInfo
.templateIndex
== null) {
1224 stationInfo
.templateIndex
= this.index
1226 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1227 if (stationInfo
.templateName
== null) {
1228 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1235 private getStationInfo (options
?: ChargingStationOptions
): ChargingStationInfo
{
1236 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1237 options
?.persistentConfiguration
!= null &&
1238 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
)
1239 const stationInfoFromFile
= this.getStationInfoFromFile(
1240 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1242 let stationInfo
: ChargingStationInfo
1244 // 1. charging station info from template
1245 // 2. charging station info from configuration file
1247 stationInfoFromFile
!= null &&
1248 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1250 stationInfo
= stationInfoFromFile
1252 stationInfo
= stationInfoFromTemplate
1253 stationInfoFromFile
!= null &&
1254 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile
, stationInfo
)
1256 return setChargingStationOptions(
1257 mergeDeepRight(Constants
.DEFAULT_STATION_INFO
, stationInfo
),
1262 private saveStationInfo (): void {
1263 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1264 this.saveConfiguration()
1268 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1269 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1270 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1271 throw new BaseError(errorMsg
)
1274 private initialize (options
?: ChargingStationOptions
): void {
1275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1276 const stationTemplate
= this.getTemplateFromFile()!
1277 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1278 this.configurationFile
= join(
1279 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1280 `${getHashId(this.index, stationTemplate)}.json`
1282 const stationConfiguration
= this.getConfigurationFromFile()
1284 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1285 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1287 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1288 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1290 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1292 this.stationInfo
= this.getStationInfo(options
)
1294 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1295 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1296 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1298 const patternGroup
=
1299 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1300 this.stationInfo
.firmwareVersion
.split('.').length
1301 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1302 .exec(this.stationInfo
.firmwareVersion
)
1303 ?.slice(1, patternGroup
+ 1)
1304 if (match
!= null) {
1305 const patchLevelIndex
= match
.length
- 1
1306 match
[patchLevelIndex
] = (
1307 convertToInt(match
[patchLevelIndex
]) +
1308 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1309 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1311 this.stationInfo
.firmwareVersion
= match
.join('.')
1314 this.saveStationInfo()
1315 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1316 if (this.stationInfo
.enableStatistics
=== true) {
1317 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1318 this.stationInfo
.hashId
,
1319 this.stationInfo
.chargingStationId
,
1320 this.configuredSupervisionUrl
1323 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1324 if (bootNotificationRequest
== null) {
1325 const errorMsg
= 'Error while creating boot notification request'
1326 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1327 throw new BaseError(errorMsg
)
1329 this.bootNotificationRequest
= bootNotificationRequest
1330 this.powerDivider
= this.getPowerDivider()
1331 // OCPP configuration
1332 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1333 this.initializeOcppConfiguration()
1334 this.initializeOcppServices()
1335 if (this.stationInfo
.autoRegister
=== true) {
1336 this.bootNotificationResponse
= {
1337 currentTime
: new Date(),
1338 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1339 status: RegistrationStatusEnumType
.ACCEPTED
1344 private initializeOcppServices (): void {
1345 const ocppVersion
= this.stationInfo
?.ocppVersion
1346 switch (ocppVersion
) {
1347 case OCPPVersion
.VERSION_16
:
1348 this.ocppIncomingRequestService
=
1349 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1350 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1351 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1354 case OCPPVersion
.VERSION_20
:
1355 case OCPPVersion
.VERSION_201
:
1356 this.ocppIncomingRequestService
=
1357 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1358 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1359 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1363 this.handleUnsupportedVersion(ocppVersion
)
1368 private initializeOcppConfiguration (): void {
1369 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1370 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1372 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1373 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', {
1378 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1379 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1380 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1382 addConfigurationKey(
1384 this.stationInfo
.supervisionUrlOcppKey
,
1385 this.configuredSupervisionUrl
.href
,
1389 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1390 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1391 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1393 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, {
1398 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1399 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1401 addConfigurationKey(
1403 this.stationInfo
.amperageLimitationOcppKey
,
1405 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1406 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1409 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1410 addConfigurationKey(
1412 StandardParametersKey
.SupportedFeatureProfiles
,
1413 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1416 addConfigurationKey(
1418 StandardParametersKey
.NumberOfConnectors
,
1419 this.getNumberOfConnectors().toString(),
1423 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1424 addConfigurationKey(
1426 StandardParametersKey
.MeterValuesSampledData
,
1427 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1430 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1431 const connectorsPhaseRotation
: string[] = []
1432 if (this.hasEvses
) {
1433 for (const evseStatus
of this.evses
.values()) {
1434 for (const connectorId
of evseStatus
.connectors
.keys()) {
1435 connectorsPhaseRotation
.push(
1436 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1437 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1442 for (const connectorId
of this.connectors
.keys()) {
1443 connectorsPhaseRotation
.push(
1444 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1445 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1449 addConfigurationKey(
1451 StandardParametersKey
.ConnectorPhaseRotation
,
1452 connectorsPhaseRotation
.toString()
1455 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1456 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1459 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1460 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1462 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1464 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1465 addConfigurationKey(
1467 StandardParametersKey
.ConnectionTimeOut
,
1468 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1471 this.saveOcppConfiguration()
1474 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1475 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1476 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1477 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1479 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1480 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1481 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1482 delete evseStatus
.connectorsStatus
1483 this.evses
.set(evseId
, {
1484 ...(evseStatus
as EvseStatus
),
1485 connectors
: new Map
<number, ConnectorStatus
>(
1486 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1487 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1494 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1495 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1496 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1497 throw new BaseError(errorMsg
)
1499 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1500 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1501 throw new BaseError(errorMsg
)
1505 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1506 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1507 this.initializeConnectorsFromTemplate(stationTemplate
)
1508 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1509 this.initializeEvsesFromTemplate(stationTemplate
)
1510 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1511 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1512 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1513 throw new BaseError(errorMsg
)
1515 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1516 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1517 throw new BaseError(errorMsg
)
1521 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1522 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1523 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1524 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1525 throw new BaseError(errorMsg
)
1527 if (stationTemplate
.Connectors
?.[0] == null) {
1529 `${this.logPrefix()} Charging station information from template ${
1531 } with no connector id 0 configuration`
1534 if (stationTemplate
.Connectors
!= null) {
1535 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1536 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1537 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1539 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1542 const connectorsConfigChanged
=
1543 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1544 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1545 connectorsConfigChanged
&& this.connectors
.clear()
1546 this.connectorsConfigurationHash
= connectorsConfigHash
1547 if (templateMaxConnectors
> 0) {
1548 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1550 connectorId
=== 0 &&
1551 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1552 (stationTemplate
.Connectors
[connectorId
] == null ||
1553 !this.getUseConnectorId0(stationTemplate
))
1557 const templateConnectorId
=
1558 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1559 ? randomInt(1, templateMaxAvailableConnectors
)
1561 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1562 checkStationInfoConnectorStatus(
1563 templateConnectorId
,
1568 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1570 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1571 this.saveConnectorsStatus()
1574 `${this.logPrefix()} Charging station information from template ${
1576 } with no connectors configuration defined, cannot create connectors`
1582 `${this.logPrefix()} Charging station information from template ${
1584 } with no connectors configuration defined, using already defined connectors`
1589 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1590 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1591 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1592 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1593 throw new BaseError(errorMsg
)
1595 if (stationTemplate
.Evses
?.[0] == null) {
1597 `${this.logPrefix()} Charging station information from template ${
1599 } with no evse id 0 configuration`
1602 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1604 `${this.logPrefix()} Charging station information from template ${
1606 } with evse id 0 with no connector id 0 configuration`
1609 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1611 `${this.logPrefix()} Charging station information from template ${
1613 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1616 if (stationTemplate
.Evses
!= null) {
1617 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1618 .update(JSON
.stringify(stationTemplate
.Evses
))
1620 const evsesConfigChanged
=
1621 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1622 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1623 evsesConfigChanged
&& this.evses
.clear()
1624 this.evsesConfigurationHash
= evsesConfigHash
1625 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1626 if (templateMaxEvses
> 0) {
1627 for (const evseKey
in stationTemplate
.Evses
) {
1628 const evseId
= convertToInt(evseKey
)
1629 this.evses
.set(evseId
, {
1630 connectors
: buildConnectorsMap(
1631 stationTemplate
.Evses
[evseKey
].Connectors
,
1635 availability
: AvailabilityType
.Operative
1637 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1638 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1640 this.saveEvsesStatus()
1643 `${this.logPrefix()} Charging station information from template ${
1645 } with no evses configuration defined, cannot create evses`
1651 `${this.logPrefix()} Charging station information from template ${
1653 } with no evses configuration defined, using already defined evses`
1658 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1659 let configuration
: ChargingStationConfiguration
| undefined
1660 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1662 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1663 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1664 this.configurationFileHash
1667 const measureId
= `${FileType.ChargingStationConfiguration} read`
1668 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1669 configuration
= JSON
.parse(
1670 readFileSync(this.configurationFile
, 'utf8')
1671 ) as ChargingStationConfiguration
1672 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1673 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1674 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1675 this.configurationFileHash
= configuration
.configurationHash
!
1678 handleFileException(
1679 this.configurationFile
,
1680 FileType
.ChargingStationConfiguration
,
1681 error
as NodeJS
.ErrnoException
,
1686 return configuration
1689 private saveAutomaticTransactionGeneratorConfiguration (): void {
1690 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1691 this.saveConfiguration()
1695 private saveConnectorsStatus (): void {
1696 this.saveConfiguration()
1699 private saveEvsesStatus (): void {
1700 this.saveConfiguration()
1703 private saveConfiguration (): void {
1704 if (isNotEmptyString(this.configurationFile
)) {
1706 if (!existsSync(dirname(this.configurationFile
))) {
1707 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1709 const configurationFromFile
= this.getConfigurationFromFile()
1710 let configurationData
: ChargingStationConfiguration
=
1711 configurationFromFile
!= null
1712 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1714 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1715 configurationData
.stationInfo
= this.stationInfo
1717 delete configurationData
.stationInfo
1720 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1721 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1723 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1725 delete configurationData
.configurationKey
1727 configurationData
= mergeDeepRight(
1729 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1731 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1732 delete configurationData
.automaticTransactionGenerator
1734 if (this.connectors
.size
> 0) {
1735 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1737 delete configurationData
.connectorsStatus
1739 if (this.evses
.size
> 0) {
1740 configurationData
.evsesStatus
= buildEvsesStatus(this)
1742 delete configurationData
.evsesStatus
1744 delete configurationData
.configurationHash
1745 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1748 stationInfo
: configurationData
.stationInfo
,
1749 configurationKey
: configurationData
.configurationKey
,
1750 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1751 ...(this.connectors
.size
> 0 && {
1752 connectorsStatus
: configurationData
.connectorsStatus
1754 ...(this.evses
.size
> 0 && {
1755 evsesStatus
: configurationData
.evsesStatus
1757 } satisfies ChargingStationConfiguration
)
1760 if (this.configurationFileHash
!== configurationHash
) {
1761 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1762 configurationData
.configurationHash
= configurationHash
1763 const measureId
= `${FileType.ChargingStationConfiguration} write`
1764 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1766 this.configurationFile
,
1767 JSON
.stringify(configurationData
, undefined, 2),
1770 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1771 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1772 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1773 this.configurationFileHash
= configurationHash
1774 }).catch((error
: unknown
) => {
1775 handleFileException(
1776 this.configurationFile
,
1777 FileType
.ChargingStationConfiguration
,
1778 error
as NodeJS
.ErrnoException
,
1784 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1785 this.configurationFile
1790 handleFileException(
1791 this.configurationFile
,
1792 FileType
.ChargingStationConfiguration
,
1793 error
as NodeJS
.ErrnoException
,
1799 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1804 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1805 return this.getTemplateFromFile()?.Configuration
1808 private getOcppConfigurationFromFile (
1809 ocppPersistentConfiguration
?: boolean
1810 ): ChargingStationOcppConfiguration
| undefined {
1811 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1812 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1813 return { configurationKey
}
1818 private getOcppConfiguration (
1819 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1820 ): ChargingStationOcppConfiguration
| undefined {
1821 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1822 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1823 if (ocppConfiguration
== null) {
1824 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1826 return ocppConfiguration
1829 private async onOpen (): Promise
<void> {
1830 if (this.isWebSocketConnectionOpened()) {
1831 this.emit(ChargingStationEvents
.connected
)
1832 this.emit(ChargingStationEvents
.updated
)
1834 `${this.logPrefix()} Connection to OCPP server through ${
1835 this.wsConnectionUrl.href
1838 let registrationRetryCount
= 0
1839 if (!this.isRegistered()) {
1840 // Send BootNotification
1842 // FIXME: duplicated assignment with the boot notification response handler
1843 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1844 BootNotificationRequest
,
1845 BootNotificationResponse
1846 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1847 skipBufferingOnError
: true
1849 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1850 if (this.bootNotificationResponse
?.currentTime
!= null) {
1851 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1852 this.bootNotificationResponse
.currentTime
= convertToDate(
1853 this.bootNotificationResponse
.currentTime
1856 if (!this.isRegistered()) {
1857 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1859 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1860 this.bootNotificationResponse
?.interval
!= null
1861 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1862 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1866 !this.isRegistered() &&
1867 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1868 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1869 this.stationInfo
?.registrationMaxRetries
=== -1)
1872 if (!this.isRegistered()) {
1874 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1875 this.stationInfo?.registrationMaxRetries
1879 this.wsConnectionRetryCount
= 0
1880 this.emit(ChargingStationEvents
.updated
)
1883 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1888 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1889 this.emit(ChargingStationEvents
.disconnected
)
1890 this.emit(ChargingStationEvents
.updated
)
1893 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1894 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1896 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1898 )}' and reason '${reason.toString()}'`
1900 this.wsConnectionRetryCount
= 0
1905 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1907 )}' and reason '${reason.toString()}'`
1912 this.emit(ChargingStationEvents
.updated
)
1914 .catch((error
: unknown
) =>
1915 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1921 private getCachedRequest (
1922 messageType
: MessageType
| undefined,
1924 ): CachedRequest
| undefined {
1925 const cachedRequest
= this.requests
.get(messageId
)
1926 if (Array.isArray(cachedRequest
)) {
1927 return cachedRequest
1929 throw new OCPPError(
1930 ErrorType
.PROTOCOL_ERROR
,
1931 `Cached request for message id ${messageId} ${getMessageTypeString(
1933 )} is not an array`,
1939 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1940 const [messageType
, messageId
, commandName
, commandPayload
] = request
1941 if (this.stationInfo
?.enableStatistics
=== true) {
1942 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1945 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1949 // Process the message
1950 await this.ocppIncomingRequestService
.incomingRequestHandler(
1956 this.emit(ChargingStationEvents
.updated
)
1959 private handleResponseMessage (response
: Response
): void {
1960 const [messageType
, messageId
, commandPayload
] = response
1961 if (!this.requests
.has(messageId
)) {
1963 throw new OCPPError(
1964 ErrorType
.INTERNAL_ERROR
,
1965 `Response for unknown message id ${messageId}`,
1971 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1972 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1977 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1981 responseCallback(commandPayload
, requestPayload
)
1984 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
1985 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
1986 if (!this.requests
.has(messageId
)) {
1988 throw new OCPPError(
1989 ErrorType
.INTERNAL_ERROR
,
1990 `Error response for unknown message id ${messageId}`,
1992 { errorType
, errorMessage
, errorDetails
}
1995 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1996 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
1998 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
2002 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
2005 private async onMessage (data
: RawData
): Promise
<void> {
2006 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
2007 let messageType
: MessageType
| undefined
2008 let errorMsg
: string
2010 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2011 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
2012 if (Array.isArray(request
)) {
2013 [messageType
] = request
2014 // Check the type of message
2015 switch (messageType
) {
2017 case MessageType
.CALL_MESSAGE
:
2018 await this.handleIncomingMessage(request
as IncomingRequest
)
2021 case MessageType
.CALL_RESULT_MESSAGE
:
2022 this.handleResponseMessage(request
as Response
)
2025 case MessageType
.CALL_ERROR_MESSAGE
:
2026 this.handleErrorMessage(request
as ErrorResponse
)
2030 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2031 errorMsg
= `Wrong message type ${messageType}`
2032 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2033 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2036 throw new OCPPError(
2037 ErrorType
.PROTOCOL_ERROR
,
2038 'Incoming message is not an array',
2046 if (!Array.isArray(request
)) {
2047 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2050 let commandName
: IncomingRequestCommand
| undefined
2051 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2052 let errorCallback
: ErrorCallback
2053 const [, messageId
] = request
2054 switch (messageType
) {
2055 case MessageType
.CALL_MESSAGE
:
2056 [, , commandName
] = request
as IncomingRequest
2058 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2060 case MessageType
.CALL_RESULT_MESSAGE
:
2061 case MessageType
.CALL_ERROR_MESSAGE
:
2062 if (this.requests
.has(messageId
)) {
2063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2064 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2065 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2066 errorCallback(error
as OCPPError
, false)
2068 // Remove the request from the cache in case of error at response handling
2069 this.requests
.delete(messageId
)
2073 if (!(error
instanceof OCPPError
)) {
2075 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2076 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2077 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2078 }' message '${data.toString()}' handling is not an OCPPError:`,
2083 `${this.logPrefix()} Incoming OCPP command '${
2084 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2085 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2086 }' message '${data.toString()}'${
2087 this.requests.has(messageId)
2088 ? ` matching cached request
'${JSON.stringify(
2089 this.getCachedRequest(messageType, messageId)
2092 } processing error:`,
2098 private onPing (): void {
2099 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2102 private onPong (): void {
2103 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2106 private onError (error
: WSError
): void {
2107 this.closeWSConnection()
2108 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2111 private getEnergyActiveImportRegister (
2112 connectorStatus
: ConnectorStatus
| undefined,
2115 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2118 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2119 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2121 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2126 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2127 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2129 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2133 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2134 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2135 return stationTemplate
?.useConnectorId0
?? Constants
.DEFAULT_STATION_INFO
.useConnectorId0
!
2138 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2139 if (this.hasEvses
) {
2140 for (const [evseId
, evseStatus
] of this.evses
) {
2144 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2145 if (connectorStatus
.transactionStarted
=== true) {
2146 await this.stopTransactionOnConnector(connectorId
, reason
)
2151 for (const connectorId
of this.connectors
.keys()) {
2152 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2153 await this.stopTransactionOnConnector(connectorId
, reason
)
2160 private getConnectionTimeout (): number {
2161 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2162 return convertToInt(
2163 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2164 Constants
.DEFAULT_CONNECTION_TIMEOUT
2167 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2170 private getPowerDivider (): number {
2171 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2172 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2173 powerDivider
= this.getNumberOfRunningTransactions()
2178 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2180 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2181 switch (this.getCurrentOutType(stationInfo
)) {
2182 case CurrentType
.AC
:
2183 return ACElectricUtils
.amperagePerPhaseFromPower(
2184 this.getNumberOfPhases(stationInfo
),
2185 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2186 this.getVoltageOut(stationInfo
)
2188 case CurrentType
.DC
:
2189 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2193 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2196 (stationInfo
?? this.stationInfo
!).currentOutType
??
2197 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2198 Constants
.DEFAULT_STATION_INFO
.currentOutType
!
2202 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2204 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2205 (stationInfo
?? this.stationInfo
!).voltageOut
??
2206 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2210 private getAmperageLimitation (): number | undefined {
2212 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2213 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2216 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2217 getAmperageLimitationUnitDivider(this.stationInfo
)
2222 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2223 if (this.stationInfo
?.autoRegister
=== true) {
2224 await this.ocppRequestService
.requestHandler
<
2225 BootNotificationRequest
,
2226 BootNotificationResponse
2227 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2228 skipBufferingOnError
: true
2231 // Start WebSocket ping
2232 if (this.wsPingSetInterval
== null) {
2233 this.startWebSocketPing()
2236 if (this.heartbeatSetInterval
== null) {
2237 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 await sendAndSetConnectorStatus(
2247 getBootConnectorStatus(this, connectorId
, connectorStatus
),
2254 for (const connectorId
of this.connectors
.keys()) {
2255 if (connectorId
> 0) {
2256 await sendAndSetConnectorStatus(
2259 getBootConnectorStatus(
2262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2263 this.getConnectorStatus(connectorId
)!
2269 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2270 await this.ocppRequestService
.requestHandler
<
2271 FirmwareStatusNotificationRequest
,
2272 FirmwareStatusNotificationResponse
2273 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2274 status: FirmwareStatus
.Installed
2276 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2280 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2281 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2283 this.flushMessageBuffer()
2286 private internalStopMessageSequence (): void {
2287 // Stop WebSocket ping
2288 this.stopWebSocketPing()
2290 this.stopHeartbeat()
2292 if (this.automaticTransactionGenerator
?.started
=== true) {
2293 this.stopAutomaticTransactionGenerator()
2297 private async stopMessageSequence (
2298 reason
?: StopTransactionReason
,
2299 stopTransactions
?: boolean
2301 this.internalStopMessageSequence()
2302 // Stop ongoing transactions
2303 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2304 if (this.hasEvses
) {
2305 for (const [evseId
, evseStatus
] of this.evses
) {
2307 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2308 await sendAndSetConnectorStatus(
2311 ConnectorStatusEnum
.Unavailable
,
2314 delete connectorStatus
.status
2319 for (const connectorId
of this.connectors
.keys()) {
2320 if (connectorId
> 0) {
2321 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2322 delete this.getConnectorStatus(connectorId
)?.status
2328 private getWebSocketPingInterval (): number {
2329 return getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2330 ? convertToInt(getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
)
2334 private startWebSocketPing (): void {
2335 const webSocketPingInterval
= this.getWebSocketPingInterval()
2336 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2337 this.wsPingSetInterval
= setInterval(() => {
2338 if (this.isWebSocketConnectionOpened()) {
2339 this.wsConnection
?.ping()
2341 }, secondsToMilliseconds(webSocketPingInterval
))
2343 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2344 webSocketPingInterval
2347 } else if (this.wsPingSetInterval
!= null) {
2349 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2350 webSocketPingInterval
2355 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2360 private stopWebSocketPing (): void {
2361 if (this.wsPingSetInterval
!= null) {
2362 clearInterval(this.wsPingSetInterval
)
2363 delete this.wsPingSetInterval
2367 private getConfiguredSupervisionUrl (): URL
{
2368 let configuredSupervisionUrl
: string
2369 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2370 if (isNotEmptyArray(supervisionUrls
)) {
2371 let configuredSupervisionUrlIndex
: number
2372 switch (Configuration
.getSupervisionUrlDistribution()) {
2373 case SupervisionUrlDistribution
.RANDOM
:
2374 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2376 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2377 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2379 !Object.values(SupervisionUrlDistribution
).includes(
2380 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2381 Configuration
.getSupervisionUrlDistribution()!
2384 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2385 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2386 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2389 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2392 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2395 configuredSupervisionUrl
= supervisionUrls
!
2397 if (isNotEmptyString(configuredSupervisionUrl
)) {
2398 return new URL(configuredSupervisionUrl
)
2400 const errorMsg
= 'No supervision url(s) configured'
2401 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2402 throw new BaseError(errorMsg
)
2405 private stopHeartbeat (): void {
2406 if (this.heartbeatSetInterval
!= null) {
2407 clearInterval(this.heartbeatSetInterval
)
2408 delete this.heartbeatSetInterval
2412 private terminateWSConnection (): void {
2413 if (this.isWebSocketConnectionOpened()) {
2414 this.wsConnection
?.terminate()
2415 this.wsConnection
= null
2419 private async reconnect (): Promise
<void> {
2421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2422 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2423 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2425 this.wsConnectionRetried
= true
2426 ++this.wsConnectionRetryCount
2427 const reconnectDelay
=
2428 this.stationInfo
?.reconnectExponentialDelay
=== true
2429 ? exponentialDelay(this.wsConnectionRetryCount
)
2430 : secondsToMilliseconds(this.getConnectionTimeout())
2431 const reconnectDelayWithdraw
= 1000
2432 const reconnectTimeout
=
2433 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2435 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2438 )}ms, timeout ${reconnectTimeout}ms`
2440 await sleep(reconnectDelay
)
2442 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2444 this.openWSConnection(
2446 handshakeTimeout
: reconnectTimeout
2448 { closeOpened
: true }
2450 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2452 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`