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