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
.disconnected
, () => {
241 this.internalStopMessageSequence()
244 `${this.logPrefix()} Error while stopping the internal message sequence:`,
250 this.initialize(options
)
254 if (this.stationInfo
?.autoStart
=== true) {
259 public get
hasEvses (): boolean {
260 return this.connectors
.size
=== 0 && this.evses
.size
> 0
263 public get
wsConnectionUrl (): URL
{
264 const wsConnectionBaseUrlStr
= `${
265 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
266 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
267 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
268 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
269 : this.configuredSupervisionUrl.href
272 `${wsConnectionBaseUrlStr}${
273 !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
274 }${this.stationInfo?.chargingStationId}`
278 public logPrefix
= (): string => {
280 this instanceof ChargingStation
&&
281 this.stationInfo
!= null &&
282 isNotEmptyString(this.stationInfo
.chargingStationId
)
284 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
286 let stationTemplate
: ChargingStationTemplate
| undefined
288 stationTemplate
= JSON
.parse(
289 readFileSync(this.templateFile
, 'utf8')
290 ) as ChargingStationTemplate
294 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
297 public hasIdTags (): boolean {
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
302 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
303 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304 const localStationInfo
= stationInfo
?? this.stationInfo
!
305 switch (this.getCurrentOutType(stationInfo
)) {
307 return localStationInfo
.numberOfPhases
?? 3
313 public isWebSocketConnectionOpened (): boolean {
314 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
317 public inUnknownState (): boolean {
318 return this.bootNotificationResponse
?.status == null
321 public inPendingState (): boolean {
322 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
325 public inAcceptedState (): boolean {
326 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
329 public inRejectedState (): boolean {
330 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
333 public isRegistered (): boolean {
334 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
337 public isChargingStationAvailable (): boolean {
338 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
341 public hasConnector (connectorId
: number): boolean {
343 for (const evseStatus
of this.evses
.values()) {
344 if (evseStatus
.connectors
.has(connectorId
)) {
350 return this.connectors
.has(connectorId
)
353 public isConnectorAvailable (connectorId
: number): boolean {
356 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
360 public getNumberOfConnectors (): number {
362 let numberOfConnectors
= 0
363 for (const [evseId
, evseStatus
] of this.evses
) {
365 numberOfConnectors
+= evseStatus
.connectors
.size
368 return numberOfConnectors
370 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
373 public getNumberOfEvses (): number {
374 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
377 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
379 for (const evseStatus
of this.evses
.values()) {
380 if (evseStatus
.connectors
.has(connectorId
)) {
381 return evseStatus
.connectors
.get(connectorId
)
386 return this.connectors
.get(connectorId
)
389 public getConnectorMaximumAvailablePower (connectorId
: number): number {
390 let connectorAmperageLimitationPowerLimit
: number | undefined
391 const amperageLimitation
= this.getAmperageLimitation()
393 amperageLimitation
!= null &&
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
397 connectorAmperageLimitationPowerLimit
=
398 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
399 ? ACElectricUtils
.powerTotal(
400 this.getNumberOfPhases(),
401 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
402 this.stationInfo
.voltageOut
!,
404 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
406 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
407 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
411 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
412 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
413 const connectorChargingProfilesPowerLimit
=
414 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
416 isNaN(connectorMaximumPower
) ? Number.POSITIVE_INFINITY
: connectorMaximumPower
,
417 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
418 isNaN(connectorAmperageLimitationPowerLimit
!)
419 ? Number.POSITIVE_INFINITY
420 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
421 connectorAmperageLimitationPowerLimit
!,
422 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
423 isNaN(connectorChargingProfilesPowerLimit
!)
424 ? Number.POSITIVE_INFINITY
425 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426 connectorChargingProfilesPowerLimit
!
430 public getTransactionIdTag (transactionId
: number): string | undefined {
432 for (const evseStatus
of this.evses
.values()) {
433 for (const connectorStatus
of evseStatus
.connectors
.values()) {
434 if (connectorStatus
.transactionId
=== transactionId
) {
435 return connectorStatus
.transactionIdTag
440 for (const connectorId
of this.connectors
.keys()) {
441 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
442 return this.getConnectorStatus(connectorId
)?.transactionIdTag
448 public getNumberOfRunningTransactions (): number {
449 let numberOfRunningTransactions
= 0
451 for (const [evseId
, evseStatus
] of this.evses
) {
455 for (const connectorStatus
of evseStatus
.connectors
.values()) {
456 if (connectorStatus
.transactionStarted
=== true) {
457 ++numberOfRunningTransactions
462 for (const connectorId
of this.connectors
.keys()) {
463 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
464 ++numberOfRunningTransactions
468 return numberOfRunningTransactions
471 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
472 if (transactionId
== null) {
474 } else if (this.hasEvses
) {
475 for (const evseStatus
of this.evses
.values()) {
476 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
477 if (connectorStatus
.transactionId
=== transactionId
) {
483 for (const connectorId
of this.connectors
.keys()) {
484 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
491 public getEnergyActiveImportRegisterByTransactionId (
492 transactionId
: number | undefined,
495 return this.getEnergyActiveImportRegister(
496 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
497 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
502 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
503 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
506 public getAuthorizeRemoteTxRequests (): boolean {
507 const authorizeRemoteTxRequests
= getConfigurationKey(
509 StandardParametersKey
.AuthorizeRemoteTxRequests
511 return authorizeRemoteTxRequests
!= null
512 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
516 public getLocalAuthListEnabled (): boolean {
517 const localAuthListEnabled
= getConfigurationKey(
519 StandardParametersKey
.LocalAuthListEnabled
521 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
524 public getHeartbeatInterval (): number {
525 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
526 if (HeartbeatInterval
!= null) {
527 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
529 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
530 if (HeartBeatInterval
!= null) {
531 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
533 this.stationInfo
?.autoRegister
=== false &&
535 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
536 Constants.DEFAULT_HEARTBEAT_INTERVAL
539 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
542 public setSupervisionUrl (url
: string): void {
544 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
545 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
547 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
549 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
550 this.stationInfo
!.supervisionUrls
= url
551 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
552 this.saveStationInfo()
556 public startHeartbeat (): void {
557 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval
== null) {
558 this.heartbeatSetInterval
= setInterval(() => {
559 this.ocppRequestService
560 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
561 .catch((error
: unknown
) => {
563 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
567 }, this.getHeartbeatInterval())
569 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
570 this.getHeartbeatInterval()
573 } else if (this.heartbeatSetInterval
!= null) {
575 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
576 this.getHeartbeatInterval()
581 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
586 public restartHeartbeat (): void {
590 this.startHeartbeat()
593 public restartWebSocketPing (): void {
594 // Stop WebSocket ping
595 this.stopWebSocketPing()
596 // Start WebSocket ping
597 this.startWebSocketPing()
600 public startMeterValues (connectorId
: number, interval
: number): void {
601 if (connectorId
=== 0) {
602 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
605 const connectorStatus
= this.getConnectorStatus(connectorId
)
606 if (connectorStatus
== null) {
608 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
613 if (connectorStatus
.transactionStarted
=== false) {
615 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
619 connectorStatus
.transactionStarted
=== true &&
620 connectorStatus
.transactionId
== null
623 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
628 connectorStatus
.transactionSetInterval
= setInterval(() => {
629 const meterValue
= buildMeterValue(
632 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
633 connectorStatus
.transactionId
!,
636 this.ocppRequestService
637 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
639 RequestCommand
.METER_VALUES
,
642 transactionId
: connectorStatus
.transactionId
,
643 meterValue
: [meterValue
]
646 .catch((error
: unknown
) => {
648 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
655 `${this.logPrefix()} Charging station ${
656 StandardParametersKey.MeterValueSampleInterval
657 } configuration set to ${interval}, not sending MeterValues`
662 public stopMeterValues (connectorId
: number): void {
663 const connectorStatus
= this.getConnectorStatus(connectorId
)
664 if (connectorStatus
?.transactionSetInterval
!= null) {
665 clearInterval(connectorStatus
.transactionSetInterval
)
669 private add (): void {
670 this.emit(ChargingStationEvents
.added
)
673 public async delete (deleteConfiguration
= true): Promise
<void> {
677 AutomaticTransactionGenerator
.deleteInstance(this)
678 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
679 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
680 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
681 this.requests
.clear()
682 this.connectors
.clear()
684 this.templateFileWatcher
?.unref()
685 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
686 this.chargingStationWorkerBroadcastChannel
.unref()
687 this.emit(ChargingStationEvents
.deleted
)
688 this.removeAllListeners()
691 public start (): void {
693 if (!this.starting
) {
695 if (this.stationInfo
?.enableStatistics
=== true) {
696 this.performanceStatistics
?.start()
698 this.openWSConnection()
699 // Monitor charging station template file
700 this.templateFileWatcher
= watchJsonFile(
702 FileType
.ChargingStationTemplate
,
705 (event
, filename
): void => {
706 if (isNotEmptyString(filename
) && event
=== 'change') {
709 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
711 } file have changed, reload`
713 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
714 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
715 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
719 const ATGStarted
= this.automaticTransactionGenerator
?.started
720 if (ATGStarted
=== true) {
721 this.stopAutomaticTransactionGenerator()
723 delete this.automaticTransactionGeneratorConfiguration
725 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
728 this.startAutomaticTransactionGenerator(undefined, true)
730 if (this.stationInfo
?.enableStatistics
=== true) {
731 this.performanceStatistics
?.restart()
733 this.performanceStatistics
?.stop()
735 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
738 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
746 this.emit(ChargingStationEvents
.started
)
747 this.starting
= false
749 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
752 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
757 reason
?: StopTransactionReason
,
758 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
761 if (!this.stopping
) {
763 await this.stopMessageSequence(reason
, stopTransactions
)
764 this.closeWSConnection()
765 if (this.stationInfo
?.enableStatistics
=== true) {
766 this.performanceStatistics
?.stop()
768 this.templateFileWatcher
?.close()
769 delete this.bootNotificationResponse
771 this.saveConfiguration()
772 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
773 this.emit(ChargingStationEvents
.stopped
)
774 this.stopping
= false
776 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
779 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
783 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
784 await this.stop(reason
)
785 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
786 await sleep(this.stationInfo
!.resetTime
!)
791 public saveOcppConfiguration (): void {
792 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
793 this.saveConfiguration()
797 public bufferMessage (message
: string): void {
798 this.messageBuffer
.add(message
)
799 this.setIntervalFlushMessageBuffer()
802 public openWSConnection (
804 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
807 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
808 ...this.stationInfo
?.wsOptions
,
811 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
812 if (!checkChargingStation(this, this.logPrefix())) {
815 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
816 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
818 if (params
.closeOpened
=== true) {
819 this.closeWSConnection()
821 if (params
.terminateOpened
=== true) {
822 this.terminateWSConnection()
825 if (this.isWebSocketConnectionOpened()) {
827 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
832 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
834 this.wsConnection
= new WebSocket(
835 this.wsConnectionUrl
,
836 `ocpp${this.stationInfo?.ocppVersion}`,
840 // Handle WebSocket message
841 this.wsConnection
.on('message', data
=> {
842 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
844 // Handle WebSocket error
845 this.wsConnection
.on('error', this.onError
.bind(this))
846 // Handle WebSocket close
847 this.wsConnection
.on('close', this.onClose
.bind(this))
848 // Handle WebSocket open
849 this.wsConnection
.on('open', () => {
850 this.onOpen().catch((error
: unknown
) =>
851 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
854 // Handle WebSocket ping
855 this.wsConnection
.on('ping', this.onPing
.bind(this))
856 // Handle WebSocket pong
857 this.wsConnection
.on('pong', this.onPong
.bind(this))
860 public closeWSConnection (): void {
861 if (this.isWebSocketConnectionOpened()) {
862 this.wsConnection
?.close()
863 this.wsConnection
= null
867 public getAutomaticTransactionGeneratorConfiguration ():
868 | AutomaticTransactionGeneratorConfiguration
870 if (this.automaticTransactionGeneratorConfiguration
== null) {
871 let automaticTransactionGeneratorConfiguration
:
872 | AutomaticTransactionGeneratorConfiguration
874 const stationTemplate
= this.getTemplateFromFile()
875 const stationConfiguration
= this.getConfigurationFromFile()
877 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
878 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
879 stationConfiguration
?.automaticTransactionGenerator
!= null
881 automaticTransactionGeneratorConfiguration
=
882 stationConfiguration
.automaticTransactionGenerator
884 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
886 this.automaticTransactionGeneratorConfiguration
= {
887 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
888 ...automaticTransactionGeneratorConfiguration
891 return this.automaticTransactionGeneratorConfiguration
894 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
895 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
898 public startAutomaticTransactionGenerator (
899 connectorIds
?: number[],
900 stopAbsoluteDuration
?: boolean
902 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
903 if (isNotEmptyArray(connectorIds
)) {
904 for (const connectorId
of connectorIds
) {
905 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
908 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
910 this.saveAutomaticTransactionGeneratorConfiguration()
911 this.emit(ChargingStationEvents
.updated
)
914 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
915 if (isNotEmptyArray(connectorIds
)) {
916 for (const connectorId
of connectorIds
) {
917 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
920 this.automaticTransactionGenerator
?.stop()
922 this.saveAutomaticTransactionGeneratorConfiguration()
923 this.emit(ChargingStationEvents
.updated
)
926 public async stopTransactionOnConnector (
928 reason
?: StopTransactionReason
929 ): Promise
<StopTransactionResponse
> {
930 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
932 this.stationInfo
?.beginEndMeterValues
=== true &&
933 this.stationInfo
.ocppStrictCompliance
=== true &&
934 this.stationInfo
.outOfOrderEndMeterValues
=== false
936 const transactionEndMeterValue
= buildTransactionEndMeterValue(
939 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
941 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
943 RequestCommand
.METER_VALUES
,
947 meterValue
: [transactionEndMeterValue
]
951 return await this.ocppRequestService
.requestHandler
<
952 Partial
<StopTransactionRequest
>,
953 StopTransactionResponse
954 >(this, RequestCommand
.STOP_TRANSACTION
, {
956 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
957 ...(reason
!= null && { reason
})
961 public getReserveConnectorZeroSupported (): boolean {
962 return convertToBoolean(
963 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
964 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
968 public async addReservation (reservation
: Reservation
): Promise
<void> {
969 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
970 if (reservationFound
!= null) {
971 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
973 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
974 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
975 await sendAndSetConnectorStatus(
977 reservation
.connectorId
,
978 ConnectorStatusEnum
.Reserved
,
980 { send
: reservation
.connectorId
!== 0 }
984 public async removeReservation (
985 reservation
: Reservation
,
986 reason
: ReservationTerminationReason
988 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
989 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
991 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
992 case ReservationTerminationReason
.TRANSACTION_STARTED
:
993 delete connector
.reservation
995 case ReservationTerminationReason
.RESERVATION_CANCELED
:
996 case ReservationTerminationReason
.REPLACE_EXISTING
:
997 case ReservationTerminationReason
.EXPIRED
:
998 await sendAndSetConnectorStatus(
1000 reservation
.connectorId
,
1001 ConnectorStatusEnum
.Available
,
1003 { send
: reservation
.connectorId
!== 0 }
1005 delete connector
.reservation
1008 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1009 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1013 public getReservationBy (
1014 filterKey
: ReservationKey
,
1015 value
: number | string
1016 ): Reservation
| undefined {
1017 if (this.hasEvses
) {
1018 for (const evseStatus
of this.evses
.values()) {
1019 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1020 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1021 return connectorStatus
.reservation
1026 for (const connectorStatus
of this.connectors
.values()) {
1027 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1028 return connectorStatus
.reservation
1034 public isConnectorReservable (
1035 reservationId
: number,
1037 connectorId
?: number
1039 const reservation
= this.getReservationBy('reservationId', reservationId
)
1040 const reservationExists
= reservation
!= null && !hasReservationExpired(reservation
)
1041 if (arguments.length
=== 1) {
1042 return !reservationExists
1043 } else if (arguments.length
> 1) {
1044 const userReservation
= idTag
!= null ? this.getReservationBy('idTag', idTag
) : undefined
1045 const userReservationExists
=
1046 userReservation
!= null && !hasReservationExpired(userReservation
)
1047 const notConnectorZero
= connectorId
== null ? true : connectorId
> 0
1048 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1050 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1056 private setIntervalFlushMessageBuffer (): void {
1057 if (this.flushMessageBufferSetInterval
== null) {
1058 this.flushMessageBufferSetInterval
= setInterval(() => {
1059 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1060 this.flushMessageBuffer()
1062 if (this.messageBuffer
.size
=== 0) {
1063 this.clearIntervalFlushMessageBuffer()
1065 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1069 private clearIntervalFlushMessageBuffer (): void {
1070 if (this.flushMessageBufferSetInterval
!= null) {
1071 clearInterval(this.flushMessageBufferSetInterval
)
1072 delete this.flushMessageBufferSetInterval
1076 private getNumberOfReservableConnectors (): number {
1077 let numberOfReservableConnectors
= 0
1078 if (this.hasEvses
) {
1079 for (const evseStatus
of this.evses
.values()) {
1080 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1083 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1085 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1088 private getNumberOfReservationsOnConnectorZero (): number {
1090 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1091 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1098 private flushMessageBuffer (): void {
1099 if (this.messageBuffer
.size
> 0) {
1100 for (const message
of this.messageBuffer
.values()) {
1101 let beginId
: string | undefined
1102 let commandName
: RequestCommand
| undefined
1103 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1104 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1106 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1107 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1109 this.wsConnection
?.send(message
, (error
?: Error) => {
1110 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1111 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1112 if (error
== null) {
1114 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1116 )} OCPP message sent '${JSON.stringify(message)}'`
1118 this.messageBuffer
.delete(message
)
1121 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1123 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1132 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1133 let template
: ChargingStationTemplate
| undefined
1135 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1136 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1138 const measureId
= `${FileType.ChargingStationTemplate} read`
1139 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1140 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1141 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1142 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1143 .update(JSON
.stringify(template
))
1145 this.sharedLRUCache
.setChargingStationTemplate(template
)
1146 this.templateFileHash
= template
.templateHash
1149 handleFileException(
1151 FileType
.ChargingStationTemplate
,
1152 error
as NodeJS
.ErrnoException
,
1159 private getStationInfoFromTemplate (): ChargingStationInfo
{
1160 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1161 const stationTemplate
= this.getTemplateFromFile()!
1162 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1163 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
)
1164 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1165 if (stationTemplate
.Connectors
!= null) {
1166 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1168 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1169 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1170 stationInfo
.templateIndex
= this.index
1171 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1172 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1173 createSerialNumber(stationTemplate
, stationInfo
)
1174 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1175 if (isNotEmptyArray(stationTemplate
.power
)) {
1176 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1177 stationInfo
.maximumPower
=
1178 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1179 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1180 : stationTemplate
.power
[powerArrayRandomIndex
]
1182 stationInfo
.maximumPower
=
1183 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1184 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1185 stationTemplate
.power
! * 1000
1186 : stationTemplate
.power
1188 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1190 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1191 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1192 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1195 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1197 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1200 if (stationTemplate
.resetTime
!= null) {
1201 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1206 private getStationInfoFromFile (
1207 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1208 .stationInfoPersistentConfiguration
1209 ): ChargingStationInfo
| undefined {
1210 let stationInfo
: ChargingStationInfo
| undefined
1211 if (stationInfoPersistentConfiguration
=== true) {
1212 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1213 if (stationInfo
!= null) {
1214 delete stationInfo
.infoHash
1215 delete (stationInfo
as ChargingStationTemplate
).numberOfConnectors
1216 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1217 if (stationInfo
.templateIndex
== null) {
1218 stationInfo
.templateIndex
= this.index
1220 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1221 if (stationInfo
.templateName
== null) {
1222 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1229 private getStationInfo (options
?: ChargingStationOptions
): ChargingStationInfo
{
1230 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1231 options
?.persistentConfiguration
!= null &&
1232 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
)
1233 const stationInfoFromFile
= this.getStationInfoFromFile(
1234 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1236 let stationInfo
: ChargingStationInfo
1238 // 1. charging station info from template
1239 // 2. charging station info from configuration file
1241 stationInfoFromFile
!= null &&
1242 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1244 stationInfo
= stationInfoFromFile
1246 stationInfo
= stationInfoFromTemplate
1247 stationInfoFromFile
!= null &&
1248 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile
, stationInfo
)
1250 return setChargingStationOptions(
1251 mergeDeepRight(Constants
.DEFAULT_STATION_INFO
, stationInfo
),
1256 private saveStationInfo (): void {
1257 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1258 this.saveConfiguration()
1262 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1263 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1264 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1265 throw new BaseError(errorMsg
)
1268 private initialize (options
?: ChargingStationOptions
): void {
1269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1270 const stationTemplate
= this.getTemplateFromFile()!
1271 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1272 this.configurationFile
= join(
1273 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1274 `${getHashId(this.index, stationTemplate)}.json`
1276 const stationConfiguration
= this.getConfigurationFromFile()
1278 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1279 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1281 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1282 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1284 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1286 this.stationInfo
= this.getStationInfo(options
)
1288 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1289 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1290 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1292 const patternGroup
=
1293 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1294 this.stationInfo
.firmwareVersion
.split('.').length
1295 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1296 .exec(this.stationInfo
.firmwareVersion
)
1297 ?.slice(1, patternGroup
+ 1)
1298 if (match
!= null) {
1299 const patchLevelIndex
= match
.length
- 1
1300 match
[patchLevelIndex
] = (
1301 convertToInt(match
[patchLevelIndex
]) +
1302 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1303 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1305 this.stationInfo
.firmwareVersion
= match
.join('.')
1308 this.saveStationInfo()
1309 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1310 if (this.stationInfo
.enableStatistics
=== true) {
1311 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1312 this.stationInfo
.hashId
,
1313 this.stationInfo
.chargingStationId
,
1314 this.configuredSupervisionUrl
1317 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1318 if (bootNotificationRequest
== null) {
1319 const errorMsg
= 'Error while creating boot notification request'
1320 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1321 throw new BaseError(errorMsg
)
1323 this.bootNotificationRequest
= bootNotificationRequest
1324 this.powerDivider
= this.getPowerDivider()
1325 // OCPP configuration
1326 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1327 this.initializeOcppConfiguration()
1328 this.initializeOcppServices()
1329 if (this.stationInfo
.autoRegister
=== true) {
1330 this.bootNotificationResponse
= {
1331 currentTime
: new Date(),
1332 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1333 status: RegistrationStatusEnumType
.ACCEPTED
1338 private initializeOcppServices (): void {
1339 const ocppVersion
= this.stationInfo
?.ocppVersion
1340 switch (ocppVersion
) {
1341 case OCPPVersion
.VERSION_16
:
1342 this.ocppIncomingRequestService
=
1343 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1344 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1345 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1348 case OCPPVersion
.VERSION_20
:
1349 case OCPPVersion
.VERSION_201
:
1350 this.ocppIncomingRequestService
=
1351 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1352 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1353 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1357 this.handleUnsupportedVersion(ocppVersion
)
1362 private initializeOcppConfiguration (): void {
1363 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1364 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1366 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1367 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', {
1372 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1373 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1374 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1376 addConfigurationKey(
1378 this.stationInfo
.supervisionUrlOcppKey
,
1379 this.configuredSupervisionUrl
.href
,
1383 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1384 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1385 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1387 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, {
1392 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1393 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1395 addConfigurationKey(
1397 this.stationInfo
.amperageLimitationOcppKey
,
1399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1400 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1403 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1404 addConfigurationKey(
1406 StandardParametersKey
.SupportedFeatureProfiles
,
1407 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1410 addConfigurationKey(
1412 StandardParametersKey
.NumberOfConnectors
,
1413 this.getNumberOfConnectors().toString(),
1417 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1418 addConfigurationKey(
1420 StandardParametersKey
.MeterValuesSampledData
,
1421 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1424 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1425 const connectorsPhaseRotation
: string[] = []
1426 if (this.hasEvses
) {
1427 for (const evseStatus
of this.evses
.values()) {
1428 for (const connectorId
of evseStatus
.connectors
.keys()) {
1429 connectorsPhaseRotation
.push(
1430 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1431 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1436 for (const connectorId
of this.connectors
.keys()) {
1437 connectorsPhaseRotation
.push(
1438 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1439 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1443 addConfigurationKey(
1445 StandardParametersKey
.ConnectorPhaseRotation
,
1446 connectorsPhaseRotation
.toString()
1449 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1450 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1453 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1454 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1456 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1458 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1459 addConfigurationKey(
1461 StandardParametersKey
.ConnectionTimeOut
,
1462 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1465 this.saveOcppConfiguration()
1468 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1469 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1470 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1471 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1473 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1474 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1475 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1476 delete evseStatus
.connectorsStatus
1477 this.evses
.set(evseId
, {
1478 ...(evseStatus
as EvseStatus
),
1479 connectors
: new Map
<number, ConnectorStatus
>(
1480 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1481 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1488 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1489 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1490 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1491 throw new BaseError(errorMsg
)
1493 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1494 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1495 throw new BaseError(errorMsg
)
1499 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1500 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1501 this.initializeConnectorsFromTemplate(stationTemplate
)
1502 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1503 this.initializeEvsesFromTemplate(stationTemplate
)
1504 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1505 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1506 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1507 throw new BaseError(errorMsg
)
1509 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1510 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1511 throw new BaseError(errorMsg
)
1515 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1516 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1517 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1518 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1519 throw new BaseError(errorMsg
)
1521 if (stationTemplate
.Connectors
?.[0] == null) {
1523 `${this.logPrefix()} Charging station information from template ${
1525 } with no connector id 0 configuration`
1528 if (stationTemplate
.Connectors
!= null) {
1529 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1530 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1531 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1533 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1536 const connectorsConfigChanged
=
1537 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1538 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1539 connectorsConfigChanged
&& this.connectors
.clear()
1540 this.connectorsConfigurationHash
= connectorsConfigHash
1541 if (templateMaxConnectors
> 0) {
1542 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1544 connectorId
=== 0 &&
1545 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1546 (stationTemplate
.Connectors
[connectorId
] == null ||
1547 !this.getUseConnectorId0(stationTemplate
))
1551 const templateConnectorId
=
1552 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1553 ? randomInt(1, templateMaxAvailableConnectors
)
1555 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1556 checkStationInfoConnectorStatus(
1557 templateConnectorId
,
1562 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1564 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1565 this.saveConnectorsStatus()
1568 `${this.logPrefix()} Charging station information from template ${
1570 } with no connectors configuration defined, cannot create connectors`
1576 `${this.logPrefix()} Charging station information from template ${
1578 } with no connectors configuration defined, using already defined connectors`
1583 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1584 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1585 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1586 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1587 throw new BaseError(errorMsg
)
1589 if (stationTemplate
.Evses
?.[0] == null) {
1591 `${this.logPrefix()} Charging station information from template ${
1593 } with no evse id 0 configuration`
1596 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1598 `${this.logPrefix()} Charging station information from template ${
1600 } with evse id 0 with no connector id 0 configuration`
1603 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1605 `${this.logPrefix()} Charging station information from template ${
1607 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1610 if (stationTemplate
.Evses
!= null) {
1611 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1612 .update(JSON
.stringify(stationTemplate
.Evses
))
1614 const evsesConfigChanged
=
1615 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1616 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1617 evsesConfigChanged
&& this.evses
.clear()
1618 this.evsesConfigurationHash
= evsesConfigHash
1619 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1620 if (templateMaxEvses
> 0) {
1621 for (const evseKey
in stationTemplate
.Evses
) {
1622 const evseId
= convertToInt(evseKey
)
1623 this.evses
.set(evseId
, {
1624 connectors
: buildConnectorsMap(
1625 stationTemplate
.Evses
[evseKey
].Connectors
,
1629 availability
: AvailabilityType
.Operative
1631 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1632 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1634 this.saveEvsesStatus()
1637 `${this.logPrefix()} Charging station information from template ${
1639 } with no evses configuration defined, cannot create evses`
1645 `${this.logPrefix()} Charging station information from template ${
1647 } with no evses configuration defined, using already defined evses`
1652 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1653 let configuration
: ChargingStationConfiguration
| undefined
1654 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1656 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1657 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1658 this.configurationFileHash
1661 const measureId
= `${FileType.ChargingStationConfiguration} read`
1662 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1663 configuration
= JSON
.parse(
1664 readFileSync(this.configurationFile
, 'utf8')
1665 ) as ChargingStationConfiguration
1666 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1667 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1668 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1669 this.configurationFileHash
= configuration
.configurationHash
!
1672 handleFileException(
1673 this.configurationFile
,
1674 FileType
.ChargingStationConfiguration
,
1675 error
as NodeJS
.ErrnoException
,
1680 return configuration
1683 private saveAutomaticTransactionGeneratorConfiguration (): void {
1684 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1685 this.saveConfiguration()
1689 private saveConnectorsStatus (): void {
1690 this.saveConfiguration()
1693 private saveEvsesStatus (): void {
1694 this.saveConfiguration()
1697 private saveConfiguration (): void {
1698 if (isNotEmptyString(this.configurationFile
)) {
1700 if (!existsSync(dirname(this.configurationFile
))) {
1701 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1703 const configurationFromFile
= this.getConfigurationFromFile()
1704 let configurationData
: ChargingStationConfiguration
=
1705 configurationFromFile
!= null
1706 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1708 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1709 configurationData
.stationInfo
= this.stationInfo
1711 delete configurationData
.stationInfo
1714 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1715 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1717 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1719 delete configurationData
.configurationKey
1721 configurationData
= mergeDeepRight(
1723 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1725 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1726 delete configurationData
.automaticTransactionGenerator
1728 if (this.connectors
.size
> 0) {
1729 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1731 delete configurationData
.connectorsStatus
1733 if (this.evses
.size
> 0) {
1734 configurationData
.evsesStatus
= buildEvsesStatus(this)
1736 delete configurationData
.evsesStatus
1738 delete configurationData
.configurationHash
1739 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1742 stationInfo
: configurationData
.stationInfo
,
1743 configurationKey
: configurationData
.configurationKey
,
1744 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1745 ...(this.connectors
.size
> 0 && {
1746 connectorsStatus
: configurationData
.connectorsStatus
1748 ...(this.evses
.size
> 0 && {
1749 evsesStatus
: configurationData
.evsesStatus
1751 } satisfies ChargingStationConfiguration
)
1754 if (this.configurationFileHash
!== configurationHash
) {
1755 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1756 configurationData
.configurationHash
= configurationHash
1757 const measureId
= `${FileType.ChargingStationConfiguration} write`
1758 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1760 this.configurationFile
,
1761 JSON
.stringify(configurationData
, undefined, 2),
1764 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1765 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1766 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1767 this.configurationFileHash
= configurationHash
1768 }).catch((error
: unknown
) => {
1769 handleFileException(
1770 this.configurationFile
,
1771 FileType
.ChargingStationConfiguration
,
1772 error
as NodeJS
.ErrnoException
,
1778 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1779 this.configurationFile
1784 handleFileException(
1785 this.configurationFile
,
1786 FileType
.ChargingStationConfiguration
,
1787 error
as NodeJS
.ErrnoException
,
1793 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1798 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1799 return this.getTemplateFromFile()?.Configuration
1802 private getOcppConfigurationFromFile (
1803 ocppPersistentConfiguration
?: boolean
1804 ): ChargingStationOcppConfiguration
| undefined {
1805 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1806 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1807 return { configurationKey
}
1812 private getOcppConfiguration (
1813 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1814 ): ChargingStationOcppConfiguration
| undefined {
1815 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1816 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1817 if (ocppConfiguration
== null) {
1818 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1820 return ocppConfiguration
1823 private async onOpen (): Promise
<void> {
1824 if (this.isWebSocketConnectionOpened()) {
1825 this.emit(ChargingStationEvents
.updated
)
1827 `${this.logPrefix()} Connection to OCPP server through ${
1828 this.wsConnectionUrl.href
1831 let registrationRetryCount
= 0
1832 if (!this.isRegistered()) {
1833 // Send BootNotification
1835 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1836 BootNotificationRequest
,
1837 BootNotificationResponse
1838 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1839 skipBufferingOnError
: true
1841 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1842 if (this.bootNotificationResponse
?.currentTime
!= null) {
1843 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1844 this.bootNotificationResponse
.currentTime
= convertToDate(
1845 this.bootNotificationResponse
.currentTime
1848 if (!this.isRegistered()) {
1849 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1851 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1852 this.bootNotificationResponse
?.interval
!= null
1853 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1854 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1858 !this.isRegistered() &&
1859 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1860 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1861 this.stationInfo
?.registrationMaxRetries
=== -1)
1864 if (this.isRegistered()) {
1865 this.emit(ChargingStationEvents
.registered
)
1866 if (this.inAcceptedState()) {
1867 this.emit(ChargingStationEvents
.accepted
)
1870 if (this.inRejectedState()) {
1871 this.emit(ChargingStationEvents
.rejected
)
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 this.startWebSocketPing()
2234 this.startHeartbeat()
2235 // Initialize connectors status
2236 if (this.hasEvses
) {
2237 for (const [evseId
, evseStatus
] of this.evses
) {
2239 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2240 await sendAndSetConnectorStatus(
2243 getBootConnectorStatus(this, connectorId
, connectorStatus
),
2250 for (const connectorId
of this.connectors
.keys()) {
2251 if (connectorId
> 0) {
2252 await sendAndSetConnectorStatus(
2255 getBootConnectorStatus(
2258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2259 this.getConnectorStatus(connectorId
)!
2265 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2266 await this.ocppRequestService
.requestHandler
<
2267 FirmwareStatusNotificationRequest
,
2268 FirmwareStatusNotificationResponse
2269 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2270 status: FirmwareStatus
.Installed
2272 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2276 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2277 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2279 this.flushMessageBuffer()
2282 private internalStopMessageSequence (): void {
2283 // Stop WebSocket ping
2284 this.stopWebSocketPing()
2286 this.stopHeartbeat()
2288 if (this.automaticTransactionGenerator
?.started
=== true) {
2289 this.stopAutomaticTransactionGenerator()
2293 private async stopMessageSequence (
2294 reason
?: StopTransactionReason
,
2295 stopTransactions
?: boolean
2297 this.internalStopMessageSequence()
2298 // Stop ongoing transactions
2299 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2300 if (this.hasEvses
) {
2301 for (const [evseId
, evseStatus
] of this.evses
) {
2303 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2304 await sendAndSetConnectorStatus(
2307 ConnectorStatusEnum
.Unavailable
,
2310 delete connectorStatus
.status
2315 for (const connectorId
of this.connectors
.keys()) {
2316 if (connectorId
> 0) {
2317 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2318 delete this.getConnectorStatus(connectorId
)?.status
2324 private startWebSocketPing (): void {
2325 const webSocketPingInterval
=
2326 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2328 getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
2331 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2332 this.wsPingSetInterval
= setInterval(() => {
2333 if (this.isWebSocketConnectionOpened()) {
2334 this.wsConnection
?.ping()
2336 }, secondsToMilliseconds(webSocketPingInterval
))
2338 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2339 webSocketPingInterval
2342 } else if (this.wsPingSetInterval
!= null) {
2344 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2345 webSocketPingInterval
2350 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2355 private stopWebSocketPing (): void {
2356 if (this.wsPingSetInterval
!= null) {
2357 clearInterval(this.wsPingSetInterval
)
2358 delete this.wsPingSetInterval
2362 private getConfiguredSupervisionUrl (): URL
{
2363 let configuredSupervisionUrl
: string
2364 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2365 if (isNotEmptyArray(supervisionUrls
)) {
2366 let configuredSupervisionUrlIndex
: number
2367 switch (Configuration
.getSupervisionUrlDistribution()) {
2368 case SupervisionUrlDistribution
.RANDOM
:
2369 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2371 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2372 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2374 !Object.values(SupervisionUrlDistribution
).includes(
2375 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2376 Configuration
.getSupervisionUrlDistribution()!
2379 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2380 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2381 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2384 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2387 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2390 configuredSupervisionUrl
= supervisionUrls
!
2392 if (isNotEmptyString(configuredSupervisionUrl
)) {
2393 return new URL(configuredSupervisionUrl
)
2395 const errorMsg
= 'No supervision url(s) configured'
2396 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2397 throw new BaseError(errorMsg
)
2400 private stopHeartbeat (): void {
2401 if (this.heartbeatSetInterval
!= null) {
2402 clearInterval(this.heartbeatSetInterval
)
2403 delete this.heartbeatSetInterval
2407 private terminateWSConnection (): void {
2408 if (this.isWebSocketConnectionOpened()) {
2409 this.wsConnection
?.terminate()
2410 this.wsConnection
= null
2414 private async reconnect (): Promise
<void> {
2416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2417 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2418 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2420 this.wsConnectionRetried
= true
2421 ++this.wsConnectionRetryCount
2422 const reconnectDelay
=
2423 this.stationInfo
?.reconnectExponentialDelay
=== true
2424 ? exponentialDelay(this.wsConnectionRetryCount
)
2425 : secondsToMilliseconds(this.getConnectionTimeout())
2426 const reconnectDelayWithdraw
= 1000
2427 const reconnectTimeout
=
2428 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2430 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2433 )}ms, timeout ${reconnectTimeout}ms`
2435 await sleep(reconnectDelay
)
2437 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2439 this.openWSConnection(
2441 handshakeTimeout
: reconnectTimeout
2443 { closeOpened
: true }
2445 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2447 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2448 this.wsConnectionRetryCount
2449 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`