fix: handle WebSocket opening error
[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('message', data => {
772 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
773 })
774 // Handle WebSocket error
775 this.wsConnection.on('error', this.onError.bind(this))
776 // Handle WebSocket close
777 this.wsConnection.on('close', this.onClose.bind(this))
778 // Handle WebSocket open
779 this.wsConnection.on('open', () => {
780 this.onOpen().catch(error =>
781 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
782 )
783 })
784 // Handle WebSocket ping
785 this.wsConnection.on('ping', this.onPing.bind(this))
786 // Handle WebSocket pong
787 this.wsConnection.on('pong', this.onPong.bind(this))
788 }
789
790 public closeWSConnection (): void {
791 if (this.isWebSocketConnectionOpened()) {
792 this.wsConnection?.close()
793 this.wsConnection = null
794 }
795 }
796
797 public getAutomaticTransactionGeneratorConfiguration ():
798 | AutomaticTransactionGeneratorConfiguration
799 | undefined {
800 if (this.automaticTransactionGeneratorConfiguration == null) {
801 let automaticTransactionGeneratorConfiguration:
802 | AutomaticTransactionGeneratorConfiguration
803 | undefined
804 const stationTemplate = this.getTemplateFromFile()
805 const stationConfiguration = this.getConfigurationFromFile()
806 if (
807 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
808 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
809 stationConfiguration?.automaticTransactionGenerator != null
810 ) {
811 automaticTransactionGeneratorConfiguration =
812 stationConfiguration.automaticTransactionGenerator
813 } else {
814 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
815 }
816 this.automaticTransactionGeneratorConfiguration = {
817 ...Constants.DEFAULT_ATG_CONFIGURATION,
818 ...automaticTransactionGeneratorConfiguration
819 }
820 }
821 return this.automaticTransactionGeneratorConfiguration
822 }
823
824 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
825 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
826 }
827
828 public startAutomaticTransactionGenerator (connectorIds?: number[]): void {
829 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
830 if (isNotEmptyArray(connectorIds)) {
831 for (const connectorId of connectorIds) {
832 this.automaticTransactionGenerator?.startConnector(connectorId)
833 }
834 } else {
835 this.automaticTransactionGenerator?.start()
836 }
837 this.saveAutomaticTransactionGeneratorConfiguration()
838 this.emit(ChargingStationEvents.updated)
839 }
840
841 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
842 if (isNotEmptyArray(connectorIds)) {
843 for (const connectorId of connectorIds) {
844 this.automaticTransactionGenerator?.stopConnector(connectorId)
845 }
846 } else {
847 this.automaticTransactionGenerator?.stop()
848 }
849 this.saveAutomaticTransactionGeneratorConfiguration()
850 this.emit(ChargingStationEvents.updated)
851 }
852
853 public async stopTransactionOnConnector (
854 connectorId: number,
855 reason?: StopTransactionReason
856 ): Promise<StopTransactionResponse> {
857 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
858 if (
859 this.stationInfo?.beginEndMeterValues === true &&
860 this.stationInfo.ocppStrictCompliance === true &&
861 this.stationInfo.outOfOrderEndMeterValues === false
862 ) {
863 const transactionEndMeterValue = buildTransactionEndMeterValue(
864 this,
865 connectorId,
866 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
867 )
868 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
869 this,
870 RequestCommand.METER_VALUES,
871 {
872 connectorId,
873 transactionId,
874 meterValue: [transactionEndMeterValue]
875 }
876 )
877 }
878 return await this.ocppRequestService.requestHandler<
879 StopTransactionRequest,
880 StopTransactionResponse
881 >(this, RequestCommand.STOP_TRANSACTION, {
882 transactionId,
883 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
884 ...(reason != null && { reason })
885 })
886 }
887
888 public getReserveConnectorZeroSupported (): boolean {
889 return convertToBoolean(
890 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
891 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
892 )
893 }
894
895 public async addReservation (reservation: Reservation): Promise<void> {
896 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
897 if (reservationFound != null) {
898 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
899 }
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
901 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
902 await sendAndSetConnectorStatus(
903 this,
904 reservation.connectorId,
905 ConnectorStatusEnum.Reserved,
906 undefined,
907 { send: reservation.connectorId !== 0 }
908 )
909 }
910
911 public async removeReservation (
912 reservation: Reservation,
913 reason: ReservationTerminationReason
914 ): Promise<void> {
915 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
916 const connector = this.getConnectorStatus(reservation.connectorId)!
917 switch (reason) {
918 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
919 case ReservationTerminationReason.TRANSACTION_STARTED:
920 delete connector.reservation
921 break
922 case ReservationTerminationReason.RESERVATION_CANCELED:
923 case ReservationTerminationReason.REPLACE_EXISTING:
924 case ReservationTerminationReason.EXPIRED:
925 await sendAndSetConnectorStatus(
926 this,
927 reservation.connectorId,
928 ConnectorStatusEnum.Available,
929 undefined,
930 { send: reservation.connectorId !== 0 }
931 )
932 delete connector.reservation
933 break
934 default:
935 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
936 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
937 }
938 }
939
940 public getReservationBy (
941 filterKey: ReservationKey,
942 value: number | string
943 ): Reservation | undefined {
944 if (this.hasEvses) {
945 for (const evseStatus of this.evses.values()) {
946 for (const connectorStatus of evseStatus.connectors.values()) {
947 if (connectorStatus.reservation?.[filterKey] === value) {
948 return connectorStatus.reservation
949 }
950 }
951 }
952 } else {
953 for (const connectorStatus of this.connectors.values()) {
954 if (connectorStatus.reservation?.[filterKey] === value) {
955 return connectorStatus.reservation
956 }
957 }
958 }
959 }
960
961 public isConnectorReservable (
962 reservationId: number,
963 idTag?: string,
964 connectorId?: number
965 ): boolean {
966 const reservation = this.getReservationBy('reservationId', reservationId)
967 const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
968 if (arguments.length === 1) {
969 return !reservationExists
970 } else if (arguments.length > 1) {
971 const userReservation =
972 idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
973 const userReservationExists =
974 userReservation !== undefined && !hasReservationExpired(userReservation)
975 const notConnectorZero = connectorId === undefined ? true : connectorId > 0
976 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
977 return (
978 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
979 )
980 }
981 return false
982 }
983
984 private setIntervalFlushMessageBuffer (): void {
985 if (this.flushMessageBufferSetInterval == null) {
986 this.flushMessageBufferSetInterval = setInterval(() => {
987 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
988 this.flushMessageBuffer()
989 }
990 if (this.messageBuffer.size === 0) {
991 this.clearIntervalFlushMessageBuffer()
992 }
993 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
994 }
995 }
996
997 private clearIntervalFlushMessageBuffer (): void {
998 if (this.flushMessageBufferSetInterval != null) {
999 clearInterval(this.flushMessageBufferSetInterval)
1000 delete this.flushMessageBufferSetInterval
1001 }
1002 }
1003
1004 private getNumberOfReservableConnectors (): number {
1005 let numberOfReservableConnectors = 0
1006 if (this.hasEvses) {
1007 for (const evseStatus of this.evses.values()) {
1008 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1009 }
1010 } else {
1011 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1012 }
1013 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1014 }
1015
1016 private getNumberOfReservationsOnConnectorZero (): number {
1017 if (
1018 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1019 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1020 ) {
1021 return 1
1022 }
1023 return 0
1024 }
1025
1026 private flushMessageBuffer (): void {
1027 if (this.messageBuffer.size > 0) {
1028 for (const message of this.messageBuffer.values()) {
1029 let beginId: string | undefined
1030 let commandName: RequestCommand | undefined
1031 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1032 const isRequest = messageType === MessageType.CALL_MESSAGE
1033 if (isRequest) {
1034 [, , commandName] = JSON.parse(message) as OutgoingRequest
1035 beginId = PerformanceStatistics.beginMeasure(commandName)
1036 }
1037 this.wsConnection?.send(message, (error?: Error) => {
1038 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1039 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1040 if (error == null) {
1041 logger.debug(
1042 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1043 messageType
1044 )} OCPP message sent '${JSON.stringify(message)}'`
1045 )
1046 this.messageBuffer.delete(message)
1047 } else {
1048 logger.debug(
1049 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1050 messageType
1051 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1052 error
1053 )
1054 }
1055 })
1056 }
1057 }
1058 }
1059
1060 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1061 let template: ChargingStationTemplate | undefined
1062 try {
1063 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1064 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1065 } else {
1066 const measureId = `${FileType.ChargingStationTemplate} read`
1067 const beginId = PerformanceStatistics.beginMeasure(measureId)
1068 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1069 PerformanceStatistics.endMeasure(measureId, beginId)
1070 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1071 .update(JSON.stringify(template))
1072 .digest('hex')
1073 this.sharedLRUCache.setChargingStationTemplate(template)
1074 this.templateFileHash = template.templateHash
1075 }
1076 } catch (error) {
1077 handleFileException(
1078 this.templateFile,
1079 FileType.ChargingStationTemplate,
1080 error as NodeJS.ErrnoException,
1081 this.logPrefix()
1082 )
1083 }
1084 return template
1085 }
1086
1087 private getStationInfoFromTemplate (): ChargingStationInfo {
1088 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1089 const stationTemplate = this.getTemplateFromFile()!
1090 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1091 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1092 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1093 if (stationTemplate.Connectors != null) {
1094 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1095 }
1096 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1097 stationInfo.hashId = getHashId(this.index, stationTemplate)
1098 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1099 stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
1100 createSerialNumber(stationTemplate, stationInfo)
1101 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1102 if (isNotEmptyArray(stationTemplate.power)) {
1103 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1104 stationInfo.maximumPower =
1105 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1106 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1107 : stationTemplate.power[powerArrayRandomIndex]
1108 } else {
1109 stationInfo.maximumPower =
1110 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1111 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1112 stationTemplate.power! * 1000
1113 : stationTemplate.power
1114 }
1115 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1116 stationInfo.firmwareVersionPattern =
1117 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
1118 if (
1119 isNotEmptyString(stationInfo.firmwareVersion) &&
1120 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1121 ) {
1122 logger.warn(
1123 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1124 this.templateFile
1125 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1126 )
1127 }
1128 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1129 {
1130 versionUpgrade: {
1131 step: 1
1132 },
1133 reset: true
1134 },
1135 stationTemplate.firmwareUpgrade ?? {}
1136 )
1137 stationInfo.resetTime =
1138 stationTemplate.resetTime != null
1139 ? secondsToMilliseconds(stationTemplate.resetTime)
1140 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
1141 return stationInfo
1142 }
1143
1144 private getStationInfoFromFile (
1145 stationInfoPersistentConfiguration = true
1146 ): ChargingStationInfo | undefined {
1147 let stationInfo: ChargingStationInfo | undefined
1148 if (stationInfoPersistentConfiguration) {
1149 stationInfo = this.getConfigurationFromFile()?.stationInfo
1150 if (stationInfo != null) {
1151 delete stationInfo.infoHash
1152 }
1153 }
1154 return stationInfo
1155 }
1156
1157 private getStationInfo (): ChargingStationInfo {
1158 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
1159 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1160 const stationInfoFromFile = this.getStationInfoFromFile(
1161 stationInfoFromTemplate.stationInfoPersistentConfiguration
1162 )
1163 // Priority:
1164 // 1. charging station info from template
1165 // 2. charging station info from configuration file
1166 if (
1167 stationInfoFromFile != null &&
1168 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1169 ) {
1170 return { ...defaultStationInfo, ...stationInfoFromFile }
1171 }
1172 stationInfoFromFile != null &&
1173 propagateSerialNumber(
1174 this.getTemplateFromFile(),
1175 stationInfoFromFile,
1176 stationInfoFromTemplate
1177 )
1178 return { ...defaultStationInfo, ...stationInfoFromTemplate }
1179 }
1180
1181 private saveStationInfo (): void {
1182 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1183 this.saveConfiguration()
1184 }
1185 }
1186
1187 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1188 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1189 logger.error(`${this.logPrefix()} ${errorMsg}`)
1190 throw new BaseError(errorMsg)
1191 }
1192
1193 private initialize (): void {
1194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1195 const stationTemplate = this.getTemplateFromFile()!
1196 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1197 this.configurationFile = join(
1198 dirname(this.templateFile.replace('station-templates', 'configurations')),
1199 `${getHashId(this.index, stationTemplate)}.json`
1200 )
1201 const stationConfiguration = this.getConfigurationFromFile()
1202 if (
1203 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1204 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1205 ) {
1206 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1207 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1208 } else {
1209 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1210 }
1211 this.stationInfo = this.getStationInfo()
1212 if (
1213 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1214 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1215 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1216 ) {
1217 const patternGroup =
1218 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1219 this.stationInfo.firmwareVersion.split('.').length
1220 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1221 .exec(this.stationInfo.firmwareVersion)
1222 ?.slice(1, patternGroup + 1)
1223 if (match != null) {
1224 const patchLevelIndex = match.length - 1
1225 match[patchLevelIndex] = (
1226 convertToInt(match[patchLevelIndex]) +
1227 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1228 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1229 ).toString()
1230 this.stationInfo.firmwareVersion = match.join('.')
1231 }
1232 }
1233 this.saveStationInfo()
1234 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1235 if (this.stationInfo.enableStatistics === true) {
1236 this.performanceStatistics = PerformanceStatistics.getInstance(
1237 this.stationInfo.hashId,
1238 this.stationInfo.chargingStationId,
1239 this.configuredSupervisionUrl
1240 )
1241 }
1242 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1243 if (bootNotificationRequest == null) {
1244 const errorMsg = 'Error while creating boot notification request'
1245 logger.error(`${this.logPrefix()} ${errorMsg}`)
1246 throw new BaseError(errorMsg)
1247 }
1248 this.bootNotificationRequest = bootNotificationRequest
1249 this.powerDivider = this.getPowerDivider()
1250 // OCPP configuration
1251 this.ocppConfiguration = this.getOcppConfiguration()
1252 this.initializeOcppConfiguration()
1253 this.initializeOcppServices()
1254 this.once(ChargingStationEvents.accepted, () => {
1255 this.startMessageSequence().catch(error => {
1256 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
1257 })
1258 })
1259 if (this.stationInfo.autoRegister === true) {
1260 this.bootNotificationResponse = {
1261 currentTime: new Date(),
1262 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1263 status: RegistrationStatusEnumType.ACCEPTED
1264 }
1265 }
1266 }
1267
1268 private initializeOcppServices (): void {
1269 const ocppVersion = this.stationInfo?.ocppVersion
1270 switch (ocppVersion) {
1271 case OCPPVersion.VERSION_16:
1272 this.ocppIncomingRequestService =
1273 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1274 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1275 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1276 )
1277 break
1278 case OCPPVersion.VERSION_20:
1279 case OCPPVersion.VERSION_201:
1280 this.ocppIncomingRequestService =
1281 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1282 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1283 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1284 )
1285 break
1286 default:
1287 this.handleUnsupportedVersion(ocppVersion)
1288 break
1289 }
1290 }
1291
1292 private initializeOcppConfiguration (): void {
1293 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1294 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1295 }
1296 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1297 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1298 }
1299 if (
1300 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1301 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1302 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
1303 ) {
1304 addConfigurationKey(
1305 this,
1306 this.stationInfo.supervisionUrlOcppKey,
1307 this.configuredSupervisionUrl.href,
1308 { reboot: true }
1309 )
1310 } else if (
1311 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1312 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1313 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
1314 ) {
1315 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
1316 }
1317 if (
1318 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1319 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
1320 ) {
1321 addConfigurationKey(
1322 this,
1323 this.stationInfo.amperageLimitationOcppKey,
1324 // prettier-ignore
1325 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1326 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1327 )
1328 }
1329 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1330 addConfigurationKey(
1331 this,
1332 StandardParametersKey.SupportedFeatureProfiles,
1333 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1334 )
1335 }
1336 addConfigurationKey(
1337 this,
1338 StandardParametersKey.NumberOfConnectors,
1339 this.getNumberOfConnectors().toString(),
1340 { readonly: true },
1341 { overwrite: true }
1342 )
1343 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1344 addConfigurationKey(
1345 this,
1346 StandardParametersKey.MeterValuesSampledData,
1347 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1348 )
1349 }
1350 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1351 const connectorsPhaseRotation: string[] = []
1352 if (this.hasEvses) {
1353 for (const evseStatus of this.evses.values()) {
1354 for (const connectorId of evseStatus.connectors.keys()) {
1355 connectorsPhaseRotation.push(
1356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1357 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1358 )
1359 }
1360 }
1361 } else {
1362 for (const connectorId of this.connectors.keys()) {
1363 connectorsPhaseRotation.push(
1364 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1365 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1366 )
1367 }
1368 }
1369 addConfigurationKey(
1370 this,
1371 StandardParametersKey.ConnectorPhaseRotation,
1372 connectorsPhaseRotation.toString()
1373 )
1374 }
1375 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1376 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1377 }
1378 if (
1379 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1380 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1381 ) {
1382 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1383 }
1384 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1385 addConfigurationKey(
1386 this,
1387 StandardParametersKey.ConnectionTimeOut,
1388 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1389 )
1390 }
1391 this.saveOcppConfiguration()
1392 }
1393
1394 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1395 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
1396 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1397 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1398 }
1399 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
1400 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1401 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
1402 delete evseStatus.connectorsStatus
1403 this.evses.set(evseId, {
1404 ...(evseStatus as EvseStatus),
1405 connectors: new Map<number, ConnectorStatus>(
1406 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1407 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1408 connectorId,
1409 connectorStatus
1410 ])
1411 )
1412 })
1413 }
1414 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
1415 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1416 logger.error(`${this.logPrefix()} ${errorMsg}`)
1417 throw new BaseError(errorMsg)
1418 } else {
1419 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1420 logger.error(`${this.logPrefix()} ${errorMsg}`)
1421 throw new BaseError(errorMsg)
1422 }
1423 }
1424
1425 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1426 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
1427 this.initializeConnectorsFromTemplate(stationTemplate)
1428 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
1429 this.initializeEvsesFromTemplate(stationTemplate)
1430 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
1431 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1432 logger.error(`${this.logPrefix()} ${errorMsg}`)
1433 throw new BaseError(errorMsg)
1434 } else {
1435 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1436 logger.error(`${this.logPrefix()} ${errorMsg}`)
1437 throw new BaseError(errorMsg)
1438 }
1439 }
1440
1441 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1442 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
1443 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1444 logger.error(`${this.logPrefix()} ${errorMsg}`)
1445 throw new BaseError(errorMsg)
1446 }
1447 if (stationTemplate.Connectors?.[0] == null) {
1448 logger.warn(
1449 `${this.logPrefix()} Charging station information from template ${
1450 this.templateFile
1451 } with no connector id 0 configuration`
1452 )
1453 }
1454 if (stationTemplate.Connectors != null) {
1455 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1456 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1457 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1458 .update(
1459 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1460 )
1461 .digest('hex')
1462 const connectorsConfigChanged =
1463 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1464 if (this.connectors.size === 0 || connectorsConfigChanged) {
1465 connectorsConfigChanged && this.connectors.clear()
1466 this.connectorsConfigurationHash = connectorsConfigHash
1467 if (templateMaxConnectors > 0) {
1468 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1469 if (
1470 connectorId === 0 &&
1471 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1472 (stationTemplate.Connectors[connectorId] == null ||
1473 !this.getUseConnectorId0(stationTemplate))
1474 ) {
1475 continue
1476 }
1477 const templateConnectorId =
1478 connectorId > 0 && stationTemplate.randomConnectors === true
1479 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1480 : connectorId
1481 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
1482 checkStationInfoConnectorStatus(
1483 templateConnectorId,
1484 connectorStatus,
1485 this.logPrefix(),
1486 this.templateFile
1487 )
1488 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1489 }
1490 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1491 this.saveConnectorsStatus()
1492 } else {
1493 logger.warn(
1494 `${this.logPrefix()} Charging station information from template ${
1495 this.templateFile
1496 } with no connectors configuration defined, cannot create connectors`
1497 )
1498 }
1499 }
1500 } else {
1501 logger.warn(
1502 `${this.logPrefix()} Charging station information from template ${
1503 this.templateFile
1504 } with no connectors configuration defined, using already defined connectors`
1505 )
1506 }
1507 }
1508
1509 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1510 if (stationTemplate.Evses == null && this.evses.size === 0) {
1511 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1512 logger.error(`${this.logPrefix()} ${errorMsg}`)
1513 throw new BaseError(errorMsg)
1514 }
1515 if (stationTemplate.Evses?.[0] == null) {
1516 logger.warn(
1517 `${this.logPrefix()} Charging station information from template ${
1518 this.templateFile
1519 } with no evse id 0 configuration`
1520 )
1521 }
1522 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
1523 logger.warn(
1524 `${this.logPrefix()} Charging station information from template ${
1525 this.templateFile
1526 } with evse id 0 with no connector id 0 configuration`
1527 )
1528 }
1529 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
1530 logger.warn(
1531 `${this.logPrefix()} Charging station information from template ${
1532 this.templateFile
1533 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1534 )
1535 }
1536 if (stationTemplate.Evses != null) {
1537 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1538 .update(JSON.stringify(stationTemplate.Evses))
1539 .digest('hex')
1540 const evsesConfigChanged =
1541 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1542 if (this.evses.size === 0 || evsesConfigChanged) {
1543 evsesConfigChanged && this.evses.clear()
1544 this.evsesConfigurationHash = evsesConfigHash
1545 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
1546 if (templateMaxEvses > 0) {
1547 for (const evseKey in stationTemplate.Evses) {
1548 const evseId = convertToInt(evseKey)
1549 this.evses.set(evseId, {
1550 connectors: buildConnectorsMap(
1551 stationTemplate.Evses[evseKey].Connectors,
1552 this.logPrefix(),
1553 this.templateFile
1554 ),
1555 availability: AvailabilityType.Operative
1556 })
1557 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1558 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1559 }
1560 this.saveEvsesStatus()
1561 } else {
1562 logger.warn(
1563 `${this.logPrefix()} Charging station information from template ${
1564 this.templateFile
1565 } with no evses configuration defined, cannot create evses`
1566 )
1567 }
1568 }
1569 } else {
1570 logger.warn(
1571 `${this.logPrefix()} Charging station information from template ${
1572 this.templateFile
1573 } with no evses configuration defined, using already defined evses`
1574 )
1575 }
1576 }
1577
1578 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1579 let configuration: ChargingStationConfiguration | undefined
1580 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1581 try {
1582 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1583 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1584 this.configurationFileHash
1585 )
1586 } else {
1587 const measureId = `${FileType.ChargingStationConfiguration} read`
1588 const beginId = PerformanceStatistics.beginMeasure(measureId)
1589 configuration = JSON.parse(
1590 readFileSync(this.configurationFile, 'utf8')
1591 ) as ChargingStationConfiguration
1592 PerformanceStatistics.endMeasure(measureId, beginId)
1593 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1594 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1595 this.configurationFileHash = configuration.configurationHash!
1596 }
1597 } catch (error) {
1598 handleFileException(
1599 this.configurationFile,
1600 FileType.ChargingStationConfiguration,
1601 error as NodeJS.ErrnoException,
1602 this.logPrefix()
1603 )
1604 }
1605 }
1606 return configuration
1607 }
1608
1609 private saveAutomaticTransactionGeneratorConfiguration (): void {
1610 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1611 this.saveConfiguration()
1612 }
1613 }
1614
1615 private saveConnectorsStatus (): void {
1616 this.saveConfiguration()
1617 }
1618
1619 private saveEvsesStatus (): void {
1620 this.saveConfiguration()
1621 }
1622
1623 private saveConfiguration (): void {
1624 if (isNotEmptyString(this.configurationFile)) {
1625 try {
1626 if (!existsSync(dirname(this.configurationFile))) {
1627 mkdirSync(dirname(this.configurationFile), { recursive: true })
1628 }
1629 const configurationFromFile = this.getConfigurationFromFile()
1630 let configurationData: ChargingStationConfiguration =
1631 configurationFromFile != null
1632 ? clone<ChargingStationConfiguration>(configurationFromFile)
1633 : {}
1634 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1635 configurationData.stationInfo = this.stationInfo
1636 } else {
1637 delete configurationData.stationInfo
1638 }
1639 if (
1640 this.stationInfo?.ocppPersistentConfiguration === true &&
1641 Array.isArray(this.ocppConfiguration?.configurationKey)
1642 ) {
1643 configurationData.configurationKey = this.ocppConfiguration.configurationKey
1644 } else {
1645 delete configurationData.configurationKey
1646 }
1647 configurationData = merge<ChargingStationConfiguration>(
1648 configurationData,
1649 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1650 )
1651 if (
1652 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1653 this.getAutomaticTransactionGeneratorConfiguration() == null
1654 ) {
1655 delete configurationData.automaticTransactionGenerator
1656 }
1657 if (this.connectors.size > 0) {
1658 configurationData.connectorsStatus = buildConnectorsStatus(this)
1659 } else {
1660 delete configurationData.connectorsStatus
1661 }
1662 if (this.evses.size > 0) {
1663 configurationData.evsesStatus = buildEvsesStatus(this)
1664 } else {
1665 delete configurationData.evsesStatus
1666 }
1667 delete configurationData.configurationHash
1668 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1669 .update(
1670 JSON.stringify({
1671 stationInfo: configurationData.stationInfo,
1672 configurationKey: configurationData.configurationKey,
1673 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1674 ...(this.connectors.size > 0 && {
1675 connectorsStatus: configurationData.connectorsStatus
1676 }),
1677 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1678 } satisfies ChargingStationConfiguration)
1679 )
1680 .digest('hex')
1681 if (this.configurationFileHash !== configurationHash) {
1682 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1683 configurationData.configurationHash = configurationHash
1684 const measureId = `${FileType.ChargingStationConfiguration} write`
1685 const beginId = PerformanceStatistics.beginMeasure(measureId)
1686 writeFileSync(
1687 this.configurationFile,
1688 JSON.stringify(configurationData, undefined, 2),
1689 'utf8'
1690 )
1691 PerformanceStatistics.endMeasure(measureId, beginId)
1692 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1693 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1694 this.configurationFileHash = configurationHash
1695 }).catch(error => {
1696 handleFileException(
1697 this.configurationFile,
1698 FileType.ChargingStationConfiguration,
1699 error as NodeJS.ErrnoException,
1700 this.logPrefix()
1701 )
1702 })
1703 } else {
1704 logger.debug(
1705 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1706 this.configurationFile
1707 }`
1708 )
1709 }
1710 } catch (error) {
1711 handleFileException(
1712 this.configurationFile,
1713 FileType.ChargingStationConfiguration,
1714 error as NodeJS.ErrnoException,
1715 this.logPrefix()
1716 )
1717 }
1718 } else {
1719 logger.error(
1720 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1721 )
1722 }
1723 }
1724
1725 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1726 return this.getTemplateFromFile()?.Configuration
1727 }
1728
1729 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1730 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1731 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1732 return { configurationKey }
1733 }
1734 return undefined
1735 }
1736
1737 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
1738 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1739 this.getOcppConfigurationFromFile()
1740 if (ocppConfiguration == null) {
1741 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1742 }
1743 return ocppConfiguration
1744 }
1745
1746 private async onOpen (): Promise<void> {
1747 if (this.isWebSocketConnectionOpened()) {
1748 logger.info(
1749 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1750 )
1751 let registrationRetryCount = 0
1752 if (!this.isRegistered()) {
1753 // Send BootNotification
1754 do {
1755 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1756 BootNotificationRequest,
1757 BootNotificationResponse
1758 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1759 skipBufferingOnError: true
1760 })
1761 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1762 if (this.bootNotificationResponse?.currentTime != null) {
1763 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1764 this.bootNotificationResponse.currentTime = convertToDate(
1765 this.bootNotificationResponse.currentTime
1766 )!
1767 }
1768 if (!this.isRegistered()) {
1769 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1770 await sleep(
1771 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1772 this.bootNotificationResponse?.interval != null
1773 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1774 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1775 )
1776 }
1777 } while (
1778 !this.isRegistered() &&
1779 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1780 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1781 this.stationInfo?.registrationMaxRetries === -1)
1782 )
1783 }
1784 if (this.isRegistered()) {
1785 this.emit(ChargingStationEvents.registered)
1786 if (this.inAcceptedState()) {
1787 this.emit(ChargingStationEvents.accepted)
1788 }
1789 } else {
1790 logger.error(
1791 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1792 this.stationInfo?.registrationMaxRetries
1793 })`
1794 )
1795 }
1796 this.autoReconnectRetryCount = 0
1797 this.emit(ChargingStationEvents.updated)
1798 } else {
1799 logger.warn(
1800 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1801 )
1802 }
1803 }
1804
1805 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1806 switch (code) {
1807 // Normal close
1808 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1809 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1810 logger.info(
1811 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1812 code
1813 )}' and reason '${reason.toString()}'`
1814 )
1815 this.autoReconnectRetryCount = 0
1816 break
1817 // Abnormal close
1818 default:
1819 logger.error(
1820 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1821 code
1822 )}' and reason '${reason.toString()}'`
1823 )
1824 this.started && this.reconnect().catch(Constants.EMPTY_FUNCTION)
1825 break
1826 }
1827 this.emit(ChargingStationEvents.updated)
1828 }
1829
1830 private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
1831 const cachedRequest = this.requests.get(messageId)
1832 if (Array.isArray(cachedRequest)) {
1833 return cachedRequest
1834 }
1835 throw new OCPPError(
1836 ErrorType.PROTOCOL_ERROR,
1837 `Cached request for message id ${messageId} ${getMessageTypeString(
1838 messageType
1839 )} is not an array`,
1840 undefined,
1841 cachedRequest
1842 )
1843 }
1844
1845 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1846 const [messageType, messageId, commandName, commandPayload] = request
1847 if (this.stationInfo?.enableStatistics === true) {
1848 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1849 }
1850 logger.debug(
1851 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1852 request
1853 )}`
1854 )
1855 // Process the message
1856 await this.ocppIncomingRequestService.incomingRequestHandler(
1857 this,
1858 messageId,
1859 commandName,
1860 commandPayload
1861 )
1862 this.emit(ChargingStationEvents.updated)
1863 }
1864
1865 private handleResponseMessage (response: Response): void {
1866 const [messageType, messageId, commandPayload] = response
1867 if (!this.requests.has(messageId)) {
1868 // Error
1869 throw new OCPPError(
1870 ErrorType.INTERNAL_ERROR,
1871 `Response for unknown message id ${messageId}`,
1872 undefined,
1873 commandPayload
1874 )
1875 }
1876 // Respond
1877 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1878 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1879 messageType,
1880 messageId
1881 )!
1882 logger.debug(
1883 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1884 response
1885 )}`
1886 )
1887 responseCallback(commandPayload, requestPayload)
1888 }
1889
1890 private handleErrorMessage (errorResponse: ErrorResponse): void {
1891 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1892 if (!this.requests.has(messageId)) {
1893 // Error
1894 throw new OCPPError(
1895 ErrorType.INTERNAL_ERROR,
1896 `Error response for unknown message id ${messageId}`,
1897 undefined,
1898 { errorType, errorMessage, errorDetails }
1899 )
1900 }
1901 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1902 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1903 logger.debug(
1904 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1905 errorResponse
1906 )}`
1907 )
1908 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1909 }
1910
1911 private async onMessage (data: RawData): Promise<void> {
1912 let request: IncomingRequest | Response | ErrorResponse | undefined
1913 let messageType: MessageType | undefined
1914 let errorMsg: string
1915 try {
1916 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1917 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1918 if (Array.isArray(request)) {
1919 [messageType] = request
1920 // Check the type of message
1921 switch (messageType) {
1922 // Incoming Message
1923 case MessageType.CALL_MESSAGE:
1924 await this.handleIncomingMessage(request as IncomingRequest)
1925 break
1926 // Response Message
1927 case MessageType.CALL_RESULT_MESSAGE:
1928 this.handleResponseMessage(request as Response)
1929 break
1930 // Error Message
1931 case MessageType.CALL_ERROR_MESSAGE:
1932 this.handleErrorMessage(request as ErrorResponse)
1933 break
1934 // Unknown Message
1935 default:
1936 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1937 errorMsg = `Wrong message type ${messageType}`
1938 logger.error(`${this.logPrefix()} ${errorMsg}`)
1939 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
1940 }
1941 } else {
1942 throw new OCPPError(
1943 ErrorType.PROTOCOL_ERROR,
1944 'Incoming message is not an array',
1945 undefined,
1946 {
1947 request
1948 }
1949 )
1950 }
1951 } catch (error) {
1952 if (!Array.isArray(request)) {
1953 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
1954 return
1955 }
1956 let commandName: IncomingRequestCommand | undefined
1957 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1958 let errorCallback: ErrorCallback
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 }