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