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