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