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