refactor: cleanup charging station events handling
[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 this.on(ChargingStationEvents.accepted, () => {
220 this.startMessageSequence().catch(error => {
221 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
222 })
223 })
224
225 this.initialize()
226 }
227
228 public get hasEvses (): boolean {
229 return this.connectors.size === 0 && this.evses.size > 0
230 }
231
232 private get wsConnectionUrl (): URL {
233 return new URL(
234 `${
235 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
236 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
237 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
238 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
239 : this.configuredSupervisionUrl.href
240 }/${this.stationInfo?.chargingStationId}`
241 )
242 }
243
244 public logPrefix = (): string => {
245 if (
246 this instanceof ChargingStation &&
247 this.stationInfo != null &&
248 isNotEmptyString(this.stationInfo.chargingStationId)
249 ) {
250 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
251 }
252 let stationTemplate: ChargingStationTemplate | undefined
253 try {
254 stationTemplate = JSON.parse(
255 readFileSync(this.templateFile, 'utf8')
256 ) as ChargingStationTemplate
257 } catch {
258 stationTemplate = undefined
259 }
260 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
261 }
262
263 public hasIdTags (): boolean {
264 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
265 return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
266 }
267
268 public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 const localStationInfo = stationInfo ?? this.stationInfo!
271 switch (this.getCurrentOutType(stationInfo)) {
272 case CurrentType.AC:
273 return localStationInfo.numberOfPhases ?? 3
274 case CurrentType.DC:
275 return 0
276 }
277 }
278
279 public isWebSocketConnectionOpened (): boolean {
280 return this.wsConnection?.readyState === WebSocket.OPEN
281 }
282
283 public inUnknownState (): boolean {
284 return this.bootNotificationResponse?.status == null
285 }
286
287 public inPendingState (): boolean {
288 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
289 }
290
291 public inAcceptedState (): boolean {
292 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
293 }
294
295 public inRejectedState (): boolean {
296 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
297 }
298
299 public isRegistered (): boolean {
300 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
301 }
302
303 public isChargingStationAvailable (): boolean {
304 return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
305 }
306
307 public hasConnector (connectorId: number): boolean {
308 if (this.hasEvses) {
309 for (const evseStatus of this.evses.values()) {
310 if (evseStatus.connectors.has(connectorId)) {
311 return true
312 }
313 }
314 return false
315 }
316 return this.connectors.has(connectorId)
317 }
318
319 public isConnectorAvailable (connectorId: number): boolean {
320 return (
321 connectorId > 0 &&
322 this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
323 )
324 }
325
326 public getNumberOfConnectors (): number {
327 if (this.hasEvses) {
328 let numberOfConnectors = 0
329 for (const [evseId, evseStatus] of this.evses) {
330 if (evseId > 0) {
331 numberOfConnectors += evseStatus.connectors.size
332 }
333 }
334 return numberOfConnectors
335 }
336 return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
337 }
338
339 public getNumberOfEvses (): number {
340 return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
341 }
342
343 public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
344 if (this.hasEvses) {
345 for (const evseStatus of this.evses.values()) {
346 if (evseStatus.connectors.has(connectorId)) {
347 return evseStatus.connectors.get(connectorId)
348 }
349 }
350 return undefined
351 }
352 return this.connectors.get(connectorId)
353 }
354
355 public getConnectorMaximumAvailablePower (connectorId: number): number {
356 let connectorAmperageLimitationPowerLimit: number | undefined
357 const amperageLimitation = this.getAmperageLimitation()
358 if (
359 amperageLimitation != null &&
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 amperageLimitation < this.stationInfo!.maximumAmperage!
362 ) {
363 connectorAmperageLimitationPowerLimit =
364 (this.stationInfo?.currentOutType === CurrentType.AC
365 ? ACElectricUtils.powerTotal(
366 this.getNumberOfPhases(),
367 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
368 this.stationInfo.voltageOut!,
369 amperageLimitation *
370 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
371 )
372 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373 DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) /
374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
375 this.powerDivider!
376 }
377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
379 const connectorChargingProfilesPowerLimit =
380 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId)
381 return min(
382 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
383 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384 isNaN(connectorAmperageLimitationPowerLimit!)
385 ? Infinity
386 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
387 connectorAmperageLimitationPowerLimit!,
388 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
389 isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit!
390 )
391 }
392
393 public getTransactionIdTag (transactionId: number): string | undefined {
394 if (this.hasEvses) {
395 for (const evseStatus of this.evses.values()) {
396 for (const connectorStatus of evseStatus.connectors.values()) {
397 if (connectorStatus.transactionId === transactionId) {
398 return connectorStatus.transactionIdTag
399 }
400 }
401 }
402 } else {
403 for (const connectorId of this.connectors.keys()) {
404 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
405 return this.getConnectorStatus(connectorId)?.transactionIdTag
406 }
407 }
408 }
409 }
410
411 public getNumberOfRunningTransactions (): number {
412 let numberOfRunningTransactions = 0
413 if (this.hasEvses) {
414 for (const [evseId, evseStatus] of this.evses) {
415 if (evseId === 0) {
416 continue
417 }
418 for (const connectorStatus of evseStatus.connectors.values()) {
419 if (connectorStatus.transactionStarted === true) {
420 ++numberOfRunningTransactions
421 }
422 }
423 }
424 } else {
425 for (const connectorId of this.connectors.keys()) {
426 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
427 ++numberOfRunningTransactions
428 }
429 }
430 }
431 return numberOfRunningTransactions
432 }
433
434 public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
435 if (transactionId == null) {
436 return undefined
437 } else if (this.hasEvses) {
438 for (const evseStatus of this.evses.values()) {
439 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
440 if (connectorStatus.transactionId === transactionId) {
441 return connectorId
442 }
443 }
444 }
445 } else {
446 for (const connectorId of this.connectors.keys()) {
447 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
448 return connectorId
449 }
450 }
451 }
452 }
453
454 public getEnergyActiveImportRegisterByTransactionId (
455 transactionId: number | undefined,
456 rounded = false
457 ): number {
458 return this.getEnergyActiveImportRegister(
459 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
460 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!),
461 rounded
462 )
463 }
464
465 public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
466 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
467 }
468
469 public getAuthorizeRemoteTxRequests (): boolean {
470 const authorizeRemoteTxRequests = getConfigurationKey(
471 this,
472 StandardParametersKey.AuthorizeRemoteTxRequests
473 )
474 return authorizeRemoteTxRequests != null
475 ? convertToBoolean(authorizeRemoteTxRequests.value)
476 : false
477 }
478
479 public getLocalAuthListEnabled (): boolean {
480 const localAuthListEnabled = getConfigurationKey(
481 this,
482 StandardParametersKey.LocalAuthListEnabled
483 )
484 return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
485 }
486
487 public getHeartbeatInterval (): number {
488 const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
489 if (HeartbeatInterval != null) {
490 return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
491 }
492 const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
493 if (HeartBeatInterval != null) {
494 return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
495 }
496 this.stationInfo?.autoRegister === false &&
497 logger.warn(
498 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
499 Constants.DEFAULT_HEARTBEAT_INTERVAL
500 }`
501 )
502 return Constants.DEFAULT_HEARTBEAT_INTERVAL
503 }
504
505 public setSupervisionUrl (url: string): void {
506 if (
507 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
508 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
509 ) {
510 setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
511 } else {
512 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
513 this.stationInfo!.supervisionUrls = url
514 this.saveStationInfo()
515 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
516 }
517 }
518
519 public startHeartbeat (): void {
520 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
521 this.heartbeatSetInterval = setInterval(() => {
522 this.ocppRequestService
523 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
524 .catch(error => {
525 logger.error(
526 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
527 error
528 )
529 })
530 }, this.getHeartbeatInterval())
531 logger.info(
532 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
533 this.getHeartbeatInterval()
534 )}`
535 )
536 } else if (this.heartbeatSetInterval != null) {
537 logger.info(
538 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
539 this.getHeartbeatInterval()
540 )}`
541 )
542 } else {
543 logger.error(
544 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
545 )
546 }
547 }
548
549 public restartHeartbeat (): void {
550 // Stop heartbeat
551 this.stopHeartbeat()
552 // Start heartbeat
553 this.startHeartbeat()
554 }
555
556 public restartWebSocketPing (): void {
557 // Stop WebSocket ping
558 this.stopWebSocketPing()
559 // Start WebSocket ping
560 this.startWebSocketPing()
561 }
562
563 public startMeterValues (connectorId: number, interval: number): void {
564 if (connectorId === 0) {
565 logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
566 return
567 }
568 const connectorStatus = this.getConnectorStatus(connectorId)
569 if (connectorStatus == null) {
570 logger.error(
571 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
572 ${connectorId}`
573 )
574 return
575 }
576 if (connectorStatus.transactionStarted === false) {
577 logger.error(
578 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
579 )
580 return
581 } else if (
582 connectorStatus.transactionStarted === true &&
583 connectorStatus.transactionId == null
584 ) {
585 logger.error(
586 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
587 )
588 return
589 }
590 if (interval > 0) {
591 connectorStatus.transactionSetInterval = setInterval(() => {
592 const meterValue = buildMeterValue(
593 this,
594 connectorId,
595 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
596 connectorStatus.transactionId!,
597 interval
598 )
599 this.ocppRequestService
600 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
601 this,
602 RequestCommand.METER_VALUES,
603 {
604 connectorId,
605 transactionId: connectorStatus.transactionId,
606 meterValue: [meterValue]
607 }
608 )
609 .catch(error => {
610 logger.error(
611 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
612 error
613 )
614 })
615 }, interval)
616 } else {
617 logger.error(
618 `${this.logPrefix()} Charging station ${
619 StandardParametersKey.MeterValueSampleInterval
620 } configuration set to ${interval}, not sending MeterValues`
621 )
622 }
623 }
624
625 public stopMeterValues (connectorId: number): void {
626 const connectorStatus = this.getConnectorStatus(connectorId)
627 if (connectorStatus?.transactionSetInterval != null) {
628 clearInterval(connectorStatus.transactionSetInterval)
629 }
630 }
631
632 public start (): void {
633 if (!this.started) {
634 if (!this.starting) {
635 this.starting = true
636 if (this.stationInfo?.enableStatistics === true) {
637 this.performanceStatistics?.start()
638 }
639 this.openWSConnection()
640 // Monitor charging station template file
641 this.templateFileWatcher = watchJsonFile(
642 this.templateFile,
643 FileType.ChargingStationTemplate,
644 this.logPrefix(),
645 undefined,
646 (event, filename): void => {
647 if (isNotEmptyString(filename) && event === 'change') {
648 try {
649 logger.debug(
650 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
651 this.templateFile
652 } file have changed, reload`
653 )
654 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
655 // Initialize
656 this.initialize()
657 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
658 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
659 // Restart the ATG
660 this.stopAutomaticTransactionGenerator()
661 delete this.automaticTransactionGeneratorConfiguration
662 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
663 this.startAutomaticTransactionGenerator()
664 }
665 if (this.stationInfo?.enableStatistics === true) {
666 this.performanceStatistics?.restart()
667 } else {
668 this.performanceStatistics?.stop()
669 }
670 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
671 } catch (error) {
672 logger.error(
673 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
674 error
675 )
676 }
677 }
678 }
679 )
680 this.started = true
681 this.emit(ChargingStationEvents.started)
682 this.starting = false
683 } else {
684 logger.warn(`${this.logPrefix()} Charging station is already starting...`)
685 }
686 } else {
687 logger.warn(`${this.logPrefix()} Charging station is already started...`)
688 }
689 }
690
691 public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
692 if (this.started) {
693 if (!this.stopping) {
694 this.stopping = true
695 await this.stopMessageSequence(reason, stopTransactions)
696 this.closeWSConnection()
697 if (this.stationInfo?.enableStatistics === true) {
698 this.performanceStatistics?.stop()
699 }
700 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
701 this.templateFileWatcher?.close()
702 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
703 delete this.bootNotificationResponse
704 this.started = false
705 this.saveConfiguration()
706 this.emit(ChargingStationEvents.stopped)
707 this.stopping = false
708 } else {
709 logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
710 }
711 } else {
712 logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
713 }
714 }
715
716 public async reset (reason?: StopTransactionReason): Promise<void> {
717 await this.stop(reason)
718 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
719 await sleep(this.stationInfo!.resetTime!)
720 this.initialize()
721 this.start()
722 }
723
724 public saveOcppConfiguration (): void {
725 if (this.stationInfo?.ocppPersistentConfiguration === true) {
726 this.saveConfiguration()
727 }
728 }
729
730 public bufferMessage (message: string): void {
731 this.messageBuffer.add(message)
732 this.setIntervalFlushMessageBuffer()
733 }
734
735 public openWSConnection (
736 options?: WsOptions,
737 params?: { closeOpened?: boolean, terminateOpened?: boolean }
738 ): void {
739 options = {
740 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
741 ...this.stationInfo?.wsOptions,
742 ...options
743 }
744 params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
745 if (!checkChargingStation(this, this.logPrefix())) {
746 return
747 }
748 if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
749 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
750 }
751 if (params.closeOpened === true) {
752 this.closeWSConnection()
753 }
754 if (params.terminateOpened === true) {
755 this.terminateWSConnection()
756 }
757
758 if (this.isWebSocketConnectionOpened()) {
759 logger.warn(
760 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
761 )
762 return
763 }
764
765 logger.info(
766 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
767 )
768
769 this.wsConnection = new WebSocket(
770 this.wsConnectionUrl,
771 `ocpp${this.stationInfo?.ocppVersion}`,
772 options
773 )
774
775 // Handle WebSocket message
776 this.wsConnection.on('message', data => {
777 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
778 })
779 // Handle WebSocket error
780 this.wsConnection.on('error', this.onError.bind(this))
781 // Handle WebSocket close
782 this.wsConnection.on('close', this.onClose.bind(this))
783 // Handle WebSocket open
784 this.wsConnection.on('open', () => {
785 this.onOpen().catch(error =>
786 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
787 )
788 })
789 // Handle WebSocket ping
790 this.wsConnection.on('ping', this.onPing.bind(this))
791 // Handle WebSocket pong
792 this.wsConnection.on('pong', this.onPong.bind(this))
793 }
794
795 public closeWSConnection (): void {
796 if (this.isWebSocketConnectionOpened()) {
797 this.wsConnection?.close()
798 this.wsConnection = null
799 }
800 }
801
802 public getAutomaticTransactionGeneratorConfiguration ():
803 | AutomaticTransactionGeneratorConfiguration
804 | undefined {
805 if (this.automaticTransactionGeneratorConfiguration == null) {
806 let automaticTransactionGeneratorConfiguration:
807 | AutomaticTransactionGeneratorConfiguration
808 | undefined
809 const stationTemplate = this.getTemplateFromFile()
810 const stationConfiguration = this.getConfigurationFromFile()
811 if (
812 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
813 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
814 stationConfiguration?.automaticTransactionGenerator != null
815 ) {
816 automaticTransactionGeneratorConfiguration =
817 stationConfiguration.automaticTransactionGenerator
818 } else {
819 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
820 }
821 this.automaticTransactionGeneratorConfiguration = {
822 ...Constants.DEFAULT_ATG_CONFIGURATION,
823 ...automaticTransactionGeneratorConfiguration
824 }
825 }
826 return this.automaticTransactionGeneratorConfiguration
827 }
828
829 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
830 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
831 }
832
833 public startAutomaticTransactionGenerator (connectorIds?: number[]): void {
834 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
835 if (isNotEmptyArray(connectorIds)) {
836 for (const connectorId of connectorIds) {
837 this.automaticTransactionGenerator?.startConnector(connectorId)
838 }
839 } else {
840 this.automaticTransactionGenerator?.start()
841 }
842 this.saveAutomaticTransactionGeneratorConfiguration()
843 this.emit(ChargingStationEvents.updated)
844 }
845
846 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
847 if (isNotEmptyArray(connectorIds)) {
848 for (const connectorId of connectorIds) {
849 this.automaticTransactionGenerator?.stopConnector(connectorId)
850 }
851 } else {
852 this.automaticTransactionGenerator?.stop()
853 }
854 this.saveAutomaticTransactionGeneratorConfiguration()
855 this.emit(ChargingStationEvents.updated)
856 }
857
858 public async stopTransactionOnConnector (
859 connectorId: number,
860 reason?: StopTransactionReason
861 ): Promise<StopTransactionResponse> {
862 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
863 if (
864 this.stationInfo?.beginEndMeterValues === true &&
865 this.stationInfo.ocppStrictCompliance === true &&
866 this.stationInfo.outOfOrderEndMeterValues === false
867 ) {
868 const transactionEndMeterValue = buildTransactionEndMeterValue(
869 this,
870 connectorId,
871 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
872 )
873 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
874 this,
875 RequestCommand.METER_VALUES,
876 {
877 connectorId,
878 transactionId,
879 meterValue: [transactionEndMeterValue]
880 }
881 )
882 }
883 return await this.ocppRequestService.requestHandler<
884 StopTransactionRequest,
885 StopTransactionResponse
886 >(this, RequestCommand.STOP_TRANSACTION, {
887 transactionId,
888 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
889 ...(reason != null && { reason })
890 })
891 }
892
893 public getReserveConnectorZeroSupported (): boolean {
894 return convertToBoolean(
895 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
896 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
897 )
898 }
899
900 public async addReservation (reservation: Reservation): Promise<void> {
901 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
902 if (reservationFound != null) {
903 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
904 }
905 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
906 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
907 await sendAndSetConnectorStatus(
908 this,
909 reservation.connectorId,
910 ConnectorStatusEnum.Reserved,
911 undefined,
912 { send: reservation.connectorId !== 0 }
913 )
914 }
915
916 public async removeReservation (
917 reservation: Reservation,
918 reason: ReservationTerminationReason
919 ): Promise<void> {
920 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
921 const connector = this.getConnectorStatus(reservation.connectorId)!
922 switch (reason) {
923 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
924 case ReservationTerminationReason.TRANSACTION_STARTED:
925 delete connector.reservation
926 break
927 case ReservationTerminationReason.RESERVATION_CANCELED:
928 case ReservationTerminationReason.REPLACE_EXISTING:
929 case ReservationTerminationReason.EXPIRED:
930 await sendAndSetConnectorStatus(
931 this,
932 reservation.connectorId,
933 ConnectorStatusEnum.Available,
934 undefined,
935 { send: reservation.connectorId !== 0 }
936 )
937 delete connector.reservation
938 break
939 default:
940 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
941 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
942 }
943 }
944
945 public getReservationBy (
946 filterKey: ReservationKey,
947 value: number | string
948 ): Reservation | undefined {
949 if (this.hasEvses) {
950 for (const evseStatus of this.evses.values()) {
951 for (const connectorStatus of evseStatus.connectors.values()) {
952 if (connectorStatus.reservation?.[filterKey] === value) {
953 return connectorStatus.reservation
954 }
955 }
956 }
957 } else {
958 for (const connectorStatus of this.connectors.values()) {
959 if (connectorStatus.reservation?.[filterKey] === value) {
960 return connectorStatus.reservation
961 }
962 }
963 }
964 }
965
966 public isConnectorReservable (
967 reservationId: number,
968 idTag?: string,
969 connectorId?: number
970 ): boolean {
971 const reservation = this.getReservationBy('reservationId', reservationId)
972 const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
973 if (arguments.length === 1) {
974 return !reservationExists
975 } else if (arguments.length > 1) {
976 const userReservation =
977 idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
978 const userReservationExists =
979 userReservation !== undefined && !hasReservationExpired(userReservation)
980 const notConnectorZero = connectorId === undefined ? true : connectorId > 0
981 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
982 return (
983 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
984 )
985 }
986 return false
987 }
988
989 private setIntervalFlushMessageBuffer (): void {
990 if (this.flushMessageBufferSetInterval == null) {
991 this.flushMessageBufferSetInterval = setInterval(() => {
992 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
993 this.flushMessageBuffer()
994 }
995 if (this.messageBuffer.size === 0) {
996 this.clearIntervalFlushMessageBuffer()
997 }
998 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
999 }
1000 }
1001
1002 private clearIntervalFlushMessageBuffer (): void {
1003 if (this.flushMessageBufferSetInterval != null) {
1004 clearInterval(this.flushMessageBufferSetInterval)
1005 delete this.flushMessageBufferSetInterval
1006 }
1007 }
1008
1009 private getNumberOfReservableConnectors (): number {
1010 let numberOfReservableConnectors = 0
1011 if (this.hasEvses) {
1012 for (const evseStatus of this.evses.values()) {
1013 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1014 }
1015 } else {
1016 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1017 }
1018 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1019 }
1020
1021 private getNumberOfReservationsOnConnectorZero (): number {
1022 if (
1023 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1024 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1025 ) {
1026 return 1
1027 }
1028 return 0
1029 }
1030
1031 private flushMessageBuffer (): void {
1032 if (this.messageBuffer.size > 0) {
1033 for (const message of this.messageBuffer.values()) {
1034 let beginId: string | undefined
1035 let commandName: RequestCommand | undefined
1036 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1037 const isRequest = messageType === MessageType.CALL_MESSAGE
1038 if (isRequest) {
1039 [, , commandName] = JSON.parse(message) as OutgoingRequest
1040 beginId = PerformanceStatistics.beginMeasure(commandName)
1041 }
1042 this.wsConnection?.send(message, (error?: Error) => {
1043 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1044 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1045 if (error == null) {
1046 logger.debug(
1047 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1048 messageType
1049 )} OCPP message sent '${JSON.stringify(message)}'`
1050 )
1051 this.messageBuffer.delete(message)
1052 } else {
1053 logger.debug(
1054 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1055 messageType
1056 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1057 error
1058 )
1059 }
1060 })
1061 }
1062 }
1063 }
1064
1065 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1066 let template: ChargingStationTemplate | undefined
1067 try {
1068 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1069 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1070 } else {
1071 const measureId = `${FileType.ChargingStationTemplate} read`
1072 const beginId = PerformanceStatistics.beginMeasure(measureId)
1073 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1074 PerformanceStatistics.endMeasure(measureId, beginId)
1075 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1076 .update(JSON.stringify(template))
1077 .digest('hex')
1078 this.sharedLRUCache.setChargingStationTemplate(template)
1079 this.templateFileHash = template.templateHash
1080 }
1081 } catch (error) {
1082 handleFileException(
1083 this.templateFile,
1084 FileType.ChargingStationTemplate,
1085 error as NodeJS.ErrnoException,
1086 this.logPrefix()
1087 )
1088 }
1089 return template
1090 }
1091
1092 private getStationInfoFromTemplate (): ChargingStationInfo {
1093 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1094 const stationTemplate = this.getTemplateFromFile()!
1095 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1096 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1097 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1098 if (stationTemplate.Connectors != null) {
1099 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1100 }
1101 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1102 stationInfo.hashId = getHashId(this.index, stationTemplate)
1103 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1104 stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
1105 createSerialNumber(stationTemplate, stationInfo)
1106 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1107 if (isNotEmptyArray(stationTemplate.power)) {
1108 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1109 stationInfo.maximumPower =
1110 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1111 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1112 : stationTemplate.power[powerArrayRandomIndex]
1113 } else {
1114 stationInfo.maximumPower =
1115 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1116 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1117 stationTemplate.power! * 1000
1118 : stationTemplate.power
1119 }
1120 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1121 stationInfo.firmwareVersionPattern =
1122 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
1123 if (
1124 isNotEmptyString(stationInfo.firmwareVersion) &&
1125 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1126 ) {
1127 logger.warn(
1128 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1129 this.templateFile
1130 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1131 )
1132 }
1133 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1134 {
1135 versionUpgrade: {
1136 step: 1
1137 },
1138 reset: true
1139 },
1140 stationTemplate.firmwareUpgrade ?? {}
1141 )
1142 stationInfo.resetTime =
1143 stationTemplate.resetTime != null
1144 ? secondsToMilliseconds(stationTemplate.resetTime)
1145 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
1146 return stationInfo
1147 }
1148
1149 private getStationInfoFromFile (
1150 stationInfoPersistentConfiguration = true
1151 ): ChargingStationInfo | undefined {
1152 let stationInfo: ChargingStationInfo | undefined
1153 if (stationInfoPersistentConfiguration) {
1154 stationInfo = this.getConfigurationFromFile()?.stationInfo
1155 if (stationInfo != null) {
1156 delete stationInfo.infoHash
1157 }
1158 }
1159 return stationInfo
1160 }
1161
1162 private getStationInfo (): ChargingStationInfo {
1163 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
1164 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1165 const stationInfoFromFile = this.getStationInfoFromFile(
1166 stationInfoFromTemplate.stationInfoPersistentConfiguration
1167 )
1168 // Priority:
1169 // 1. charging station info from template
1170 // 2. charging station info from configuration file
1171 if (
1172 stationInfoFromFile != null &&
1173 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1174 ) {
1175 return { ...defaultStationInfo, ...stationInfoFromFile }
1176 }
1177 stationInfoFromFile != null &&
1178 propagateSerialNumber(
1179 this.getTemplateFromFile(),
1180 stationInfoFromFile,
1181 stationInfoFromTemplate
1182 )
1183 return { ...defaultStationInfo, ...stationInfoFromTemplate }
1184 }
1185
1186 private saveStationInfo (): void {
1187 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1188 this.saveConfiguration()
1189 }
1190 }
1191
1192 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1193 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1194 logger.error(`${this.logPrefix()} ${errorMsg}`)
1195 throw new BaseError(errorMsg)
1196 }
1197
1198 private initialize (): void {
1199 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1200 const stationTemplate = this.getTemplateFromFile()!
1201 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1202 this.configurationFile = join(
1203 dirname(this.templateFile.replace('station-templates', 'configurations')),
1204 `${getHashId(this.index, stationTemplate)}.json`
1205 )
1206 const stationConfiguration = this.getConfigurationFromFile()
1207 if (
1208 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1209 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1210 ) {
1211 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1212 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1213 } else {
1214 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1215 }
1216 this.stationInfo = this.getStationInfo()
1217 if (
1218 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1219 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1220 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1221 ) {
1222 const patternGroup =
1223 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1224 this.stationInfo.firmwareVersion.split('.').length
1225 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1226 .exec(this.stationInfo.firmwareVersion)
1227 ?.slice(1, patternGroup + 1)
1228 if (match != null) {
1229 const patchLevelIndex = match.length - 1
1230 match[patchLevelIndex] = (
1231 convertToInt(match[patchLevelIndex]) +
1232 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1233 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1234 ).toString()
1235 this.stationInfo.firmwareVersion = match.join('.')
1236 }
1237 }
1238 this.saveStationInfo()
1239 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1240 if (this.stationInfo.enableStatistics === true) {
1241 this.performanceStatistics = PerformanceStatistics.getInstance(
1242 this.stationInfo.hashId,
1243 this.stationInfo.chargingStationId,
1244 this.configuredSupervisionUrl
1245 )
1246 }
1247 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1248 if (bootNotificationRequest == null) {
1249 const errorMsg = 'Error while creating boot notification request'
1250 logger.error(`${this.logPrefix()} ${errorMsg}`)
1251 throw new BaseError(errorMsg)
1252 }
1253 this.bootNotificationRequest = bootNotificationRequest
1254 this.powerDivider = this.getPowerDivider()
1255 // OCPP configuration
1256 this.ocppConfiguration = this.getOcppConfiguration()
1257 this.initializeOcppConfiguration()
1258 this.initializeOcppServices()
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 &&
1825 this.reconnect().catch(error =>
1826 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1827 )
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 if (!Array.isArray(request)) {
1956 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
1957 return
1958 }
1959 let commandName: IncomingRequestCommand | undefined
1960 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1961 let errorCallback: ErrorCallback
1962 const [, messageId] = request
1963 switch (messageType) {
1964 case MessageType.CALL_MESSAGE:
1965 [, , commandName] = request as IncomingRequest
1966 // Send error
1967 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
1968 break
1969 case MessageType.CALL_RESULT_MESSAGE:
1970 case MessageType.CALL_ERROR_MESSAGE:
1971 if (this.requests.has(messageId)) {
1972 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1973 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1974 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1975 errorCallback(error as OCPPError, false)
1976 } else {
1977 // Remove the request from the cache in case of error at response handling
1978 this.requests.delete(messageId)
1979 }
1980 break
1981 }
1982 if (!(error instanceof OCPPError)) {
1983 logger.warn(
1984 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1985 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1986 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1987 }' message '${data.toString()}' handling is not an OCPPError:`,
1988 error
1989 )
1990 }
1991 logger.error(
1992 `${this.logPrefix()} Incoming OCPP command '${
1993 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1994 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1995 }' message '${data.toString()}'${
1996 messageType !== MessageType.CALL_MESSAGE
1997 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1998 : ''
1999 } processing error:`,
2000 error
2001 )
2002 }
2003 }
2004
2005 private onPing (): void {
2006 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2007 }
2008
2009 private onPong (): void {
2010 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2011 }
2012
2013 private onError (error: WSError): void {
2014 this.closeWSConnection()
2015 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2016 }
2017
2018 private getEnergyActiveImportRegister (
2019 connectorStatus: ConnectorStatus | undefined,
2020 rounded = false
2021 ): number {
2022 if (this.stationInfo?.meteringPerTransaction === true) {
2023 return (
2024 (rounded
2025 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2026 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2027 : undefined
2028 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2029 )
2030 }
2031 return (
2032 (rounded
2033 ? connectorStatus?.energyActiveImportRegisterValue != null
2034 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2035 : undefined
2036 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2037 )
2038 }
2039
2040 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2041 return stationTemplate?.useConnectorId0 ?? true
2042 }
2043
2044 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2045 if (this.hasEvses) {
2046 for (const [evseId, evseStatus] of this.evses) {
2047 if (evseId === 0) {
2048 continue
2049 }
2050 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2051 if (connectorStatus.transactionStarted === true) {
2052 await this.stopTransactionOnConnector(connectorId, reason)
2053 }
2054 }
2055 }
2056 } else {
2057 for (const connectorId of this.connectors.keys()) {
2058 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2059 await this.stopTransactionOnConnector(connectorId, reason)
2060 }
2061 }
2062 }
2063 }
2064
2065 // 0 for disabling
2066 private getConnectionTimeout (): number {
2067 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2068 return convertToInt(
2069 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2070 Constants.DEFAULT_CONNECTION_TIMEOUT
2071 )
2072 }
2073 return Constants.DEFAULT_CONNECTION_TIMEOUT
2074 }
2075
2076 private getPowerDivider (): number {
2077 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2078 if (this.stationInfo?.powerSharedByConnectors === true) {
2079 powerDivider = this.getNumberOfRunningTransactions()
2080 }
2081 return powerDivider
2082 }
2083
2084 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2085 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2086 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2087 switch (this.getCurrentOutType(stationInfo)) {
2088 case CurrentType.AC:
2089 return ACElectricUtils.amperagePerPhaseFromPower(
2090 this.getNumberOfPhases(stationInfo),
2091 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2092 this.getVoltageOut(stationInfo)
2093 )
2094 case CurrentType.DC:
2095 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2096 }
2097 }
2098
2099 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2101 return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC
2102 }
2103
2104 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2105 return (
2106 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2107 (stationInfo ?? this.stationInfo!).voltageOut ??
2108 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2109 )
2110 }
2111
2112 private getAmperageLimitation (): number | undefined {
2113 if (
2114 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2115 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2116 ) {
2117 return (
2118 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2119 getAmperageLimitationUnitDivider(this.stationInfo)
2120 )
2121 }
2122 }
2123
2124 private async startMessageSequence (): Promise<void> {
2125 if (this.stationInfo?.autoRegister === true) {
2126 await this.ocppRequestService.requestHandler<
2127 BootNotificationRequest,
2128 BootNotificationResponse
2129 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2130 skipBufferingOnError: true
2131 })
2132 }
2133 // Start WebSocket ping
2134 this.startWebSocketPing()
2135 // Start heartbeat
2136 this.startHeartbeat()
2137 // Initialize connectors status
2138 if (this.hasEvses) {
2139 for (const [evseId, evseStatus] of this.evses) {
2140 if (evseId > 0) {
2141 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2142 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2143 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
2144 }
2145 }
2146 }
2147 } else {
2148 for (const connectorId of this.connectors.keys()) {
2149 if (connectorId > 0) {
2150 const connectorBootStatus = getBootConnectorStatus(
2151 this,
2152 connectorId,
2153 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2154 this.getConnectorStatus(connectorId)!
2155 )
2156 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
2157 }
2158 }
2159 }
2160 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2161 await this.ocppRequestService.requestHandler<
2162 FirmwareStatusNotificationRequest,
2163 FirmwareStatusNotificationResponse
2164 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2165 status: FirmwareStatus.Installed
2166 })
2167 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2168 }
2169
2170 // Start the ATG
2171 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2172 this.startAutomaticTransactionGenerator()
2173 }
2174 this.flushMessageBuffer()
2175 }
2176
2177 private async stopMessageSequence (
2178 reason?: StopTransactionReason,
2179 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
2180 ): Promise<void> {
2181 // Stop WebSocket ping
2182 this.stopWebSocketPing()
2183 // Stop heartbeat
2184 this.stopHeartbeat()
2185 // Stop the ATG
2186 if (this.automaticTransactionGenerator?.started === true) {
2187 this.stopAutomaticTransactionGenerator()
2188 }
2189 // Stop ongoing transactions
2190 stopTransactions === true && (await this.stopRunningTransactions(reason))
2191 if (this.hasEvses) {
2192 for (const [evseId, evseStatus] of this.evses) {
2193 if (evseId > 0) {
2194 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2195 await this.ocppRequestService.requestHandler<
2196 StatusNotificationRequest,
2197 StatusNotificationResponse
2198 >(
2199 this,
2200 RequestCommand.STATUS_NOTIFICATION,
2201 buildStatusNotificationRequest(
2202 this,
2203 connectorId,
2204 ConnectorStatusEnum.Unavailable,
2205 evseId
2206 )
2207 )
2208 delete connectorStatus.status
2209 }
2210 }
2211 }
2212 } else {
2213 for (const connectorId of this.connectors.keys()) {
2214 if (connectorId > 0) {
2215 await this.ocppRequestService.requestHandler<
2216 StatusNotificationRequest,
2217 StatusNotificationResponse
2218 >(
2219 this,
2220 RequestCommand.STATUS_NOTIFICATION,
2221 buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable)
2222 )
2223 delete this.getConnectorStatus(connectorId)?.status
2224 }
2225 }
2226 }
2227 }
2228
2229 private startWebSocketPing (): void {
2230 const webSocketPingInterval =
2231 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2232 ? convertToInt(
2233 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2234 )
2235 : 0
2236 if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) {
2237 this.webSocketPingSetInterval = setInterval(() => {
2238 if (this.isWebSocketConnectionOpened()) {
2239 this.wsConnection?.ping()
2240 }
2241 }, secondsToMilliseconds(webSocketPingInterval))
2242 logger.info(
2243 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2244 webSocketPingInterval
2245 )}`
2246 )
2247 } else if (this.webSocketPingSetInterval != null) {
2248 logger.info(
2249 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2250 webSocketPingInterval
2251 )}`
2252 )
2253 } else {
2254 logger.error(
2255 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2256 )
2257 }
2258 }
2259
2260 private stopWebSocketPing (): void {
2261 if (this.webSocketPingSetInterval != null) {
2262 clearInterval(this.webSocketPingSetInterval)
2263 delete this.webSocketPingSetInterval
2264 }
2265 }
2266
2267 private getConfiguredSupervisionUrl (): URL {
2268 let configuredSupervisionUrl: string
2269 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2270 if (isNotEmptyArray(supervisionUrls)) {
2271 let configuredSupervisionUrlIndex: number
2272 switch (Configuration.getSupervisionUrlDistribution()) {
2273 case SupervisionUrlDistribution.RANDOM:
2274 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2275 break
2276 case SupervisionUrlDistribution.ROUND_ROBIN:
2277 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2278 default:
2279 !Object.values(SupervisionUrlDistribution).includes(
2280 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2281 Configuration.getSupervisionUrlDistribution()!
2282 ) &&
2283 logger.error(
2284 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2285 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2286 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2287 }`
2288 )
2289 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2290 break
2291 }
2292 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2293 } else {
2294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2295 configuredSupervisionUrl = supervisionUrls!
2296 }
2297 if (isNotEmptyString(configuredSupervisionUrl)) {
2298 return new URL(configuredSupervisionUrl)
2299 }
2300 const errorMsg = 'No supervision url(s) configured'
2301 logger.error(`${this.logPrefix()} ${errorMsg}`)
2302 throw new BaseError(errorMsg)
2303 }
2304
2305 private stopHeartbeat (): void {
2306 if (this.heartbeatSetInterval != null) {
2307 clearInterval(this.heartbeatSetInterval)
2308 delete this.heartbeatSetInterval
2309 }
2310 }
2311
2312 private terminateWSConnection (): void {
2313 if (this.isWebSocketConnectionOpened()) {
2314 this.wsConnection?.terminate()
2315 this.wsConnection = null
2316 }
2317 }
2318
2319 private async reconnect (): Promise<void> {
2320 // Stop WebSocket ping
2321 this.stopWebSocketPing()
2322 // Stop heartbeat
2323 this.stopHeartbeat()
2324 // Stop the ATG if needed
2325 if (this.getAutomaticTransactionGeneratorConfiguration()?.stopOnConnectionFailure === true) {
2326 this.stopAutomaticTransactionGenerator()
2327 }
2328 if (
2329 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2330 this.autoReconnectRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2331 this.stationInfo?.autoReconnectMaxRetries === -1
2332 ) {
2333 ++this.autoReconnectRetryCount
2334 const reconnectDelay =
2335 this.stationInfo?.reconnectExponentialDelay === true
2336 ? exponentialDelay(this.autoReconnectRetryCount)
2337 : secondsToMilliseconds(this.getConnectionTimeout())
2338 const reconnectDelayWithdraw = 1000
2339 const reconnectTimeout =
2340 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2341 logger.error(
2342 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2343 reconnectDelay,
2344 2
2345 )}ms, timeout ${reconnectTimeout}ms`
2346 )
2347 await sleep(reconnectDelay)
2348 logger.error(
2349 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2350 )
2351 this.openWSConnection(
2352 {
2353 handshakeTimeout: reconnectTimeout
2354 },
2355 { closeOpened: true }
2356 )
2357 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2358 logger.error(
2359 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2360 this.autoReconnectRetryCount
2361 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2362 )
2363 }
2364 }
2365 }