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