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