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