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