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 prepareConnectorStatus
,
135 propagateSerialNumber
,
136 setChargingStationOptions
,
137 stationTemplateToStationInfo
,
138 warnTemplateKeysDeprecation
139 } from
'./Helpers.js'
140 import { IdTagsCache
} from
'./IdTagsCache.js'
143 buildTransactionEndMeterValue
,
144 getMessageTypeString
,
145 OCPP16IncomingRequestService
,
146 OCPP16RequestService
,
147 OCPP16ResponseService
,
148 OCPP20IncomingRequestService
,
149 OCPP20RequestService
,
150 OCPP20ResponseService
,
151 type OCPPIncomingRequestService
,
152 type OCPPRequestService
,
153 sendAndSetConnectorStatus
154 } from
'./ocpp/index.js'
155 import { SharedLRUCache
} from
'./SharedLRUCache.js'
157 export class ChargingStation
extends EventEmitter
{
158 public readonly index
: number
159 public readonly templateFile
: string
160 public stationInfo
?: ChargingStationInfo
161 public started
: boolean
162 public starting
: boolean
163 public idTagsCache
: IdTagsCache
164 public automaticTransactionGenerator
?: AutomaticTransactionGenerator
165 public ocppConfiguration
?: ChargingStationOcppConfiguration
166 public wsConnection
: WebSocket
| null
167 public readonly connectors
: Map
<number, ConnectorStatus
>
168 public readonly evses
: Map
<number, EvseStatus
>
169 public readonly requests
: Map
<string, CachedRequest
>
170 public performanceStatistics
?: PerformanceStatistics
171 public heartbeatSetInterval
?: NodeJS
.Timeout
172 public ocppRequestService
!: OCPPRequestService
173 public bootNotificationRequest
?: BootNotificationRequest
174 public bootNotificationResponse
?: BootNotificationResponse
175 public powerDivider
?: number
176 private stopping
: boolean
177 private configurationFile
!: string
178 private configurationFileHash
!: string
179 private connectorsConfigurationHash
!: string
180 private evsesConfigurationHash
!: string
181 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
182 private ocppIncomingRequestService
!: OCPPIncomingRequestService
183 private readonly messageBuffer
: Set
<string>
184 private configuredSupervisionUrl
!: URL
185 private 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.wsConnectionRetryCount
= 0
201 this.templateFile
= templateFile
202 this.connectors
= new Map
<number, ConnectorStatus
>()
203 this.evses
= new Map
<number, EvseStatus
>()
204 this.requests
= new Map
<string, CachedRequest
>()
205 this.messageBuffer
= new Set
<string>()
206 this.sharedLRUCache
= SharedLRUCache
.getInstance()
207 this.idTagsCache
= IdTagsCache
.getInstance()
208 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this)
210 this.on(ChargingStationEvents
.added
, () => {
211 parentPort
?.postMessage(buildAddedMessage(this))
213 this.on(ChargingStationEvents
.deleted
, () => {
214 parentPort
?.postMessage(buildDeletedMessage(this))
216 this.on(ChargingStationEvents
.started
, () => {
217 parentPort
?.postMessage(buildStartedMessage(this))
219 this.on(ChargingStationEvents
.stopped
, () => {
220 parentPort
?.postMessage(buildStoppedMessage(this))
222 this.on(ChargingStationEvents
.updated
, () => {
223 parentPort
?.postMessage(buildUpdatedMessage(this))
225 this.on(ChargingStationEvents
.accepted
, () => {
226 this.startMessageSequence(
227 this.wsConnectionRetryCount
> 0
229 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
230 ).catch((error
: unknown
) => {
231 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
233 this.wsConnectionRetryCount
= 0
235 this.on(ChargingStationEvents
.rejected
, () => {
236 this.wsConnectionRetryCount
= 0
238 this.on(ChargingStationEvents
.connected
, () => {
239 if (this.wsPingSetInterval
== null) {
240 this.startWebSocketPing()
243 this.on(ChargingStationEvents
.disconnected
, () => {
245 this.internalStopMessageSequence()
248 `${this.logPrefix()} Error while stopping the internal message sequence:`,
254 this.initialize(options
)
258 if (this.stationInfo
?.autoStart
=== true) {
263 public get
hasEvses (): boolean {
264 return this.connectors
.size
=== 0 && this.evses
.size
> 0
267 public get
wsConnectionUrl (): URL
{
268 const wsConnectionBaseUrlStr
= `${
269 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
270 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
271 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
272 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
273 : this.configuredSupervisionUrl.href
276 `${wsConnectionBaseUrlStr}${
277 !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
278 }${this.stationInfo?.chargingStationId}`
282 public logPrefix
= (): string => {
284 this instanceof ChargingStation
&&
285 this.stationInfo
!= null &&
286 isNotEmptyString(this.stationInfo
.chargingStationId
)
288 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
290 let stationTemplate
: ChargingStationTemplate
| undefined
292 stationTemplate
= JSON
.parse(
293 readFileSync(this.templateFile
, 'utf8')
294 ) as ChargingStationTemplate
298 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
301 public hasIdTags (): boolean {
302 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
303 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
306 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
307 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308 const localStationInfo
= stationInfo
?? this.stationInfo
!
309 switch (this.getCurrentOutType(stationInfo
)) {
311 return localStationInfo
.numberOfPhases
?? 3
317 public isWebSocketConnectionOpened (): boolean {
318 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
321 public inUnknownState (): boolean {
322 return this.bootNotificationResponse
?.status == null
325 public inPendingState (): boolean {
326 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
329 public inAcceptedState (): boolean {
330 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
333 public inRejectedState (): boolean {
334 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
337 public isRegistered (): boolean {
338 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
341 public isChargingStationAvailable (): boolean {
342 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
345 public hasConnector (connectorId
: number): boolean {
347 for (const evseStatus
of this.evses
.values()) {
348 if (evseStatus
.connectors
.has(connectorId
)) {
354 return this.connectors
.has(connectorId
)
357 public isConnectorAvailable (connectorId
: number): boolean {
360 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
364 public getNumberOfConnectors (): number {
366 let numberOfConnectors
= 0
367 for (const [evseId
, evseStatus
] of this.evses
) {
369 numberOfConnectors
+= evseStatus
.connectors
.size
372 return numberOfConnectors
374 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
377 public getNumberOfEvses (): number {
378 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
381 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
383 for (const evseStatus
of this.evses
.values()) {
384 if (evseStatus
.connectors
.has(connectorId
)) {
385 return evseStatus
.connectors
.get(connectorId
)
390 return this.connectors
.get(connectorId
)
393 public getConnectorMaximumAvailablePower (connectorId
: number): number {
394 let connectorAmperageLimitationPowerLimit
: number | undefined
395 const amperageLimitation
= this.getAmperageLimitation()
397 amperageLimitation
!= null &&
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
401 connectorAmperageLimitationPowerLimit
=
402 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
403 ? ACElectricUtils
.powerTotal(
404 this.getNumberOfPhases(),
405 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
406 this.stationInfo
.voltageOut
!,
408 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
410 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
411 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
412 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
417 const connectorChargingProfilesPowerLimit
=
418 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
420 isNaN(connectorMaximumPower
) ? Number.POSITIVE_INFINITY
: connectorMaximumPower
,
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 isNaN(connectorAmperageLimitationPowerLimit
!)
423 ? Number.POSITIVE_INFINITY
424 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
425 connectorAmperageLimitationPowerLimit
!,
426 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
427 isNaN(connectorChargingProfilesPowerLimit
!)
428 ? Number.POSITIVE_INFINITY
429 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
430 connectorChargingProfilesPowerLimit
!
434 public getTransactionIdTag (transactionId
: number): string | undefined {
436 for (const evseStatus
of this.evses
.values()) {
437 for (const connectorStatus
of evseStatus
.connectors
.values()) {
438 if (connectorStatus
.transactionId
=== transactionId
) {
439 return connectorStatus
.transactionIdTag
444 for (const connectorId
of this.connectors
.keys()) {
445 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
446 return this.getConnectorStatus(connectorId
)?.transactionIdTag
452 public getNumberOfRunningTransactions (): number {
453 let numberOfRunningTransactions
= 0
455 for (const [evseId
, evseStatus
] of this.evses
) {
459 for (const connectorStatus
of evseStatus
.connectors
.values()) {
460 if (connectorStatus
.transactionStarted
=== true) {
461 ++numberOfRunningTransactions
466 for (const connectorId
of this.connectors
.keys()) {
467 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
468 ++numberOfRunningTransactions
472 return numberOfRunningTransactions
475 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
476 if (transactionId
== null) {
478 } else if (this.hasEvses
) {
479 for (const evseStatus
of this.evses
.values()) {
480 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
481 if (connectorStatus
.transactionId
=== transactionId
) {
487 for (const connectorId
of this.connectors
.keys()) {
488 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
495 public getEnergyActiveImportRegisterByTransactionId (
496 transactionId
: number | undefined,
499 return this.getEnergyActiveImportRegister(
500 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
501 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
506 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
507 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
510 public getAuthorizeRemoteTxRequests (): boolean {
511 const authorizeRemoteTxRequests
= getConfigurationKey(
513 StandardParametersKey
.AuthorizeRemoteTxRequests
515 return authorizeRemoteTxRequests
!= null
516 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
520 public getLocalAuthListEnabled (): boolean {
521 const localAuthListEnabled
= getConfigurationKey(
523 StandardParametersKey
.LocalAuthListEnabled
525 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
528 public getHeartbeatInterval (): number {
529 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
530 if (HeartbeatInterval
!= null) {
531 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
533 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
534 if (HeartBeatInterval
!= null) {
535 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
537 this.stationInfo
?.autoRegister
=== false &&
539 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
540 Constants.DEFAULT_HEARTBEAT_INTERVAL
543 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
546 public setSupervisionUrl (url
: string): void {
548 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
549 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
551 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
553 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
554 this.stationInfo
!.supervisionUrls
= url
555 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
556 this.saveStationInfo()
560 public startHeartbeat (): void {
561 const heartbeatInterval
= this.getHeartbeatInterval()
562 if (heartbeatInterval
> 0 && this.heartbeatSetInterval
== null) {
563 this.heartbeatSetInterval
= setInterval(() => {
564 this.ocppRequestService
565 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
566 .catch((error
: unknown
) => {
568 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
572 }, heartbeatInterval
)
574 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
578 } else if (this.heartbeatSetInterval
!= null) {
580 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
586 `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat`
591 public restartHeartbeat (): void {
595 this.startHeartbeat()
598 public restartWebSocketPing (): void {
599 // Stop WebSocket ping
600 this.stopWebSocketPing()
601 // Start WebSocket ping
602 this.startWebSocketPing()
605 public startMeterValues (connectorId
: number, interval
: number): void {
606 if (connectorId
=== 0) {
607 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
610 const connectorStatus
= this.getConnectorStatus(connectorId
)
611 if (connectorStatus
== null) {
613 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
618 if (connectorStatus
.transactionStarted
=== false) {
620 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
624 connectorStatus
.transactionStarted
=== true &&
625 connectorStatus
.transactionId
== null
628 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
633 connectorStatus
.transactionSetInterval
= setInterval(() => {
634 const meterValue
= buildMeterValue(
637 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
638 connectorStatus
.transactionId
!,
641 this.ocppRequestService
642 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
644 RequestCommand
.METER_VALUES
,
647 transactionId
: connectorStatus
.transactionId
,
648 meterValue
: [meterValue
]
651 .catch((error
: unknown
) => {
653 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
660 `${this.logPrefix()} Charging station ${
661 StandardParametersKey.MeterValueSampleInterval
662 } configuration set to ${interval}, not sending MeterValues`
667 public stopMeterValues (connectorId
: number): void {
668 const connectorStatus
= this.getConnectorStatus(connectorId
)
669 if (connectorStatus
?.transactionSetInterval
!= null) {
670 clearInterval(connectorStatus
.transactionSetInterval
)
674 public restartMeterValues (connectorId
: number, interval
: number): void {
675 this.stopMeterValues(connectorId
)
676 this.startMeterValues(connectorId
, interval
)
679 private add (): void {
680 this.emit(ChargingStationEvents
.added
)
683 public async delete (deleteConfiguration
= true): Promise
<void> {
687 AutomaticTransactionGenerator
.deleteInstance(this)
688 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
689 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
690 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
691 this.requests
.clear()
692 this.connectors
.clear()
694 this.templateFileWatcher
?.unref()
695 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
696 this.chargingStationWorkerBroadcastChannel
.unref()
697 this.emit(ChargingStationEvents
.deleted
)
698 this.removeAllListeners()
701 public start (): void {
703 if (!this.starting
) {
705 if (this.stationInfo
?.enableStatistics
=== true) {
706 this.performanceStatistics
?.start()
708 this.openWSConnection()
709 // Monitor charging station template file
710 this.templateFileWatcher
= watchJsonFile(
712 FileType
.ChargingStationTemplate
,
715 (event
, filename
): void => {
716 if (isNotEmptyString(filename
) && event
=== 'change') {
719 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
721 } file have changed, reload`
723 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
724 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
725 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
729 const ATGStarted
= this.automaticTransactionGenerator
?.started
730 if (ATGStarted
=== true) {
731 this.stopAutomaticTransactionGenerator()
733 delete this.automaticTransactionGeneratorConfiguration
735 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
738 this.startAutomaticTransactionGenerator(undefined, true)
740 if (this.stationInfo
?.enableStatistics
=== true) {
741 this.performanceStatistics
?.restart()
743 this.performanceStatistics
?.stop()
745 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
748 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
756 this.emit(ChargingStationEvents
.started
)
757 this.starting
= false
759 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
762 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
767 reason
?: StopTransactionReason
,
768 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
771 if (!this.stopping
) {
773 await this.stopMessageSequence(reason
, stopTransactions
)
774 this.closeWSConnection()
775 if (this.stationInfo
?.enableStatistics
=== true) {
776 this.performanceStatistics
?.stop()
778 this.templateFileWatcher
?.close()
779 delete this.bootNotificationResponse
781 this.saveConfiguration()
782 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
783 this.emit(ChargingStationEvents
.stopped
)
784 this.stopping
= false
786 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
789 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
793 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
794 await this.stop(reason
)
795 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
796 await sleep(this.stationInfo
!.resetTime
!)
801 public saveOcppConfiguration (): void {
802 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
803 this.saveConfiguration()
807 public bufferMessage (message
: string): void {
808 this.messageBuffer
.add(message
)
809 this.setIntervalFlushMessageBuffer()
812 public openWSConnection (
814 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
817 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
818 ...this.stationInfo
?.wsOptions
,
821 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
822 if (!checkChargingStation(this, this.logPrefix())) {
825 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
826 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
828 if (params
.closeOpened
=== true) {
829 this.closeWSConnection()
831 if (params
.terminateOpened
=== true) {
832 this.terminateWSConnection()
835 if (this.isWebSocketConnectionOpened()) {
837 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
842 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
844 this.wsConnection
= new WebSocket(
845 this.wsConnectionUrl
,
846 `ocpp${this.stationInfo?.ocppVersion}`,
850 // Handle WebSocket message
851 this.wsConnection
.on('message', data
=> {
852 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
854 // Handle WebSocket error
855 this.wsConnection
.on('error', this.onError
.bind(this))
856 // Handle WebSocket close
857 this.wsConnection
.on('close', this.onClose
.bind(this))
858 // Handle WebSocket open
859 this.wsConnection
.on('open', () => {
860 this.onOpen().catch((error
: unknown
) =>
861 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
864 // Handle WebSocket ping
865 this.wsConnection
.on('ping', this.onPing
.bind(this))
866 // Handle WebSocket pong
867 this.wsConnection
.on('pong', this.onPong
.bind(this))
870 public closeWSConnection (): void {
871 if (this.isWebSocketConnectionOpened()) {
872 this.wsConnection
?.close()
873 this.wsConnection
= null
877 public getAutomaticTransactionGeneratorConfiguration ():
878 | AutomaticTransactionGeneratorConfiguration
880 if (this.automaticTransactionGeneratorConfiguration
== null) {
881 let automaticTransactionGeneratorConfiguration
:
882 | AutomaticTransactionGeneratorConfiguration
884 const stationTemplate
= this.getTemplateFromFile()
885 const stationConfiguration
= this.getConfigurationFromFile()
887 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
888 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
889 stationConfiguration
?.automaticTransactionGenerator
!= null
891 automaticTransactionGeneratorConfiguration
=
892 stationConfiguration
.automaticTransactionGenerator
894 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
896 this.automaticTransactionGeneratorConfiguration
= {
897 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
898 ...automaticTransactionGeneratorConfiguration
901 return this.automaticTransactionGeneratorConfiguration
904 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
905 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
908 public startAutomaticTransactionGenerator (
909 connectorIds
?: number[],
910 stopAbsoluteDuration
?: boolean
912 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
913 if (isNotEmptyArray(connectorIds
)) {
914 for (const connectorId
of connectorIds
) {
915 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
918 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
920 this.saveAutomaticTransactionGeneratorConfiguration()
921 this.emit(ChargingStationEvents
.updated
)
924 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
925 if (isNotEmptyArray(connectorIds
)) {
926 for (const connectorId
of connectorIds
) {
927 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
930 this.automaticTransactionGenerator
?.stop()
932 this.saveAutomaticTransactionGeneratorConfiguration()
933 this.emit(ChargingStationEvents
.updated
)
936 public async stopTransactionOnConnector (
938 reason
?: StopTransactionReason
939 ): Promise
<StopTransactionResponse
> {
940 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
942 this.stationInfo
?.beginEndMeterValues
=== true &&
943 this.stationInfo
.ocppStrictCompliance
=== true &&
944 this.stationInfo
.outOfOrderEndMeterValues
=== false
946 const transactionEndMeterValue
= buildTransactionEndMeterValue(
949 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
951 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
953 RequestCommand
.METER_VALUES
,
957 meterValue
: [transactionEndMeterValue
]
961 return await this.ocppRequestService
.requestHandler
<
962 Partial
<StopTransactionRequest
>,
963 StopTransactionResponse
964 >(this, RequestCommand
.STOP_TRANSACTION
, {
966 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
967 ...(reason
!= null && { reason
})
971 public getReserveConnectorZeroSupported (): boolean {
972 return convertToBoolean(
973 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
974 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
978 public async addReservation (reservation
: Reservation
): Promise
<void> {
979 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
980 if (reservationFound
!= null) {
981 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
983 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
984 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
985 await sendAndSetConnectorStatus(
987 reservation
.connectorId
,
988 ConnectorStatusEnum
.Reserved
,
990 { send
: reservation
.connectorId
!== 0 }
994 public async removeReservation (
995 reservation
: Reservation
,
996 reason
: ReservationTerminationReason
998 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
999 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
1001 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
1002 case ReservationTerminationReason
.TRANSACTION_STARTED
:
1003 delete connector
.reservation
1005 case ReservationTerminationReason
.RESERVATION_CANCELED
:
1006 case ReservationTerminationReason
.REPLACE_EXISTING
:
1007 case ReservationTerminationReason
.EXPIRED
:
1008 await sendAndSetConnectorStatus(
1010 reservation
.connectorId
,
1011 ConnectorStatusEnum
.Available
,
1013 { send
: reservation
.connectorId
!== 0 }
1015 delete connector
.reservation
1018 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1019 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1023 public getReservationBy (
1024 filterKey
: ReservationKey
,
1025 value
: number | string
1026 ): Reservation
| undefined {
1027 if (this.hasEvses
) {
1028 for (const evseStatus
of this.evses
.values()) {
1029 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1030 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1031 return connectorStatus
.reservation
1036 for (const connectorStatus
of this.connectors
.values()) {
1037 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1038 return connectorStatus
.reservation
1044 public isConnectorReservable (
1045 reservationId
: number,
1047 connectorId
?: number
1049 const reservation
= this.getReservationBy('reservationId', reservationId
)
1050 const reservationExists
= reservation
!= null && !hasReservationExpired(reservation
)
1051 if (arguments.length
=== 1) {
1052 return !reservationExists
1053 } else if (arguments.length
> 1) {
1054 const userReservation
= idTag
!= null ? this.getReservationBy('idTag', idTag
) : undefined
1055 const userReservationExists
=
1056 userReservation
!= null && !hasReservationExpired(userReservation
)
1057 const notConnectorZero
= connectorId
== null ? true : connectorId
> 0
1058 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1060 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1066 private setIntervalFlushMessageBuffer (): void {
1067 if (this.flushMessageBufferSetInterval
== null) {
1068 this.flushMessageBufferSetInterval
= setInterval(() => {
1069 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1070 this.flushMessageBuffer()
1072 if (this.messageBuffer
.size
=== 0) {
1073 this.clearIntervalFlushMessageBuffer()
1075 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1079 private clearIntervalFlushMessageBuffer (): void {
1080 if (this.flushMessageBufferSetInterval
!= null) {
1081 clearInterval(this.flushMessageBufferSetInterval
)
1082 delete this.flushMessageBufferSetInterval
1086 private getNumberOfReservableConnectors (): number {
1087 let numberOfReservableConnectors
= 0
1088 if (this.hasEvses
) {
1089 for (const evseStatus
of this.evses
.values()) {
1090 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1093 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1095 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1098 private getNumberOfReservationsOnConnectorZero (): number {
1100 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1101 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1108 private flushMessageBuffer (): void {
1109 if (this.messageBuffer
.size
> 0) {
1110 for (const message
of this.messageBuffer
.values()) {
1111 let beginId
: string | undefined
1112 let commandName
: RequestCommand
| undefined
1113 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1114 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1116 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1117 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1119 this.wsConnection
?.send(message
, (error
?: Error) => {
1120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1121 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1122 if (error
== null) {
1124 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1126 )} OCPP message sent '${JSON.stringify(message)}'`
1128 this.messageBuffer
.delete(message
)
1131 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1133 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1142 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1143 let template
: ChargingStationTemplate
| undefined
1145 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1146 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1148 const measureId
= `${FileType.ChargingStationTemplate} read`
1149 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1150 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1151 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1152 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1153 .update(JSON
.stringify(template
))
1155 this.sharedLRUCache
.setChargingStationTemplate(template
)
1156 this.templateFileHash
= template
.templateHash
1159 handleFileException(
1161 FileType
.ChargingStationTemplate
,
1162 error
as NodeJS
.ErrnoException
,
1169 private getStationInfoFromTemplate (): ChargingStationInfo
{
1170 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1171 const stationTemplate
= this.getTemplateFromFile()!
1172 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1173 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
)
1174 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1175 if (stationTemplate
.Connectors
!= null) {
1176 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1178 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1179 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1180 stationInfo
.templateIndex
= this.index
1181 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1182 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1183 createSerialNumber(stationTemplate
, stationInfo
)
1184 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1185 if (isNotEmptyArray(stationTemplate
.power
)) {
1186 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1187 stationInfo
.maximumPower
=
1188 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1189 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1190 : stationTemplate
.power
[powerArrayRandomIndex
]
1192 stationInfo
.maximumPower
=
1193 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1194 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1195 stationTemplate
.power
! * 1000
1196 : stationTemplate
.power
1198 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1200 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1201 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1202 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1205 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1207 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1210 if (stationTemplate
.resetTime
!= null) {
1211 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1216 private getStationInfoFromFile (
1217 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1218 .stationInfoPersistentConfiguration
1219 ): ChargingStationInfo
| undefined {
1220 let stationInfo
: ChargingStationInfo
| undefined
1221 if (stationInfoPersistentConfiguration
=== true) {
1222 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1223 if (stationInfo
!= null) {
1224 delete stationInfo
.infoHash
1225 delete (stationInfo
as ChargingStationTemplate
).numberOfConnectors
1226 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1227 if (stationInfo
.templateIndex
== null) {
1228 stationInfo
.templateIndex
= this.index
1230 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1231 if (stationInfo
.templateName
== null) {
1232 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1239 private getStationInfo (options
?: ChargingStationOptions
): ChargingStationInfo
{
1240 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1241 options
?.persistentConfiguration
!= null &&
1242 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
)
1243 const stationInfoFromFile
= this.getStationInfoFromFile(
1244 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1246 let stationInfo
: ChargingStationInfo
1248 // 1. charging station info from template
1249 // 2. charging station info from configuration file
1251 stationInfoFromFile
!= null &&
1252 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1254 stationInfo
= stationInfoFromFile
1256 stationInfo
= stationInfoFromTemplate
1257 stationInfoFromFile
!= null &&
1258 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile
, stationInfo
)
1260 return setChargingStationOptions(
1261 mergeDeepRight(Constants
.DEFAULT_STATION_INFO
, stationInfo
),
1266 private saveStationInfo (): void {
1267 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1268 this.saveConfiguration()
1272 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1273 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1274 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1275 throw new BaseError(errorMsg
)
1278 private initialize (options
?: ChargingStationOptions
): void {
1279 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1280 const stationTemplate
= this.getTemplateFromFile()!
1281 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1282 this.configurationFile
= join(
1283 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1284 `${getHashId(this.index, stationTemplate)}.json`
1286 const stationConfiguration
= this.getConfigurationFromFile()
1288 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1289 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1291 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1292 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1294 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1296 this.stationInfo
= this.getStationInfo(options
)
1298 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1299 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1300 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1302 const patternGroup
=
1303 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1304 this.stationInfo
.firmwareVersion
.split('.').length
1305 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1306 .exec(this.stationInfo
.firmwareVersion
)
1307 ?.slice(1, patternGroup
+ 1)
1308 if (match
!= null) {
1309 const patchLevelIndex
= match
.length
- 1
1310 match
[patchLevelIndex
] = (
1311 convertToInt(match
[patchLevelIndex
]) +
1312 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1313 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1315 this.stationInfo
.firmwareVersion
= match
.join('.')
1318 this.saveStationInfo()
1319 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1320 if (this.stationInfo
.enableStatistics
=== true) {
1321 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1322 this.stationInfo
.hashId
,
1323 this.stationInfo
.chargingStationId
,
1324 this.configuredSupervisionUrl
1327 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1328 if (bootNotificationRequest
== null) {
1329 const errorMsg
= 'Error while creating boot notification request'
1330 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1331 throw new BaseError(errorMsg
)
1333 this.bootNotificationRequest
= bootNotificationRequest
1334 this.powerDivider
= this.getPowerDivider()
1335 // OCPP configuration
1336 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1337 this.initializeOcppConfiguration()
1338 this.initializeOcppServices()
1339 if (this.stationInfo
.autoRegister
=== true) {
1340 this.bootNotificationResponse
= {
1341 currentTime
: new Date(),
1342 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1343 status: RegistrationStatusEnumType
.ACCEPTED
1348 private initializeOcppServices (): void {
1349 const ocppVersion
= this.stationInfo
?.ocppVersion
1350 switch (ocppVersion
) {
1351 case OCPPVersion
.VERSION_16
:
1352 this.ocppIncomingRequestService
=
1353 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1354 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1355 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1358 case OCPPVersion
.VERSION_20
:
1359 case OCPPVersion
.VERSION_201
:
1360 this.ocppIncomingRequestService
=
1361 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1362 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1363 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1367 this.handleUnsupportedVersion(ocppVersion
)
1372 private initializeOcppConfiguration (): void {
1373 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1374 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1376 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1377 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', {
1382 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1383 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1384 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1386 addConfigurationKey(
1388 this.stationInfo
.supervisionUrlOcppKey
,
1389 this.configuredSupervisionUrl
.href
,
1393 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1394 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1395 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1397 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, {
1402 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1403 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1405 addConfigurationKey(
1407 this.stationInfo
.amperageLimitationOcppKey
,
1409 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1410 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1413 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1414 addConfigurationKey(
1416 StandardParametersKey
.SupportedFeatureProfiles
,
1417 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1420 addConfigurationKey(
1422 StandardParametersKey
.NumberOfConnectors
,
1423 this.getNumberOfConnectors().toString(),
1427 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1428 addConfigurationKey(
1430 StandardParametersKey
.MeterValuesSampledData
,
1431 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1434 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1435 const connectorsPhaseRotation
: string[] = []
1436 if (this.hasEvses
) {
1437 for (const evseStatus
of this.evses
.values()) {
1438 for (const connectorId
of evseStatus
.connectors
.keys()) {
1439 connectorsPhaseRotation
.push(
1440 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1441 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1446 for (const connectorId
of this.connectors
.keys()) {
1447 connectorsPhaseRotation
.push(
1448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1449 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1453 addConfigurationKey(
1455 StandardParametersKey
.ConnectorPhaseRotation
,
1456 connectorsPhaseRotation
.toString()
1459 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1460 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1463 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1464 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1466 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1468 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1469 addConfigurationKey(
1471 StandardParametersKey
.ConnectionTimeOut
,
1472 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1475 this.saveOcppConfiguration()
1478 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1479 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1480 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1481 this.connectors
.set(
1483 prepareConnectorStatus(clone
<ConnectorStatus
>(connectorStatus
))
1486 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1487 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1488 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1489 delete evseStatus
.connectorsStatus
1490 this.evses
.set(evseId
, {
1491 ...(evseStatus
as EvseStatus
),
1492 connectors
: new Map
<number, ConnectorStatus
>(
1493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1494 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1496 prepareConnectorStatus(connectorStatus
)
1501 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1502 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1503 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1504 throw new BaseError(errorMsg
)
1506 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1507 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1508 throw new BaseError(errorMsg
)
1512 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1513 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1514 this.initializeConnectorsFromTemplate(stationTemplate
)
1515 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1516 this.initializeEvsesFromTemplate(stationTemplate
)
1517 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1518 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1519 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1520 throw new BaseError(errorMsg
)
1522 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1523 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1524 throw new BaseError(errorMsg
)
1528 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1529 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1530 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1531 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1532 throw new BaseError(errorMsg
)
1534 if (stationTemplate
.Connectors
?.[0] == null) {
1536 `${this.logPrefix()} Charging station information from template ${
1538 } with no connector id 0 configuration`
1541 if (stationTemplate
.Connectors
!= null) {
1542 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1543 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1544 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1546 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1549 const connectorsConfigChanged
=
1550 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1551 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1552 connectorsConfigChanged
&& this.connectors
.clear()
1553 this.connectorsConfigurationHash
= connectorsConfigHash
1554 if (templateMaxConnectors
> 0) {
1555 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1557 connectorId
=== 0 &&
1558 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1559 (stationTemplate
.Connectors
[connectorId
] == null ||
1560 !this.getUseConnectorId0(stationTemplate
))
1564 const templateConnectorId
=
1565 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1566 ? randomInt(1, templateMaxAvailableConnectors
)
1568 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1569 checkStationInfoConnectorStatus(
1570 templateConnectorId
,
1575 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1577 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1578 this.saveConnectorsStatus()
1581 `${this.logPrefix()} Charging station information from template ${
1583 } with no connectors configuration defined, cannot create connectors`
1589 `${this.logPrefix()} Charging station information from template ${
1591 } with no connectors configuration defined, using already defined connectors`
1596 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1597 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1598 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1599 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1600 throw new BaseError(errorMsg
)
1602 if (stationTemplate
.Evses
?.[0] == null) {
1604 `${this.logPrefix()} Charging station information from template ${
1606 } with no evse id 0 configuration`
1609 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1611 `${this.logPrefix()} Charging station information from template ${
1613 } with evse id 0 with no connector id 0 configuration`
1616 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1618 `${this.logPrefix()} Charging station information from template ${
1620 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1623 if (stationTemplate
.Evses
!= null) {
1624 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1625 .update(JSON
.stringify(stationTemplate
.Evses
))
1627 const evsesConfigChanged
=
1628 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1629 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1630 evsesConfigChanged
&& this.evses
.clear()
1631 this.evsesConfigurationHash
= evsesConfigHash
1632 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1633 if (templateMaxEvses
> 0) {
1634 for (const evseKey
in stationTemplate
.Evses
) {
1635 const evseId
= convertToInt(evseKey
)
1636 this.evses
.set(evseId
, {
1637 connectors
: buildConnectorsMap(
1638 stationTemplate
.Evses
[evseKey
].Connectors
,
1642 availability
: AvailabilityType
.Operative
1644 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1645 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1647 this.saveEvsesStatus()
1650 `${this.logPrefix()} Charging station information from template ${
1652 } with no evses configuration defined, cannot create evses`
1658 `${this.logPrefix()} Charging station information from template ${
1660 } with no evses configuration defined, using already defined evses`
1665 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1666 let configuration
: ChargingStationConfiguration
| undefined
1667 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1669 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1670 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1671 this.configurationFileHash
1674 const measureId
= `${FileType.ChargingStationConfiguration} read`
1675 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1676 configuration
= JSON
.parse(
1677 readFileSync(this.configurationFile
, 'utf8')
1678 ) as ChargingStationConfiguration
1679 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1680 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1681 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1682 this.configurationFileHash
= configuration
.configurationHash
!
1685 handleFileException(
1686 this.configurationFile
,
1687 FileType
.ChargingStationConfiguration
,
1688 error
as NodeJS
.ErrnoException
,
1693 return configuration
1696 private saveAutomaticTransactionGeneratorConfiguration (): void {
1697 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1698 this.saveConfiguration()
1702 private saveConnectorsStatus (): void {
1703 this.saveConfiguration()
1706 private saveEvsesStatus (): void {
1707 this.saveConfiguration()
1710 private saveConfiguration (): void {
1711 if (isNotEmptyString(this.configurationFile
)) {
1713 if (!existsSync(dirname(this.configurationFile
))) {
1714 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1716 const configurationFromFile
= this.getConfigurationFromFile()
1717 let configurationData
: ChargingStationConfiguration
=
1718 configurationFromFile
!= null
1719 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1721 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1722 configurationData
.stationInfo
= this.stationInfo
1724 delete configurationData
.stationInfo
1727 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1728 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1730 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1732 delete configurationData
.configurationKey
1734 configurationData
= mergeDeepRight(
1736 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1738 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1739 delete configurationData
.automaticTransactionGenerator
1741 if (this.connectors
.size
> 0) {
1742 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1744 delete configurationData
.connectorsStatus
1746 if (this.evses
.size
> 0) {
1747 configurationData
.evsesStatus
= buildEvsesStatus(this)
1749 delete configurationData
.evsesStatus
1751 delete configurationData
.configurationHash
1752 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1755 stationInfo
: configurationData
.stationInfo
,
1756 configurationKey
: configurationData
.configurationKey
,
1757 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1758 ...(this.connectors
.size
> 0 && {
1759 connectorsStatus
: configurationData
.connectorsStatus
1761 ...(this.evses
.size
> 0 && {
1762 evsesStatus
: configurationData
.evsesStatus
1764 } satisfies ChargingStationConfiguration
)
1767 if (this.configurationFileHash
!== configurationHash
) {
1768 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1769 configurationData
.configurationHash
= configurationHash
1770 const measureId
= `${FileType.ChargingStationConfiguration} write`
1771 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1773 this.configurationFile
,
1774 JSON
.stringify(configurationData
, undefined, 2),
1777 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1778 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1779 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1780 this.configurationFileHash
= configurationHash
1781 }).catch((error
: unknown
) => {
1782 handleFileException(
1783 this.configurationFile
,
1784 FileType
.ChargingStationConfiguration
,
1785 error
as NodeJS
.ErrnoException
,
1791 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1792 this.configurationFile
1797 handleFileException(
1798 this.configurationFile
,
1799 FileType
.ChargingStationConfiguration
,
1800 error
as NodeJS
.ErrnoException
,
1806 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1811 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1812 return this.getTemplateFromFile()?.Configuration
1815 private getOcppConfigurationFromFile (
1816 ocppPersistentConfiguration
?: boolean
1817 ): ChargingStationOcppConfiguration
| undefined {
1818 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1819 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1820 return { configurationKey
}
1825 private getOcppConfiguration (
1826 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1827 ): ChargingStationOcppConfiguration
| undefined {
1828 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1829 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1830 if (ocppConfiguration
== null) {
1831 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1833 return ocppConfiguration
1836 private async onOpen (): Promise
<void> {
1837 if (this.isWebSocketConnectionOpened()) {
1838 this.emit(ChargingStationEvents
.connected
)
1839 this.emit(ChargingStationEvents
.updated
)
1841 `${this.logPrefix()} Connection to OCPP server through ${
1842 this.wsConnectionUrl.href
1845 let registrationRetryCount
= 0
1846 if (!this.isRegistered()) {
1847 // Send BootNotification
1849 await this.ocppRequestService
.requestHandler
<
1850 BootNotificationRequest
,
1851 BootNotificationResponse
1852 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1853 skipBufferingOnError
: true
1855 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1856 this.bootNotificationResponse
!.currentTime
= convertToDate(
1857 this.bootNotificationResponse
?.currentTime
1859 if (!this.isRegistered()) {
1860 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1862 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1863 this.bootNotificationResponse
?.interval
!= null
1864 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1865 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1869 !this.isRegistered() &&
1870 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1871 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1872 this.stationInfo
?.registrationMaxRetries
=== -1)
1875 if (!this.isRegistered()) {
1877 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1878 this.stationInfo?.registrationMaxRetries
1882 this.emit(ChargingStationEvents
.updated
)
1885 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1890 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1891 this.emit(ChargingStationEvents
.disconnected
)
1892 this.emit(ChargingStationEvents
.updated
)
1895 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1896 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1898 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1900 )}' and reason '${reason.toString()}'`
1902 this.wsConnectionRetryCount
= 0
1907 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1909 )}' and reason '${reason.toString()}'`
1914 this.emit(ChargingStationEvents
.updated
)
1916 .catch((error
: unknown
) =>
1917 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1923 private getCachedRequest (
1924 messageType
: MessageType
| undefined,
1926 ): CachedRequest
| undefined {
1927 const cachedRequest
= this.requests
.get(messageId
)
1928 if (Array.isArray(cachedRequest
)) {
1929 return cachedRequest
1931 throw new OCPPError(
1932 ErrorType
.PROTOCOL_ERROR
,
1933 `Cached request for message id '${messageId}' ${getMessageTypeString(
1935 )} is not an array`,
1941 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1942 const [messageType
, messageId
, commandName
, commandPayload
] = request
1943 if (this.requests
.has(messageId
)) {
1944 throw new OCPPError(
1945 ErrorType
.SECURITY_ERROR
,
1946 `Received message with duplicate message id '${messageId}'`,
1951 if (this.stationInfo
?.enableStatistics
=== true) {
1952 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1955 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1959 // Process the message
1960 await this.ocppIncomingRequestService
.incomingRequestHandler(
1966 this.emit(ChargingStationEvents
.updated
)
1969 private handleResponseMessage (response
: Response
): void {
1970 const [messageType
, messageId
, commandPayload
] = response
1971 if (!this.requests
.has(messageId
)) {
1973 throw new OCPPError(
1974 ErrorType
.INTERNAL_ERROR
,
1975 `Response for unknown message id '${messageId}'`,
1981 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1982 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1987 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1991 responseCallback(commandPayload
, requestPayload
)
1994 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
1995 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
1996 if (!this.requests
.has(messageId
)) {
1998 throw new OCPPError(
1999 ErrorType
.INTERNAL_ERROR
,
2000 `Error response for unknown message id '${messageId}'`,
2002 { errorType
, errorMessage
, errorDetails
}
2005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2006 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2008 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
2012 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
2015 private async onMessage (data
: RawData
): Promise
<void> {
2016 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
2017 let messageType
: MessageType
| undefined
2018 let errorMsg
: string
2020 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2021 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
2022 if (Array.isArray(request
)) {
2023 [messageType
] = request
2024 // Check the type of message
2025 switch (messageType
) {
2027 case MessageType
.CALL_MESSAGE
:
2028 await this.handleIncomingMessage(request
as IncomingRequest
)
2031 case MessageType
.CALL_RESULT_MESSAGE
:
2032 this.handleResponseMessage(request
as Response
)
2035 case MessageType
.CALL_ERROR_MESSAGE
:
2036 this.handleErrorMessage(request
as ErrorResponse
)
2040 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2041 errorMsg
= `Wrong message type ${messageType}`
2042 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2043 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2046 throw new OCPPError(
2047 ErrorType
.PROTOCOL_ERROR
,
2048 'Incoming message is not an array',
2056 if (!Array.isArray(request
)) {
2057 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2060 let commandName
: IncomingRequestCommand
| undefined
2061 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2062 let errorCallback
: ErrorCallback
2063 const [, messageId
] = request
2064 switch (messageType
) {
2065 case MessageType
.CALL_MESSAGE
:
2066 [, , commandName
] = request
as IncomingRequest
2068 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2070 case MessageType
.CALL_RESULT_MESSAGE
:
2071 case MessageType
.CALL_ERROR_MESSAGE
:
2072 if (this.requests
.has(messageId
)) {
2073 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2074 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2075 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2076 errorCallback(error
as OCPPError
, false)
2078 // Remove the request from the cache in case of error at response handling
2079 this.requests
.delete(messageId
)
2083 if (!(error
instanceof OCPPError
)) {
2085 `${this.logPrefix()} Error thrown at incoming OCPP command ${
2086 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2087 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2088 } message '${data.toString()}' handling is not an OCPPError:`,
2093 `${this.logPrefix()} Incoming OCPP command '${
2094 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2095 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2096 }' message '${data.toString()}'${
2097 this.requests.has(messageId)
2098 ? ` matching cached request
'${JSON.stringify(
2099 this.getCachedRequest(messageType, messageId)
2102 } processing error:`,
2108 private onPing (): void {
2109 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2112 private onPong (): void {
2113 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2116 private onError (error
: WSError
): void {
2117 this.closeWSConnection()
2118 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2121 private getEnergyActiveImportRegister (
2122 connectorStatus
: ConnectorStatus
| undefined,
2125 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2128 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2129 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2131 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2136 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2137 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2139 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2143 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2145 return stationTemplate
?.useConnectorId0
?? Constants
.DEFAULT_STATION_INFO
.useConnectorId0
!
2148 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2149 if (this.hasEvses
) {
2150 for (const [evseId
, evseStatus
] of this.evses
) {
2154 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2155 if (connectorStatus
.transactionStarted
=== true) {
2156 await this.stopTransactionOnConnector(connectorId
, reason
)
2161 for (const connectorId
of this.connectors
.keys()) {
2162 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2163 await this.stopTransactionOnConnector(connectorId
, reason
)
2170 private getConnectionTimeout (): number {
2171 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2172 return convertToInt(
2173 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2174 Constants
.DEFAULT_CONNECTION_TIMEOUT
2177 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2180 private getPowerDivider (): number {
2181 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2182 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2183 powerDivider
= this.getNumberOfRunningTransactions()
2188 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2189 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2190 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2191 switch (this.getCurrentOutType(stationInfo
)) {
2192 case CurrentType
.AC
:
2193 return ACElectricUtils
.amperagePerPhaseFromPower(
2194 this.getNumberOfPhases(stationInfo
),
2195 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2196 this.getVoltageOut(stationInfo
)
2198 case CurrentType
.DC
:
2199 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2203 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2205 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2206 (stationInfo
?? this.stationInfo
!).currentOutType
??
2207 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2208 Constants
.DEFAULT_STATION_INFO
.currentOutType
!
2212 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2214 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2215 (stationInfo
?? this.stationInfo
!).voltageOut
??
2216 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2220 private getAmperageLimitation (): number | undefined {
2222 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2223 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2226 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2227 getAmperageLimitationUnitDivider(this.stationInfo
)
2232 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2233 if (this.stationInfo
?.autoRegister
=== true) {
2234 await this.ocppRequestService
.requestHandler
<
2235 BootNotificationRequest
,
2236 BootNotificationResponse
2237 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2238 skipBufferingOnError
: true
2241 // Start WebSocket ping
2242 if (this.wsPingSetInterval
== null) {
2243 this.startWebSocketPing()
2246 if (this.heartbeatSetInterval
== null) {
2247 this.startHeartbeat()
2249 // Initialize connectors status
2250 if (this.hasEvses
) {
2251 for (const [evseId
, evseStatus
] of this.evses
) {
2253 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2254 await sendAndSetConnectorStatus(
2257 getBootConnectorStatus(this, connectorId
, connectorStatus
),
2264 for (const connectorId
of this.connectors
.keys()) {
2265 if (connectorId
> 0) {
2266 await sendAndSetConnectorStatus(
2269 getBootConnectorStatus(
2272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2273 this.getConnectorStatus(connectorId
)!
2279 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2280 await this.ocppRequestService
.requestHandler
<
2281 FirmwareStatusNotificationRequest
,
2282 FirmwareStatusNotificationResponse
2283 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2284 status: FirmwareStatus
.Installed
2286 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
2290 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2291 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration
)
2293 this.flushMessageBuffer()
2296 private internalStopMessageSequence (): void {
2297 // Stop WebSocket ping
2298 this.stopWebSocketPing()
2300 this.stopHeartbeat()
2302 if (this.automaticTransactionGenerator
?.started
=== true) {
2303 this.stopAutomaticTransactionGenerator()
2307 private async stopMessageSequence (
2308 reason
?: StopTransactionReason
,
2309 stopTransactions
?: boolean
2311 this.internalStopMessageSequence()
2312 // Stop ongoing transactions
2313 stopTransactions
=== true && (await this.stopRunningTransactions(reason
))
2314 if (this.hasEvses
) {
2315 for (const [evseId
, evseStatus
] of this.evses
) {
2317 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2318 await sendAndSetConnectorStatus(
2321 ConnectorStatusEnum
.Unavailable
,
2324 delete connectorStatus
.status
2329 for (const connectorId
of this.connectors
.keys()) {
2330 if (connectorId
> 0) {
2331 await sendAndSetConnectorStatus(this, connectorId
, ConnectorStatusEnum
.Unavailable
)
2332 delete this.getConnectorStatus(connectorId
)?.status
2338 private getWebSocketPingInterval (): number {
2339 return getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2340 ? convertToInt(getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
)
2344 private startWebSocketPing (): void {
2345 const webSocketPingInterval
= this.getWebSocketPingInterval()
2346 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2347 this.wsPingSetInterval
= setInterval(() => {
2348 if (this.isWebSocketConnectionOpened()) {
2349 this.wsConnection
?.ping()
2351 }, secondsToMilliseconds(webSocketPingInterval
))
2353 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2354 webSocketPingInterval
2357 } else if (this.wsPingSetInterval
!= null) {
2359 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2360 webSocketPingInterval
2365 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2370 private stopWebSocketPing (): void {
2371 if (this.wsPingSetInterval
!= null) {
2372 clearInterval(this.wsPingSetInterval
)
2373 delete this.wsPingSetInterval
2377 private getConfiguredSupervisionUrl (): URL
{
2378 let configuredSupervisionUrl
: string
2379 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2380 if (isNotEmptyArray(supervisionUrls
)) {
2381 let configuredSupervisionUrlIndex
: number
2382 switch (Configuration
.getSupervisionUrlDistribution()) {
2383 case SupervisionUrlDistribution
.RANDOM
:
2384 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2386 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2387 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2389 !Object.values(SupervisionUrlDistribution
).includes(
2390 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2391 Configuration
.getSupervisionUrlDistribution()!
2394 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2395 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2396 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2399 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2402 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2404 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2405 configuredSupervisionUrl
= supervisionUrls
!
2407 if (isNotEmptyString(configuredSupervisionUrl
)) {
2408 return new URL(configuredSupervisionUrl
)
2410 const errorMsg
= 'No supervision url(s) configured'
2411 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2412 throw new BaseError(errorMsg
)
2415 private stopHeartbeat (): void {
2416 if (this.heartbeatSetInterval
!= null) {
2417 clearInterval(this.heartbeatSetInterval
)
2418 delete this.heartbeatSetInterval
2422 private terminateWSConnection (): void {
2423 if (this.isWebSocketConnectionOpened()) {
2424 this.wsConnection
?.terminate()
2425 this.wsConnection
= null
2429 private async reconnect (): Promise
<void> {
2431 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2432 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2433 this.stationInfo
?.autoReconnectMaxRetries
=== -1
2435 ++this.wsConnectionRetryCount
2436 const reconnectDelay
=
2437 this.stationInfo
?.reconnectExponentialDelay
=== true
2438 ? exponentialDelay(this.wsConnectionRetryCount
)
2439 : secondsToMilliseconds(this.getConnectionTimeout())
2440 const reconnectDelayWithdraw
= 1000
2441 const reconnectTimeout
=
2442 reconnectDelay
- reconnectDelayWithdraw
> 0 ? reconnectDelay
- reconnectDelayWithdraw
: 0
2444 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2447 )}ms, timeout ${reconnectTimeout}ms`
2449 await sleep(reconnectDelay
)
2451 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2453 this.openWSConnection(
2455 handshakeTimeout
: reconnectTimeout
2457 { closeOpened
: true }
2459 } else if (this.stationInfo
?.autoReconnectMaxRetries
!== -1) {
2461 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`