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