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 wsConnectionRetryCount
: number
185 private templateFileWatcher
?: FSWatcher
186 private templateFileHash
!: string
187 private readonly sharedLRUCache
: SharedLRUCache
188 private wsPingSetInterval
?: NodeJS
.Timeout
189 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
190 private flushMessageBufferSetInterval
?: NodeJS
.Timeout
192 constructor (index
: number, templateFile
: string, options
?: ChargingStationOptions
) {
195 this.starting
= false
196 this.stopping
= false
197 this.wsConnection
= null
198 this.wsConnectionRetryCount
= 0
200 this.templateFile
= templateFile
201 this.connectors
= new Map
<number, ConnectorStatus
>()
202 this.evses
= new Map
<number, EvseStatus
>()
203 this.requests
= new Map
<string, CachedRequest
>()
204 this.messageBuffer
= new Set
<string>()
205 this.sharedLRUCache
= SharedLRUCache
.getInstance()
206 this.idTagsCache
= IdTagsCache
.getInstance()
207 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this)
209 this.on(ChargingStationEvents
.added
, () => {
210 parentPort
?.postMessage(buildAddedMessage(this))
212 this.on(ChargingStationEvents
.deleted
, () => {
213 parentPort
?.postMessage(buildDeletedMessage(this))
215 this.on(ChargingStationEvents
.started
, () => {
216 parentPort
?.postMessage(buildStartedMessage(this))
218 this.on(ChargingStationEvents
.stopped
, () => {
219 parentPort
?.postMessage(buildStoppedMessage(this))
221 this.on(ChargingStationEvents
.updated
, () => {
222 parentPort
?.postMessage(buildUpdatedMessage(this))
224 this.on(ChargingStationEvents
.accepted
, () => {
225 this.startMessageSequence(
226 this.wsConnectionRetryCount
> 0
228 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
229 ).catch((error
: unknown
) => {
230 logger
.error(`${this.logPrefix()} Error while starting the message sequence:`, error
)
232 this.wsConnectionRetryCount
= 0
234 this.on(ChargingStationEvents
.rejected
, () => {
235 this.wsConnectionRetryCount
= 0
237 this.on(ChargingStationEvents
.connected
, () => {
238 if (this.wsPingSetInterval
== null) {
239 this.startWebSocketPing()
242 this.on(ChargingStationEvents
.disconnected
, () => {
244 this.internalStopMessageSequence()
247 `${this.logPrefix()} Error while stopping the internal message sequence:`,
253 this.initialize(options
)
257 if (this.stationInfo
?.autoStart
=== true) {
262 public get
hasEvses (): boolean {
263 return this.connectors
.size
=== 0 && this.evses
.size
> 0
266 public get
wsConnectionUrl (): URL
{
267 const wsConnectionBaseUrlStr
= `${
268 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
269 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
270 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
271 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
272 : this.configuredSupervisionUrl.href
275 `${wsConnectionBaseUrlStr}${
276 !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
277 }${this.stationInfo?.chargingStationId}`
281 public logPrefix
= (): string => {
283 this instanceof ChargingStation
&&
284 this.stationInfo
!= null &&
285 isNotEmptyString(this.stationInfo
.chargingStationId
)
287 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
289 let stationTemplate
: ChargingStationTemplate
| undefined
291 stationTemplate
= JSON
.parse(
292 readFileSync(this.templateFile
, 'utf8')
293 ) as ChargingStationTemplate
297 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
300 public hasIdTags (): boolean {
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
!)!))
305 public getNumberOfPhases (stationInfo
?: ChargingStationInfo
): number {
306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307 const localStationInfo
= stationInfo
?? this.stationInfo
!
308 switch (this.getCurrentOutType(stationInfo
)) {
310 return localStationInfo
.numberOfPhases
?? 3
316 public isWebSocketConnectionOpened (): boolean {
317 return this.wsConnection
?.readyState
=== WebSocket
.OPEN
320 public inUnknownState (): boolean {
321 return this.bootNotificationResponse
?.status == null
324 public inPendingState (): boolean {
325 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
328 public inAcceptedState (): boolean {
329 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
332 public inRejectedState (): boolean {
333 return this.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
336 public isRegistered (): boolean {
337 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
340 public isChargingStationAvailable (): boolean {
341 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
344 public hasConnector (connectorId
: number): boolean {
346 for (const evseStatus
of this.evses
.values()) {
347 if (evseStatus
.connectors
.has(connectorId
)) {
353 return this.connectors
.has(connectorId
)
356 public isConnectorAvailable (connectorId
: number): boolean {
359 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
363 public getNumberOfConnectors (): number {
365 let numberOfConnectors
= 0
366 for (const [evseId
, evseStatus
] of this.evses
) {
368 numberOfConnectors
+= evseStatus
.connectors
.size
371 return numberOfConnectors
373 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
376 public getNumberOfEvses (): number {
377 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
380 public getConnectorStatus (connectorId
: number): ConnectorStatus
| undefined {
382 for (const evseStatus
of this.evses
.values()) {
383 if (evseStatus
.connectors
.has(connectorId
)) {
384 return evseStatus
.connectors
.get(connectorId
)
389 return this.connectors
.get(connectorId
)
392 public getConnectorMaximumAvailablePower (connectorId
: number): number {
393 let connectorAmperageLimitationPowerLimit
: number | undefined
394 const amperageLimitation
= this.getAmperageLimitation()
396 amperageLimitation
!= null &&
397 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
398 amperageLimitation
< this.stationInfo
!.maximumAmperage
!
400 connectorAmperageLimitationPowerLimit
=
401 (this.stationInfo
?.currentOutType
=== CurrentType
.AC
402 ? ACElectricUtils
.powerTotal(
403 this.getNumberOfPhases(),
404 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
405 this.stationInfo
.voltageOut
!,
407 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
409 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
410 DCElectricUtils
.power(this.stationInfo
!.voltageOut
!, amperageLimitation
)) /
411 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 const connectorMaximumPower
= this.stationInfo
!.maximumPower
! / this.powerDivider
!
416 const connectorChargingProfilesPowerLimit
=
417 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
)
419 isNaN(connectorMaximumPower
) ? Number.POSITIVE_INFINITY
: connectorMaximumPower
,
420 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
421 isNaN(connectorAmperageLimitationPowerLimit
!)
422 ? Number.POSITIVE_INFINITY
423 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
424 connectorAmperageLimitationPowerLimit
!,
425 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426 isNaN(connectorChargingProfilesPowerLimit
!)
427 ? Number.POSITIVE_INFINITY
428 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
429 connectorChargingProfilesPowerLimit
!
433 public getTransactionIdTag (transactionId
: number): string | undefined {
435 for (const evseStatus
of this.evses
.values()) {
436 for (const connectorStatus
of evseStatus
.connectors
.values()) {
437 if (connectorStatus
.transactionId
=== transactionId
) {
438 return connectorStatus
.transactionIdTag
443 for (const connectorId
of this.connectors
.keys()) {
444 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
445 return this.getConnectorStatus(connectorId
)?.transactionIdTag
451 public getNumberOfRunningTransactions (): number {
452 let numberOfRunningTransactions
= 0
454 for (const [evseId
, evseStatus
] of this.evses
) {
458 for (const connectorStatus
of evseStatus
.connectors
.values()) {
459 if (connectorStatus
.transactionStarted
=== true) {
460 ++numberOfRunningTransactions
465 for (const connectorId
of this.connectors
.keys()) {
466 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
467 ++numberOfRunningTransactions
471 return numberOfRunningTransactions
474 public getConnectorIdByTransactionId (transactionId
: number | undefined): number | undefined {
475 if (transactionId
== null) {
477 } else if (this.hasEvses
) {
478 for (const evseStatus
of this.evses
.values()) {
479 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
480 if (connectorStatus
.transactionId
=== transactionId
) {
486 for (const connectorId
of this.connectors
.keys()) {
487 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
494 public getEnergyActiveImportRegisterByTransactionId (
495 transactionId
: number | undefined,
498 return this.getEnergyActiveImportRegister(
499 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
500 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!),
505 public getEnergyActiveImportRegisterByConnectorId (connectorId
: number, rounded
= false): number {
506 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
)
509 public getAuthorizeRemoteTxRequests (): boolean {
510 const authorizeRemoteTxRequests
= getConfigurationKey(
512 StandardParametersKey
.AuthorizeRemoteTxRequests
514 return authorizeRemoteTxRequests
!= null
515 ? convertToBoolean(authorizeRemoteTxRequests
.value
)
519 public getLocalAuthListEnabled (): boolean {
520 const localAuthListEnabled
= getConfigurationKey(
522 StandardParametersKey
.LocalAuthListEnabled
524 return localAuthListEnabled
!= null ? convertToBoolean(localAuthListEnabled
.value
) : false
527 public getHeartbeatInterval (): number {
528 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)
529 if (HeartbeatInterval
!= null) {
530 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
))
532 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)
533 if (HeartBeatInterval
!= null) {
534 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
))
536 this.stationInfo
?.autoRegister
=== false &&
538 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
539 Constants.DEFAULT_HEARTBEAT_INTERVAL
542 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
545 public setSupervisionUrl (url
: string): void {
547 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
548 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
)
550 setConfigurationKeyValue(this, this.stationInfo
.supervisionUrlOcppKey
, url
)
552 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
553 this.stationInfo
!.supervisionUrls
= url
554 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
555 this.saveStationInfo()
559 public startHeartbeat (): void {
560 const heartbeatInterval
= this.getHeartbeatInterval()
561 if (heartbeatInterval
> 0 && this.heartbeatSetInterval
== null) {
562 this.heartbeatSetInterval
= setInterval(() => {
563 this.ocppRequestService
564 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
565 .catch((error
: unknown
) => {
567 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
571 }, heartbeatInterval
)
573 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
577 } else if (this.heartbeatSetInterval
!= null) {
579 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
585 `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat`
590 public restartHeartbeat (): void {
594 this.startHeartbeat()
597 public restartWebSocketPing (): void {
598 // Stop WebSocket ping
599 this.stopWebSocketPing()
600 // Start WebSocket ping
601 this.startWebSocketPing()
604 public startMeterValues (connectorId
: number, interval
: number): void {
605 if (connectorId
=== 0) {
606 logger
.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
609 const connectorStatus
= this.getConnectorStatus(connectorId
)
610 if (connectorStatus
== null) {
612 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
617 if (connectorStatus
.transactionStarted
=== false) {
619 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
623 connectorStatus
.transactionStarted
=== true &&
624 connectorStatus
.transactionId
== null
627 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
632 connectorStatus
.transactionSetInterval
= setInterval(() => {
633 const meterValue
= buildMeterValue(
636 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
637 connectorStatus
.transactionId
!,
640 this.ocppRequestService
641 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
643 RequestCommand
.METER_VALUES
,
646 transactionId
: connectorStatus
.transactionId
,
647 meterValue
: [meterValue
]
650 .catch((error
: unknown
) => {
652 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
659 `${this.logPrefix()} Charging station ${
660 StandardParametersKey.MeterValueSampleInterval
661 } configuration set to ${interval}, not sending MeterValues`
666 public stopMeterValues (connectorId
: number): void {
667 const connectorStatus
= this.getConnectorStatus(connectorId
)
668 if (connectorStatus
?.transactionSetInterval
!= null) {
669 clearInterval(connectorStatus
.transactionSetInterval
)
673 private add (): void {
674 this.emit(ChargingStationEvents
.added
)
677 public async delete (deleteConfiguration
= true): Promise
<void> {
681 AutomaticTransactionGenerator
.deleteInstance(this)
682 PerformanceStatistics
.deleteInstance(this.stationInfo
?.hashId
)
683 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
684 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
685 this.requests
.clear()
686 this.connectors
.clear()
688 this.templateFileWatcher
?.unref()
689 deleteConfiguration
&& rmSync(this.configurationFile
, { force
: true })
690 this.chargingStationWorkerBroadcastChannel
.unref()
691 this.emit(ChargingStationEvents
.deleted
)
692 this.removeAllListeners()
695 public start (): void {
697 if (!this.starting
) {
699 if (this.stationInfo
?.enableStatistics
=== true) {
700 this.performanceStatistics
?.start()
702 this.openWSConnection()
703 // Monitor charging station template file
704 this.templateFileWatcher
= watchJsonFile(
706 FileType
.ChargingStationTemplate
,
709 (event
, filename
): void => {
710 if (isNotEmptyString(filename
) && event
=== 'change') {
713 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
715 } file have changed, reload`
717 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
)
718 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
719 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
!)!)
723 const ATGStarted
= this.automaticTransactionGenerator
?.started
724 if (ATGStarted
=== true) {
725 this.stopAutomaticTransactionGenerator()
727 delete this.automaticTransactionGeneratorConfiguration
729 this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true &&
732 this.startAutomaticTransactionGenerator(undefined, true)
734 if (this.stationInfo
?.enableStatistics
=== true) {
735 this.performanceStatistics
?.restart()
737 this.performanceStatistics
?.stop()
739 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
742 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
750 this.emit(ChargingStationEvents
.started
)
751 this.starting
= false
753 logger
.warn(`${this.logPrefix()} Charging station is already starting...`)
756 logger
.warn(`${this.logPrefix()} Charging station is already started...`)
761 reason
?: StopTransactionReason
,
762 stopTransactions
= this.stationInfo
?.stopTransactionsOnStopped
765 if (!this.stopping
) {
767 await this.stopMessageSequence(reason
, stopTransactions
)
768 this.closeWSConnection()
769 if (this.stationInfo
?.enableStatistics
=== true) {
770 this.performanceStatistics
?.stop()
772 this.templateFileWatcher
?.close()
773 delete this.bootNotificationResponse
775 this.saveConfiguration()
776 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
777 this.emit(ChargingStationEvents
.stopped
)
778 this.stopping
= false
780 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`)
783 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`)
787 public async reset (reason
?: StopTransactionReason
): Promise
<void> {
788 await this.stop(reason
)
789 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
790 await sleep(this.stationInfo
!.resetTime
!)
795 public saveOcppConfiguration (): void {
796 if (this.stationInfo
?.ocppPersistentConfiguration
=== true) {
797 this.saveConfiguration()
801 public bufferMessage (message
: string): void {
802 this.messageBuffer
.add(message
)
803 this.setIntervalFlushMessageBuffer()
806 public openWSConnection (
808 params
?: { closeOpened
?: boolean, terminateOpened
?: boolean }
811 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
812 ...this.stationInfo
?.wsOptions
,
815 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
}
816 if (!checkChargingStation(this, this.logPrefix())) {
819 if (this.stationInfo
?.supervisionUser
!= null && this.stationInfo
.supervisionPassword
!= null) {
820 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
822 if (params
.closeOpened
=== true) {
823 this.closeWSConnection()
825 if (params
.terminateOpened
=== true) {
826 this.terminateWSConnection()
829 if (this.isWebSocketConnectionOpened()) {
831 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
836 logger
.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
838 this.wsConnection
= new WebSocket(
839 this.wsConnectionUrl
,
840 `ocpp${this.stationInfo?.ocppVersion}`,
844 // Handle WebSocket message
845 this.wsConnection
.on('message', data
=> {
846 this.onMessage(data
).catch(Constants
.EMPTY_FUNCTION
)
848 // Handle WebSocket error
849 this.wsConnection
.on('error', this.onError
.bind(this))
850 // Handle WebSocket close
851 this.wsConnection
.on('close', this.onClose
.bind(this))
852 // Handle WebSocket open
853 this.wsConnection
.on('open', () => {
854 this.onOpen().catch((error
: unknown
) =>
855 logger
.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error
)
858 // Handle WebSocket ping
859 this.wsConnection
.on('ping', this.onPing
.bind(this))
860 // Handle WebSocket pong
861 this.wsConnection
.on('pong', this.onPong
.bind(this))
864 public closeWSConnection (): void {
865 if (this.isWebSocketConnectionOpened()) {
866 this.wsConnection
?.close()
867 this.wsConnection
= null
871 public getAutomaticTransactionGeneratorConfiguration ():
872 | AutomaticTransactionGeneratorConfiguration
874 if (this.automaticTransactionGeneratorConfiguration
== null) {
875 let automaticTransactionGeneratorConfiguration
:
876 | AutomaticTransactionGeneratorConfiguration
878 const stationTemplate
= this.getTemplateFromFile()
879 const stationConfiguration
= this.getConfigurationFromFile()
881 this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true &&
882 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
883 stationConfiguration
?.automaticTransactionGenerator
!= null
885 automaticTransactionGeneratorConfiguration
=
886 stationConfiguration
.automaticTransactionGenerator
888 automaticTransactionGeneratorConfiguration
= stationTemplate
?.AutomaticTransactionGenerator
890 this.automaticTransactionGeneratorConfiguration
= {
891 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
892 ...automaticTransactionGeneratorConfiguration
895 return this.automaticTransactionGeneratorConfiguration
898 public getAutomaticTransactionGeneratorStatuses (): Status
[] | undefined {
899 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
902 public startAutomaticTransactionGenerator (
903 connectorIds
?: number[],
904 stopAbsoluteDuration
?: boolean
906 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this)
907 if (isNotEmptyArray(connectorIds
)) {
908 for (const connectorId
of connectorIds
) {
909 this.automaticTransactionGenerator
?.startConnector(connectorId
, stopAbsoluteDuration
)
912 this.automaticTransactionGenerator
?.start(stopAbsoluteDuration
)
914 this.saveAutomaticTransactionGeneratorConfiguration()
915 this.emit(ChargingStationEvents
.updated
)
918 public stopAutomaticTransactionGenerator (connectorIds
?: number[]): void {
919 if (isNotEmptyArray(connectorIds
)) {
920 for (const connectorId
of connectorIds
) {
921 this.automaticTransactionGenerator
?.stopConnector(connectorId
)
924 this.automaticTransactionGenerator
?.stop()
926 this.saveAutomaticTransactionGeneratorConfiguration()
927 this.emit(ChargingStationEvents
.updated
)
930 public async stopTransactionOnConnector (
932 reason
?: StopTransactionReason
933 ): Promise
<StopTransactionResponse
> {
934 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
936 this.stationInfo
?.beginEndMeterValues
=== true &&
937 this.stationInfo
.ocppStrictCompliance
=== true &&
938 this.stationInfo
.outOfOrderEndMeterValues
=== false
940 const transactionEndMeterValue
= buildTransactionEndMeterValue(
943 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
945 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
947 RequestCommand
.METER_VALUES
,
951 meterValue
: [transactionEndMeterValue
]
955 return await this.ocppRequestService
.requestHandler
<
956 Partial
<StopTransactionRequest
>,
957 StopTransactionResponse
958 >(this, RequestCommand
.STOP_TRANSACTION
, {
960 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
961 ...(reason
!= null && { reason
})
965 public getReserveConnectorZeroSupported (): boolean {
966 return convertToBoolean(
967 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
968 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
972 public async addReservation (reservation
: Reservation
): Promise
<void> {
973 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
)
974 if (reservationFound
!= null) {
975 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
)
977 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
978 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
979 await sendAndSetConnectorStatus(
981 reservation
.connectorId
,
982 ConnectorStatusEnum
.Reserved
,
984 { send
: reservation
.connectorId
!== 0 }
988 public async removeReservation (
989 reservation
: Reservation
,
990 reason
: ReservationTerminationReason
992 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
993 const connector
= this.getConnectorStatus(reservation
.connectorId
)!
995 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
996 case ReservationTerminationReason
.TRANSACTION_STARTED
:
997 delete connector
.reservation
999 case ReservationTerminationReason
.RESERVATION_CANCELED
:
1000 case ReservationTerminationReason
.REPLACE_EXISTING
:
1001 case ReservationTerminationReason
.EXPIRED
:
1002 await sendAndSetConnectorStatus(
1004 reservation
.connectorId
,
1005 ConnectorStatusEnum
.Available
,
1007 { send
: reservation
.connectorId
!== 0 }
1009 delete connector
.reservation
1012 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1013 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1017 public getReservationBy (
1018 filterKey
: ReservationKey
,
1019 value
: number | string
1020 ): Reservation
| undefined {
1021 if (this.hasEvses
) {
1022 for (const evseStatus
of this.evses
.values()) {
1023 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1024 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1025 return connectorStatus
.reservation
1030 for (const connectorStatus
of this.connectors
.values()) {
1031 if (connectorStatus
.reservation
?.[filterKey
] === value
) {
1032 return connectorStatus
.reservation
1038 public isConnectorReservable (
1039 reservationId
: number,
1041 connectorId
?: number
1043 const reservation
= this.getReservationBy('reservationId', reservationId
)
1044 const reservationExists
= reservation
!= null && !hasReservationExpired(reservation
)
1045 if (arguments.length
=== 1) {
1046 return !reservationExists
1047 } else if (arguments.length
> 1) {
1048 const userReservation
= idTag
!= null ? this.getReservationBy('idTag', idTag
) : undefined
1049 const userReservationExists
=
1050 userReservation
!= null && !hasReservationExpired(userReservation
)
1051 const notConnectorZero
= connectorId
== null ? true : connectorId
> 0
1052 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0
1054 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1060 private setIntervalFlushMessageBuffer (): void {
1061 if (this.flushMessageBufferSetInterval
== null) {
1062 this.flushMessageBufferSetInterval
= setInterval(() => {
1063 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1064 this.flushMessageBuffer()
1066 if (this.messageBuffer
.size
=== 0) {
1067 this.clearIntervalFlushMessageBuffer()
1069 }, Constants
.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL
)
1073 private clearIntervalFlushMessageBuffer (): void {
1074 if (this.flushMessageBufferSetInterval
!= null) {
1075 clearInterval(this.flushMessageBufferSetInterval
)
1076 delete this.flushMessageBufferSetInterval
1080 private getNumberOfReservableConnectors (): number {
1081 let numberOfReservableConnectors
= 0
1082 if (this.hasEvses
) {
1083 for (const evseStatus
of this.evses
.values()) {
1084 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
)
1087 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
)
1089 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero()
1092 private getNumberOfReservationsOnConnectorZero (): number {
1094 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
!= null) ||
1095 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
!= null)
1102 private flushMessageBuffer (): void {
1103 if (this.messageBuffer
.size
> 0) {
1104 for (const message
of this.messageBuffer
.values()) {
1105 let beginId
: string | undefined
1106 let commandName
: RequestCommand
| undefined
1107 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
1108 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
1110 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
1111 beginId
= PerformanceStatistics
.beginMeasure(commandName
)
1113 this.wsConnection
?.send(message
, (error
?: Error) => {
1114 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1115 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!)
1116 if (error
== null) {
1118 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1120 )} OCPP message sent '${JSON.stringify(message)}'`
1122 this.messageBuffer
.delete(message
)
1125 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1127 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1136 private getTemplateFromFile (): ChargingStationTemplate
| undefined {
1137 let template
: ChargingStationTemplate
| undefined
1139 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1140 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
)
1142 const measureId
= `${FileType.ChargingStationTemplate} read`
1143 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1144 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
1145 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1146 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1147 .update(JSON
.stringify(template
))
1149 this.sharedLRUCache
.setChargingStationTemplate(template
)
1150 this.templateFileHash
= template
.templateHash
1153 handleFileException(
1155 FileType
.ChargingStationTemplate
,
1156 error
as NodeJS
.ErrnoException
,
1163 private getStationInfoFromTemplate (): ChargingStationInfo
{
1164 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1165 const stationTemplate
= this.getTemplateFromFile()!
1166 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1167 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
)
1168 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
)
1169 if (stationTemplate
.Connectors
!= null) {
1170 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1172 const stationInfo
= stationTemplateToStationInfo(stationTemplate
)
1173 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
)
1174 stationInfo
.templateIndex
= this.index
1175 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1176 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
)
1177 createSerialNumber(stationTemplate
, stationInfo
)
1178 stationInfo
.voltageOut
= this.getVoltageOut(stationInfo
)
1179 if (isNotEmptyArray(stationTemplate
.power
)) {
1180 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
)
1181 stationInfo
.maximumPower
=
1182 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1183 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1184 : stationTemplate
.power
[powerArrayRandomIndex
]
1186 stationInfo
.maximumPower
=
1187 stationTemplate
.powerUnit
=== PowerUnits
.KILO_WATT
1188 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1189 stationTemplate
.power
! * 1000
1190 : stationTemplate
.power
1192 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
)
1194 isNotEmptyString(stationInfo
.firmwareVersionPattern
) &&
1195 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1196 !new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
)
1199 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1201 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1204 if (stationTemplate
.resetTime
!= null) {
1205 stationInfo
.resetTime
= secondsToMilliseconds(stationTemplate
.resetTime
)
1210 private getStationInfoFromFile (
1211 stationInfoPersistentConfiguration
: boolean | undefined = Constants
.DEFAULT_STATION_INFO
1212 .stationInfoPersistentConfiguration
1213 ): ChargingStationInfo
| undefined {
1214 let stationInfo
: ChargingStationInfo
| undefined
1215 if (stationInfoPersistentConfiguration
=== true) {
1216 stationInfo
= this.getConfigurationFromFile()?.stationInfo
1217 if (stationInfo
!= null) {
1218 delete stationInfo
.infoHash
1219 delete (stationInfo
as ChargingStationTemplate
).numberOfConnectors
1220 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1221 if (stationInfo
.templateIndex
== null) {
1222 stationInfo
.templateIndex
= this.index
1224 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1225 if (stationInfo
.templateName
== null) {
1226 stationInfo
.templateName
= buildTemplateName(this.templateFile
)
1233 private getStationInfo (options
?: ChargingStationOptions
): ChargingStationInfo
{
1234 const stationInfoFromTemplate
= this.getStationInfoFromTemplate()
1235 options
?.persistentConfiguration
!= null &&
1236 (stationInfoFromTemplate
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
)
1237 const stationInfoFromFile
= this.getStationInfoFromFile(
1238 stationInfoFromTemplate
.stationInfoPersistentConfiguration
1240 let stationInfo
: ChargingStationInfo
1242 // 1. charging station info from template
1243 // 2. charging station info from configuration file
1245 stationInfoFromFile
!= null &&
1246 stationInfoFromFile
.templateHash
=== stationInfoFromTemplate
.templateHash
1248 stationInfo
= stationInfoFromFile
1250 stationInfo
= stationInfoFromTemplate
1251 stationInfoFromFile
!= null &&
1252 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile
, stationInfo
)
1254 return setChargingStationOptions(
1255 mergeDeepRight(Constants
.DEFAULT_STATION_INFO
, stationInfo
),
1260 private saveStationInfo (): void {
1261 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1262 this.saveConfiguration()
1266 private handleUnsupportedVersion (version
: OCPPVersion
| undefined): void {
1267 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1268 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1269 throw new BaseError(errorMsg
)
1272 private initialize (options
?: ChargingStationOptions
): void {
1273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1274 const stationTemplate
= this.getTemplateFromFile()!
1275 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
)
1276 this.configurationFile
= join(
1277 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1278 `${getHashId(this.index, stationTemplate)}.json`
1280 const stationConfiguration
= this.getConfigurationFromFile()
1282 stationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
.templateHash
&&
1283 (stationConfiguration
?.connectorsStatus
!= null || stationConfiguration
?.evsesStatus
!= null)
1285 checkConfiguration(stationConfiguration
, this.logPrefix(), this.configurationFile
)
1286 this.initializeConnectorsOrEvsesFromFile(stationConfiguration
)
1288 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
)
1290 this.stationInfo
= this.getStationInfo(options
)
1292 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1293 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
) &&
1294 isNotEmptyString(this.stationInfo
.firmwareVersion
)
1296 const patternGroup
=
1297 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1298 this.stationInfo
.firmwareVersion
.split('.').length
1299 const match
= new RegExp(this.stationInfo
.firmwareVersionPattern
)
1300 .exec(this.stationInfo
.firmwareVersion
)
1301 ?.slice(1, patternGroup
+ 1)
1302 if (match
!= null) {
1303 const patchLevelIndex
= match
.length
- 1
1304 match
[patchLevelIndex
] = (
1305 convertToInt(match
[patchLevelIndex
]) +
1306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1307 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1309 this.stationInfo
.firmwareVersion
= match
.join('.')
1312 this.saveStationInfo()
1313 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl()
1314 if (this.stationInfo
.enableStatistics
=== true) {
1315 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1316 this.stationInfo
.hashId
,
1317 this.stationInfo
.chargingStationId
,
1318 this.configuredSupervisionUrl
1321 const bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
)
1322 if (bootNotificationRequest
== null) {
1323 const errorMsg
= 'Error while creating boot notification request'
1324 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1325 throw new BaseError(errorMsg
)
1327 this.bootNotificationRequest
= bootNotificationRequest
1328 this.powerDivider
= this.getPowerDivider()
1329 // OCPP configuration
1330 this.ocppConfiguration
= this.getOcppConfiguration(options
?.persistentConfiguration
)
1331 this.initializeOcppConfiguration()
1332 this.initializeOcppServices()
1333 if (this.stationInfo
.autoRegister
=== true) {
1334 this.bootNotificationResponse
= {
1335 currentTime
: new Date(),
1336 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1337 status: RegistrationStatusEnumType
.ACCEPTED
1342 private initializeOcppServices (): void {
1343 const ocppVersion
= this.stationInfo
?.ocppVersion
1344 switch (ocppVersion
) {
1345 case OCPPVersion
.VERSION_16
:
1346 this.ocppIncomingRequestService
=
1347 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>()
1348 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1349 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1352 case OCPPVersion
.VERSION_20
:
1353 case OCPPVersion
.VERSION_201
:
1354 this.ocppIncomingRequestService
=
1355 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>()
1356 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1357 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1361 this.handleUnsupportedVersion(ocppVersion
)
1366 private initializeOcppConfiguration (): void {
1367 if (getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
) == null) {
1368 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0')
1370 if (getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
) == null) {
1371 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', {
1376 this.stationInfo
?.supervisionUrlOcppConfiguration
=== true &&
1377 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1378 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) == null
1380 addConfigurationKey(
1382 this.stationInfo
.supervisionUrlOcppKey
,
1383 this.configuredSupervisionUrl
.href
,
1387 this.stationInfo
?.supervisionUrlOcppConfiguration
=== false &&
1388 isNotEmptyString(this.stationInfo
.supervisionUrlOcppKey
) &&
1389 getConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
) != null
1391 deleteConfigurationKey(this, this.stationInfo
.supervisionUrlOcppKey
, {
1396 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1397 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) == null
1399 addConfigurationKey(
1401 this.stationInfo
.amperageLimitationOcppKey
,
1403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1404 (this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)).toString()
1407 if (getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
) == null) {
1408 addConfigurationKey(
1410 StandardParametersKey
.SupportedFeatureProfiles
,
1411 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1414 addConfigurationKey(
1416 StandardParametersKey
.NumberOfConnectors
,
1417 this.getNumberOfConnectors().toString(),
1421 if (getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
) == null) {
1422 addConfigurationKey(
1424 StandardParametersKey
.MeterValuesSampledData
,
1425 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1428 if (getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
) == null) {
1429 const connectorsPhaseRotation
: string[] = []
1430 if (this.hasEvses
) {
1431 for (const evseStatus
of this.evses
.values()) {
1432 for (const connectorId
of evseStatus
.connectors
.keys()) {
1433 connectorsPhaseRotation
.push(
1434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1435 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1440 for (const connectorId
of this.connectors
.keys()) {
1441 connectorsPhaseRotation
.push(
1442 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1443 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!
1447 addConfigurationKey(
1449 StandardParametersKey
.ConnectorPhaseRotation
,
1450 connectorsPhaseRotation
.toString()
1453 if (getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
) == null) {
1454 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true')
1457 getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) == null &&
1458 hasFeatureProfile(this, SupportedFeatureProfiles
.LocalAuthListManagement
) === true
1460 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false')
1462 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) == null) {
1463 addConfigurationKey(
1465 StandardParametersKey
.ConnectionTimeOut
,
1466 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1469 this.saveOcppConfiguration()
1472 private initializeConnectorsOrEvsesFromFile (configuration
: ChargingStationConfiguration
): void {
1473 if (configuration
.connectorsStatus
!= null && configuration
.evsesStatus
== null) {
1474 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1475 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1477 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
== null) {
1478 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1479 const evseStatus
= clone
<EvseStatusConfiguration
>(evseStatusConfiguration
)
1480 delete evseStatus
.connectorsStatus
1481 this.evses
.set(evseId
, {
1482 ...(evseStatus
as EvseStatus
),
1483 connectors
: new Map
<number, ConnectorStatus
>(
1484 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1485 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1492 } else if (configuration
.evsesStatus
!= null && configuration
.connectorsStatus
!= null) {
1493 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1494 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1495 throw new BaseError(errorMsg
)
1497 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`
1498 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1499 throw new BaseError(errorMsg
)
1503 private initializeConnectorsOrEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1504 if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
1505 this.initializeConnectorsFromTemplate(stationTemplate
)
1506 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
1507 this.initializeEvsesFromTemplate(stationTemplate
)
1508 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
!= null) {
1509 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`
1510 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1511 throw new BaseError(errorMsg
)
1513 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`
1514 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1515 throw new BaseError(errorMsg
)
1519 private initializeConnectorsFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1520 if (stationTemplate
.Connectors
== null && this.connectors
.size
=== 0) {
1521 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1522 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1523 throw new BaseError(errorMsg
)
1525 if (stationTemplate
.Connectors
?.[0] == null) {
1527 `${this.logPrefix()} Charging station information from template ${
1529 } with no connector id 0 configuration`
1532 if (stationTemplate
.Connectors
!= null) {
1533 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1534 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
)
1535 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1537 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1540 const connectorsConfigChanged
=
1541 this.connectors
.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
1542 if (this.connectors
.size
=== 0 || connectorsConfigChanged
) {
1543 connectorsConfigChanged
&& this.connectors
.clear()
1544 this.connectorsConfigurationHash
= connectorsConfigHash
1545 if (templateMaxConnectors
> 0) {
1546 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1548 connectorId
=== 0 &&
1549 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1550 (stationTemplate
.Connectors
[connectorId
] == null ||
1551 !this.getUseConnectorId0(stationTemplate
))
1555 const templateConnectorId
=
1556 connectorId
> 0 && stationTemplate
.randomConnectors
=== true
1557 ? randomInt(1, templateMaxAvailableConnectors
)
1559 const connectorStatus
= stationTemplate
.Connectors
[templateConnectorId
]
1560 checkStationInfoConnectorStatus(
1561 templateConnectorId
,
1566 this.connectors
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
1568 initializeConnectorsMapStatus(this.connectors
, this.logPrefix())
1569 this.saveConnectorsStatus()
1572 `${this.logPrefix()} Charging station information from template ${
1574 } with no connectors configuration defined, cannot create connectors`
1580 `${this.logPrefix()} Charging station information from template ${
1582 } with no connectors configuration defined, using already defined connectors`
1587 private initializeEvsesFromTemplate (stationTemplate
: ChargingStationTemplate
): void {
1588 if (stationTemplate
.Evses
== null && this.evses
.size
=== 0) {
1589 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1590 logger
.error(`${this.logPrefix()} ${errorMsg}`)
1591 throw new BaseError(errorMsg
)
1593 if (stationTemplate
.Evses
?.[0] == null) {
1595 `${this.logPrefix()} Charging station information from template ${
1597 } with no evse id 0 configuration`
1600 if (stationTemplate
.Evses
?.[0]?.Connectors
[0] == null) {
1602 `${this.logPrefix()} Charging station information from template ${
1604 } with evse id 0 with no connector id 0 configuration`
1607 if (Object.keys(stationTemplate
.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1609 `${this.logPrefix()} Charging station information from template ${
1611 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1614 if (stationTemplate
.Evses
!= null) {
1615 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1616 .update(JSON
.stringify(stationTemplate
.Evses
))
1618 const evsesConfigChanged
=
1619 this.evses
.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
1620 if (this.evses
.size
=== 0 || evsesConfigChanged
) {
1621 evsesConfigChanged
&& this.evses
.clear()
1622 this.evsesConfigurationHash
= evsesConfigHash
1623 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
.Evses
)
1624 if (templateMaxEvses
> 0) {
1625 for (const evseKey
in stationTemplate
.Evses
) {
1626 const evseId
= convertToInt(evseKey
)
1627 this.evses
.set(evseId
, {
1628 connectors
: buildConnectorsMap(
1629 stationTemplate
.Evses
[evseKey
].Connectors
,
1633 availability
: AvailabilityType
.Operative
1635 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1636 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix())
1638 this.saveEvsesStatus()
1641 `${this.logPrefix()} Charging station information from template ${
1643 } with no evses configuration defined, cannot create evses`
1649 `${this.logPrefix()} Charging station information from template ${
1651 } with no evses configuration defined, using already defined evses`
1656 private getConfigurationFromFile (): ChargingStationConfiguration
| undefined {
1657 let configuration
: ChargingStationConfiguration
| undefined
1658 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1660 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1661 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1662 this.configurationFileHash
1665 const measureId
= `${FileType.ChargingStationConfiguration} read`
1666 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1667 configuration
= JSON
.parse(
1668 readFileSync(this.configurationFile
, 'utf8')
1669 ) as ChargingStationConfiguration
1670 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1671 this.sharedLRUCache
.setChargingStationConfiguration(configuration
)
1672 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1673 this.configurationFileHash
= configuration
.configurationHash
!
1676 handleFileException(
1677 this.configurationFile
,
1678 FileType
.ChargingStationConfiguration
,
1679 error
as NodeJS
.ErrnoException
,
1684 return configuration
1687 private saveAutomaticTransactionGeneratorConfiguration (): void {
1688 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
=== true) {
1689 this.saveConfiguration()
1693 private saveConnectorsStatus (): void {
1694 this.saveConfiguration()
1697 private saveEvsesStatus (): void {
1698 this.saveConfiguration()
1701 private saveConfiguration (): void {
1702 if (isNotEmptyString(this.configurationFile
)) {
1704 if (!existsSync(dirname(this.configurationFile
))) {
1705 mkdirSync(dirname(this.configurationFile
), { recursive
: true })
1707 const configurationFromFile
= this.getConfigurationFromFile()
1708 let configurationData
: ChargingStationConfiguration
=
1709 configurationFromFile
!= null
1710 ? clone
<ChargingStationConfiguration
>(configurationFromFile
)
1712 if (this.stationInfo
?.stationInfoPersistentConfiguration
=== true) {
1713 configurationData
.stationInfo
= this.stationInfo
1715 delete configurationData
.stationInfo
1718 this.stationInfo
?.ocppPersistentConfiguration
=== true &&
1719 Array.isArray(this.ocppConfiguration
?.configurationKey
)
1721 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
1723 delete configurationData
.configurationKey
1725 configurationData
= mergeDeepRight(
1727 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1729 if (this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
!== true) {
1730 delete configurationData
.automaticTransactionGenerator
1732 if (this.connectors
.size
> 0) {
1733 configurationData
.connectorsStatus
= buildConnectorsStatus(this)
1735 delete configurationData
.connectorsStatus
1737 if (this.evses
.size
> 0) {
1738 configurationData
.evsesStatus
= buildEvsesStatus(this)
1740 delete configurationData
.evsesStatus
1742 delete configurationData
.configurationHash
1743 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1746 stationInfo
: configurationData
.stationInfo
,
1747 configurationKey
: configurationData
.configurationKey
,
1748 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1749 ...(this.connectors
.size
> 0 && {
1750 connectorsStatus
: configurationData
.connectorsStatus
1752 ...(this.evses
.size
> 0 && {
1753 evsesStatus
: configurationData
.evsesStatus
1755 } satisfies ChargingStationConfiguration
)
1758 if (this.configurationFileHash
!== configurationHash
) {
1759 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1760 configurationData
.configurationHash
= configurationHash
1761 const measureId
= `${FileType.ChargingStationConfiguration} write`
1762 const beginId
= PerformanceStatistics
.beginMeasure(measureId
)
1764 this.configurationFile
,
1765 JSON
.stringify(configurationData
, undefined, 2),
1768 PerformanceStatistics
.endMeasure(measureId
, beginId
)
1769 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
)
1770 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
)
1771 this.configurationFileHash
= configurationHash
1772 }).catch((error
: unknown
) => {
1773 handleFileException(
1774 this.configurationFile
,
1775 FileType
.ChargingStationConfiguration
,
1776 error
as NodeJS
.ErrnoException
,
1782 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1783 this.configurationFile
1788 handleFileException(
1789 this.configurationFile
,
1790 FileType
.ChargingStationConfiguration
,
1791 error
as NodeJS
.ErrnoException
,
1797 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1802 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration
| undefined {
1803 return this.getTemplateFromFile()?.Configuration
1806 private getOcppConfigurationFromFile (
1807 ocppPersistentConfiguration
?: boolean
1808 ): ChargingStationOcppConfiguration
| undefined {
1809 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
1810 if (ocppPersistentConfiguration
=== true && Array.isArray(configurationKey
)) {
1811 return { configurationKey
}
1816 private getOcppConfiguration (
1817 ocppPersistentConfiguration
: boolean | undefined = this.stationInfo
?.ocppPersistentConfiguration
1818 ): ChargingStationOcppConfiguration
| undefined {
1819 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1820 this.getOcppConfigurationFromFile(ocppPersistentConfiguration
)
1821 if (ocppConfiguration
== null) {
1822 ocppConfiguration
= this.getOcppConfigurationFromTemplate()
1824 return ocppConfiguration
1827 private async onOpen (): Promise
<void> {
1828 if (this.isWebSocketConnectionOpened()) {
1829 this.emit(ChargingStationEvents
.connected
)
1830 this.emit(ChargingStationEvents
.updated
)
1832 `${this.logPrefix()} Connection to OCPP server through ${
1833 this.wsConnectionUrl.href
1836 let registrationRetryCount
= 0
1837 if (!this.isRegistered()) {
1838 // Send BootNotification
1840 await this.ocppRequestService
.requestHandler
<
1841 BootNotificationRequest
,
1842 BootNotificationResponse
1843 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1844 skipBufferingOnError
: true
1846 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1847 if (this.bootNotificationResponse
?.currentTime
!= null) {
1848 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1849 this.bootNotificationResponse
.currentTime
= convertToDate(
1850 this.bootNotificationResponse
.currentTime
1853 if (!this.isRegistered()) {
1854 this.stationInfo
?.registrationMaxRetries
!== -1 && ++registrationRetryCount
1856 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1857 this.bootNotificationResponse
?.interval
!= null
1858 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1859 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1863 !this.isRegistered() &&
1864 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1865 (registrationRetryCount
<= this.stationInfo
!.registrationMaxRetries
! ||
1866 this.stationInfo
?.registrationMaxRetries
=== -1)
1869 if (!this.isRegistered()) {
1871 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1872 this.stationInfo?.registrationMaxRetries
1876 this.emit(ChargingStationEvents
.updated
)
1879 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1884 private onClose (code
: WebSocketCloseEventStatusCode
, reason
: Buffer
): void {
1885 this.emit(ChargingStationEvents
.disconnected
)
1886 this.emit(ChargingStationEvents
.updated
)
1889 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1890 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1892 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1894 )}' and reason '${reason.toString()}'`
1896 this.wsConnectionRetryCount
= 0
1901 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1903 )}' and reason '${reason.toString()}'`
1908 this.emit(ChargingStationEvents
.updated
)
1910 .catch((error
: unknown
) =>
1911 logger
.error(`${this.logPrefix()} Error while reconnecting:`, error
)
1917 private getCachedRequest (
1918 messageType
: MessageType
| undefined,
1920 ): CachedRequest
| undefined {
1921 const cachedRequest
= this.requests
.get(messageId
)
1922 if (Array.isArray(cachedRequest
)) {
1923 return cachedRequest
1925 throw new OCPPError(
1926 ErrorType
.PROTOCOL_ERROR
,
1927 `Cached request for message id ${messageId} ${getMessageTypeString(
1929 )} is not an array`,
1935 private async handleIncomingMessage (request
: IncomingRequest
): Promise
<void> {
1936 const [messageType
, messageId
, commandName
, commandPayload
] = request
1937 if (this.stationInfo
?.enableStatistics
=== true) {
1938 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
)
1941 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1945 // Process the message
1946 await this.ocppIncomingRequestService
.incomingRequestHandler(
1952 this.emit(ChargingStationEvents
.updated
)
1955 private handleResponseMessage (response
: Response
): void {
1956 const [messageType
, messageId
, commandPayload
] = response
1957 if (!this.requests
.has(messageId
)) {
1959 throw new OCPPError(
1960 ErrorType
.INTERNAL_ERROR
,
1961 `Response for unknown message id ${messageId}`,
1967 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1968 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1973 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1977 responseCallback(commandPayload
, requestPayload
)
1980 private handleErrorMessage (errorResponse
: ErrorResponse
): void {
1981 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
1982 if (!this.requests
.has(messageId
)) {
1984 throw new OCPPError(
1985 ErrorType
.INTERNAL_ERROR
,
1986 `Error response for unknown message id ${messageId}`,
1988 { errorType
, errorMessage
, errorDetails
}
1991 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1992 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
1994 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1998 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
))
2001 private async onMessage (data
: RawData
): Promise
<void> {
2002 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined
2003 let messageType
: MessageType
| undefined
2004 let errorMsg
: string
2006 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2007 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
2008 if (Array.isArray(request
)) {
2009 [messageType
] = request
2010 // Check the type of message
2011 switch (messageType
) {
2013 case MessageType
.CALL_MESSAGE
:
2014 await this.handleIncomingMessage(request
as IncomingRequest
)
2017 case MessageType
.CALL_RESULT_MESSAGE
:
2018 this.handleResponseMessage(request
as Response
)
2021 case MessageType
.CALL_ERROR_MESSAGE
:
2022 this.handleErrorMessage(request
as ErrorResponse
)
2026 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2027 errorMsg
= `Wrong message type ${messageType}`
2028 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2029 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
)
2032 throw new OCPPError(
2033 ErrorType
.PROTOCOL_ERROR
,
2034 'Incoming message is not an array',
2042 if (!Array.isArray(request
)) {
2043 logger
.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error
)
2046 let commandName
: IncomingRequestCommand
| undefined
2047 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined
2048 let errorCallback
: ErrorCallback
2049 const [, messageId
] = request
2050 switch (messageType
) {
2051 case MessageType
.CALL_MESSAGE
:
2052 [, , commandName
] = request
as IncomingRequest
2054 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
)
2056 case MessageType
.CALL_RESULT_MESSAGE
:
2057 case MessageType
.CALL_ERROR_MESSAGE
:
2058 if (this.requests
.has(messageId
)) {
2059 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2060 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!
2061 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2062 errorCallback(error
as OCPPError
, false)
2064 // Remove the request from the cache in case of error at response handling
2065 this.requests
.delete(messageId
)
2069 if (!(error
instanceof OCPPError
)) {
2071 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2072 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2073 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2074 }' message '${data.toString()}' handling is not an OCPPError:`,
2079 `${this.logPrefix()} Incoming OCPP command '${
2080 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2081 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2082 }' message '${data.toString()}'${
2083 this.requests.has(messageId)
2084 ? ` matching cached request
'${JSON.stringify(
2085 this.getCachedRequest(messageType, messageId)
2088 } processing error:`,
2094 private onPing (): void {
2095 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2098 private onPong (): void {
2099 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2102 private onError (error
: WSError
): void {
2103 this.closeWSConnection()
2104 logger
.error(`${this.logPrefix()} WebSocket error:`, error
)
2107 private getEnergyActiveImportRegister (
2108 connectorStatus
: ConnectorStatus
| undefined,
2111 if (this.stationInfo
?.meteringPerTransaction
=== true) {
2114 ? connectorStatus
?.transactionEnergyActiveImportRegisterValue
!= null
2115 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
)
2117 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2122 ? connectorStatus
?.energyActiveImportRegisterValue
!= null
2123 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
)
2125 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2129 private getUseConnectorId0 (stationTemplate
?: ChargingStationTemplate
): boolean {
2130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2131 return stationTemplate
?.useConnectorId0
?? Constants
.DEFAULT_STATION_INFO
.useConnectorId0
!
2134 private async stopRunningTransactions (reason
?: StopTransactionReason
): Promise
<void> {
2135 if (this.hasEvses
) {
2136 for (const [evseId
, evseStatus
] of this.evses
) {
2140 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2141 if (connectorStatus
.transactionStarted
=== true) {
2142 await this.stopTransactionOnConnector(connectorId
, reason
)
2147 for (const connectorId
of this.connectors
.keys()) {
2148 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2149 await this.stopTransactionOnConnector(connectorId
, reason
)
2156 private getConnectionTimeout (): number {
2157 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
) != null) {
2158 return convertToInt(
2159 getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)?.value
??
2160 Constants
.DEFAULT_CONNECTION_TIMEOUT
2163 return Constants
.DEFAULT_CONNECTION_TIMEOUT
2166 private getPowerDivider (): number {
2167 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()
2168 if (this.stationInfo
?.powerSharedByConnectors
=== true) {
2169 powerDivider
= this.getNumberOfRunningTransactions()
2174 private getMaximumAmperage (stationInfo
?: ChargingStationInfo
): number | undefined {
2175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2176 const maximumPower
= (stationInfo
?? this.stationInfo
!).maximumPower
!
2177 switch (this.getCurrentOutType(stationInfo
)) {
2178 case CurrentType
.AC
:
2179 return ACElectricUtils
.amperagePerPhaseFromPower(
2180 this.getNumberOfPhases(stationInfo
),
2181 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2182 this.getVoltageOut(stationInfo
)
2184 case CurrentType
.DC
:
2185 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
))
2189 private getCurrentOutType (stationInfo
?: ChargingStationInfo
): CurrentType
{
2191 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2192 (stationInfo
?? this.stationInfo
!).currentOutType
??
2193 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2194 Constants
.DEFAULT_STATION_INFO
.currentOutType
!
2198 private getVoltageOut (stationInfo
?: ChargingStationInfo
): Voltage
{
2200 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2201 (stationInfo
?? this.stationInfo
!).voltageOut
??
2202 getDefaultVoltageOut(this.getCurrentOutType(stationInfo
), this.logPrefix(), this.templateFile
)
2206 private getAmperageLimitation (): number | undefined {
2208 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2209 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
) != null
2212 convertToInt(getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
)?.value
) /
2213 getAmperageLimitationUnitDivider(this.stationInfo
)
2218 private async startMessageSequence (ATGStopAbsoluteDuration
?: boolean): Promise
<void> {
2219 if (this.stationInfo
?.autoRegister
=== true) {
2220 await this.ocppRequestService
.requestHandler
<
2221 BootNotificationRequest
,
2222 BootNotificationResponse
2223 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2224 skipBufferingOnError
: true
2227 // Start WebSocket ping
2228 if (this.wsPingSetInterval
== null) {
2229 this.startWebSocketPing()
2232 if (this.heartbeatSetInterval
== null) {
2233 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 getWebSocketPingInterval (): number {
2325 return getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
) != null
2326 ? convertToInt(getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
)
2330 private startWebSocketPing (): void {
2331 const webSocketPingInterval
= this.getWebSocketPingInterval()
2332 if (webSocketPingInterval
> 0 && this.wsPingSetInterval
== null) {
2333 this.wsPingSetInterval
= setInterval(() => {
2334 if (this.isWebSocketConnectionOpened()) {
2335 this.wsConnection
?.ping()
2337 }, secondsToMilliseconds(webSocketPingInterval
))
2339 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2340 webSocketPingInterval
2343 } else if (this.wsPingSetInterval
!= null) {
2345 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2346 webSocketPingInterval
2351 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2356 private stopWebSocketPing (): void {
2357 if (this.wsPingSetInterval
!= null) {
2358 clearInterval(this.wsPingSetInterval
)
2359 delete this.wsPingSetInterval
2363 private getConfiguredSupervisionUrl (): URL
{
2364 let configuredSupervisionUrl
: string
2365 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls()
2366 if (isNotEmptyArray(supervisionUrls
)) {
2367 let configuredSupervisionUrlIndex
: number
2368 switch (Configuration
.getSupervisionUrlDistribution()) {
2369 case SupervisionUrlDistribution
.RANDOM
:
2370 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
)
2372 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2373 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2375 !Object.values(SupervisionUrlDistribution
).includes(
2376 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2377 Configuration
.getSupervisionUrlDistribution()!
2380 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2381 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2382 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2385 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
2388 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
]
2390 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2391 configuredSupervisionUrl
= supervisionUrls
!
2393 if (isNotEmptyString(configuredSupervisionUrl
)) {
2394 return new URL(configuredSupervisionUrl
)
2396 const errorMsg
= 'No supervision url(s) configured'
2397 logger
.error(`${this.logPrefix()} ${errorMsg}`)
2398 throw new BaseError(errorMsg
)
2401 private stopHeartbeat (): void {
2402 if (this.heartbeatSetInterval
!= null) {
2403 clearInterval(this.heartbeatSetInterval
)
2404 delete this.heartbeatSetInterval
2408 private terminateWSConnection (): void {
2409 if (this.isWebSocketConnectionOpened()) {
2410 this.wsConnection
?.terminate()
2411 this.wsConnection
= null
2415 private async reconnect (): Promise
<void> {
2417 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2418 this.wsConnectionRetryCount
< this.stationInfo
!.autoReconnectMaxRetries
! ||
2419 this.stationInfo
?.autoReconnectMaxRetries
=== -1
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 (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`