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