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