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