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