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