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