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