refactor: update MikroORM entities definition
[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(
772 'message',
773 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
774 )
775 // Handle WebSocket error
776 this.wsConnection.on(
777 'error',
778 this.onError.bind(this) as (this: WebSocket, error: Error) => void
779 )
780 // Handle WebSocket close
781 this.wsConnection.on(
782 'close',
783 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
784 )
785 // Handle WebSocket open
786 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void)
787 // Handle WebSocket ping
788 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void)
789 // Handle WebSocket pong
790 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void)
791 }
792
793 public closeWSConnection (): void {
794 if (this.isWebSocketConnectionOpened()) {
795 this.wsConnection?.close()
796 this.wsConnection = null
797 }
798 }
799
800 public getAutomaticTransactionGeneratorConfiguration ():
801 | AutomaticTransactionGeneratorConfiguration
802 | undefined {
803 if (this.automaticTransactionGeneratorConfiguration == null) {
804 let automaticTransactionGeneratorConfiguration:
805 | AutomaticTransactionGeneratorConfiguration
806 | undefined
807 const stationTemplate = this.getTemplateFromFile()
808 const stationConfiguration = this.getConfigurationFromFile()
809 if (
810 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
811 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
812 stationConfiguration?.automaticTransactionGenerator != null
813 ) {
814 automaticTransactionGeneratorConfiguration =
815 stationConfiguration.automaticTransactionGenerator
816 } else {
817 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
818 }
819 this.automaticTransactionGeneratorConfiguration = {
820 ...Constants.DEFAULT_ATG_CONFIGURATION,
821 ...automaticTransactionGeneratorConfiguration
822 }
823 }
824 return this.automaticTransactionGeneratorConfiguration
825 }
826
827 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
828 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
829 }
830
831 public startAutomaticTransactionGenerator (connectorIds?: number[]): void {
832 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
833 if (isNotEmptyArray(connectorIds)) {
834 for (const connectorId of connectorIds) {
835 this.automaticTransactionGenerator?.startConnector(connectorId)
836 }
837 } else {
838 this.automaticTransactionGenerator?.start()
839 }
840 this.saveAutomaticTransactionGeneratorConfiguration()
841 this.emit(ChargingStationEvents.updated)
842 }
843
844 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
845 if (isNotEmptyArray(connectorIds)) {
846 for (const connectorId of connectorIds) {
847 this.automaticTransactionGenerator?.stopConnector(connectorId)
848 }
849 } else {
850 this.automaticTransactionGenerator?.stop()
851 }
852 this.saveAutomaticTransactionGeneratorConfiguration()
853 this.emit(ChargingStationEvents.updated)
854 }
855
856 public async stopTransactionOnConnector (
857 connectorId: number,
858 reason?: StopTransactionReason
859 ): Promise<StopTransactionResponse> {
860 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
861 if (
862 this.stationInfo?.beginEndMeterValues === true &&
863 this.stationInfo.ocppStrictCompliance === true &&
864 this.stationInfo.outOfOrderEndMeterValues === false
865 ) {
866 const transactionEndMeterValue = buildTransactionEndMeterValue(
867 this,
868 connectorId,
869 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
870 )
871 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
872 this,
873 RequestCommand.METER_VALUES,
874 {
875 connectorId,
876 transactionId,
877 meterValue: [transactionEndMeterValue]
878 }
879 )
880 }
881 return await this.ocppRequestService.requestHandler<
882 StopTransactionRequest,
883 StopTransactionResponse
884 >(this, RequestCommand.STOP_TRANSACTION, {
885 transactionId,
886 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
887 ...(reason != null && { reason })
888 })
889 }
890
891 public getReserveConnectorZeroSupported (): boolean {
892 return convertToBoolean(
893 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
894 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
895 )
896 }
897
898 public async addReservation (reservation: Reservation): Promise<void> {
899 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
900 if (reservationFound != null) {
901 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
902 }
903 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
904 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
905 await sendAndSetConnectorStatus(
906 this,
907 reservation.connectorId,
908 ConnectorStatusEnum.Reserved,
909 undefined,
910 { send: reservation.connectorId !== 0 }
911 )
912 }
913
914 public async removeReservation (
915 reservation: Reservation,
916 reason: ReservationTerminationReason
917 ): Promise<void> {
918 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
919 const connector = this.getConnectorStatus(reservation.connectorId)!
920 switch (reason) {
921 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
922 case ReservationTerminationReason.TRANSACTION_STARTED:
923 delete connector.reservation
924 break
925 case ReservationTerminationReason.RESERVATION_CANCELED:
926 case ReservationTerminationReason.REPLACE_EXISTING:
927 case ReservationTerminationReason.EXPIRED:
928 await sendAndSetConnectorStatus(
929 this,
930 reservation.connectorId,
931 ConnectorStatusEnum.Available,
932 undefined,
933 { send: reservation.connectorId !== 0 }
934 )
935 delete connector.reservation
936 break
937 default:
938 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
939 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
940 }
941 }
942
943 public getReservationBy (
944 filterKey: ReservationKey,
945 value: number | string
946 ): Reservation | undefined {
947 if (this.hasEvses) {
948 for (const evseStatus of this.evses.values()) {
949 for (const connectorStatus of evseStatus.connectors.values()) {
950 if (connectorStatus.reservation?.[filterKey] === value) {
951 return connectorStatus.reservation
952 }
953 }
954 }
955 } else {
956 for (const connectorStatus of this.connectors.values()) {
957 if (connectorStatus.reservation?.[filterKey] === value) {
958 return connectorStatus.reservation
959 }
960 }
961 }
962 }
963
964 public isConnectorReservable (
965 reservationId: number,
966 idTag?: string,
967 connectorId?: number
968 ): boolean {
969 const reservation = this.getReservationBy('reservationId', reservationId)
970 const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
971 if (arguments.length === 1) {
972 return !reservationExists
973 } else if (arguments.length > 1) {
974 const userReservation =
975 idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
976 const userReservationExists =
977 userReservation !== undefined && !hasReservationExpired(userReservation)
978 const notConnectorZero = connectorId === undefined ? true : connectorId > 0
979 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
980 return (
981 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
982 )
983 }
984 return false
985 }
986
987 private setIntervalFlushMessageBuffer (): void {
988 if (this.flushMessageBufferSetInterval == null) {
989 this.flushMessageBufferSetInterval = setInterval(() => {
990 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
991 this.flushMessageBuffer()
992 }
993 if (this.messageBuffer.size === 0) {
994 this.clearIntervalFlushMessageBuffer()
995 }
996 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
997 }
998 }
999
1000 private clearIntervalFlushMessageBuffer (): void {
1001 if (this.flushMessageBufferSetInterval != null) {
1002 clearInterval(this.flushMessageBufferSetInterval)
1003 delete this.flushMessageBufferSetInterval
1004 }
1005 }
1006
1007 private getNumberOfReservableConnectors (): number {
1008 let numberOfReservableConnectors = 0
1009 if (this.hasEvses) {
1010 for (const evseStatus of this.evses.values()) {
1011 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1012 }
1013 } else {
1014 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1015 }
1016 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1017 }
1018
1019 private getNumberOfReservationsOnConnectorZero (): number {
1020 if (
1021 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1022 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1023 ) {
1024 return 1
1025 }
1026 return 0
1027 }
1028
1029 private flushMessageBuffer (): void {
1030 if (this.messageBuffer.size > 0) {
1031 for (const message of this.messageBuffer.values()) {
1032 let beginId: string | undefined
1033 let commandName: RequestCommand | undefined
1034 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1035 const isRequest = messageType === MessageType.CALL_MESSAGE
1036 if (isRequest) {
1037 [, , commandName] = JSON.parse(message) as OutgoingRequest
1038 beginId = PerformanceStatistics.beginMeasure(commandName)
1039 }
1040 this.wsConnection?.send(message, (error?: Error) => {
1041 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1042 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1043 if (error == null) {
1044 logger.debug(
1045 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1046 messageType
1047 )} OCPP message sent '${JSON.stringify(message)}'`
1048 )
1049 this.messageBuffer.delete(message)
1050 } else {
1051 logger.debug(
1052 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1053 messageType
1054 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1055 error
1056 )
1057 }
1058 })
1059 }
1060 }
1061 }
1062
1063 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1064 let template: ChargingStationTemplate | undefined
1065 try {
1066 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1067 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1068 } else {
1069 const measureId = `${FileType.ChargingStationTemplate} read`
1070 const beginId = PerformanceStatistics.beginMeasure(measureId)
1071 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1072 PerformanceStatistics.endMeasure(measureId, beginId)
1073 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1074 .update(JSON.stringify(template))
1075 .digest('hex')
1076 this.sharedLRUCache.setChargingStationTemplate(template)
1077 this.templateFileHash = template.templateHash
1078 }
1079 } catch (error) {
1080 handleFileException(
1081 this.templateFile,
1082 FileType.ChargingStationTemplate,
1083 error as NodeJS.ErrnoException,
1084 this.logPrefix()
1085 )
1086 }
1087 return template
1088 }
1089
1090 private getStationInfoFromTemplate (): ChargingStationInfo {
1091 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1092 const stationTemplate = this.getTemplateFromFile()!
1093 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1094 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1095 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1096 if (stationTemplate.Connectors != null) {
1097 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1098 }
1099 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1100 stationInfo.hashId = getHashId(this.index, stationTemplate)
1101 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1102 stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
1103 createSerialNumber(stationTemplate, stationInfo)
1104 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1105 if (isNotEmptyArray(stationTemplate.power)) {
1106 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1107 stationInfo.maximumPower =
1108 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1109 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1110 : stationTemplate.power[powerArrayRandomIndex]
1111 } else {
1112 stationInfo.maximumPower =
1113 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1114 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1115 stationTemplate.power! * 1000
1116 : stationTemplate.power
1117 }
1118 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1119 stationInfo.firmwareVersionPattern =
1120 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
1121 if (
1122 isNotEmptyString(stationInfo.firmwareVersion) &&
1123 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1124 ) {
1125 logger.warn(
1126 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1127 this.templateFile
1128 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1129 )
1130 }
1131 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1132 {
1133 versionUpgrade: {
1134 step: 1
1135 },
1136 reset: true
1137 },
1138 stationTemplate.firmwareUpgrade ?? {}
1139 )
1140 stationInfo.resetTime =
1141 stationTemplate.resetTime != null
1142 ? secondsToMilliseconds(stationTemplate.resetTime)
1143 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
1144 return stationInfo
1145 }
1146
1147 private getStationInfoFromFile (
1148 stationInfoPersistentConfiguration = true
1149 ): ChargingStationInfo | undefined {
1150 let stationInfo: ChargingStationInfo | undefined
1151 if (stationInfoPersistentConfiguration) {
1152 stationInfo = this.getConfigurationFromFile()?.stationInfo
1153 if (stationInfo != null) {
1154 delete stationInfo.infoHash
1155 }
1156 }
1157 return stationInfo
1158 }
1159
1160 private getStationInfo (): ChargingStationInfo {
1161 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
1162 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1163 const stationInfoFromFile = this.getStationInfoFromFile(
1164 stationInfoFromTemplate.stationInfoPersistentConfiguration
1165 )
1166 // Priority:
1167 // 1. charging station info from template
1168 // 2. charging station info from configuration file
1169 if (
1170 stationInfoFromFile != null &&
1171 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1172 ) {
1173 return { ...defaultStationInfo, ...stationInfoFromFile }
1174 }
1175 stationInfoFromFile != null &&
1176 propagateSerialNumber(
1177 this.getTemplateFromFile(),
1178 stationInfoFromFile,
1179 stationInfoFromTemplate
1180 )
1181 return { ...defaultStationInfo, ...stationInfoFromTemplate }
1182 }
1183
1184 private saveStationInfo (): void {
1185 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1186 this.saveConfiguration()
1187 }
1188 }
1189
1190 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1191 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1192 logger.error(`${this.logPrefix()} ${errorMsg}`)
1193 throw new BaseError(errorMsg)
1194 }
1195
1196 private initialize (): void {
1197 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1198 const stationTemplate = this.getTemplateFromFile()!
1199 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1200 this.configurationFile = join(
1201 dirname(this.templateFile.replace('station-templates', 'configurations')),
1202 `${getHashId(this.index, stationTemplate)}.json`
1203 )
1204 const stationConfiguration = this.getConfigurationFromFile()
1205 if (
1206 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1207 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1208 ) {
1209 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1210 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1211 } else {
1212 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1213 }
1214 this.stationInfo = this.getStationInfo()
1215 if (
1216 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1217 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1218 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1219 ) {
1220 const patternGroup =
1221 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1222 this.stationInfo.firmwareVersion.split('.').length
1223 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1224 .exec(this.stationInfo.firmwareVersion)
1225 ?.slice(1, patternGroup + 1)
1226 if (match != null) {
1227 const patchLevelIndex = match.length - 1
1228 match[patchLevelIndex] = (
1229 convertToInt(match[patchLevelIndex]) +
1230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1231 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1232 ).toString()
1233 this.stationInfo.firmwareVersion = match.join('.')
1234 }
1235 }
1236 this.saveStationInfo()
1237 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1238 if (this.stationInfo.enableStatistics === true) {
1239 this.performanceStatistics = PerformanceStatistics.getInstance(
1240 this.stationInfo.hashId,
1241 this.stationInfo.chargingStationId,
1242 this.configuredSupervisionUrl
1243 )
1244 }
1245 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1246 if (bootNotificationRequest == null) {
1247 const errorMsg = 'Error while creating boot notification request'
1248 logger.error(`${this.logPrefix()} ${errorMsg}`)
1249 throw new BaseError(errorMsg)
1250 }
1251 this.bootNotificationRequest = bootNotificationRequest
1252 this.powerDivider = this.getPowerDivider()
1253 // OCPP configuration
1254 this.ocppConfiguration = this.getOcppConfiguration()
1255 this.initializeOcppConfiguration()
1256 this.initializeOcppServices()
1257 this.once(ChargingStationEvents.accepted, () => {
1258 this.startMessageSequence().catch(error => {
1259 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
1260 })
1261 })
1262 if (this.stationInfo.autoRegister === true) {
1263 this.bootNotificationResponse = {
1264 currentTime: new Date(),
1265 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1266 status: RegistrationStatusEnumType.ACCEPTED
1267 }
1268 }
1269 }
1270
1271 private initializeOcppServices (): void {
1272 const ocppVersion = this.stationInfo?.ocppVersion
1273 switch (ocppVersion) {
1274 case OCPPVersion.VERSION_16:
1275 this.ocppIncomingRequestService =
1276 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1277 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1278 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1279 )
1280 break
1281 case OCPPVersion.VERSION_20:
1282 case OCPPVersion.VERSION_201:
1283 this.ocppIncomingRequestService =
1284 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1285 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1286 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1287 )
1288 break
1289 default:
1290 this.handleUnsupportedVersion(ocppVersion)
1291 break
1292 }
1293 }
1294
1295 private initializeOcppConfiguration (): void {
1296 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1297 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1298 }
1299 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1300 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1301 }
1302 if (
1303 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1304 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1305 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
1306 ) {
1307 addConfigurationKey(
1308 this,
1309 this.stationInfo.supervisionUrlOcppKey,
1310 this.configuredSupervisionUrl.href,
1311 { reboot: true }
1312 )
1313 } else if (
1314 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1315 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1316 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
1317 ) {
1318 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
1319 }
1320 if (
1321 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1322 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
1323 ) {
1324 addConfigurationKey(
1325 this,
1326 this.stationInfo.amperageLimitationOcppKey,
1327 // prettier-ignore
1328 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1329 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1330 )
1331 }
1332 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1333 addConfigurationKey(
1334 this,
1335 StandardParametersKey.SupportedFeatureProfiles,
1336 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1337 )
1338 }
1339 addConfigurationKey(
1340 this,
1341 StandardParametersKey.NumberOfConnectors,
1342 this.getNumberOfConnectors().toString(),
1343 { readonly: true },
1344 { overwrite: true }
1345 )
1346 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1347 addConfigurationKey(
1348 this,
1349 StandardParametersKey.MeterValuesSampledData,
1350 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1351 )
1352 }
1353 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1354 const connectorsPhaseRotation: string[] = []
1355 if (this.hasEvses) {
1356 for (const evseStatus of this.evses.values()) {
1357 for (const connectorId of evseStatus.connectors.keys()) {
1358 connectorsPhaseRotation.push(
1359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1360 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1361 )
1362 }
1363 }
1364 } else {
1365 for (const connectorId of this.connectors.keys()) {
1366 connectorsPhaseRotation.push(
1367 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1368 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1369 )
1370 }
1371 }
1372 addConfigurationKey(
1373 this,
1374 StandardParametersKey.ConnectorPhaseRotation,
1375 connectorsPhaseRotation.toString()
1376 )
1377 }
1378 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1379 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1380 }
1381 if (
1382 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1383 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1384 ) {
1385 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1386 }
1387 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1388 addConfigurationKey(
1389 this,
1390 StandardParametersKey.ConnectionTimeOut,
1391 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1392 )
1393 }
1394 this.saveOcppConfiguration()
1395 }
1396
1397 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1398 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
1399 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1400 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1401 }
1402 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
1403 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1404 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
1405 delete evseStatus.connectorsStatus
1406 this.evses.set(evseId, {
1407 ...(evseStatus as EvseStatus),
1408 connectors: new Map<number, ConnectorStatus>(
1409 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1410 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1411 connectorId,
1412 connectorStatus
1413 ])
1414 )
1415 })
1416 }
1417 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
1418 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1419 logger.error(`${this.logPrefix()} ${errorMsg}`)
1420 throw new BaseError(errorMsg)
1421 } else {
1422 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1423 logger.error(`${this.logPrefix()} ${errorMsg}`)
1424 throw new BaseError(errorMsg)
1425 }
1426 }
1427
1428 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1429 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
1430 this.initializeConnectorsFromTemplate(stationTemplate)
1431 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
1432 this.initializeEvsesFromTemplate(stationTemplate)
1433 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
1434 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1435 logger.error(`${this.logPrefix()} ${errorMsg}`)
1436 throw new BaseError(errorMsg)
1437 } else {
1438 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1439 logger.error(`${this.logPrefix()} ${errorMsg}`)
1440 throw new BaseError(errorMsg)
1441 }
1442 }
1443
1444 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1445 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
1446 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1447 logger.error(`${this.logPrefix()} ${errorMsg}`)
1448 throw new BaseError(errorMsg)
1449 }
1450 if (stationTemplate.Connectors?.[0] == null) {
1451 logger.warn(
1452 `${this.logPrefix()} Charging station information from template ${
1453 this.templateFile
1454 } with no connector id 0 configuration`
1455 )
1456 }
1457 if (stationTemplate.Connectors != null) {
1458 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1459 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1460 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1461 .update(
1462 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1463 )
1464 .digest('hex')
1465 const connectorsConfigChanged =
1466 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1467 if (this.connectors.size === 0 || connectorsConfigChanged) {
1468 connectorsConfigChanged && this.connectors.clear()
1469 this.connectorsConfigurationHash = connectorsConfigHash
1470 if (templateMaxConnectors > 0) {
1471 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1472 if (
1473 connectorId === 0 &&
1474 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1475 (stationTemplate.Connectors[connectorId] == null ||
1476 !this.getUseConnectorId0(stationTemplate))
1477 ) {
1478 continue
1479 }
1480 const templateConnectorId =
1481 connectorId > 0 && stationTemplate.randomConnectors === true
1482 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1483 : connectorId
1484 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
1485 checkStationInfoConnectorStatus(
1486 templateConnectorId,
1487 connectorStatus,
1488 this.logPrefix(),
1489 this.templateFile
1490 )
1491 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1492 }
1493 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1494 this.saveConnectorsStatus()
1495 } else {
1496 logger.warn(
1497 `${this.logPrefix()} Charging station information from template ${
1498 this.templateFile
1499 } with no connectors configuration defined, cannot create connectors`
1500 )
1501 }
1502 }
1503 } else {
1504 logger.warn(
1505 `${this.logPrefix()} Charging station information from template ${
1506 this.templateFile
1507 } with no connectors configuration defined, using already defined connectors`
1508 )
1509 }
1510 }
1511
1512 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1513 if (stationTemplate.Evses == null && this.evses.size === 0) {
1514 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1515 logger.error(`${this.logPrefix()} ${errorMsg}`)
1516 throw new BaseError(errorMsg)
1517 }
1518 if (stationTemplate.Evses?.[0] == null) {
1519 logger.warn(
1520 `${this.logPrefix()} Charging station information from template ${
1521 this.templateFile
1522 } with no evse id 0 configuration`
1523 )
1524 }
1525 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
1526 logger.warn(
1527 `${this.logPrefix()} Charging station information from template ${
1528 this.templateFile
1529 } with evse id 0 with no connector id 0 configuration`
1530 )
1531 }
1532 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
1533 logger.warn(
1534 `${this.logPrefix()} Charging station information from template ${
1535 this.templateFile
1536 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1537 )
1538 }
1539 if (stationTemplate.Evses != null) {
1540 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1541 .update(JSON.stringify(stationTemplate.Evses))
1542 .digest('hex')
1543 const evsesConfigChanged =
1544 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1545 if (this.evses.size === 0 || evsesConfigChanged) {
1546 evsesConfigChanged && this.evses.clear()
1547 this.evsesConfigurationHash = evsesConfigHash
1548 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
1549 if (templateMaxEvses > 0) {
1550 for (const evseKey in stationTemplate.Evses) {
1551 const evseId = convertToInt(evseKey)
1552 this.evses.set(evseId, {
1553 connectors: buildConnectorsMap(
1554 stationTemplate.Evses[evseKey].Connectors,
1555 this.logPrefix(),
1556 this.templateFile
1557 ),
1558 availability: AvailabilityType.Operative
1559 })
1560 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1561 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1562 }
1563 this.saveEvsesStatus()
1564 } else {
1565 logger.warn(
1566 `${this.logPrefix()} Charging station information from template ${
1567 this.templateFile
1568 } with no evses configuration defined, cannot create evses`
1569 )
1570 }
1571 }
1572 } else {
1573 logger.warn(
1574 `${this.logPrefix()} Charging station information from template ${
1575 this.templateFile
1576 } with no evses configuration defined, using already defined evses`
1577 )
1578 }
1579 }
1580
1581 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1582 let configuration: ChargingStationConfiguration | undefined
1583 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1584 try {
1585 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1586 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1587 this.configurationFileHash
1588 )
1589 } else {
1590 const measureId = `${FileType.ChargingStationConfiguration} read`
1591 const beginId = PerformanceStatistics.beginMeasure(measureId)
1592 configuration = JSON.parse(
1593 readFileSync(this.configurationFile, 'utf8')
1594 ) as ChargingStationConfiguration
1595 PerformanceStatistics.endMeasure(measureId, beginId)
1596 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1597 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1598 this.configurationFileHash = configuration.configurationHash!
1599 }
1600 } catch (error) {
1601 handleFileException(
1602 this.configurationFile,
1603 FileType.ChargingStationConfiguration,
1604 error as NodeJS.ErrnoException,
1605 this.logPrefix()
1606 )
1607 }
1608 }
1609 return configuration
1610 }
1611
1612 private saveAutomaticTransactionGeneratorConfiguration (): void {
1613 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1614 this.saveConfiguration()
1615 }
1616 }
1617
1618 private saveConnectorsStatus (): void {
1619 this.saveConfiguration()
1620 }
1621
1622 private saveEvsesStatus (): void {
1623 this.saveConfiguration()
1624 }
1625
1626 private saveConfiguration (): void {
1627 if (isNotEmptyString(this.configurationFile)) {
1628 try {
1629 if (!existsSync(dirname(this.configurationFile))) {
1630 mkdirSync(dirname(this.configurationFile), { recursive: true })
1631 }
1632 const configurationFromFile = this.getConfigurationFromFile()
1633 let configurationData: ChargingStationConfiguration =
1634 configurationFromFile != null
1635 ? clone<ChargingStationConfiguration>(configurationFromFile)
1636 : {}
1637 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1638 configurationData.stationInfo = this.stationInfo
1639 } else {
1640 delete configurationData.stationInfo
1641 }
1642 if (
1643 this.stationInfo?.ocppPersistentConfiguration === true &&
1644 Array.isArray(this.ocppConfiguration?.configurationKey)
1645 ) {
1646 configurationData.configurationKey = this.ocppConfiguration.configurationKey
1647 } else {
1648 delete configurationData.configurationKey
1649 }
1650 configurationData = merge<ChargingStationConfiguration>(
1651 configurationData,
1652 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1653 )
1654 if (
1655 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1656 this.getAutomaticTransactionGeneratorConfiguration() == null
1657 ) {
1658 delete configurationData.automaticTransactionGenerator
1659 }
1660 if (this.connectors.size > 0) {
1661 configurationData.connectorsStatus = buildConnectorsStatus(this)
1662 } else {
1663 delete configurationData.connectorsStatus
1664 }
1665 if (this.evses.size > 0) {
1666 configurationData.evsesStatus = buildEvsesStatus(this)
1667 } else {
1668 delete configurationData.evsesStatus
1669 }
1670 delete configurationData.configurationHash
1671 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1672 .update(
1673 JSON.stringify({
1674 stationInfo: configurationData.stationInfo,
1675 configurationKey: configurationData.configurationKey,
1676 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1677 ...(this.connectors.size > 0 && {
1678 connectorsStatus: configurationData.connectorsStatus
1679 }),
1680 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1681 } satisfies ChargingStationConfiguration)
1682 )
1683 .digest('hex')
1684 if (this.configurationFileHash !== configurationHash) {
1685 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1686 configurationData.configurationHash = configurationHash
1687 const measureId = `${FileType.ChargingStationConfiguration} write`
1688 const beginId = PerformanceStatistics.beginMeasure(measureId)
1689 writeFileSync(
1690 this.configurationFile,
1691 JSON.stringify(configurationData, undefined, 2),
1692 'utf8'
1693 )
1694 PerformanceStatistics.endMeasure(measureId, beginId)
1695 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1696 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1697 this.configurationFileHash = configurationHash
1698 }).catch(error => {
1699 handleFileException(
1700 this.configurationFile,
1701 FileType.ChargingStationConfiguration,
1702 error as NodeJS.ErrnoException,
1703 this.logPrefix()
1704 )
1705 })
1706 } else {
1707 logger.debug(
1708 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1709 this.configurationFile
1710 }`
1711 )
1712 }
1713 } catch (error) {
1714 handleFileException(
1715 this.configurationFile,
1716 FileType.ChargingStationConfiguration,
1717 error as NodeJS.ErrnoException,
1718 this.logPrefix()
1719 )
1720 }
1721 } else {
1722 logger.error(
1723 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1724 )
1725 }
1726 }
1727
1728 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1729 return this.getTemplateFromFile()?.Configuration
1730 }
1731
1732 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1733 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1734 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1735 return { configurationKey }
1736 }
1737 return undefined
1738 }
1739
1740 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
1741 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1742 this.getOcppConfigurationFromFile()
1743 if (ocppConfiguration == null) {
1744 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1745 }
1746 return ocppConfiguration
1747 }
1748
1749 private async onOpen (): Promise<void> {
1750 if (this.isWebSocketConnectionOpened()) {
1751 logger.info(
1752 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1753 )
1754 let registrationRetryCount = 0
1755 if (!this.isRegistered()) {
1756 // Send BootNotification
1757 do {
1758 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1759 BootNotificationRequest,
1760 BootNotificationResponse
1761 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1762 skipBufferingOnError: true
1763 })
1764 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1765 this.bootNotificationResponse.currentTime = convertToDate(
1766 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1767 this.bootNotificationResponse?.currentTime
1768 )!
1769 if (!this.isRegistered()) {
1770 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1771 await sleep(
1772 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1773 this.bootNotificationResponse?.interval != null
1774 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1775 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1776 )
1777 }
1778 } while (
1779 !this.isRegistered() &&
1780 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1781 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1782 this.stationInfo?.registrationMaxRetries === -1)
1783 )
1784 }
1785 if (this.isRegistered()) {
1786 this.emit(ChargingStationEvents.registered)
1787 if (this.inAcceptedState()) {
1788 this.emit(ChargingStationEvents.accepted)
1789 }
1790 } else {
1791 logger.error(
1792 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1793 this.stationInfo?.registrationMaxRetries
1794 })`
1795 )
1796 }
1797 this.autoReconnectRetryCount = 0
1798 this.emit(ChargingStationEvents.updated)
1799 } else {
1800 logger.warn(
1801 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1802 )
1803 }
1804 }
1805
1806 private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
1807 switch (code) {
1808 // Normal close
1809 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1810 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1811 logger.info(
1812 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1813 code
1814 )}' and reason '${reason.toString()}'`
1815 )
1816 this.autoReconnectRetryCount = 0
1817 break
1818 // Abnormal close
1819 default:
1820 logger.error(
1821 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1822 code
1823 )}' and reason '${reason.toString()}'`
1824 )
1825 this.started && (await this.reconnect())
1826 break
1827 }
1828 this.emit(ChargingStationEvents.updated)
1829 }
1830
1831 private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
1832 const cachedRequest = this.requests.get(messageId)
1833 if (Array.isArray(cachedRequest)) {
1834 return cachedRequest
1835 }
1836 throw new OCPPError(
1837 ErrorType.PROTOCOL_ERROR,
1838 `Cached request for message id ${messageId} ${getMessageTypeString(
1839 messageType
1840 )} is not an array`,
1841 undefined,
1842 cachedRequest
1843 )
1844 }
1845
1846 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1847 const [messageType, messageId, commandName, commandPayload] = request
1848 if (this.stationInfo?.enableStatistics === true) {
1849 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1850 }
1851 logger.debug(
1852 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1853 request
1854 )}`
1855 )
1856 // Process the message
1857 await this.ocppIncomingRequestService.incomingRequestHandler(
1858 this,
1859 messageId,
1860 commandName,
1861 commandPayload
1862 )
1863 this.emit(ChargingStationEvents.updated)
1864 }
1865
1866 private handleResponseMessage (response: Response): void {
1867 const [messageType, messageId, commandPayload] = response
1868 if (!this.requests.has(messageId)) {
1869 // Error
1870 throw new OCPPError(
1871 ErrorType.INTERNAL_ERROR,
1872 `Response for unknown message id ${messageId}`,
1873 undefined,
1874 commandPayload
1875 )
1876 }
1877 // Respond
1878 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1879 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1880 messageType,
1881 messageId
1882 )!
1883 logger.debug(
1884 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1885 response
1886 )}`
1887 )
1888 responseCallback(commandPayload, requestPayload)
1889 }
1890
1891 private handleErrorMessage (errorResponse: ErrorResponse): void {
1892 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1893 if (!this.requests.has(messageId)) {
1894 // Error
1895 throw new OCPPError(
1896 ErrorType.INTERNAL_ERROR,
1897 `Error response for unknown message id ${messageId}`,
1898 undefined,
1899 { errorType, errorMessage, errorDetails }
1900 )
1901 }
1902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1903 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1904 logger.debug(
1905 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1906 errorResponse
1907 )}`
1908 )
1909 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1910 }
1911
1912 private async onMessage (data: RawData): Promise<void> {
1913 let request: IncomingRequest | Response | ErrorResponse | undefined
1914 let messageType: MessageType | undefined
1915 let errorMsg: string
1916 try {
1917 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1918 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1919 if (Array.isArray(request)) {
1920 [messageType] = request
1921 // Check the type of message
1922 switch (messageType) {
1923 // Incoming Message
1924 case MessageType.CALL_MESSAGE:
1925 await this.handleIncomingMessage(request as IncomingRequest)
1926 break
1927 // Response Message
1928 case MessageType.CALL_RESULT_MESSAGE:
1929 this.handleResponseMessage(request as Response)
1930 break
1931 // Error Message
1932 case MessageType.CALL_ERROR_MESSAGE:
1933 this.handleErrorMessage(request as ErrorResponse)
1934 break
1935 // Unknown Message
1936 default:
1937 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1938 errorMsg = `Wrong message type ${messageType}`
1939 logger.error(`${this.logPrefix()} ${errorMsg}`)
1940 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
1941 }
1942 } else {
1943 throw new OCPPError(
1944 ErrorType.PROTOCOL_ERROR,
1945 'Incoming message is not an array',
1946 undefined,
1947 {
1948 request
1949 }
1950 )
1951 }
1952 } catch (error) {
1953 let commandName: IncomingRequestCommand | undefined
1954 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1955 let errorCallback: ErrorCallback
1956 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 }