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