fix: ensure the ATG is properly restored after disconnection to CSMS
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import { createHash } from 'node:crypto'
4 import { EventEmitter } from 'node:events'
5 import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6 import { dirname, join } from 'node:path'
7 import { URL } from 'node:url'
8 import { parentPort } from 'node:worker_threads'
9
10 import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
11 import merge from 'just-merge'
12 import { type RawData, WebSocket } from 'ws'
13
14 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
15 import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
16 import {
17 addConfigurationKey,
18 deleteConfigurationKey,
19 getConfigurationKey,
20 setConfigurationKeyValue
21 } from './ConfigurationKeyUtils.js'
22 import {
23 buildConnectorsMap,
24 checkChargingStation,
25 checkConfiguration,
26 checkConnectorsConfiguration,
27 checkStationInfoConnectorStatus,
28 checkTemplate,
29 createBootNotificationRequest,
30 createSerialNumber,
31 getAmperageLimitationUnitDivider,
32 getBootConnectorStatus,
33 getChargingStationConnectorChargingProfilesPowerLimit,
34 getChargingStationId,
35 getDefaultVoltageOut,
36 getHashId,
37 getIdTagsFile,
38 getMaxNumberOfEvses,
39 getNumberOfReservableConnectors,
40 getPhaseRotationValue,
41 hasFeatureProfile,
42 hasReservationExpired,
43 initializeConnectorsMapStatus,
44 propagateSerialNumber,
45 stationTemplateToStationInfo,
46 warnTemplateKeysDeprecation
47 } from './Helpers.js'
48 import { IdTagsCache } from './IdTagsCache.js'
49 import {
50 OCPP16IncomingRequestService,
51 OCPP16RequestService,
52 OCPP16ResponseService,
53 OCPP20IncomingRequestService,
54 OCPP20RequestService,
55 OCPP20ResponseService,
56 type OCPPIncomingRequestService,
57 type OCPPRequestService,
58 buildMeterValue,
59 buildTransactionEndMeterValue,
60 getMessageTypeString,
61 sendAndSetConnectorStatus
62 } from './ocpp/index.js'
63 import { SharedLRUCache } from './SharedLRUCache.js'
64 import { BaseError, OCPPError } from '../exception/index.js'
65 import { PerformanceStatistics } from '../performance/index.js'
66 import {
67 type AutomaticTransactionGeneratorConfiguration,
68 AvailabilityType,
69 type BootNotificationRequest,
70 type BootNotificationResponse,
71 type CachedRequest,
72 type ChargingStationConfiguration,
73 ChargingStationEvents,
74 type ChargingStationInfo,
75 type ChargingStationOcppConfiguration,
76 type ChargingStationTemplate,
77 type ConnectorStatus,
78 ConnectorStatusEnum,
79 CurrentType,
80 type ErrorCallback,
81 type ErrorResponse,
82 ErrorType,
83 type EvseStatus,
84 type EvseStatusConfiguration,
85 FileType,
86 FirmwareStatus,
87 type FirmwareStatusNotificationRequest,
88 type FirmwareStatusNotificationResponse,
89 type FirmwareUpgrade,
90 type HeartbeatRequest,
91 type HeartbeatResponse,
92 type IncomingRequest,
93 type IncomingRequestCommand,
94 MessageType,
95 MeterValueMeasurand,
96 type MeterValuesRequest,
97 type MeterValuesResponse,
98 OCPPVersion,
99 type OutgoingRequest,
100 PowerUnits,
101 RegistrationStatusEnumType,
102 RequestCommand,
103 type Reservation,
104 type ReservationKey,
105 ReservationTerminationReason,
106 type Response,
107 StandardParametersKey,
108 type Status,
109 type StopTransactionReason,
110 type StopTransactionRequest,
111 type StopTransactionResponse,
112 SupervisionUrlDistribution,
113 SupportedFeatureProfiles,
114 type Voltage,
115 type WSError,
116 WebSocketCloseEventStatusCode,
117 type WsOptions
118 } from '../types/index.js'
119 import {
120 ACElectricUtils,
121 AsyncLock,
122 AsyncLockType,
123 Configuration,
124 Constants,
125 DCElectricUtils,
126 buildChargingStationAutomaticTransactionGeneratorConfiguration,
127 buildConnectorsStatus,
128 buildEvsesStatus,
129 buildStartedMessage,
130 buildStoppedMessage,
131 buildUpdatedMessage,
132 clone,
133 convertToBoolean,
134 convertToDate,
135 convertToInt,
136 exponentialDelay,
137 formatDurationMilliSeconds,
138 formatDurationSeconds,
139 getRandomInteger,
140 getWebSocketCloseEventStatusString,
141 handleFileException,
142 isNotEmptyArray,
143 isNotEmptyString,
144 logPrefix,
145 logger,
146 min,
147 once,
148 roundTo,
149 secureRandom,
150 sleep,
151 watchJsonFile
152 } from '../utils/index.js'
153
154 export class ChargingStation extends EventEmitter {
155 public readonly index: number
156 public readonly templateFile: string
157 public stationInfo?: ChargingStationInfo
158 public started: boolean
159 public starting: boolean
160 public idTagsCache: IdTagsCache
161 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined
162 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined
163 public wsConnection: WebSocket | null
164 public readonly connectors: Map<number, ConnectorStatus>
165 public readonly evses: Map<number, EvseStatus>
166 public readonly requests: Map<string, CachedRequest>
167 public performanceStatistics!: PerformanceStatistics | undefined
168 public heartbeatSetInterval?: NodeJS.Timeout
169 public ocppRequestService!: OCPPRequestService
170 public bootNotificationRequest?: BootNotificationRequest
171 public bootNotificationResponse?: BootNotificationResponse
172 public powerDivider?: number
173 private stopping: boolean
174 private configurationFile!: string
175 private configurationFileHash!: string
176 private connectorsConfigurationHash!: string
177 private evsesConfigurationHash!: string
178 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
179 private ocppIncomingRequestService!: OCPPIncomingRequestService
180 private readonly messageBuffer: Set<string>
181 private configuredSupervisionUrl!: URL
182 private autoReconnectRetryCount: number
183 private templateFileWatcher!: FSWatcher | undefined
184 private templateFileHash!: string
185 private readonly sharedLRUCache: SharedLRUCache
186 private webSocketPingSetInterval?: NodeJS.Timeout
187 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
188 private flushMessageBufferSetInterval?: NodeJS.Timeout
189
190 constructor (index: number, templateFile: string) {
191 super()
192 this.started = false
193 this.starting = false
194 this.stopping = false
195 this.wsConnection = null
196 this.autoReconnectRetryCount = 0
197 this.index = index
198 this.templateFile = templateFile
199 this.connectors = new Map<number, ConnectorStatus>()
200 this.evses = new Map<number, EvseStatus>()
201 this.requests = new Map<string, CachedRequest>()
202 this.messageBuffer = new Set<string>()
203 this.sharedLRUCache = SharedLRUCache.getInstance()
204 this.idTagsCache = IdTagsCache.getInstance()
205 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
206
207 this.on(ChargingStationEvents.started, () => {
208 parentPort?.postMessage(buildStartedMessage(this))
209 })
210 this.on(ChargingStationEvents.stopped, () => {
211 parentPort?.postMessage(buildStoppedMessage(this))
212 })
213 this.on(ChargingStationEvents.updated, () => {
214 parentPort?.postMessage(buildUpdatedMessage(this))
215 })
216 this.on(ChargingStationEvents.accepted, () => {
217 this.startMessageSequence(
218 this.autoReconnectRetryCount > 0
219 ? true
220 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
221 ).catch(error => {
222 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
223 })
224 })
225 this.on(ChargingStationEvents.disconnected, () => {
226 try {
227 this.internalStopMessageSequence()
228 } catch (error) {
229 logger.error(
230 `${this.logPrefix()} Error while stopping the internal message sequence:`,
231 error
232 )
233 }
234 })
235
236 this.initialize()
237 }
238
239 public get hasEvses (): boolean {
240 return this.connectors.size === 0 && this.evses.size > 0
241 }
242
243 private get wsConnectionUrl (): URL {
244 return new URL(
245 `${
246 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
247 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
248 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
249 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
250 : this.configuredSupervisionUrl.href
251 }/${this.stationInfo?.chargingStationId}`
252 )
253 }
254
255 public logPrefix = (): string => {
256 if (
257 this instanceof ChargingStation &&
258 this.stationInfo != null &&
259 isNotEmptyString(this.stationInfo.chargingStationId)
260 ) {
261 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
262 }
263 let stationTemplate: ChargingStationTemplate | undefined
264 try {
265 stationTemplate = JSON.parse(
266 readFileSync(this.templateFile, 'utf8')
267 ) as ChargingStationTemplate
268 } catch {
269 stationTemplate = undefined
270 }
271 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
272 }
273
274 public hasIdTags (): boolean {
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
277 }
278
279 public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
280 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
281 const localStationInfo = stationInfo ?? this.stationInfo!
282 switch (this.getCurrentOutType(stationInfo)) {
283 case CurrentType.AC:
284 return localStationInfo.numberOfPhases ?? 3
285 case CurrentType.DC:
286 return 0
287 }
288 }
289
290 public isWebSocketConnectionOpened (): boolean {
291 return this.wsConnection?.readyState === WebSocket.OPEN
292 }
293
294 public inUnknownState (): boolean {
295 return this.bootNotificationResponse?.status == null
296 }
297
298 public inPendingState (): boolean {
299 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
300 }
301
302 public inAcceptedState (): boolean {
303 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
304 }
305
306 public inRejectedState (): boolean {
307 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
308 }
309
310 public isRegistered (): boolean {
311 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
312 }
313
314 public isChargingStationAvailable (): boolean {
315 return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
316 }
317
318 public hasConnector (connectorId: number): boolean {
319 if (this.hasEvses) {
320 for (const evseStatus of this.evses.values()) {
321 if (evseStatus.connectors.has(connectorId)) {
322 return true
323 }
324 }
325 return false
326 }
327 return this.connectors.has(connectorId)
328 }
329
330 public isConnectorAvailable (connectorId: number): boolean {
331 return (
332 connectorId > 0 &&
333 this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
334 )
335 }
336
337 public getNumberOfConnectors (): number {
338 if (this.hasEvses) {
339 let numberOfConnectors = 0
340 for (const [evseId, evseStatus] of this.evses) {
341 if (evseId > 0) {
342 numberOfConnectors += evseStatus.connectors.size
343 }
344 }
345 return numberOfConnectors
346 }
347 return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
348 }
349
350 public getNumberOfEvses (): number {
351 return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
352 }
353
354 public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
355 if (this.hasEvses) {
356 for (const evseStatus of this.evses.values()) {
357 if (evseStatus.connectors.has(connectorId)) {
358 return evseStatus.connectors.get(connectorId)
359 }
360 }
361 return undefined
362 }
363 return this.connectors.get(connectorId)
364 }
365
366 public getConnectorMaximumAvailablePower (connectorId: number): number {
367 let connectorAmperageLimitationPowerLimit: number | undefined
368 const amperageLimitation = this.getAmperageLimitation()
369 if (
370 amperageLimitation != null &&
371 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
372 amperageLimitation < this.stationInfo!.maximumAmperage!
373 ) {
374 connectorAmperageLimitationPowerLimit =
375 (this.stationInfo?.currentOutType === CurrentType.AC
376 ? ACElectricUtils.powerTotal(
377 this.getNumberOfPhases(),
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 this.stationInfo.voltageOut!,
380 amperageLimitation *
381 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
382 )
383 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384 DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) /
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 this.powerDivider!
387 }
388 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
389 const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
390 const connectorChargingProfilesPowerLimit =
391 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId)
392 return min(
393 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 isNaN(connectorAmperageLimitationPowerLimit!)
396 ? Infinity
397 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
398 connectorAmperageLimitationPowerLimit!,
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit!
401 )
402 }
403
404 public getTransactionIdTag (transactionId: number): string | undefined {
405 if (this.hasEvses) {
406 for (const evseStatus of this.evses.values()) {
407 for (const connectorStatus of evseStatus.connectors.values()) {
408 if (connectorStatus.transactionId === transactionId) {
409 return connectorStatus.transactionIdTag
410 }
411 }
412 }
413 } else {
414 for (const connectorId of this.connectors.keys()) {
415 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
416 return this.getConnectorStatus(connectorId)?.transactionIdTag
417 }
418 }
419 }
420 }
421
422 public getNumberOfRunningTransactions (): number {
423 let numberOfRunningTransactions = 0
424 if (this.hasEvses) {
425 for (const [evseId, evseStatus] of this.evses) {
426 if (evseId === 0) {
427 continue
428 }
429 for (const connectorStatus of evseStatus.connectors.values()) {
430 if (connectorStatus.transactionStarted === true) {
431 ++numberOfRunningTransactions
432 }
433 }
434 }
435 } else {
436 for (const connectorId of this.connectors.keys()) {
437 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
438 ++numberOfRunningTransactions
439 }
440 }
441 }
442 return numberOfRunningTransactions
443 }
444
445 public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
446 if (transactionId == null) {
447 return undefined
448 } else if (this.hasEvses) {
449 for (const evseStatus of this.evses.values()) {
450 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
451 if (connectorStatus.transactionId === transactionId) {
452 return connectorId
453 }
454 }
455 }
456 } else {
457 for (const connectorId of this.connectors.keys()) {
458 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
459 return connectorId
460 }
461 }
462 }
463 }
464
465 public getEnergyActiveImportRegisterByTransactionId (
466 transactionId: number | undefined,
467 rounded = false
468 ): number {
469 return this.getEnergyActiveImportRegister(
470 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
471 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!),
472 rounded
473 )
474 }
475
476 public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
477 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
478 }
479
480 public getAuthorizeRemoteTxRequests (): boolean {
481 const authorizeRemoteTxRequests = getConfigurationKey(
482 this,
483 StandardParametersKey.AuthorizeRemoteTxRequests
484 )
485 return authorizeRemoteTxRequests != null
486 ? convertToBoolean(authorizeRemoteTxRequests.value)
487 : false
488 }
489
490 public getLocalAuthListEnabled (): boolean {
491 const localAuthListEnabled = getConfigurationKey(
492 this,
493 StandardParametersKey.LocalAuthListEnabled
494 )
495 return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
496 }
497
498 public getHeartbeatInterval (): number {
499 const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
500 if (HeartbeatInterval != null) {
501 return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
502 }
503 const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
504 if (HeartBeatInterval != null) {
505 return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
506 }
507 this.stationInfo?.autoRegister === false &&
508 logger.warn(
509 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
510 Constants.DEFAULT_HEARTBEAT_INTERVAL
511 }`
512 )
513 return Constants.DEFAULT_HEARTBEAT_INTERVAL
514 }
515
516 public setSupervisionUrl (url: string): void {
517 if (
518 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
519 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
520 ) {
521 setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
522 } else {
523 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
524 this.stationInfo!.supervisionUrls = url
525 this.saveStationInfo()
526 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
527 }
528 }
529
530 public startHeartbeat (): void {
531 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
532 this.heartbeatSetInterval = setInterval(() => {
533 this.ocppRequestService
534 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
535 .catch(error => {
536 logger.error(
537 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
538 error
539 )
540 })
541 }, this.getHeartbeatInterval())
542 logger.info(
543 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
544 this.getHeartbeatInterval()
545 )}`
546 )
547 } else if (this.heartbeatSetInterval != null) {
548 logger.info(
549 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
550 this.getHeartbeatInterval()
551 )}`
552 )
553 } else {
554 logger.error(
555 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
556 )
557 }
558 }
559
560 public restartHeartbeat (): void {
561 // Stop heartbeat
562 this.stopHeartbeat()
563 // Start heartbeat
564 this.startHeartbeat()
565 }
566
567 public restartWebSocketPing (): void {
568 // Stop WebSocket ping
569 this.stopWebSocketPing()
570 // Start WebSocket ping
571 this.startWebSocketPing()
572 }
573
574 public startMeterValues (connectorId: number, interval: number): void {
575 if (connectorId === 0) {
576 logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
577 return
578 }
579 const connectorStatus = this.getConnectorStatus(connectorId)
580 if (connectorStatus == null) {
581 logger.error(
582 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
583 ${connectorId}`
584 )
585 return
586 }
587 if (connectorStatus.transactionStarted === false) {
588 logger.error(
589 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
590 )
591 return
592 } else if (
593 connectorStatus.transactionStarted === true &&
594 connectorStatus.transactionId == null
595 ) {
596 logger.error(
597 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
598 )
599 return
600 }
601 if (interval > 0) {
602 connectorStatus.transactionSetInterval = setInterval(() => {
603 const meterValue = buildMeterValue(
604 this,
605 connectorId,
606 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
607 connectorStatus.transactionId!,
608 interval
609 )
610 this.ocppRequestService
611 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
612 this,
613 RequestCommand.METER_VALUES,
614 {
615 connectorId,
616 transactionId: connectorStatus.transactionId,
617 meterValue: [meterValue]
618 }
619 )
620 .catch(error => {
621 logger.error(
622 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
623 error
624 )
625 })
626 }, interval)
627 } else {
628 logger.error(
629 `${this.logPrefix()} Charging station ${
630 StandardParametersKey.MeterValueSampleInterval
631 } configuration set to ${interval}, not sending MeterValues`
632 )
633 }
634 }
635
636 public stopMeterValues (connectorId: number): void {
637 const connectorStatus = this.getConnectorStatus(connectorId)
638 if (connectorStatus?.transactionSetInterval != null) {
639 clearInterval(connectorStatus.transactionSetInterval)
640 }
641 }
642
643 public start (): void {
644 if (!this.started) {
645 if (!this.starting) {
646 this.starting = true
647 if (this.stationInfo?.enableStatistics === true) {
648 this.performanceStatistics?.start()
649 }
650 this.openWSConnection()
651 // Monitor charging station template file
652 this.templateFileWatcher = watchJsonFile(
653 this.templateFile,
654 FileType.ChargingStationTemplate,
655 this.logPrefix(),
656 undefined,
657 (event, filename): void => {
658 if (isNotEmptyString(filename) && event === 'change') {
659 try {
660 logger.debug(
661 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
662 this.templateFile
663 } file have changed, reload`
664 )
665 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
666 // Initialize
667 this.initialize()
668 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
669 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
670 // Restart the ATG
671 const ATGStarted = this.automaticTransactionGenerator?.started
672 if (ATGStarted === true) {
673 this.stopAutomaticTransactionGenerator()
674 }
675 delete this.automaticTransactionGeneratorConfiguration
676 if (
677 this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
678 ATGStarted === true
679 ) {
680 this.startAutomaticTransactionGenerator(undefined, true)
681 }
682 if (this.stationInfo?.enableStatistics === true) {
683 this.performanceStatistics?.restart()
684 } else {
685 this.performanceStatistics?.stop()
686 }
687 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
688 } catch (error) {
689 logger.error(
690 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
691 error
692 )
693 }
694 }
695 }
696 )
697 this.started = true
698 this.emit(ChargingStationEvents.started)
699 this.starting = false
700 } else {
701 logger.warn(`${this.logPrefix()} Charging station is already starting...`)
702 }
703 } else {
704 logger.warn(`${this.logPrefix()} Charging station is already started...`)
705 }
706 }
707
708 public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
709 if (this.started) {
710 if (!this.stopping) {
711 this.stopping = true
712 await this.stopMessageSequence(reason, stopTransactions)
713 this.closeWSConnection()
714 if (this.stationInfo?.enableStatistics === true) {
715 this.performanceStatistics?.stop()
716 }
717 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
718 this.templateFileWatcher?.close()
719 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
720 delete this.bootNotificationResponse
721 this.started = false
722 this.saveConfiguration()
723 this.emit(ChargingStationEvents.stopped)
724 this.stopping = false
725 } else {
726 logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
727 }
728 } else {
729 logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
730 }
731 }
732
733 public async reset (reason?: StopTransactionReason): Promise<void> {
734 await this.stop(reason)
735 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
736 await sleep(this.stationInfo!.resetTime!)
737 this.initialize()
738 this.start()
739 }
740
741 public saveOcppConfiguration (): void {
742 if (this.stationInfo?.ocppPersistentConfiguration === true) {
743 this.saveConfiguration()
744 }
745 }
746
747 public bufferMessage (message: string): void {
748 this.messageBuffer.add(message)
749 this.setIntervalFlushMessageBuffer()
750 }
751
752 public openWSConnection (
753 options?: WsOptions,
754 params?: { closeOpened?: boolean, terminateOpened?: boolean }
755 ): void {
756 options = {
757 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
758 ...this.stationInfo?.wsOptions,
759 ...options
760 }
761 params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
762 if (!checkChargingStation(this, this.logPrefix())) {
763 return
764 }
765 if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
766 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
767 }
768 if (params.closeOpened === true) {
769 this.closeWSConnection()
770 }
771 if (params.terminateOpened === true) {
772 this.terminateWSConnection()
773 }
774
775 if (this.isWebSocketConnectionOpened()) {
776 logger.warn(
777 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
778 )
779 return
780 }
781
782 logger.info(
783 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
784 )
785
786 this.wsConnection = new WebSocket(
787 this.wsConnectionUrl,
788 `ocpp${this.stationInfo?.ocppVersion}`,
789 options
790 )
791
792 // Handle WebSocket message
793 this.wsConnection.on('message', data => {
794 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
795 })
796 // Handle WebSocket error
797 this.wsConnection.on('error', this.onError.bind(this))
798 // Handle WebSocket close
799 this.wsConnection.on('close', this.onClose.bind(this))
800 // Handle WebSocket open
801 this.wsConnection.on('open', () => {
802 this.onOpen().catch(error =>
803 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
804 )
805 })
806 // Handle WebSocket ping
807 this.wsConnection.on('ping', this.onPing.bind(this))
808 // Handle WebSocket pong
809 this.wsConnection.on('pong', this.onPong.bind(this))
810 }
811
812 public closeWSConnection (): void {
813 if (this.isWebSocketConnectionOpened()) {
814 this.wsConnection?.close()
815 this.wsConnection = null
816 }
817 }
818
819 public getAutomaticTransactionGeneratorConfiguration ():
820 | AutomaticTransactionGeneratorConfiguration
821 | undefined {
822 if (this.automaticTransactionGeneratorConfiguration == null) {
823 let automaticTransactionGeneratorConfiguration:
824 | AutomaticTransactionGeneratorConfiguration
825 | undefined
826 const stationTemplate = this.getTemplateFromFile()
827 const stationConfiguration = this.getConfigurationFromFile()
828 if (
829 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
830 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
831 stationConfiguration?.automaticTransactionGenerator != null
832 ) {
833 automaticTransactionGeneratorConfiguration =
834 stationConfiguration.automaticTransactionGenerator
835 } else {
836 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
837 }
838 this.automaticTransactionGeneratorConfiguration = {
839 ...Constants.DEFAULT_ATG_CONFIGURATION,
840 ...automaticTransactionGeneratorConfiguration
841 }
842 }
843 return this.automaticTransactionGeneratorConfiguration
844 }
845
846 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
847 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
848 }
849
850 public startAutomaticTransactionGenerator (
851 connectorIds?: number[],
852 stopAbsoluteDuration?: boolean
853 ): void {
854 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
855 if (isNotEmptyArray(connectorIds)) {
856 for (const connectorId of connectorIds) {
857 this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
858 }
859 } else {
860 this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
861 }
862 this.saveAutomaticTransactionGeneratorConfiguration()
863 this.emit(ChargingStationEvents.updated)
864 }
865
866 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
867 if (isNotEmptyArray(connectorIds)) {
868 for (const connectorId of connectorIds) {
869 this.automaticTransactionGenerator?.stopConnector(connectorId)
870 }
871 } else {
872 this.automaticTransactionGenerator?.stop()
873 }
874 this.saveAutomaticTransactionGeneratorConfiguration()
875 this.emit(ChargingStationEvents.updated)
876 }
877
878 public async stopTransactionOnConnector (
879 connectorId: number,
880 reason?: StopTransactionReason
881 ): Promise<StopTransactionResponse> {
882 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
883 if (
884 this.stationInfo?.beginEndMeterValues === true &&
885 this.stationInfo.ocppStrictCompliance === true &&
886 this.stationInfo.outOfOrderEndMeterValues === false
887 ) {
888 const transactionEndMeterValue = buildTransactionEndMeterValue(
889 this,
890 connectorId,
891 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
892 )
893 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
894 this,
895 RequestCommand.METER_VALUES,
896 {
897 connectorId,
898 transactionId,
899 meterValue: [transactionEndMeterValue]
900 }
901 )
902 }
903 return await this.ocppRequestService.requestHandler<
904 StopTransactionRequest,
905 StopTransactionResponse
906 >(this, RequestCommand.STOP_TRANSACTION, {
907 transactionId,
908 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
909 ...(reason != null && { reason })
910 })
911 }
912
913 public getReserveConnectorZeroSupported (): boolean {
914 return convertToBoolean(
915 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
916 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
917 )
918 }
919
920 public async addReservation (reservation: Reservation): Promise<void> {
921 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
922 if (reservationFound != null) {
923 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
924 }
925 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
926 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
927 await sendAndSetConnectorStatus(
928 this,
929 reservation.connectorId,
930 ConnectorStatusEnum.Reserved,
931 undefined,
932 { send: reservation.connectorId !== 0 }
933 )
934 }
935
936 public async removeReservation (
937 reservation: Reservation,
938 reason: ReservationTerminationReason
939 ): Promise<void> {
940 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
941 const connector = this.getConnectorStatus(reservation.connectorId)!
942 switch (reason) {
943 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
944 case ReservationTerminationReason.TRANSACTION_STARTED:
945 delete connector.reservation
946 break
947 case ReservationTerminationReason.RESERVATION_CANCELED:
948 case ReservationTerminationReason.REPLACE_EXISTING:
949 case ReservationTerminationReason.EXPIRED:
950 await sendAndSetConnectorStatus(
951 this,
952 reservation.connectorId,
953 ConnectorStatusEnum.Available,
954 undefined,
955 { send: reservation.connectorId !== 0 }
956 )
957 delete connector.reservation
958 break
959 default:
960 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
961 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
962 }
963 }
964
965 public getReservationBy (
966 filterKey: ReservationKey,
967 value: number | string
968 ): Reservation | undefined {
969 if (this.hasEvses) {
970 for (const evseStatus of this.evses.values()) {
971 for (const connectorStatus of evseStatus.connectors.values()) {
972 if (connectorStatus.reservation?.[filterKey] === value) {
973 return connectorStatus.reservation
974 }
975 }
976 }
977 } else {
978 for (const connectorStatus of this.connectors.values()) {
979 if (connectorStatus.reservation?.[filterKey] === value) {
980 return connectorStatus.reservation
981 }
982 }
983 }
984 }
985
986 public isConnectorReservable (
987 reservationId: number,
988 idTag?: string,
989 connectorId?: number
990 ): boolean {
991 const reservation = this.getReservationBy('reservationId', reservationId)
992 const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
993 if (arguments.length === 1) {
994 return !reservationExists
995 } else if (arguments.length > 1) {
996 const userReservation =
997 idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
998 const userReservationExists =
999 userReservation !== undefined && !hasReservationExpired(userReservation)
1000 const notConnectorZero = connectorId === undefined ? true : connectorId > 0
1001 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
1002 return (
1003 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
1004 )
1005 }
1006 return false
1007 }
1008
1009 private setIntervalFlushMessageBuffer (): void {
1010 if (this.flushMessageBufferSetInterval == null) {
1011 this.flushMessageBufferSetInterval = setInterval(() => {
1012 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1013 this.flushMessageBuffer()
1014 }
1015 if (this.messageBuffer.size === 0) {
1016 this.clearIntervalFlushMessageBuffer()
1017 }
1018 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
1019 }
1020 }
1021
1022 private clearIntervalFlushMessageBuffer (): void {
1023 if (this.flushMessageBufferSetInterval != null) {
1024 clearInterval(this.flushMessageBufferSetInterval)
1025 delete this.flushMessageBufferSetInterval
1026 }
1027 }
1028
1029 private getNumberOfReservableConnectors (): number {
1030 let numberOfReservableConnectors = 0
1031 if (this.hasEvses) {
1032 for (const evseStatus of this.evses.values()) {
1033 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1034 }
1035 } else {
1036 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1037 }
1038 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1039 }
1040
1041 private getNumberOfReservationsOnConnectorZero (): number {
1042 if (
1043 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1044 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1045 ) {
1046 return 1
1047 }
1048 return 0
1049 }
1050
1051 private flushMessageBuffer (): void {
1052 if (this.messageBuffer.size > 0) {
1053 for (const message of this.messageBuffer.values()) {
1054 let beginId: string | undefined
1055 let commandName: RequestCommand | undefined
1056 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1057 const isRequest = messageType === MessageType.CALL_MESSAGE
1058 if (isRequest) {
1059 [, , commandName] = JSON.parse(message) as OutgoingRequest
1060 beginId = PerformanceStatistics.beginMeasure(commandName)
1061 }
1062 this.wsConnection?.send(message, (error?: Error) => {
1063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1064 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1065 if (error == null) {
1066 logger.debug(
1067 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1068 messageType
1069 )} OCPP message sent '${JSON.stringify(message)}'`
1070 )
1071 this.messageBuffer.delete(message)
1072 } else {
1073 logger.debug(
1074 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1075 messageType
1076 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1077 error
1078 )
1079 }
1080 })
1081 }
1082 }
1083 }
1084
1085 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1086 let template: ChargingStationTemplate | undefined
1087 try {
1088 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1089 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1090 } else {
1091 const measureId = `${FileType.ChargingStationTemplate} read`
1092 const beginId = PerformanceStatistics.beginMeasure(measureId)
1093 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1094 PerformanceStatistics.endMeasure(measureId, beginId)
1095 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1096 .update(JSON.stringify(template))
1097 .digest('hex')
1098 this.sharedLRUCache.setChargingStationTemplate(template)
1099 this.templateFileHash = template.templateHash
1100 }
1101 } catch (error) {
1102 handleFileException(
1103 this.templateFile,
1104 FileType.ChargingStationTemplate,
1105 error as NodeJS.ErrnoException,
1106 this.logPrefix()
1107 )
1108 }
1109 return template
1110 }
1111
1112 private getStationInfoFromTemplate (): ChargingStationInfo {
1113 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1114 const stationTemplate = this.getTemplateFromFile()!
1115 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1116 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1117 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1118 if (stationTemplate.Connectors != null) {
1119 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1120 }
1121 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1122 stationInfo.hashId = getHashId(this.index, stationTemplate)
1123 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1124 stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
1125 createSerialNumber(stationTemplate, stationInfo)
1126 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1127 if (isNotEmptyArray(stationTemplate.power)) {
1128 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1129 stationInfo.maximumPower =
1130 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1131 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1132 : stationTemplate.power[powerArrayRandomIndex]
1133 } else {
1134 stationInfo.maximumPower =
1135 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1136 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1137 stationTemplate.power! * 1000
1138 : stationTemplate.power
1139 }
1140 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1141 stationInfo.firmwareVersionPattern =
1142 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
1143 if (
1144 isNotEmptyString(stationInfo.firmwareVersion) &&
1145 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1146 ) {
1147 logger.warn(
1148 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1149 this.templateFile
1150 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1151 )
1152 }
1153 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1154 {
1155 versionUpgrade: {
1156 step: 1
1157 },
1158 reset: true
1159 },
1160 stationTemplate.firmwareUpgrade ?? {}
1161 )
1162 stationInfo.resetTime =
1163 stationTemplate.resetTime != null
1164 ? secondsToMilliseconds(stationTemplate.resetTime)
1165 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
1166 return stationInfo
1167 }
1168
1169 private getStationInfoFromFile (
1170 stationInfoPersistentConfiguration = true
1171 ): ChargingStationInfo | undefined {
1172 let stationInfo: ChargingStationInfo | undefined
1173 if (stationInfoPersistentConfiguration) {
1174 stationInfo = this.getConfigurationFromFile()?.stationInfo
1175 if (stationInfo != null) {
1176 delete stationInfo.infoHash
1177 }
1178 }
1179 return stationInfo
1180 }
1181
1182 private getStationInfo (): ChargingStationInfo {
1183 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
1184 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1185 const stationInfoFromFile = this.getStationInfoFromFile(
1186 stationInfoFromTemplate.stationInfoPersistentConfiguration
1187 )
1188 // Priority:
1189 // 1. charging station info from template
1190 // 2. charging station info from configuration file
1191 if (
1192 stationInfoFromFile != null &&
1193 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1194 ) {
1195 return { ...defaultStationInfo, ...stationInfoFromFile }
1196 }
1197 stationInfoFromFile != null &&
1198 propagateSerialNumber(
1199 this.getTemplateFromFile(),
1200 stationInfoFromFile,
1201 stationInfoFromTemplate
1202 )
1203 return { ...defaultStationInfo, ...stationInfoFromTemplate }
1204 }
1205
1206 private saveStationInfo (): void {
1207 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1208 this.saveConfiguration()
1209 }
1210 }
1211
1212 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1213 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1214 logger.error(`${this.logPrefix()} ${errorMsg}`)
1215 throw new BaseError(errorMsg)
1216 }
1217
1218 private initialize (): void {
1219 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1220 const stationTemplate = this.getTemplateFromFile()!
1221 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1222 this.configurationFile = join(
1223 dirname(this.templateFile.replace('station-templates', 'configurations')),
1224 `${getHashId(this.index, stationTemplate)}.json`
1225 )
1226 const stationConfiguration = this.getConfigurationFromFile()
1227 if (
1228 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1229 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1230 ) {
1231 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1232 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1233 } else {
1234 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1235 }
1236 this.stationInfo = this.getStationInfo()
1237 if (
1238 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1239 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1240 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1241 ) {
1242 const patternGroup =
1243 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1244 this.stationInfo.firmwareVersion.split('.').length
1245 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1246 .exec(this.stationInfo.firmwareVersion)
1247 ?.slice(1, patternGroup + 1)
1248 if (match != null) {
1249 const patchLevelIndex = match.length - 1
1250 match[patchLevelIndex] = (
1251 convertToInt(match[patchLevelIndex]) +
1252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1253 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1254 ).toString()
1255 this.stationInfo.firmwareVersion = match.join('.')
1256 }
1257 }
1258 this.saveStationInfo()
1259 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1260 if (this.stationInfo.enableStatistics === true) {
1261 this.performanceStatistics = PerformanceStatistics.getInstance(
1262 this.stationInfo.hashId,
1263 this.stationInfo.chargingStationId,
1264 this.configuredSupervisionUrl
1265 )
1266 }
1267 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1268 if (bootNotificationRequest == null) {
1269 const errorMsg = 'Error while creating boot notification request'
1270 logger.error(`${this.logPrefix()} ${errorMsg}`)
1271 throw new BaseError(errorMsg)
1272 }
1273 this.bootNotificationRequest = bootNotificationRequest
1274 this.powerDivider = this.getPowerDivider()
1275 // OCPP configuration
1276 this.ocppConfiguration = this.getOcppConfiguration()
1277 this.initializeOcppConfiguration()
1278 this.initializeOcppServices()
1279 if (this.stationInfo.autoRegister === true) {
1280 this.bootNotificationResponse = {
1281 currentTime: new Date(),
1282 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1283 status: RegistrationStatusEnumType.ACCEPTED
1284 }
1285 }
1286 }
1287
1288 private initializeOcppServices (): void {
1289 const ocppVersion = this.stationInfo?.ocppVersion
1290 switch (ocppVersion) {
1291 case OCPPVersion.VERSION_16:
1292 this.ocppIncomingRequestService =
1293 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1294 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1295 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1296 )
1297 break
1298 case OCPPVersion.VERSION_20:
1299 case OCPPVersion.VERSION_201:
1300 this.ocppIncomingRequestService =
1301 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1302 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1303 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1304 )
1305 break
1306 default:
1307 this.handleUnsupportedVersion(ocppVersion)
1308 break
1309 }
1310 }
1311
1312 private initializeOcppConfiguration (): void {
1313 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1314 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1315 }
1316 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1317 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1318 }
1319 if (
1320 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1321 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1322 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
1323 ) {
1324 addConfigurationKey(
1325 this,
1326 this.stationInfo.supervisionUrlOcppKey,
1327 this.configuredSupervisionUrl.href,
1328 { reboot: true }
1329 )
1330 } else if (
1331 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1332 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1333 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
1334 ) {
1335 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
1336 }
1337 if (
1338 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1339 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
1340 ) {
1341 addConfigurationKey(
1342 this,
1343 this.stationInfo.amperageLimitationOcppKey,
1344 // prettier-ignore
1345 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1346 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1347 )
1348 }
1349 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1350 addConfigurationKey(
1351 this,
1352 StandardParametersKey.SupportedFeatureProfiles,
1353 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1354 )
1355 }
1356 addConfigurationKey(
1357 this,
1358 StandardParametersKey.NumberOfConnectors,
1359 this.getNumberOfConnectors().toString(),
1360 { readonly: true },
1361 { overwrite: true }
1362 )
1363 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1364 addConfigurationKey(
1365 this,
1366 StandardParametersKey.MeterValuesSampledData,
1367 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1368 )
1369 }
1370 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1371 const connectorsPhaseRotation: string[] = []
1372 if (this.hasEvses) {
1373 for (const evseStatus of this.evses.values()) {
1374 for (const connectorId of evseStatus.connectors.keys()) {
1375 connectorsPhaseRotation.push(
1376 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1377 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1378 )
1379 }
1380 }
1381 } else {
1382 for (const connectorId of this.connectors.keys()) {
1383 connectorsPhaseRotation.push(
1384 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1385 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1386 )
1387 }
1388 }
1389 addConfigurationKey(
1390 this,
1391 StandardParametersKey.ConnectorPhaseRotation,
1392 connectorsPhaseRotation.toString()
1393 )
1394 }
1395 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1396 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1397 }
1398 if (
1399 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1400 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1401 ) {
1402 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1403 }
1404 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1405 addConfigurationKey(
1406 this,
1407 StandardParametersKey.ConnectionTimeOut,
1408 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1409 )
1410 }
1411 this.saveOcppConfiguration()
1412 }
1413
1414 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1415 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
1416 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1417 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1418 }
1419 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
1420 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1421 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
1422 delete evseStatus.connectorsStatus
1423 this.evses.set(evseId, {
1424 ...(evseStatus as EvseStatus),
1425 connectors: new Map<number, ConnectorStatus>(
1426 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1427 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1428 connectorId,
1429 connectorStatus
1430 ])
1431 )
1432 })
1433 }
1434 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
1435 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1436 logger.error(`${this.logPrefix()} ${errorMsg}`)
1437 throw new BaseError(errorMsg)
1438 } else {
1439 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1440 logger.error(`${this.logPrefix()} ${errorMsg}`)
1441 throw new BaseError(errorMsg)
1442 }
1443 }
1444
1445 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1446 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
1447 this.initializeConnectorsFromTemplate(stationTemplate)
1448 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
1449 this.initializeEvsesFromTemplate(stationTemplate)
1450 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
1451 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1452 logger.error(`${this.logPrefix()} ${errorMsg}`)
1453 throw new BaseError(errorMsg)
1454 } else {
1455 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1456 logger.error(`${this.logPrefix()} ${errorMsg}`)
1457 throw new BaseError(errorMsg)
1458 }
1459 }
1460
1461 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1462 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
1463 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1464 logger.error(`${this.logPrefix()} ${errorMsg}`)
1465 throw new BaseError(errorMsg)
1466 }
1467 if (stationTemplate.Connectors?.[0] == null) {
1468 logger.warn(
1469 `${this.logPrefix()} Charging station information from template ${
1470 this.templateFile
1471 } with no connector id 0 configuration`
1472 )
1473 }
1474 if (stationTemplate.Connectors != null) {
1475 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1476 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1477 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1478 .update(
1479 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1480 )
1481 .digest('hex')
1482 const connectorsConfigChanged =
1483 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1484 if (this.connectors.size === 0 || connectorsConfigChanged) {
1485 connectorsConfigChanged && this.connectors.clear()
1486 this.connectorsConfigurationHash = connectorsConfigHash
1487 if (templateMaxConnectors > 0) {
1488 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1489 if (
1490 connectorId === 0 &&
1491 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1492 (stationTemplate.Connectors[connectorId] == null ||
1493 !this.getUseConnectorId0(stationTemplate))
1494 ) {
1495 continue
1496 }
1497 const templateConnectorId =
1498 connectorId > 0 && stationTemplate.randomConnectors === true
1499 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1500 : connectorId
1501 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
1502 checkStationInfoConnectorStatus(
1503 templateConnectorId,
1504 connectorStatus,
1505 this.logPrefix(),
1506 this.templateFile
1507 )
1508 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1509 }
1510 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1511 this.saveConnectorsStatus()
1512 } else {
1513 logger.warn(
1514 `${this.logPrefix()} Charging station information from template ${
1515 this.templateFile
1516 } with no connectors configuration defined, cannot create connectors`
1517 )
1518 }
1519 }
1520 } else {
1521 logger.warn(
1522 `${this.logPrefix()} Charging station information from template ${
1523 this.templateFile
1524 } with no connectors configuration defined, using already defined connectors`
1525 )
1526 }
1527 }
1528
1529 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1530 if (stationTemplate.Evses == null && this.evses.size === 0) {
1531 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1532 logger.error(`${this.logPrefix()} ${errorMsg}`)
1533 throw new BaseError(errorMsg)
1534 }
1535 if (stationTemplate.Evses?.[0] == null) {
1536 logger.warn(
1537 `${this.logPrefix()} Charging station information from template ${
1538 this.templateFile
1539 } with no evse id 0 configuration`
1540 )
1541 }
1542 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
1543 logger.warn(
1544 `${this.logPrefix()} Charging station information from template ${
1545 this.templateFile
1546 } with evse id 0 with no connector id 0 configuration`
1547 )
1548 }
1549 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
1550 logger.warn(
1551 `${this.logPrefix()} Charging station information from template ${
1552 this.templateFile
1553 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1554 )
1555 }
1556 if (stationTemplate.Evses != null) {
1557 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1558 .update(JSON.stringify(stationTemplate.Evses))
1559 .digest('hex')
1560 const evsesConfigChanged =
1561 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1562 if (this.evses.size === 0 || evsesConfigChanged) {
1563 evsesConfigChanged && this.evses.clear()
1564 this.evsesConfigurationHash = evsesConfigHash
1565 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
1566 if (templateMaxEvses > 0) {
1567 for (const evseKey in stationTemplate.Evses) {
1568 const evseId = convertToInt(evseKey)
1569 this.evses.set(evseId, {
1570 connectors: buildConnectorsMap(
1571 stationTemplate.Evses[evseKey].Connectors,
1572 this.logPrefix(),
1573 this.templateFile
1574 ),
1575 availability: AvailabilityType.Operative
1576 })
1577 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1578 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1579 }
1580 this.saveEvsesStatus()
1581 } else {
1582 logger.warn(
1583 `${this.logPrefix()} Charging station information from template ${
1584 this.templateFile
1585 } with no evses configuration defined, cannot create evses`
1586 )
1587 }
1588 }
1589 } else {
1590 logger.warn(
1591 `${this.logPrefix()} Charging station information from template ${
1592 this.templateFile
1593 } with no evses configuration defined, using already defined evses`
1594 )
1595 }
1596 }
1597
1598 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1599 let configuration: ChargingStationConfiguration | undefined
1600 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1601 try {
1602 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1603 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1604 this.configurationFileHash
1605 )
1606 } else {
1607 const measureId = `${FileType.ChargingStationConfiguration} read`
1608 const beginId = PerformanceStatistics.beginMeasure(measureId)
1609 configuration = JSON.parse(
1610 readFileSync(this.configurationFile, 'utf8')
1611 ) as ChargingStationConfiguration
1612 PerformanceStatistics.endMeasure(measureId, beginId)
1613 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1614 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1615 this.configurationFileHash = configuration.configurationHash!
1616 }
1617 } catch (error) {
1618 handleFileException(
1619 this.configurationFile,
1620 FileType.ChargingStationConfiguration,
1621 error as NodeJS.ErrnoException,
1622 this.logPrefix()
1623 )
1624 }
1625 }
1626 return configuration
1627 }
1628
1629 private saveAutomaticTransactionGeneratorConfiguration (): void {
1630 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1631 this.saveConfiguration()
1632 }
1633 }
1634
1635 private saveConnectorsStatus (): void {
1636 this.saveConfiguration()
1637 }
1638
1639 private saveEvsesStatus (): void {
1640 this.saveConfiguration()
1641 }
1642
1643 private saveConfiguration (): void {
1644 if (isNotEmptyString(this.configurationFile)) {
1645 try {
1646 if (!existsSync(dirname(this.configurationFile))) {
1647 mkdirSync(dirname(this.configurationFile), { recursive: true })
1648 }
1649 const configurationFromFile = this.getConfigurationFromFile()
1650 let configurationData: ChargingStationConfiguration =
1651 configurationFromFile != null
1652 ? clone<ChargingStationConfiguration>(configurationFromFile)
1653 : {}
1654 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1655 configurationData.stationInfo = this.stationInfo
1656 } else {
1657 delete configurationData.stationInfo
1658 }
1659 if (
1660 this.stationInfo?.ocppPersistentConfiguration === true &&
1661 Array.isArray(this.ocppConfiguration?.configurationKey)
1662 ) {
1663 configurationData.configurationKey = this.ocppConfiguration.configurationKey
1664 } else {
1665 delete configurationData.configurationKey
1666 }
1667 configurationData = merge<ChargingStationConfiguration>(
1668 configurationData,
1669 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1670 )
1671 if (
1672 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1673 this.getAutomaticTransactionGeneratorConfiguration() == null
1674 ) {
1675 delete configurationData.automaticTransactionGenerator
1676 }
1677 if (this.connectors.size > 0) {
1678 configurationData.connectorsStatus = buildConnectorsStatus(this)
1679 } else {
1680 delete configurationData.connectorsStatus
1681 }
1682 if (this.evses.size > 0) {
1683 configurationData.evsesStatus = buildEvsesStatus(this)
1684 } else {
1685 delete configurationData.evsesStatus
1686 }
1687 delete configurationData.configurationHash
1688 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1689 .update(
1690 JSON.stringify({
1691 stationInfo: configurationData.stationInfo,
1692 configurationKey: configurationData.configurationKey,
1693 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1694 ...(this.connectors.size > 0 && {
1695 connectorsStatus: configurationData.connectorsStatus
1696 }),
1697 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1698 } satisfies ChargingStationConfiguration)
1699 )
1700 .digest('hex')
1701 if (this.configurationFileHash !== configurationHash) {
1702 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1703 configurationData.configurationHash = configurationHash
1704 const measureId = `${FileType.ChargingStationConfiguration} write`
1705 const beginId = PerformanceStatistics.beginMeasure(measureId)
1706 writeFileSync(
1707 this.configurationFile,
1708 JSON.stringify(configurationData, undefined, 2),
1709 'utf8'
1710 )
1711 PerformanceStatistics.endMeasure(measureId, beginId)
1712 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1713 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1714 this.configurationFileHash = configurationHash
1715 }).catch(error => {
1716 handleFileException(
1717 this.configurationFile,
1718 FileType.ChargingStationConfiguration,
1719 error as NodeJS.ErrnoException,
1720 this.logPrefix()
1721 )
1722 })
1723 } else {
1724 logger.debug(
1725 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1726 this.configurationFile
1727 }`
1728 )
1729 }
1730 } catch (error) {
1731 handleFileException(
1732 this.configurationFile,
1733 FileType.ChargingStationConfiguration,
1734 error as NodeJS.ErrnoException,
1735 this.logPrefix()
1736 )
1737 }
1738 } else {
1739 logger.error(
1740 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1741 )
1742 }
1743 }
1744
1745 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1746 return this.getTemplateFromFile()?.Configuration
1747 }
1748
1749 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1750 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1751 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1752 return { configurationKey }
1753 }
1754 return undefined
1755 }
1756
1757 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
1758 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1759 this.getOcppConfigurationFromFile()
1760 if (ocppConfiguration == null) {
1761 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1762 }
1763 return ocppConfiguration
1764 }
1765
1766 private async onOpen (): Promise<void> {
1767 if (this.isWebSocketConnectionOpened()) {
1768 logger.info(
1769 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1770 )
1771 let registrationRetryCount = 0
1772 if (!this.isRegistered()) {
1773 // Send BootNotification
1774 do {
1775 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1776 BootNotificationRequest,
1777 BootNotificationResponse
1778 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1779 skipBufferingOnError: true
1780 })
1781 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1782 if (this.bootNotificationResponse?.currentTime != null) {
1783 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1784 this.bootNotificationResponse.currentTime = convertToDate(
1785 this.bootNotificationResponse.currentTime
1786 )!
1787 }
1788 if (!this.isRegistered()) {
1789 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1790 await sleep(
1791 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1792 this.bootNotificationResponse?.interval != null
1793 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1794 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1795 )
1796 }
1797 } while (
1798 !this.isRegistered() &&
1799 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1800 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1801 this.stationInfo?.registrationMaxRetries === -1)
1802 )
1803 }
1804 if (this.isRegistered()) {
1805 this.emit(ChargingStationEvents.registered)
1806 if (this.inAcceptedState()) {
1807 this.emit(ChargingStationEvents.accepted)
1808 }
1809 } else {
1810 if (this.inRejectedState()) {
1811 this.emit(ChargingStationEvents.rejected)
1812 }
1813 logger.error(
1814 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1815 this.stationInfo?.registrationMaxRetries
1816 })`
1817 )
1818 }
1819 this.autoReconnectRetryCount = 0
1820 this.emit(ChargingStationEvents.updated)
1821 } else {
1822 logger.warn(
1823 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1824 )
1825 }
1826 }
1827
1828 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1829 this.emit(ChargingStationEvents.disconnected)
1830 switch (code) {
1831 // Normal close
1832 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1833 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1834 logger.info(
1835 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1836 code
1837 )}' and reason '${reason.toString()}'`
1838 )
1839 this.autoReconnectRetryCount = 0
1840 break
1841 // Abnormal close
1842 default:
1843 logger.error(
1844 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1845 code
1846 )}' and reason '${reason.toString()}'`
1847 )
1848 this.started &&
1849 this.reconnect().catch(error =>
1850 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1851 )
1852 break
1853 }
1854 this.emit(ChargingStationEvents.updated)
1855 }
1856
1857 private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
1858 const cachedRequest = this.requests.get(messageId)
1859 if (Array.isArray(cachedRequest)) {
1860 return cachedRequest
1861 }
1862 throw new OCPPError(
1863 ErrorType.PROTOCOL_ERROR,
1864 `Cached request for message id ${messageId} ${getMessageTypeString(
1865 messageType
1866 )} is not an array`,
1867 undefined,
1868 cachedRequest
1869 )
1870 }
1871
1872 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1873 const [messageType, messageId, commandName, commandPayload] = request
1874 if (this.stationInfo?.enableStatistics === true) {
1875 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1876 }
1877 logger.debug(
1878 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1879 request
1880 )}`
1881 )
1882 // Process the message
1883 await this.ocppIncomingRequestService.incomingRequestHandler(
1884 this,
1885 messageId,
1886 commandName,
1887 commandPayload
1888 )
1889 this.emit(ChargingStationEvents.updated)
1890 }
1891
1892 private handleResponseMessage (response: Response): void {
1893 const [messageType, messageId, commandPayload] = response
1894 if (!this.requests.has(messageId)) {
1895 // Error
1896 throw new OCPPError(
1897 ErrorType.INTERNAL_ERROR,
1898 `Response for unknown message id ${messageId}`,
1899 undefined,
1900 commandPayload
1901 )
1902 }
1903 // Respond
1904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1905 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1906 messageType,
1907 messageId
1908 )!
1909 logger.debug(
1910 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1911 response
1912 )}`
1913 )
1914 responseCallback(commandPayload, requestPayload)
1915 }
1916
1917 private handleErrorMessage (errorResponse: ErrorResponse): void {
1918 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1919 if (!this.requests.has(messageId)) {
1920 // Error
1921 throw new OCPPError(
1922 ErrorType.INTERNAL_ERROR,
1923 `Error response for unknown message id ${messageId}`,
1924 undefined,
1925 { errorType, errorMessage, errorDetails }
1926 )
1927 }
1928 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1929 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1930 logger.debug(
1931 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1932 errorResponse
1933 )}`
1934 )
1935 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1936 }
1937
1938 private async onMessage (data: RawData): Promise<void> {
1939 let request: IncomingRequest | Response | ErrorResponse | undefined
1940 let messageType: MessageType | undefined
1941 let errorMsg: string
1942 try {
1943 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1944 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1945 if (Array.isArray(request)) {
1946 [messageType] = request
1947 // Check the type of message
1948 switch (messageType) {
1949 // Incoming Message
1950 case MessageType.CALL_MESSAGE:
1951 await this.handleIncomingMessage(request as IncomingRequest)
1952 break
1953 // Response Message
1954 case MessageType.CALL_RESULT_MESSAGE:
1955 this.handleResponseMessage(request as Response)
1956 break
1957 // Error Message
1958 case MessageType.CALL_ERROR_MESSAGE:
1959 this.handleErrorMessage(request as ErrorResponse)
1960 break
1961 // Unknown Message
1962 default:
1963 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1964 errorMsg = `Wrong message type ${messageType}`
1965 logger.error(`${this.logPrefix()} ${errorMsg}`)
1966 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
1967 }
1968 } else {
1969 throw new OCPPError(
1970 ErrorType.PROTOCOL_ERROR,
1971 'Incoming message is not an array',
1972 undefined,
1973 {
1974 request
1975 }
1976 )
1977 }
1978 } catch (error) {
1979 if (!Array.isArray(request)) {
1980 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
1981 return
1982 }
1983 let commandName: IncomingRequestCommand | undefined
1984 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1985 let errorCallback: ErrorCallback
1986 const [, messageId] = request
1987 switch (messageType) {
1988 case MessageType.CALL_MESSAGE:
1989 [, , commandName] = request as IncomingRequest
1990 // Send error
1991 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
1992 break
1993 case MessageType.CALL_RESULT_MESSAGE:
1994 case MessageType.CALL_ERROR_MESSAGE:
1995 if (this.requests.has(messageId)) {
1996 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1997 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1998 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1999 errorCallback(error as OCPPError, false)
2000 } else {
2001 // Remove the request from the cache in case of error at response handling
2002 this.requests.delete(messageId)
2003 }
2004 break
2005 }
2006 if (!(error instanceof OCPPError)) {
2007 logger.warn(
2008 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2009 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2010 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2011 }' message '${data.toString()}' handling is not an OCPPError:`,
2012 error
2013 )
2014 }
2015 logger.error(
2016 `${this.logPrefix()} Incoming OCPP command '${
2017 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2018 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2019 }' message '${data.toString()}'${
2020 messageType !== MessageType.CALL_MESSAGE
2021 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2022 : ''
2023 } processing error:`,
2024 error
2025 )
2026 }
2027 }
2028
2029 private onPing (): void {
2030 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2031 }
2032
2033 private onPong (): void {
2034 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2035 }
2036
2037 private onError (error: WSError): void {
2038 this.closeWSConnection()
2039 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2040 }
2041
2042 private getEnergyActiveImportRegister (
2043 connectorStatus: ConnectorStatus | undefined,
2044 rounded = false
2045 ): number {
2046 if (this.stationInfo?.meteringPerTransaction === true) {
2047 return (
2048 (rounded
2049 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2050 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2051 : undefined
2052 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2053 )
2054 }
2055 return (
2056 (rounded
2057 ? connectorStatus?.energyActiveImportRegisterValue != null
2058 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2059 : undefined
2060 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2061 )
2062 }
2063
2064 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2065 return stationTemplate?.useConnectorId0 ?? true
2066 }
2067
2068 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2069 if (this.hasEvses) {
2070 for (const [evseId, evseStatus] of this.evses) {
2071 if (evseId === 0) {
2072 continue
2073 }
2074 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2075 if (connectorStatus.transactionStarted === true) {
2076 await this.stopTransactionOnConnector(connectorId, reason)
2077 }
2078 }
2079 }
2080 } else {
2081 for (const connectorId of this.connectors.keys()) {
2082 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2083 await this.stopTransactionOnConnector(connectorId, reason)
2084 }
2085 }
2086 }
2087 }
2088
2089 // 0 for disabling
2090 private getConnectionTimeout (): number {
2091 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2092 return convertToInt(
2093 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2094 Constants.DEFAULT_CONNECTION_TIMEOUT
2095 )
2096 }
2097 return Constants.DEFAULT_CONNECTION_TIMEOUT
2098 }
2099
2100 private getPowerDivider (): number {
2101 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2102 if (this.stationInfo?.powerSharedByConnectors === true) {
2103 powerDivider = this.getNumberOfRunningTransactions()
2104 }
2105 return powerDivider
2106 }
2107
2108 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2109 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2110 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2111 switch (this.getCurrentOutType(stationInfo)) {
2112 case CurrentType.AC:
2113 return ACElectricUtils.amperagePerPhaseFromPower(
2114 this.getNumberOfPhases(stationInfo),
2115 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2116 this.getVoltageOut(stationInfo)
2117 )
2118 case CurrentType.DC:
2119 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2120 }
2121 }
2122
2123 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2125 return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC
2126 }
2127
2128 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2129 return (
2130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2131 (stationInfo ?? this.stationInfo!).voltageOut ??
2132 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2133 )
2134 }
2135
2136 private getAmperageLimitation (): number | undefined {
2137 if (
2138 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2139 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2140 ) {
2141 return (
2142 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2143 getAmperageLimitationUnitDivider(this.stationInfo)
2144 )
2145 }
2146 }
2147
2148 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2149 if (this.stationInfo?.autoRegister === true) {
2150 await this.ocppRequestService.requestHandler<
2151 BootNotificationRequest,
2152 BootNotificationResponse
2153 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2154 skipBufferingOnError: true
2155 })
2156 }
2157 // Start WebSocket ping
2158 this.startWebSocketPing()
2159 // Start heartbeat
2160 this.startHeartbeat()
2161 // Initialize connectors status
2162 if (this.hasEvses) {
2163 for (const [evseId, evseStatus] of this.evses) {
2164 if (evseId > 0) {
2165 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2166 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2167 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
2168 }
2169 }
2170 }
2171 } else {
2172 for (const connectorId of this.connectors.keys()) {
2173 if (connectorId > 0) {
2174 const connectorBootStatus = getBootConnectorStatus(
2175 this,
2176 connectorId,
2177 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2178 this.getConnectorStatus(connectorId)!
2179 )
2180 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
2181 }
2182 }
2183 }
2184 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2185 await this.ocppRequestService.requestHandler<
2186 FirmwareStatusNotificationRequest,
2187 FirmwareStatusNotificationResponse
2188 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2189 status: FirmwareStatus.Installed
2190 })
2191 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2192 }
2193
2194 // Start the ATG
2195 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2196 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2197 }
2198 this.flushMessageBuffer()
2199 }
2200
2201 private internalStopMessageSequence (): void {
2202 // Stop WebSocket ping
2203 this.stopWebSocketPing()
2204 // Stop heartbeat
2205 this.stopHeartbeat()
2206 // Stop the ATG
2207 if (this.automaticTransactionGenerator?.started === true) {
2208 this.stopAutomaticTransactionGenerator()
2209 }
2210 }
2211
2212 private async stopMessageSequence (
2213 reason?: StopTransactionReason,
2214 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
2215 ): Promise<void> {
2216 this.internalStopMessageSequence()
2217 // Stop ongoing transactions
2218 stopTransactions === true && (await this.stopRunningTransactions(reason))
2219 if (this.hasEvses) {
2220 for (const [evseId, evseStatus] of this.evses) {
2221 if (evseId > 0) {
2222 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2223 await sendAndSetConnectorStatus(
2224 this,
2225 connectorId,
2226 ConnectorStatusEnum.Unavailable,
2227 evseId
2228 )
2229 delete connectorStatus.status
2230 }
2231 }
2232 }
2233 } else {
2234 for (const connectorId of this.connectors.keys()) {
2235 if (connectorId > 0) {
2236 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2237 delete this.getConnectorStatus(connectorId)?.status
2238 }
2239 }
2240 }
2241 }
2242
2243 private startWebSocketPing (): void {
2244 const webSocketPingInterval =
2245 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2246 ? convertToInt(
2247 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2248 )
2249 : 0
2250 if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) {
2251 this.webSocketPingSetInterval = setInterval(() => {
2252 if (this.isWebSocketConnectionOpened()) {
2253 this.wsConnection?.ping()
2254 }
2255 }, secondsToMilliseconds(webSocketPingInterval))
2256 logger.info(
2257 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2258 webSocketPingInterval
2259 )}`
2260 )
2261 } else if (this.webSocketPingSetInterval != null) {
2262 logger.info(
2263 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2264 webSocketPingInterval
2265 )}`
2266 )
2267 } else {
2268 logger.error(
2269 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2270 )
2271 }
2272 }
2273
2274 private stopWebSocketPing (): void {
2275 if (this.webSocketPingSetInterval != null) {
2276 clearInterval(this.webSocketPingSetInterval)
2277 delete this.webSocketPingSetInterval
2278 }
2279 }
2280
2281 private getConfiguredSupervisionUrl (): URL {
2282 let configuredSupervisionUrl: string
2283 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2284 if (isNotEmptyArray(supervisionUrls)) {
2285 let configuredSupervisionUrlIndex: number
2286 switch (Configuration.getSupervisionUrlDistribution()) {
2287 case SupervisionUrlDistribution.RANDOM:
2288 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2289 break
2290 case SupervisionUrlDistribution.ROUND_ROBIN:
2291 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2292 default:
2293 !Object.values(SupervisionUrlDistribution).includes(
2294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2295 Configuration.getSupervisionUrlDistribution()!
2296 ) &&
2297 logger.error(
2298 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2299 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2300 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2301 }`
2302 )
2303 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2304 break
2305 }
2306 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2307 } else {
2308 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2309 configuredSupervisionUrl = supervisionUrls!
2310 }
2311 if (isNotEmptyString(configuredSupervisionUrl)) {
2312 return new URL(configuredSupervisionUrl)
2313 }
2314 const errorMsg = 'No supervision url(s) configured'
2315 logger.error(`${this.logPrefix()} ${errorMsg}`)
2316 throw new BaseError(errorMsg)
2317 }
2318
2319 private stopHeartbeat (): void {
2320 if (this.heartbeatSetInterval != null) {
2321 clearInterval(this.heartbeatSetInterval)
2322 delete this.heartbeatSetInterval
2323 }
2324 }
2325
2326 private terminateWSConnection (): void {
2327 if (this.isWebSocketConnectionOpened()) {
2328 this.wsConnection?.terminate()
2329 this.wsConnection = null
2330 }
2331 }
2332
2333 private async reconnect (): Promise<void> {
2334 if (
2335 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2336 this.autoReconnectRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2337 this.stationInfo?.autoReconnectMaxRetries === -1
2338 ) {
2339 ++this.autoReconnectRetryCount
2340 const reconnectDelay =
2341 this.stationInfo?.reconnectExponentialDelay === true
2342 ? exponentialDelay(this.autoReconnectRetryCount)
2343 : secondsToMilliseconds(this.getConnectionTimeout())
2344 const reconnectDelayWithdraw = 1000
2345 const reconnectTimeout =
2346 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2347 logger.error(
2348 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2349 reconnectDelay,
2350 2
2351 )}ms, timeout ${reconnectTimeout}ms`
2352 )
2353 await sleep(reconnectDelay)
2354 logger.error(
2355 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2356 )
2357 this.openWSConnection(
2358 {
2359 handshakeTimeout: reconnectTimeout
2360 },
2361 { closeOpened: true }
2362 )
2363 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2364 logger.error(
2365 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2366 this.autoReconnectRetryCount
2367 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2368 )
2369 }
2370 }
2371 }