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