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