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