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 getChargingStationChargingProfilesLimit
,
124 getChargingStationId
,
125 getConnectorChargingProfilesLimit
,
126 getDefaultVoltageOut
,
130 getNumberOfReservableConnectors
,
131 getPhaseRotationValue
,
133 hasReservationExpired
,
134 initializeConnectorsMapStatus
,
135 prepareConnectorStatus
,
136 propagateSerialNumber
,
137 setChargingStationOptions
,
138 stationTemplateToStationInfo
,
139 warnTemplateKeysDeprecation
140 } from
'./Helpers.js'
141 import { IdTagsCache
} from
'./IdTagsCache.js'
144 buildTransactionEndMeterValue
,
145 getMessageTypeString
,
146 OCPP16IncomingRequestService
,
147 OCPP16RequestService
,
148 OCPP16ResponseService
,
149 OCPP20IncomingRequestService
,
150 OCPP20RequestService
,
151 OCPP20ResponseService
,
152 type OCPPIncomingRequestService
,
153 type OCPPRequestService
,
154 sendAndSetConnectorStatus
155 } from
'./ocpp/index.js'
156 import { SharedLRUCache
} from
'./SharedLRUCache.js'
158 export class ChargingStation
extends EventEmitter
{
159 public readonly index
: number
160 public readonly templateFile
: string
161 public stationInfo
?: ChargingStationInfo
162 public started
: boolean
163 public starting
: boolean
164 public idTagsCache
: IdTagsCache
165 public automaticTransactionGenerator
?: AutomaticTransactionGenerator
166 public ocppConfiguration
?: ChargingStationOcppConfiguration
167 public wsConnection
: WebSocket
| null
168 public readonly connectors
: Map
<number, ConnectorStatus
>
169 public readonly evses
: Map
<number, EvseStatus
>
170 public readonly requests
: Map
<string, CachedRequest
>
171 public performanceStatistics
?: PerformanceStatistics
172 public heartbeatSetInterval
?: NodeJS
.Timeout
173 public ocppRequestService
!: OCPPRequestService
174 public bootNotificationRequest
?: BootNotificationRequest
175 public bootNotificationResponse
?: BootNotificationResponse
176 public powerDivider
?: number
177 private stopping
: boolean
178 private configurationFile
!: string
179 private configurationFileHash
!: string
180 private connectorsConfigurationHash
!: string
181 private evsesConfigurationHash
!: string
182 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
183 private ocppIncomingRequestService
!: OCPPIncomingRequestService
184 private readonly messageBuffer
: Set
<string>
185 private configuredSupervisionUrl
!: URL
186 private wsConnectionRetryCount
: number
187 private templateFileWatcher
?: FSWatcher
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.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.wsConnectionRetryCount
> 0
230 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
231 ).catch((error
: unknown
) => {
232 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
234 this.wsConnectionRetryCount
= 0
236 this.on(ChargingStationEvents
.rejected
, () => {
237 this.wsConnectionRetryCount
= 0
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 connectorAmperageLimitationLimit
: 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 connectorAmperageLimitationLimit
=
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 chargingStationChargingProfilesLimit
=
419 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
420 getChargingStationChargingProfilesLimit(this)! / this.powerDivider
!
421 const connectorChargingProfilesLimit
= getConnectorChargingProfilesLimit(this, connectorId
)
423 isNaN(connectorMaximumPower
) ? Number.POSITIVE_INFINITY
: connectorMaximumPower
,
424 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
425 isNaN(connectorAmperageLimitationLimit
!)
426 ? Number.POSITIVE_INFINITY
427 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
428 connectorAmperageLimitationLimit
!,
429 isNaN(chargingStationChargingProfilesLimit
)
430 ? Number.POSITIVE_INFINITY
431 : chargingStationChargingProfilesLimit
,
432 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
433 isNaN(connectorChargingProfilesLimit
!)
434 ? Number.POSITIVE_INFINITY
435 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
436 connectorChargingProfilesLimit
!
440 public getTransactionIdTag (transactionId
: number): string | undefined {
442 for (const evseStatus
of this.evses
.values()) {
443 for (const connectorStatus
of evseStatus
.connectors
.values()) {
444 if (connectorStatus
.transactionId
=== transactionId
) {
445 return connectorStatus
.transactionIdTag
450 for (const connectorId
of this.connectors
.keys()) {
451 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
452 return this.getConnectorStatus(connectorId
)?.transactionIdTag
458 public getNumberOfRunningTransactions (): number {
459 let numberOfRunningTransactions
= 0
461 for (const [evseId
, evseStatus
] of this.evses
) {
465 for (const connectorStatus
of evseStatus
.connectors
.values()) {
466 if (connectorStatus
.transactionStarted
=== true) {
467 ++numberOfRunningTransactions
472 for (const connectorId
of this.connectors
.keys()) {
473 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
474 ++numberOfRunningTransactions
478 return numberOfRunningTransactions
481 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
482 if (transactionId
== null) {
484 } else if (this.hasEvses
) {
485 for (const evseStatus
of this.evses
.values()) {
486 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
487 if (connectorStatus
.transactionId
=== transactionId
) {
493 for (const connectorId
of this.connectors
.keys()) {
494 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
501 public getEnergyActiveImportRegisterByTransactionId (
502 transactionId
: number | undefined,
505 return this.getEnergyActiveImportRegister(
506 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
507 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
512 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
513 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
516 public getAuthorizeRemoteTxRequests (): boolean {
517 const authorizeRemoteTxRequests
= getConfigurationKey(
519 StandardParametersKey
.AuthorizeRemoteTxRequests
521 return authorizeRemoteTxRequests
!= null
522 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
526 public getLocalAuthListEnabled (): boolean {
527 const localAuthListEnabled
= getConfigurationKey(
529 StandardParametersKey
.LocalAuthListEnabled
531 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
534 public getHeartbeatInterval (): number {
535 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
536 if (HeartbeatInterval
!= null) {
537 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
539 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
540 if (HeartBeatInterval
!= null) {
541 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
543 this.stationInfo
?.autoRegister
=== false &&
545 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
546 Constants.DEFAULT_HEARTBEAT_INTERVAL
549 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
552 public setSupervisionUrl (url
: string): void {
554 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
555 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
557 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
559 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
560 this.stationInfo
!.supervisionUrls
= url
561 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
562 this.saveStationInfo()
566 public startHeartbeat (): void {
567 const heartbeatInterval
= this.getHeartbeatInterval()
568 if (heartbeatInterval
> 0 && this.heartbeatSetInterval
== null) {
569 this.heartbeatSetInterval
= setInterval(() => {
570 this.ocppRequestService
571 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
572 .catch((error
: unknown
) => {
574 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
578 }, heartbeatInterval
)
580 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
584 } else if (this.heartbeatSetInterval
!= null) {
586 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
592 `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat`
597 public restartHeartbeat (): void {
601 this.startHeartbeat()
604 public restartWebSocketPing (): void {
605 // Stop WebSocket ping
606 this.stopWebSocketPing()
607 // Start WebSocket ping
608 this.startWebSocketPing()
611 public startMeterValues (connectorId
: number, interval
: number): void {
612 if (connectorId
=== 0) {
613 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
616 const connectorStatus
= this.getConnectorStatus(connectorId
)
617 if (connectorStatus
== null) {
619 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
624 if (connectorStatus
.transactionStarted
=== false) {
626 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
630 connectorStatus
.transactionStarted
=== true &&
631 connectorStatus
.transactionId
== null
634 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
639 connectorStatus
.transactionSetInterval
= setInterval(() => {
640 const meterValue
= buildMeterValue(
643 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
644 connectorStatus
.transactionId
!,
647 this.ocppRequestService
648 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
650 RequestCommand
.METER_VALUES
,
653 transactionId
: connectorStatus
.transactionId
,
654 meterValue
: [meterValue
]
657 .catch((error
: unknown
) => {
659 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
666 `${this.logPrefix()} Charging station ${
667 StandardParametersKey.MeterValueSampleInterval
668 } configuration set to ${interval}, not sending MeterValues`
673 public stopMeterValues (connectorId
: number): void {
674 const connectorStatus
= this.getConnectorStatus(connectorId
)
675 if (connectorStatus
?.transactionSetInterval
!= null) {
676 clearInterval(connectorStatus
.transactionSetInterval
)
680 public restartMeterValues (connectorId
: number, interval
: number): void {
681 this.stopMeterValues(connectorId
)
682 this.startMeterValues(connectorId
, interval
)
685 private add (): void {
686 this.emit(ChargingStationEvents
.added
)
689 public async delete (deleteConfiguration
= true): Promise
<void> {
693 AutomaticTransactionGenerator
.deleteInstance(this)
694 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
695 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
696 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
697 this.requests
.clear()
698 this.connectors
.clear()
700 this.templateFileWatcher
?.unref()
701 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
702 this.chargingStationWorkerBroadcastChannel
.unref()
703 this.emit(ChargingStationEvents
.deleted
)
704 this.removeAllListeners()
707 public start (): void {
709 if (!this.starting
) {
711 if (this.stationInfo
?.enableStatistics
=== true) {
712 this.performanceStatistics
?.start()
714 this.openWSConnection()
715 // Monitor charging station template file
716 this.templateFileWatcher
= watchJsonFile(
718 FileType
.ChargingStationTemplate
,
721 (event
, filename
): void => {
722 if (isNotEmptyString(filename
) && event
=== 'change') {
725 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
727 } file have changed, reload`
729 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
730 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
731 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
735 const ATGStarted
= this.automaticTransactionGenerator
?.started
736 if (ATGStarted
=== true) {
737 this.stopAutomaticTransactionGenerator()
739 delete this.automaticTransactionGeneratorConfiguration
741 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
744 this.startAutomaticTransactionGenerator(undefined, true)
746 if (this.stationInfo
?.enableStatistics
=== true) {
747 this.performanceStatistics
?.restart()
749 this.performanceStatistics
?.stop()
751 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
754 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
762 this.emit(ChargingStationEvents
.started
)
763 this.starting
= false
765 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
768 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
773 reason
?: StopTransactionReason
,
774 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
777 if (!this.stopping
) {
779 await this.stopMessageSequence(reason
, stopTransactions
)
780 this.closeWSConnection()
781 if (this.stationInfo
?.enableStatistics
=== true) {
782 this.performanceStatistics
?.stop()
784 this.templateFileWatcher
?.close()
785 delete this.bootNotificationResponse
787 this.saveConfiguration()
788 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
789 this.emit(ChargingStationEvents
.stopped
)
790 this.stopping
= false
792 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
795 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
799 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
800 await this.stop(reason
)
801 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
802 await sleep(this.stationInfo
!.resetTime
!)
807 public saveOcppConfiguration (): void {
808 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
809 this.saveConfiguration()
813 public bufferMessage (message
: string): void {
814 this.messageBuffer
.add(message
)
815 this.setIntervalFlushMessageBuffer()
818 public openWSConnection (
820 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
823 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
824 ...this.stationInfo
?.wsOptions
,
827 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
828 if (!checkChargingStation(this, this.logPrefix())) {
831 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
832 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
834 if (params
.closeOpened
=== true) {
835 this.closeWSConnection()
837 if (params
.terminateOpened
=== true) {
838 this.terminateWSConnection()
841 if (this.isWebSocketConnectionOpened()) {
843 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
848 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
850 this.wsConnection
= new WebSocket(
851 this.wsConnectionUrl
,
852 `ocpp${this.stationInfo?.ocppVersion}`,
856 // Handle WebSocket message
857 this.wsConnection
.on('message', data
=> {
858 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
860 // Handle WebSocket error
861 this.wsConnection
.on('error', this.onError
.bind(this))
862 // Handle WebSocket close
863 this.wsConnection
.on('close', this.onClose
.bind(this))
864 // Handle WebSocket open
865 this.wsConnection
.on('open', () => {
866 this.onOpen().catch((error
: unknown
) =>
867 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
870 // Handle WebSocket ping
871 this.wsConnection
.on('ping', this.onPing
.bind(this))
872 // Handle WebSocket pong
873 this.wsConnection
.on('pong', this.onPong
.bind(this))
876 public closeWSConnection (): void {
877 if (this.isWebSocketConnectionOpened()) {
878 this.wsConnection
?.close()
879 this.wsConnection
= null
883 public getAutomaticTransactionGeneratorConfiguration ():
884 | AutomaticTransactionGeneratorConfiguration
886 if (this.automaticTransactionGeneratorConfiguration
== null) {
887 let automaticTransactionGeneratorConfiguration
:
888 | AutomaticTransactionGeneratorConfiguration
890 const stationTemplate
= this.getTemplateFromFile()
891 const stationConfiguration
= this.getConfigurationFromFile()
893 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
894 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
895 stationConfiguration
?.automaticTransactionGenerator
!= null
897 automaticTransactionGeneratorConfiguration
=
898 stationConfiguration
.automaticTransactionGenerator
900 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
902 this.automaticTransactionGeneratorConfiguration
= {
903 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
904 ...automaticTransactionGeneratorConfiguration
907 return this.automaticTransactionGeneratorConfiguration
910 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
911 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
914 public startAutomaticTransactionGenerator (
915 connectorIds
?: number[],
916 stopAbsoluteDuration
?: boolean
918 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
919 if (isNotEmptyArray(connectorIds
)) {
920 for (const connectorId
of connectorIds
) {
921 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
924 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
926 this.saveAutomaticTransactionGeneratorConfiguration()
927 this.emit(ChargingStationEvents
.updated
)
930 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
931 if (isNotEmptyArray(connectorIds
)) {
932 for (const connectorId
of connectorIds
) {
933 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
936 this.automaticTransactionGenerator
?.stop()
938 this.saveAutomaticTransactionGeneratorConfiguration()
939 this.emit(ChargingStationEvents
.updated
)
942 public async stopTransactionOnConnector (
944 reason
?: StopTransactionReason
945 ): Promise
<StopTransactionResponse
> {
946 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
948 this.stationInfo
?.beginEndMeterValues
=== true &&
949 this.stationInfo
.ocppStrictCompliance
=== true &&
950 this.stationInfo
.outOfOrderEndMeterValues
=== false
952 const transactionEndMeterValue
= buildTransactionEndMeterValue(
955 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
957 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
959 RequestCommand
.METER_VALUES
,
963 meterValue
: [transactionEndMeterValue
]
967 return await this.ocppRequestService
.requestHandler
<
968 Partial
<StopTransactionRequest
>,
969 StopTransactionResponse
970 >(this, RequestCommand
.STOP_TRANSACTION
, {
972 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
973 ...(reason
!= null && { reason
})
977 public getReserveConnectorZeroSupported (): boolean {
978 return convertToBoolean(
979 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
980 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
984 public async addReservation (reservation
: Reservation
): Promise
<void> {
985 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
986 if (reservationFound
!= null) {
987 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
989 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
990 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
991 await sendAndSetConnectorStatus(
993 reservation
.connectorId
,
994 ConnectorStatusEnum
.Reserved
,
996 { send
: reservation
.connectorId
!== 0 }
1000 public async removeReservation (
1001 reservation
: Reservation
,
1002 reason
: ReservationTerminationReason
1004 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1005 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
1007 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
1008 case ReservationTerminationReason
.TRANSACTION_STARTED
:
1009 delete connector
.reservation
1011 case ReservationTerminationReason
.RESERVATION_CANCELED
:
1012 case ReservationTerminationReason
.REPLACE_EXISTING
:
1013 case ReservationTerminationReason
.EXPIRED
:
1014 await sendAndSetConnectorStatus(
1016 reservation
.connectorId
,
1017 ConnectorStatusEnum
.Available
,
1019 { send
: reservation
.connectorId
!== 0 }
1021 delete connector
.reservation
1024 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1025 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1029 public getReservationBy (
1030 filterKey
: ReservationKey
,
1031 value
: number | string
1032 ): Reservation
| undefined {
1033 if (this.hasEvses
) {
1034 for (const evseStatus
of this.evses
.values()) {
1035 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1036 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1037 return connectorStatus
.reservation
1042 for (const connectorStatus
of this.connectors
.values()) {
1043 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1044 return connectorStatus
.reservation
1050 public isConnectorReservable (
1051 reservationId
: number,
1053 connectorId
?: number
1055 const reservation
= this.getReservationBy('reservationId', reservationId
)
1056 const reservationExists
= reservation
!= null && !hasReservationExpired(reservation
)
1057 if (arguments.length
=== 1) {
1058 return !reservationExists
1059 } else if (arguments.length
> 1) {
1060 const userReservation
= idTag
!= null ? this.getReservationBy('idTag', idTag
) : undefined
1061 const userReservationExists
=
1062 userReservation
!= null && !hasReservationExpired(userReservation
)
1063 const notConnectorZero
= connectorId
== null ? true : connectorId
> 0
1064 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1066 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1072 private setIntervalFlushMessageBuffer (): void {
1073 if (this.flushMessageBufferSetInterval
== null) {
1074 this.flushMessageBufferSetInterval
= setInterval(() => {
1075 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1076 this.flushMessageBuffer()
1078 if (this.messageBuffer
.size
=== 0) {
1079 this.clearIntervalFlushMessageBuffer()
1081 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1085 private clearIntervalFlushMessageBuffer (): void {
1086 if (this.flushMessageBufferSetInterval
!= null) {
1087 clearInterval(this.flushMessageBufferSetInterval
)
1088 delete this.flushMessageBufferSetInterval
1092 private getNumberOfReservableConnectors (): number {
1093 let numberOfReservableConnectors
= 0
1094 if (this.hasEvses
) {
1095 for (const evseStatus
of this.evses
.values()) {
1096 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1099 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1101 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1104 private getNumberOfReservationsOnConnectorZero (): number {
1106 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1107 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1114 private flushMessageBuffer (): void {
1115 if (this.messageBuffer
.size
> 0) {
1116 for (const message
of this.messageBuffer
.values()) {
1117 let beginId
: string | undefined
1118 let commandName
: RequestCommand
| undefined
1119 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1120 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1122 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1123 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1125 this.wsConnection
?.send(message
, (error
?: Error) => {
1126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1127 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1128 if (error
== null) {
1130 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1132 )} OCPP message sent '${JSON.stringify(message)}'`
1134 this.messageBuffer
.delete(message
)
1137 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1139 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1148 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1149 let template
: ChargingStationTemplate
| undefined
1151 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1152 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1154 const measureId
= `${FileType.ChargingStationTemplate} read`
1155 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1156 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1157 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1158 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1159 .update(JSON
.stringify(template
))
1161 this.sharedLRUCache
.setChargingStationTemplate(template
)
1162 this.templateFileHash
= template
.templateHash
1165 handleFileException(
1167 FileType
.ChargingStationTemplate
,
1168 error
as NodeJS
.ErrnoException
,
1175 private getStationInfoFromTemplate (): ChargingStationInfo
{
1176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1177 const stationTemplate
= this.getTemplateFromFile()!
1178 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1179 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
)
1180 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1181 if (stationTemplate
.Connectors
!= null) {
1182 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1184 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1185 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1186 stationInfo
.templateIndex
= this.index
1187 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1188 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1189 createSerialNumber(stationTemplate
, stationInfo
)
1190 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1191 if (isNotEmptyArray(stationTemplate
.power
)) {
1192 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1193 stationInfo
.maximumPower
=
1194 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1195 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1196 : stationTemplate
.power
[powerArrayRandomIndex
]
1198 stationInfo
.maximumPower
=
1199 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1200 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1201 stationTemplate
.power
! * 1000
1202 : stationTemplate
.power
1204 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1206 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1207 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1208 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1211 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1213 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1216 if (stationTemplate
.resetTime
!= null) {
1217 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1222 private getStationInfoFromFile (
1223 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1224 .stationInfoPersistentConfiguration
1225 ): ChargingStationInfo
| undefined {
1226 let stationInfo
: ChargingStationInfo
| undefined
1227 if (stationInfoPersistentConfiguration
=== true) {
1228 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1229 if (stationInfo
!= null) {
1230 delete stationInfo
.infoHash
1231 delete (stationInfo
as ChargingStationTemplate
).numberOfConnectors
1232 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1233 if (stationInfo
.templateIndex
== null) {
1234 stationInfo
.templateIndex
= this.index
1236 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1237 if (stationInfo
.templateName
== null) {
1238 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1245 private getStationInfo (options
?: ChargingStationOptions
): ChargingStationInfo
{
1246 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1247 options
?.persistentConfiguration
!= null &&
1248 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
)
1249 const stationInfoFromFile
= this.getStationInfoFromFile(
1250 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1252 let stationInfo
: ChargingStationInfo
1254 // 1. charging station info from template
1255 // 2. charging station info from configuration file
1257 stationInfoFromFile
!= null &&
1258 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1260 stationInfo
= stationInfoFromFile
1262 stationInfo
= stationInfoFromTemplate
1263 stationInfoFromFile
!= null &&
1264 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile
, stationInfo
)
1266 return setChargingStationOptions(
1267 mergeDeepRight(Constants
.DEFAULT_STATION_INFO
, stationInfo
),
1272 private saveStationInfo (): void {
1273 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1274 this.saveConfiguration()
1278 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1279 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1280 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1281 throw new BaseError(errorMsg
)
1284 private initialize (options
?: ChargingStationOptions
): void {
1285 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1286 const stationTemplate
= this.getTemplateFromFile()!
1287 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1288 this.configurationFile
= join(
1289 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1290 `${getHashId(this.index, stationTemplate)}.json`
1292 const stationConfiguration
= this.getConfigurationFromFile()
1294 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1295 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1297 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1298 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1300 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1302 this.stationInfo
= this.getStationInfo(options
)
1304 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1305 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1306 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1308 const patternGroup
=
1309 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1310 this.stationInfo
.firmwareVersion
.split('.').length
1311 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1312 .exec(this.stationInfo
.firmwareVersion
)
1313 ?.slice(1, patternGroup
+ 1)
1314 if (match
!= null) {
1315 const patchLevelIndex
= match
.length
- 1
1316 match
[patchLevelIndex
] = (
1317 convertToInt(match
[patchLevelIndex
]) +
1318 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1319 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1321 this.stationInfo
.firmwareVersion
= match
.join('.')
1324 this.saveStationInfo()
1325 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1326 if (this.stationInfo
.enableStatistics
=== true) {
1327 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1328 this.stationInfo
.hashId
,
1329 this.stationInfo
.chargingStationId
,
1330 this.configuredSupervisionUrl
1333 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1334 if (bootNotificationRequest
== null) {
1335 const errorMsg
= 'Error while creating boot notification request'
1336 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1337 throw new BaseError(errorMsg
)
1339 this.bootNotificationRequest
= bootNotificationRequest
1340 this.powerDivider
= this.getPowerDivider()
1341 // OCPP configuration
1342 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1343 this.initializeOcppConfiguration()
1344 this.initializeOcppServices()
1345 if (this.stationInfo
.autoRegister
=== true) {
1346 this.bootNotificationResponse
= {
1347 currentTime
: new Date(),
1348 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1349 status: RegistrationStatusEnumType
.ACCEPTED
1354 private initializeOcppServices (): void {
1355 const ocppVersion
= this.stationInfo
?.ocppVersion
1356 switch (ocppVersion
) {
1357 case OCPPVersion
.VERSION_16
:
1358 this.ocppIncomingRequestService
=
1359 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1360 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1361 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1364 case OCPPVersion
.VERSION_20
:
1365 case OCPPVersion
.VERSION_201
:
1366 this.ocppIncomingRequestService
=
1367 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1368 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1369 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1373 this.handleUnsupportedVersion(ocppVersion
)
1378 private initializeOcppConfiguration (): void {
1379 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1380 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1382 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1383 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', {
1388 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1389 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1390 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1392 addConfigurationKey(
1394 this.stationInfo
.supervisionUrlOcppKey
,
1395 this.configuredSupervisionUrl
.href
,
1399 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1400 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1401 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1403 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, {
1408 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1409 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1411 addConfigurationKey(
1413 this.stationInfo
.amperageLimitationOcppKey
,
1415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1416 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1419 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1420 addConfigurationKey(
1422 StandardParametersKey
.SupportedFeatureProfiles
,
1423 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1426 addConfigurationKey(
1428 StandardParametersKey
.NumberOfConnectors
,
1429 this.getNumberOfConnectors().toString(),
1433 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1434 addConfigurationKey(
1436 StandardParametersKey
.MeterValuesSampledData
,
1437 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1440 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1441 const connectorsPhaseRotation
: string[] = []
1442 if (this.hasEvses
) {
1443 for (const evseStatus
of this.evses
.values()) {
1444 for (const connectorId
of evseStatus
.connectors
.keys()) {
1445 connectorsPhaseRotation
.push(
1446 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1447 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1452 for (const connectorId
of this.connectors
.keys()) {
1453 connectorsPhaseRotation
.push(
1454 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1455 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1459 addConfigurationKey(
1461 StandardParametersKey
.ConnectorPhaseRotation
,
1462 connectorsPhaseRotation
.toString()
1465 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1466 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1469 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1470 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1472 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1474 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1475 addConfigurationKey(
1477 StandardParametersKey
.ConnectionTimeOut
,
1478 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1481 this.saveOcppConfiguration()
1484 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1485 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1486 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1487 this.connectors
.set(
1489 prepareConnectorStatus(clone
<ConnectorStatus
>(connectorStatus
))
1492 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1493 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1494 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1495 delete evseStatus
.connectorsStatus
1496 this.evses
.set(evseId
, {
1497 ...(evseStatus
as EvseStatus
),
1498 connectors
: new Map
<number, ConnectorStatus
>(
1499 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1500 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1502 prepareConnectorStatus(connectorStatus
)
1507 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1508 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1509 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1510 throw new BaseError(errorMsg
)
1512 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1513 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1514 throw new BaseError(errorMsg
)
1518 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1519 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1520 this.initializeConnectorsFromTemplate(stationTemplate
)
1521 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1522 this.initializeEvsesFromTemplate(stationTemplate
)
1523 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1524 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1525 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1526 throw new BaseError(errorMsg
)
1528 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1529 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1530 throw new BaseError(errorMsg
)
1534 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1535 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1536 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1537 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1538 throw new BaseError(errorMsg
)
1540 if (stationTemplate
.Connectors
?.[0] == null) {
1542 `${this.logPrefix()} Charging station information from template ${
1544 } with no connector id 0 configuration`
1547 if (stationTemplate
.Connectors
!= null) {
1548 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1549 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1550 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1552 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1555 const connectorsConfigChanged
=
1556 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1557 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1558 connectorsConfigChanged
&& this.connectors
.clear()
1559 this.connectorsConfigurationHash
= connectorsConfigHash
1560 if (templateMaxConnectors
> 0) {
1561 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1563 connectorId
=== 0 &&
1564 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1565 (stationTemplate
.Connectors
[connectorId
] == null ||
1566 !this.getUseConnectorId0(stationTemplate
))
1570 const templateConnectorId
=
1571 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1572 ? randomInt(1, templateMaxAvailableConnectors
)
1574 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1575 checkStationInfoConnectorStatus(
1576 templateConnectorId
,
1581 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1583 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1584 this.saveConnectorsStatus()
1587 `${this.logPrefix()} Charging station information from template ${
1589 } with no connectors configuration defined, cannot create connectors`
1595 `${this.logPrefix()} Charging station information from template ${
1597 } with no connectors configuration defined, using already defined connectors`
1602 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1603 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1604 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1605 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1606 throw new BaseError(errorMsg
)
1608 if (stationTemplate
.Evses
?.[0] == null) {
1610 `${this.logPrefix()} Charging station information from template ${
1612 } with no evse id 0 configuration`
1615 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1617 `${this.logPrefix()} Charging station information from template ${
1619 } with evse id 0 with no connector id 0 configuration`
1622 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1624 `${this.logPrefix()} Charging station information from template ${
1626 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1629 if (stationTemplate
.Evses
!= null) {
1630 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1631 .update(JSON
.stringify(stationTemplate
.Evses
))
1633 const evsesConfigChanged
=
1634 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1635 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1636 evsesConfigChanged
&& this.evses
.clear()
1637 this.evsesConfigurationHash
= evsesConfigHash
1638 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1639 if (templateMaxEvses
> 0) {
1640 for (const evseKey
in stationTemplate
.Evses
) {
1641 const evseId
= convertToInt(evseKey
)
1642 this.evses
.set(evseId
, {
1643 connectors
: buildConnectorsMap(
1644 stationTemplate
.Evses
[evseKey
].Connectors
,
1648 availability
: AvailabilityType
.Operative
1650 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1651 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1653 this.saveEvsesStatus()
1656 `${this.logPrefix()} Charging station information from template ${
1658 } with no evses configuration defined, cannot create evses`
1664 `${this.logPrefix()} Charging station information from template ${
1666 } with no evses configuration defined, using already defined evses`
1671 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1672 let configuration
: ChargingStationConfiguration
| undefined
1673 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1675 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1676 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1677 this.configurationFileHash
1680 const measureId
= `${FileType.ChargingStationConfiguration} read`
1681 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1682 configuration
= JSON
.parse(
1683 readFileSync(this.configurationFile
, 'utf8')
1684 ) as ChargingStationConfiguration
1685 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1686 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1687 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1688 this.configurationFileHash
= configuration
.configurationHash
!
1691 handleFileException(
1692 this.configurationFile
,
1693 FileType
.ChargingStationConfiguration
,
1694 error
as NodeJS
.ErrnoException
,
1699 return configuration
1702 private saveAutomaticTransactionGeneratorConfiguration (): void {
1703 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1704 this.saveConfiguration()
1708 private saveConnectorsStatus (): void {
1709 this.saveConfiguration()
1712 private saveEvsesStatus (): void {
1713 this.saveConfiguration()
1716 private saveConfiguration (): void {
1717 if (isNotEmptyString(this.configurationFile
)) {
1719 if (!existsSync(dirname(this.configurationFile
))) {
1720 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1722 const configurationFromFile
= this.getConfigurationFromFile()
1723 let configurationData
: ChargingStationConfiguration
=
1724 configurationFromFile
!= null
1725 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1727 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1728 configurationData
.stationInfo
= this.stationInfo
1730 delete configurationData
.stationInfo
1733 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1734 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1736 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1738 delete configurationData
.configurationKey
1740 configurationData
= mergeDeepRight(
1742 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1744 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1745 delete configurationData
.automaticTransactionGenerator
1747 if (this.connectors
.size
> 0) {
1748 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1750 delete configurationData
.connectorsStatus
1752 if (this.evses
.size
> 0) {
1753 configurationData
.evsesStatus
= buildEvsesStatus(this)
1755 delete configurationData
.evsesStatus
1757 delete configurationData
.configurationHash
1758 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1761 stationInfo
: configurationData
.stationInfo
,
1762 configurationKey
: configurationData
.configurationKey
,
1763 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1764 ...(this.connectors
.size
> 0 && {
1765 connectorsStatus
: configurationData
.connectorsStatus
1767 ...(this.evses
.size
> 0 && {
1768 evsesStatus
: configurationData
.evsesStatus
1770 } satisfies ChargingStationConfiguration
)
1773 if (this.configurationFileHash
!== configurationHash
) {
1774 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1775 configurationData
.configurationHash
= configurationHash
1776 const measureId
= `${FileType.ChargingStationConfiguration} write`
1777 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1779 this.configurationFile
,
1780 JSON
.stringify(configurationData
, undefined, 2),
1783 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1784 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1785 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1786 this.configurationFileHash
= configurationHash
1787 }).catch((error
: unknown
) => {
1788 handleFileException(
1789 this.configurationFile
,
1790 FileType
.ChargingStationConfiguration
,
1791 error
as NodeJS
.ErrnoException
,
1797 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1798 this.configurationFile
1803 handleFileException(
1804 this.configurationFile
,
1805 FileType
.ChargingStationConfiguration
,
1806 error
as NodeJS
.ErrnoException
,
1812 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1817 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1818 return this.getTemplateFromFile()?.Configuration
1821 private getOcppConfigurationFromFile (
1822 ocppPersistentConfiguration
?: boolean
1823 ): ChargingStationOcppConfiguration
| undefined {
1824 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1825 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1826 return { configurationKey
}
1831 private getOcppConfiguration (
1832 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1833 ): ChargingStationOcppConfiguration
| undefined {
1834 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1835 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1836 if (ocppConfiguration
== null) {
1837 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1839 return ocppConfiguration
1842 private async onOpen (): Promise
<void> {
1843 if (this.isWebSocketConnectionOpened()) {
1844 this.emit(ChargingStationEvents
.connected
)
1845 this.emit(ChargingStationEvents
.updated
)
1847 `${this.logPrefix()} Connection to OCPP server through ${
1848 this.wsConnectionUrl.href
1851 let registrationRetryCount
= 0
1852 if (!this.isRegistered()) {
1853 // Send BootNotification
1855 await this.ocppRequestService
.requestHandler
<
1856 BootNotificationRequest
,
1857 BootNotificationResponse
1858 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1859 skipBufferingOnError
: true
1861 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1862 this.bootNotificationResponse
!.currentTime
= convertToDate(
1863 this.bootNotificationResponse
?.currentTime
1865 if (!this.isRegistered()) {
1866 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1868 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1869 this.bootNotificationResponse
?.interval
!= null
1870 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1871 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1875 !this.isRegistered() &&
1876 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1877 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1878 this.stationInfo
?.registrationMaxRetries
=== -1)
1881 if (!this.isRegistered()) {
1883 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1884 this.stationInfo?.registrationMaxRetries
1888 this.emit(ChargingStationEvents
.updated
)
1891 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1896 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1897 this.emit(ChargingStationEvents
.disconnected
)
1898 this.emit(ChargingStationEvents
.updated
)
1901 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1902 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1904 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1906 )}' and reason '${reason.toString()}'`
1908 this.wsConnectionRetryCount
= 0
1913 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1915 )}' and reason '${reason.toString()}'`
1920 this.emit(ChargingStationEvents
.updated
)
1922 .catch((error
: unknown
) =>
1923 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1929 private getCachedRequest (
1930 messageType
: MessageType
| undefined,
1932 ): CachedRequest
| undefined {
1933 const cachedRequest
= this.requests
.get(messageId
)
1934 if (Array.isArray(cachedRequest
)) {
1935 return cachedRequest
1937 throw new OCPPError(
1938 ErrorType
.PROTOCOL_ERROR
,
1939 `Cached request for message id '${messageId}' ${getMessageTypeString(
1941 )} is not an array`,
1947 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1948 const [messageType
, messageId
, commandName
, commandPayload
] = request
1949 if (this.requests
.has(messageId
)) {
1950 throw new OCPPError(
1951 ErrorType
.SECURITY_ERROR
,
1952 `Received message with duplicate message id '${messageId}'`,
1957 if (this.stationInfo
?.enableStatistics
=== true) {
1958 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1961 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1965 // Process the message
1966 await this.ocppIncomingRequestService
.incomingRequestHandler(
1972 this.emit(ChargingStationEvents
.updated
)
1975 private handleResponseMessage (response
: Response
): void {
1976 const [messageType
, messageId
, commandPayload
] = response
1977 if (!this.requests
.has(messageId
)) {
1979 throw new OCPPError(
1980 ErrorType
.INTERNAL_ERROR
,
1981 `Response for unknown message id '${messageId}'`,
1987 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1988 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1993 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1997 responseCallback(commandPayload
, requestPayload
)
2000 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
2001 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
2002 if (!this.requests
.has(messageId
)) {
2004 throw new OCPPError(
2005 ErrorType
.INTERNAL_ERROR
,
2006 `Error response for unknown message id '${messageId}'`,
2008 { errorType
, errorMessage
, errorDetails
}
2011 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2012 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2014 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
2018 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
2021 private async onMessage (data
: RawData
): Promise
<void> {
2022 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
2023 let messageType
: MessageType
| undefined
2024 let errorMsg
: string
2026 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2027 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
2028 if (Array.isArray(request
)) {
2029 [messageType
] = request
2030 // Check the type of message
2031 switch (messageType
) {
2033 case MessageType
.CALL_MESSAGE
:
2034 await this.handleIncomingMessage(request
as IncomingRequest
)
2037 case MessageType
.CALL_RESULT_MESSAGE
:
2038 this.handleResponseMessage(request
as Response
)
2041 case MessageType
.CALL_ERROR_MESSAGE
:
2042 this.handleErrorMessage(request
as ErrorResponse
)
2046 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2047 errorMsg
= `Wrong message type ${messageType}`
2048 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2049 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2052 throw new OCPPError(
2053 ErrorType
.PROTOCOL_ERROR
,
2054 'Incoming message is not an array',
2062 if (!Array.isArray(request
)) {
2063 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2066 let commandName
: IncomingRequestCommand
| undefined
2067 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2068 let errorCallback
: ErrorCallback
2069 const [, messageId
] = request
2070 switch (messageType
) {
2071 case MessageType
.CALL_MESSAGE
:
2072 [, , commandName
] = request
as IncomingRequest
2074 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2076 case MessageType
.CALL_RESULT_MESSAGE
:
2077 case MessageType
.CALL_ERROR_MESSAGE
:
2078 if (this.requests
.has(messageId
)) {
2079 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2080 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2081 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2082 errorCallback(error
as OCPPError
, false)
2084 // Remove the request from the cache in case of error at response handling
2085 this.requests
.delete(messageId
)
2089 if (!(error
instanceof OCPPError
)) {
2091 `${this.logPrefix()} Error thrown at incoming OCPP command ${
2092 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2093 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2094 } message '${data.toString()}' handling is not an OCPPError:`,
2099 `${this.logPrefix()} Incoming OCPP command '${
2100 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2101 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2102 }' message '${data.toString()}'${
2103 this.requests.has(messageId)
2104 ? ` matching cached request
'${JSON.stringify(
2105 this.getCachedRequest(messageType, messageId)
2108 } processing error:`,
2114 private onPing (): void {
2115 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2118 private onPong (): void {
2119 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2122 private onError (error
: WSError
): void {
2123 this.closeWSConnection()
2124 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2127 private getEnergyActiveImportRegister (
2128 connectorStatus
: ConnectorStatus
| undefined,
2131 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2134 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2135 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2137 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2142 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2143 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2145 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2149 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2150 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2151 return stationTemplate
?.useConnectorId0
?? Constants
.DEFAULT_STATION_INFO
.useConnectorId0
!
2154 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2155 if (this.hasEvses
) {
2156 for (const [evseId
, evseStatus
] of this.evses
) {
2160 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2161 if (connectorStatus
.transactionStarted
=== true) {
2162 await this.stopTransactionOnConnector(connectorId
, reason
)
2167 for (const connectorId
of this.connectors
.keys()) {
2168 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2169 await this.stopTransactionOnConnector(connectorId
, reason
)
2176 private getConnectionTimeout (): number {
2177 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2178 return convertToInt(
2179 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2180 Constants
.DEFAULT_CONNECTION_TIMEOUT
2183 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2186 private getPowerDivider (): number {
2187 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2188 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2189 powerDivider
= this.getNumberOfRunningTransactions()
2194 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2196 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2197 switch (this.getCurrentOutType(stationInfo
)) {
2198 case CurrentType
.AC
:
2199 return ACElectricUtils
.amperagePerPhaseFromPower(
2200 this.getNumberOfPhases(stationInfo
),
2201 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2202 this.getVoltageOut(stationInfo
)
2204 case CurrentType
.DC
:
2205 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2209 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2211 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2212 (stationInfo
?? this.stationInfo
!).currentOutType
??
2213 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2214 Constants
.DEFAULT_STATION_INFO
.currentOutType
!
2218 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2220 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2221 (stationInfo
?? this.stationInfo
!).voltageOut
??
2222 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2226 private getAmperageLimitation (): number | undefined {
2228 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2229 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2232 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2233 getAmperageLimitationUnitDivider(this.stationInfo
)
2238 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2239 if (this.stationInfo
?.autoRegister
=== true) {
2240 await this.ocppRequestService
.requestHandler
<
2241 BootNotificationRequest
,
2242 BootNotificationResponse
2243 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2244 skipBufferingOnError
: true
2247 // Start WebSocket ping
2248 if (this.wsPingSetInterval
== null) {
2249 this.startWebSocketPing()
2252 if (this.heartbeatSetInterval
== null) {
2253 this.startHeartbeat()
2255 // Initialize connectors status
2256 if (this.hasEvses
) {
2257 for (const [evseId
, evseStatus
] of this.evses
) {
2259 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2260 await sendAndSetConnectorStatus(
2263 getBootConnectorStatus(this, connectorId
, connectorStatus
),
2270 for (const connectorId
of this.connectors
.keys()) {
2271 if (connectorId
> 0) {
2272 await sendAndSetConnectorStatus(
2275 getBootConnectorStatus(
2278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2279 this.getConnectorStatus(connectorId
)!
2285 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2286 await this.ocppRequestService
.requestHandler
<
2287 FirmwareStatusNotificationRequest
,
2288 FirmwareStatusNotificationResponse
2289 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2290 status: FirmwareStatus
.Installed
2292 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2296 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2297 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2299 this.flushMessageBuffer()
2302 private internalStopMessageSequence (): void {
2303 // Stop WebSocket ping
2304 this.stopWebSocketPing()
2306 this.stopHeartbeat()
2308 if (this.automaticTransactionGenerator
?.started
=== true) {
2309 this.stopAutomaticTransactionGenerator()
2313 private async stopMessageSequence (
2314 reason
?: StopTransactionReason
,
2315 stopTransactions
?: boolean
2317 this.internalStopMessageSequence()
2318 // Stop ongoing transactions
2319 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2320 if (this.hasEvses
) {
2321 for (const [evseId
, evseStatus
] of this.evses
) {
2323 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2324 await sendAndSetConnectorStatus(
2327 ConnectorStatusEnum
.Unavailable
,
2330 delete connectorStatus
.status
2335 for (const connectorId
of this.connectors
.keys()) {
2336 if (connectorId
> 0) {
2337 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2338 delete this.getConnectorStatus(connectorId
)?.status
2344 private getWebSocketPingInterval (): number {
2345 return getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2346 ? convertToInt(getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
)
2350 private startWebSocketPing (): void {
2351 const webSocketPingInterval
= this.getWebSocketPingInterval()
2352 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2353 this.wsPingSetInterval
= setInterval(() => {
2354 if (this.isWebSocketConnectionOpened()) {
2355 this.wsConnection
?.ping()
2357 }, secondsToMilliseconds(webSocketPingInterval
))
2359 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2360 webSocketPingInterval
2363 } else if (this.wsPingSetInterval
!= null) {
2365 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2366 webSocketPingInterval
2371 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2376 private stopWebSocketPing (): void {
2377 if (this.wsPingSetInterval
!= null) {
2378 clearInterval(this.wsPingSetInterval
)
2379 delete this.wsPingSetInterval
2383 private getConfiguredSupervisionUrl (): URL
{
2384 let configuredSupervisionUrl
: string
2385 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2386 if (isNotEmptyArray(supervisionUrls
)) {
2387 let configuredSupervisionUrlIndex
: number
2388 switch (Configuration
.getSupervisionUrlDistribution()) {
2389 case SupervisionUrlDistribution
.RANDOM
:
2390 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2392 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2393 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2395 !Object.values(SupervisionUrlDistribution
).includes(
2396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2397 Configuration
.getSupervisionUrlDistribution()!
2400 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2401 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2402 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2405 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2408 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2410 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2411 configuredSupervisionUrl
= supervisionUrls
!
2413 if (isNotEmptyString(configuredSupervisionUrl
)) {
2414 return new URL(configuredSupervisionUrl
)
2416 const errorMsg
= 'No supervision url(s) configured'
2417 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2418 throw new BaseError(errorMsg
)
2421 private stopHeartbeat (): void {
2422 if (this.heartbeatSetInterval
!= null) {
2423 clearInterval(this.heartbeatSetInterval
)
2424 delete this.heartbeatSetInterval
2428 private terminateWSConnection (): void {
2429 if (this.isWebSocketConnectionOpened()) {
2430 this.wsConnection
?.terminate()
2431 this.wsConnection
= null
2435 private async reconnect (): Promise
<void> {
2437 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2438 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2439 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2441 ++this.wsConnectionRetryCount
2442 const reconnectDelay
=
2443 this.stationInfo
?.reconnectExponentialDelay
=== true
2444 ? exponentialDelay(this.wsConnectionRetryCount
)
2445 : secondsToMilliseconds(this.getConnectionTimeout())
2446 const reconnectDelayWithdraw
= 1000
2447 const reconnectTimeout
=
2448 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2450 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2453 )}ms, timeout ${reconnectTimeout}ms`
2455 await sleep(reconnectDelay
)
2457 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2459 this.openWSConnection(
2461 handshakeTimeout
: reconnectTimeout
2463 { closeOpened
: true }
2465 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2467 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`