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