7c4b098d3de215a7781d0909572b55e665518013
[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 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1204 ) {
1205 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1206 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1207 } else {
1208 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1209 }
1210 this.stationInfo = this.getStationInfo()
1211 if (
1212 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1213 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1214 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1215 ) {
1216 const patternGroup: number | undefined =
1217 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1218 this.stationInfo.firmwareVersion?.split('.').length
1219 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1220 const match = new RegExp(this.stationInfo.firmwareVersionPattern!)
1221 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1222 .exec(this.stationInfo.firmwareVersion!)
1223 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1224 ?.slice(1, patternGroup! + 1)
1225 if (match != null) {
1226 const patchLevelIndex = match.length - 1
1227 match[patchLevelIndex] = (
1228 convertToInt(match[patchLevelIndex]) +
1229 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1230 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1231 ).toString()
1232 this.stationInfo.firmwareVersion = match.join('.')
1233 }
1234 }
1235 this.saveStationInfo()
1236 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1237 if (this.stationInfo?.enableStatistics === true) {
1238 this.performanceStatistics = PerformanceStatistics.getInstance(
1239 this.stationInfo.hashId,
1240 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1241 this.stationInfo.chargingStationId!,
1242 this.configuredSupervisionUrl
1243 )
1244 }
1245 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1246 this.powerDivider = this.getPowerDivider()
1247 // OCPP configuration
1248 this.ocppConfiguration = this.getOcppConfiguration()
1249 this.initializeOcppConfiguration()
1250 this.initializeOcppServices()
1251 this.once(ChargingStationEvents.accepted, () => {
1252 this.startMessageSequence().catch((error) => {
1253 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
1254 })
1255 })
1256 if (this.stationInfo?.autoRegister === true) {
1257 this.bootNotificationResponse = {
1258 currentTime: new Date(),
1259 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1260 status: RegistrationStatusEnumType.ACCEPTED
1261 }
1262 }
1263 }
1264
1265 private initializeOcppServices (): void {
1266 const ocppVersion = this.stationInfo?.ocppVersion
1267 switch (ocppVersion) {
1268 case OCPPVersion.VERSION_16:
1269 this.ocppIncomingRequestService =
1270 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1271 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1272 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1273 )
1274 break
1275 case OCPPVersion.VERSION_20:
1276 case OCPPVersion.VERSION_201:
1277 this.ocppIncomingRequestService =
1278 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1279 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1280 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1281 )
1282 break
1283 default:
1284 this.handleUnsupportedVersion(ocppVersion)
1285 break
1286 }
1287 }
1288
1289 private initializeOcppConfiguration (): void {
1290 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1291 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1292 }
1293 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1294 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1295 }
1296 if (
1297 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1298 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
1299 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1300 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) == null
1301 ) {
1302 addConfigurationKey(
1303 this,
1304 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1305 this.stationInfo.supervisionUrlOcppKey!,
1306 this.configuredSupervisionUrl.href,
1307 { reboot: true }
1308 )
1309 } else if (
1310 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1311 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
1312 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1313 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) != null
1314 ) {
1315 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1316 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false })
1317 }
1318 if (
1319 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1320 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1321 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) == null
1322 ) {
1323 addConfigurationKey(
1324 this,
1325 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1326 this.stationInfo.amperageLimitationOcppKey!,
1327 // prettier-ignore
1328 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1329 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1330 )
1331 }
1332 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1333 addConfigurationKey(
1334 this,
1335 StandardParametersKey.SupportedFeatureProfiles,
1336 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1337 )
1338 }
1339 addConfigurationKey(
1340 this,
1341 StandardParametersKey.NumberOfConnectors,
1342 this.getNumberOfConnectors().toString(),
1343 { readonly: true },
1344 { overwrite: true }
1345 )
1346 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1347 addConfigurationKey(
1348 this,
1349 StandardParametersKey.MeterValuesSampledData,
1350 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1351 )
1352 }
1353 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1354 const connectorsPhaseRotation: string[] = []
1355 if (this.hasEvses) {
1356 for (const evseStatus of this.evses.values()) {
1357 for (const connectorId of evseStatus.connectors.keys()) {
1358 connectorsPhaseRotation.push(
1359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1360 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1361 )
1362 }
1363 }
1364 } else {
1365 for (const connectorId of this.connectors.keys()) {
1366 connectorsPhaseRotation.push(
1367 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1368 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1369 )
1370 }
1371 }
1372 addConfigurationKey(
1373 this,
1374 StandardParametersKey.ConnectorPhaseRotation,
1375 connectorsPhaseRotation.toString()
1376 )
1377 }
1378 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1379 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1380 }
1381 if (
1382 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1383 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1384 ) {
1385 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1386 }
1387 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1388 addConfigurationKey(
1389 this,
1390 StandardParametersKey.ConnectionTimeOut,
1391 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1392 )
1393 }
1394 this.saveOcppConfiguration()
1395 }
1396
1397 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1398 if (configuration?.connectorsStatus != null && configuration?.evsesStatus == null) {
1399 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1400 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
1401 }
1402 } else if (configuration?.evsesStatus != null && configuration?.connectorsStatus == null) {
1403 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1404 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration)
1405 delete evseStatus.connectorsStatus
1406 this.evses.set(evseId, {
1407 ...(evseStatus as EvseStatus),
1408 connectors: new Map<number, ConnectorStatus>(
1409 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1410 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1411 connectorId,
1412 connectorStatus
1413 ])
1414 )
1415 })
1416 }
1417 } else if (configuration?.evsesStatus != null && configuration?.connectorsStatus != null) {
1418 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1419 logger.error(`${this.logPrefix()} ${errorMsg}`)
1420 throw new BaseError(errorMsg)
1421 } else {
1422 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1423 logger.error(`${this.logPrefix()} ${errorMsg}`)
1424 throw new BaseError(errorMsg)
1425 }
1426 }
1427
1428 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1429 if (stationTemplate?.Connectors != null && stationTemplate?.Evses == null) {
1430 this.initializeConnectorsFromTemplate(stationTemplate)
1431 } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors == null) {
1432 this.initializeEvsesFromTemplate(stationTemplate)
1433 } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors != null) {
1434 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1435 logger.error(`${this.logPrefix()} ${errorMsg}`)
1436 throw new BaseError(errorMsg)
1437 } else {
1438 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1439 logger.error(`${this.logPrefix()} ${errorMsg}`)
1440 throw new BaseError(errorMsg)
1441 }
1442 }
1443
1444 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1445 if (stationTemplate?.Connectors == null && this.connectors.size === 0) {
1446 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1447 logger.error(`${this.logPrefix()} ${errorMsg}`)
1448 throw new BaseError(errorMsg)
1449 }
1450 if (stationTemplate?.Connectors?.[0] == null) {
1451 logger.warn(
1452 `${this.logPrefix()} Charging station information from template ${
1453 this.templateFile
1454 } with no connector id 0 configuration`
1455 )
1456 }
1457 if (stationTemplate?.Connectors != null) {
1458 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1459 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1460 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1461 .update(
1462 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1463 )
1464 .digest('hex')
1465 const connectorsConfigChanged =
1466 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1467 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1468 connectorsConfigChanged && this.connectors.clear()
1469 this.connectorsConfigurationHash = connectorsConfigHash
1470 if (templateMaxConnectors > 0) {
1471 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1472 if (
1473 connectorId === 0 &&
1474 (stationTemplate?.Connectors?.[connectorId] == null ||
1475 !this.getUseConnectorId0(stationTemplate))
1476 ) {
1477 continue
1478 }
1479 const templateConnectorId =
1480 connectorId > 0 && stationTemplate?.randomConnectors === true
1481 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1482 : connectorId
1483 const connectorStatus = stationTemplate?.Connectors[templateConnectorId]
1484 checkStationInfoConnectorStatus(
1485 templateConnectorId,
1486 connectorStatus,
1487 this.logPrefix(),
1488 this.templateFile
1489 )
1490 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
1491 }
1492 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1493 this.saveConnectorsStatus()
1494 } else {
1495 logger.warn(
1496 `${this.logPrefix()} Charging station information from template ${
1497 this.templateFile
1498 } with no connectors configuration defined, cannot create connectors`
1499 )
1500 }
1501 }
1502 } else {
1503 logger.warn(
1504 `${this.logPrefix()} Charging station information from template ${
1505 this.templateFile
1506 } with no connectors configuration defined, using already defined connectors`
1507 )
1508 }
1509 }
1510
1511 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1512 if (stationTemplate?.Evses == null && this.evses.size === 0) {
1513 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1514 logger.error(`${this.logPrefix()} ${errorMsg}`)
1515 throw new BaseError(errorMsg)
1516 }
1517 if (stationTemplate?.Evses?.[0] == null) {
1518 logger.warn(
1519 `${this.logPrefix()} Charging station information from template ${
1520 this.templateFile
1521 } with no evse id 0 configuration`
1522 )
1523 }
1524 if (stationTemplate?.Evses?.[0]?.Connectors?.[0] == null) {
1525 logger.warn(
1526 `${this.logPrefix()} Charging station information from template ${
1527 this.templateFile
1528 } with evse id 0 with no connector id 0 configuration`
1529 )
1530 }
1531 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1532 logger.warn(
1533 `${this.logPrefix()} Charging station information from template ${
1534 this.templateFile
1535 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1536 )
1537 }
1538 if (stationTemplate?.Evses != null) {
1539 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1540 .update(JSON.stringify(stationTemplate?.Evses))
1541 .digest('hex')
1542 const evsesConfigChanged =
1543 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1544 if (this.evses?.size === 0 || evsesConfigChanged) {
1545 evsesConfigChanged && this.evses.clear()
1546 this.evsesConfigurationHash = evsesConfigHash
1547 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses)
1548 if (templateMaxEvses > 0) {
1549 for (const evseKey in stationTemplate.Evses) {
1550 const evseId = convertToInt(evseKey)
1551 this.evses.set(evseId, {
1552 connectors: buildConnectorsMap(
1553 stationTemplate?.Evses[evseKey]?.Connectors,
1554 this.logPrefix(),
1555 this.templateFile
1556 ),
1557 availability: AvailabilityType.Operative
1558 })
1559 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1560 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1561 }
1562 this.saveEvsesStatus()
1563 } else {
1564 logger.warn(
1565 `${this.logPrefix()} Charging station information from template ${
1566 this.templateFile
1567 } with no evses configuration defined, cannot create evses`
1568 )
1569 }
1570 }
1571 } else {
1572 logger.warn(
1573 `${this.logPrefix()} Charging station information from template ${
1574 this.templateFile
1575 } with no evses configuration defined, using already defined evses`
1576 )
1577 }
1578 }
1579
1580 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1581 let configuration: ChargingStationConfiguration | undefined
1582 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1583 try {
1584 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1585 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1586 this.configurationFileHash
1587 )
1588 } else {
1589 const measureId = `${FileType.ChargingStationConfiguration} read`
1590 const beginId = PerformanceStatistics.beginMeasure(measureId)
1591 configuration = JSON.parse(
1592 readFileSync(this.configurationFile, 'utf8')
1593 ) as ChargingStationConfiguration
1594 PerformanceStatistics.endMeasure(measureId, beginId)
1595 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1596 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1597 this.configurationFileHash = configuration.configurationHash!
1598 }
1599 } catch (error) {
1600 handleFileException(
1601 this.configurationFile,
1602 FileType.ChargingStationConfiguration,
1603 error as NodeJS.ErrnoException,
1604 this.logPrefix()
1605 )
1606 }
1607 }
1608 return configuration
1609 }
1610
1611 private saveAutomaticTransactionGeneratorConfiguration (): void {
1612 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1613 this.saveConfiguration()
1614 }
1615 }
1616
1617 private saveConnectorsStatus (): void {
1618 this.saveConfiguration()
1619 }
1620
1621 private saveEvsesStatus (): void {
1622 this.saveConfiguration()
1623 }
1624
1625 private saveConfiguration (): void {
1626 if (isNotEmptyString(this.configurationFile)) {
1627 try {
1628 if (!existsSync(dirname(this.configurationFile))) {
1629 mkdirSync(dirname(this.configurationFile), { recursive: true })
1630 }
1631 let configurationData: ChargingStationConfiguration =
1632 this.getConfigurationFromFile() != null
1633 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1634 cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1635 : {}
1636 if (
1637 this.stationInfo?.stationInfoPersistentConfiguration === true &&
1638 this.stationInfo != null
1639 ) {
1640 configurationData.stationInfo = this.stationInfo
1641 } else {
1642 delete configurationData.stationInfo
1643 }
1644 if (
1645 this.stationInfo?.ocppPersistentConfiguration === true &&
1646 Array.isArray(this.ocppConfiguration?.configurationKey)
1647 ) {
1648 configurationData.configurationKey = this.ocppConfiguration?.configurationKey
1649 } else {
1650 delete configurationData.configurationKey
1651 }
1652 configurationData = merge<ChargingStationConfiguration>(
1653 configurationData,
1654 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1655 )
1656 if (
1657 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1658 this.getAutomaticTransactionGeneratorConfiguration() == null
1659 ) {
1660 delete configurationData.automaticTransactionGenerator
1661 }
1662 if (this.connectors.size > 0) {
1663 configurationData.connectorsStatus = buildConnectorsStatus(this)
1664 } else {
1665 delete configurationData.connectorsStatus
1666 }
1667 if (this.evses.size > 0) {
1668 configurationData.evsesStatus = buildEvsesStatus(this)
1669 } else {
1670 delete configurationData.evsesStatus
1671 }
1672 delete configurationData.configurationHash
1673 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1674 .update(
1675 JSON.stringify({
1676 stationInfo: configurationData.stationInfo,
1677 configurationKey: configurationData.configurationKey,
1678 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1679 ...(this.connectors.size > 0 && {
1680 connectorsStatus: configurationData.connectorsStatus
1681 }),
1682 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1683 } satisfies ChargingStationConfiguration)
1684 )
1685 .digest('hex')
1686 if (this.configurationFileHash !== configurationHash) {
1687 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1688 configurationData.configurationHash = configurationHash
1689 const measureId = `${FileType.ChargingStationConfiguration} write`
1690 const beginId = PerformanceStatistics.beginMeasure(measureId)
1691 writeFileSync(
1692 this.configurationFile,
1693 JSON.stringify(configurationData, undefined, 2),
1694 'utf8'
1695 )
1696 PerformanceStatistics.endMeasure(measureId, beginId)
1697 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1698 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1699 this.configurationFileHash = configurationHash
1700 }).catch((error) => {
1701 handleFileException(
1702 this.configurationFile,
1703 FileType.ChargingStationConfiguration,
1704 error as NodeJS.ErrnoException,
1705 this.logPrefix()
1706 )
1707 })
1708 } else {
1709 logger.debug(
1710 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1711 this.configurationFile
1712 }`
1713 )
1714 }
1715 } catch (error) {
1716 handleFileException(
1717 this.configurationFile,
1718 FileType.ChargingStationConfiguration,
1719 error as NodeJS.ErrnoException,
1720 this.logPrefix()
1721 )
1722 }
1723 } else {
1724 logger.error(
1725 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1726 )
1727 }
1728 }
1729
1730 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1731 return this.getTemplateFromFile()?.Configuration
1732 }
1733
1734 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1735 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1736 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1737 return { configurationKey }
1738 }
1739 return undefined
1740 }
1741
1742 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
1743 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1744 this.getOcppConfigurationFromFile()
1745 if (ocppConfiguration == null) {
1746 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1747 }
1748 return ocppConfiguration
1749 }
1750
1751 private async onOpen (): Promise<void> {
1752 if (this.isWebSocketConnectionOpened()) {
1753 logger.info(
1754 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1755 )
1756 let registrationRetryCount = 0
1757 if (!this.isRegistered()) {
1758 // Send BootNotification
1759 do {
1760 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1761 BootNotificationRequest,
1762 BootNotificationResponse
1763 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1764 skipBufferingOnError: true
1765 })
1766 if (!this.isRegistered()) {
1767 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1768 await sleep(
1769 this?.bootNotificationResponse?.interval != null
1770 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1771 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1772 )
1773 }
1774 } while (
1775 !this.isRegistered() &&
1776 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1777 (registrationRetryCount <= this.stationInfo.registrationMaxRetries! ||
1778 this.stationInfo?.registrationMaxRetries === -1)
1779 )
1780 }
1781 if (this.isRegistered()) {
1782 this.emit(ChargingStationEvents.registered)
1783 if (this.inAcceptedState()) {
1784 this.emit(ChargingStationEvents.accepted)
1785 }
1786 } else {
1787 logger.error(
1788 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${this
1789 .stationInfo?.registrationMaxRetries})`
1790 )
1791 }
1792 this.autoReconnectRetryCount = 0
1793 this.emit(ChargingStationEvents.updated)
1794 } else {
1795 logger.warn(
1796 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1797 )
1798 }
1799 }
1800
1801 private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
1802 switch (code) {
1803 // Normal close
1804 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1805 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1806 logger.info(
1807 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1808 code
1809 )}' and reason '${reason.toString()}'`
1810 )
1811 this.autoReconnectRetryCount = 0
1812 break
1813 // Abnormal close
1814 default:
1815 logger.error(
1816 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1817 code
1818 )}' and reason '${reason.toString()}'`
1819 )
1820 this.started && (await this.reconnect())
1821 break
1822 }
1823 this.emit(ChargingStationEvents.updated)
1824 }
1825
1826 private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
1827 const cachedRequest = this.requests.get(messageId)
1828 if (Array.isArray(cachedRequest)) {
1829 return cachedRequest
1830 }
1831 throw new OCPPError(
1832 ErrorType.PROTOCOL_ERROR,
1833 `Cached request for message id ${messageId} ${getMessageTypeString(
1834 messageType
1835 )} is not an array`,
1836 undefined,
1837 cachedRequest
1838 )
1839 }
1840
1841 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1842 const [messageType, messageId, commandName, commandPayload] = request
1843 if (this.stationInfo?.enableStatistics === true) {
1844 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1845 }
1846 logger.debug(
1847 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1848 request
1849 )}`
1850 )
1851 // Process the message
1852 await this.ocppIncomingRequestService.incomingRequestHandler(
1853 this,
1854 messageId,
1855 commandName,
1856 commandPayload
1857 )
1858 this.emit(ChargingStationEvents.updated)
1859 }
1860
1861 private handleResponseMessage (response: Response): void {
1862 const [messageType, messageId, commandPayload] = response
1863 if (!this.requests.has(messageId)) {
1864 // Error
1865 throw new OCPPError(
1866 ErrorType.INTERNAL_ERROR,
1867 `Response for unknown message id ${messageId}`,
1868 undefined,
1869 commandPayload
1870 )
1871 }
1872 // Respond
1873 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1874 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1875 messageType,
1876 messageId
1877 )!
1878 logger.debug(
1879 `${this.logPrefix()} << Command '${
1880 requestCommandName ?? Constants.UNKNOWN_COMMAND
1881 }' received response payload: ${JSON.stringify(response)}`
1882 )
1883 responseCallback(commandPayload, requestPayload)
1884 }
1885
1886 private handleErrorMessage (errorResponse: ErrorResponse): void {
1887 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1888 if (!this.requests.has(messageId)) {
1889 // Error
1890 throw new OCPPError(
1891 ErrorType.INTERNAL_ERROR,
1892 `Error response for unknown message id ${messageId}`,
1893 undefined,
1894 { errorType, errorMessage, errorDetails }
1895 )
1896 }
1897 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1898 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1899 logger.debug(
1900 `${this.logPrefix()} << Command '${
1901 requestCommandName ?? Constants.UNKNOWN_COMMAND
1902 }' received error response payload: ${JSON.stringify(errorResponse)}`
1903 )
1904 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1905 }
1906
1907 private async onMessage (data: RawData): Promise<void> {
1908 let request: IncomingRequest | Response | ErrorResponse | undefined
1909 let messageType: MessageType | undefined
1910 let errorMsg: string
1911 try {
1912 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1913 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1914 if (Array.isArray(request)) {
1915 [messageType] = request
1916 // Check the type of message
1917 switch (messageType) {
1918 // Incoming Message
1919 case MessageType.CALL_MESSAGE:
1920 await this.handleIncomingMessage(request as IncomingRequest)
1921 break
1922 // Response Message
1923 case MessageType.CALL_RESULT_MESSAGE:
1924 this.handleResponseMessage(request as Response)
1925 break
1926 // Error Message
1927 case MessageType.CALL_ERROR_MESSAGE:
1928 this.handleErrorMessage(request as ErrorResponse)
1929 break
1930 // Unknown Message
1931 default:
1932 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1933 errorMsg = `Wrong message type ${messageType}`
1934 logger.error(`${this.logPrefix()} ${errorMsg}`)
1935 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
1936 }
1937 } else {
1938 throw new OCPPError(
1939 ErrorType.PROTOCOL_ERROR,
1940 'Incoming message is not an array',
1941 undefined,
1942 {
1943 request
1944 }
1945 )
1946 }
1947 } catch (error) {
1948 let commandName: IncomingRequestCommand | undefined
1949 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1950 let errorCallback: ErrorCallback
1951 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1952 const [, messageId] = request!
1953 switch (messageType) {
1954 case MessageType.CALL_MESSAGE:
1955 [, , commandName] = request as IncomingRequest
1956 // Send error
1957 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
1958 break
1959 case MessageType.CALL_RESULT_MESSAGE:
1960 case MessageType.CALL_ERROR_MESSAGE:
1961 if (this.requests.has(messageId)) {
1962 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1963 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1964 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1965 errorCallback(error as OCPPError, false)
1966 } else {
1967 // Remove the request from the cache in case of error at response handling
1968 this.requests.delete(messageId)
1969 }
1970 break
1971 }
1972 if (!(error instanceof OCPPError)) {
1973 logger.warn(
1974 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1975 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1976 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1977 }' message '${data.toString()}' handling is not an OCPPError:`,
1978 error
1979 )
1980 }
1981 logger.error(
1982 `${this.logPrefix()} Incoming OCPP command '${
1983 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1984 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1985 }' message '${data.toString()}'${
1986 messageType !== MessageType.CALL_MESSAGE
1987 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1988 : ''
1989 } processing error:`,
1990 error
1991 )
1992 }
1993 }
1994
1995 private onPing (): void {
1996 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
1997 }
1998
1999 private onPong (): void {
2000 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2001 }
2002
2003 private onError (error: WSError): void {
2004 this.closeWSConnection()
2005 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2006 }
2007
2008 private getEnergyActiveImportRegister (connectorStatus: ConnectorStatus, rounded = false): number {
2009 if (this.stationInfo?.meteringPerTransaction === true) {
2010 return (
2011 (rounded
2012 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2013 Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
2014 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2015 )
2016 }
2017 return (
2018 (rounded
2019 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2020 Math.round(connectorStatus.energyActiveImportRegisterValue!)
2021 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2022 )
2023 }
2024
2025 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2026 return stationTemplate?.useConnectorId0 ?? true
2027 }
2028
2029 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2030 if (this.hasEvses) {
2031 for (const [evseId, evseStatus] of this.evses) {
2032 if (evseId === 0) {
2033 continue
2034 }
2035 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2036 if (connectorStatus.transactionStarted === true) {
2037 await this.stopTransactionOnConnector(connectorId, reason)
2038 }
2039 }
2040 }
2041 } else {
2042 for (const connectorId of this.connectors.keys()) {
2043 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2044 await this.stopTransactionOnConnector(connectorId, reason)
2045 }
2046 }
2047 }
2048 }
2049
2050 // 0 for disabling
2051 private getConnectionTimeout (): number {
2052 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2053 return convertToInt(
2054 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2055 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value! ??
2056 Constants.DEFAULT_CONNECTION_TIMEOUT
2057 )
2058 }
2059 return Constants.DEFAULT_CONNECTION_TIMEOUT
2060 }
2061
2062 private getPowerDivider (): number {
2063 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2064 if (this.stationInfo?.powerSharedByConnectors === true) {
2065 powerDivider = this.getNumberOfRunningTransactions()
2066 }
2067 return powerDivider
2068 }
2069
2070 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2071 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2072 const maximumPower = (stationInfo ?? this.stationInfo).maximumPower!
2073 switch (this.getCurrentOutType(stationInfo)) {
2074 case CurrentType.AC:
2075 return ACElectricUtils.amperagePerPhaseFromPower(
2076 this.getNumberOfPhases(stationInfo),
2077 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2078 this.getVoltageOut(stationInfo)
2079 )
2080 case CurrentType.DC:
2081 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2082 }
2083 }
2084
2085 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2086 return (stationInfo ?? this.stationInfo).currentOutType ?? CurrentType.AC
2087 }
2088
2089 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2090 return (
2091 (stationInfo ?? this.stationInfo).voltageOut ??
2092 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2093 )
2094 }
2095
2096 private getAmperageLimitation (): number | undefined {
2097 if (
2098 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2099 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2100 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) != null
2101 ) {
2102 return (
2103 convertToInt(
2104 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2105 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value
2106 ) / getAmperageLimitationUnitDivider(this.stationInfo)
2107 )
2108 }
2109 }
2110
2111 private async startMessageSequence (): Promise<void> {
2112 if (this.stationInfo?.autoRegister === true) {
2113 await this.ocppRequestService.requestHandler<
2114 BootNotificationRequest,
2115 BootNotificationResponse
2116 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2117 skipBufferingOnError: true
2118 })
2119 }
2120 // Start WebSocket ping
2121 this.startWebSocketPing()
2122 // Start heartbeat
2123 this.startHeartbeat()
2124 // Initialize connectors status
2125 if (this.hasEvses) {
2126 for (const [evseId, evseStatus] of this.evses) {
2127 if (evseId > 0) {
2128 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2129 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2130 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
2131 }
2132 }
2133 }
2134 } else {
2135 for (const connectorId of this.connectors.keys()) {
2136 if (connectorId > 0) {
2137 const connectorBootStatus = getBootConnectorStatus(
2138 this,
2139 connectorId,
2140 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2141 this.getConnectorStatus(connectorId)!
2142 )
2143 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
2144 }
2145 }
2146 }
2147 if (this.stationInfo.firmwareStatus === FirmwareStatus.Installing) {
2148 await this.ocppRequestService.requestHandler<
2149 FirmwareStatusNotificationRequest,
2150 FirmwareStatusNotificationResponse
2151 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2152 status: FirmwareStatus.Installed
2153 })
2154 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2155 }
2156
2157 // Start the ATG
2158 if (this.getAutomaticTransactionGeneratorConfiguration().enable) {
2159 this.startAutomaticTransactionGenerator()
2160 }
2161 this.flushMessageBuffer()
2162 }
2163
2164 private async stopMessageSequence (
2165 reason?: StopTransactionReason,
2166 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
2167 ): Promise<void> {
2168 // Stop WebSocket ping
2169 this.stopWebSocketPing()
2170 // Stop heartbeat
2171 this.stopHeartbeat()
2172 // Stop the ATG
2173 if (this.automaticTransactionGenerator?.started === true) {
2174 this.stopAutomaticTransactionGenerator()
2175 }
2176 // Stop ongoing transactions
2177 stopTransactions === true && (await this.stopRunningTransactions(reason))
2178 if (this.hasEvses) {
2179 for (const [evseId, evseStatus] of this.evses) {
2180 if (evseId > 0) {
2181 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2182 await this.ocppRequestService.requestHandler<
2183 StatusNotificationRequest,
2184 StatusNotificationResponse
2185 >(
2186 this,
2187 RequestCommand.STATUS_NOTIFICATION,
2188 buildStatusNotificationRequest(
2189 this,
2190 connectorId,
2191 ConnectorStatusEnum.Unavailable,
2192 evseId
2193 )
2194 )
2195 delete connectorStatus?.status
2196 }
2197 }
2198 }
2199 } else {
2200 for (const connectorId of this.connectors.keys()) {
2201 if (connectorId > 0) {
2202 await this.ocppRequestService.requestHandler<
2203 StatusNotificationRequest,
2204 StatusNotificationResponse
2205 >(
2206 this,
2207 RequestCommand.STATUS_NOTIFICATION,
2208 buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable)
2209 )
2210 delete this.getConnectorStatus(connectorId)?.status
2211 }
2212 }
2213 }
2214 }
2215
2216 private startWebSocketPing (): void {
2217 const webSocketPingInterval: number =
2218 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2219 ? convertToInt(
2220 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2221 )
2222 : 0
2223 if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) {
2224 this.webSocketPingSetInterval = setInterval(() => {
2225 if (this.isWebSocketConnectionOpened()) {
2226 this.wsConnection?.ping()
2227 }
2228 }, secondsToMilliseconds(webSocketPingInterval))
2229 logger.info(
2230 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2231 webSocketPingInterval
2232 )}`
2233 )
2234 } else if (this.webSocketPingSetInterval != null) {
2235 logger.info(
2236 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2237 webSocketPingInterval
2238 )}`
2239 )
2240 } else {
2241 logger.error(
2242 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2243 )
2244 }
2245 }
2246
2247 private stopWebSocketPing (): void {
2248 if (this.webSocketPingSetInterval != null) {
2249 clearInterval(this.webSocketPingSetInterval)
2250 delete this.webSocketPingSetInterval
2251 }
2252 }
2253
2254 private getConfiguredSupervisionUrl (): URL {
2255 let configuredSupervisionUrl: string
2256 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2257 if (isNotEmptyArray(supervisionUrls)) {
2258 let configuredSupervisionUrlIndex: number
2259 switch (Configuration.getSupervisionUrlDistribution()) {
2260 case SupervisionUrlDistribution.RANDOM:
2261 configuredSupervisionUrlIndex = Math.floor(
2262 secureRandom() * (supervisionUrls as string[]).length
2263 )
2264 break
2265 case SupervisionUrlDistribution.ROUND_ROBIN:
2266 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2267 default:
2268 !Object.values(SupervisionUrlDistribution).includes(
2269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2270 Configuration.getSupervisionUrlDistribution()!
2271 ) &&
2272 logger.error(
2273 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2274 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2275 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2276 }`
2277 )
2278 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length
2279 break
2280 }
2281 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex]
2282 } else {
2283 configuredSupervisionUrl = supervisionUrls as string
2284 }
2285 if (isNotEmptyString(configuredSupervisionUrl)) {
2286 return new URL(configuredSupervisionUrl)
2287 }
2288 const errorMsg = 'No supervision url(s) configured'
2289 logger.error(`${this.logPrefix()} ${errorMsg}`)
2290 throw new BaseError(`${errorMsg}`)
2291 }
2292
2293 private stopHeartbeat (): void {
2294 if (this.heartbeatSetInterval != null) {
2295 clearInterval(this.heartbeatSetInterval)
2296 delete this.heartbeatSetInterval
2297 }
2298 }
2299
2300 private terminateWSConnection (): void {
2301 if (this.isWebSocketConnectionOpened()) {
2302 this.wsConnection?.terminate()
2303 this.wsConnection = null
2304 }
2305 }
2306
2307 private async reconnect (): Promise<void> {
2308 // Stop WebSocket ping
2309 this.stopWebSocketPing()
2310 // Stop heartbeat
2311 this.stopHeartbeat()
2312 // Stop the ATG if needed
2313 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure) {
2314 this.stopAutomaticTransactionGenerator()
2315 }
2316 if (
2317 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2318 this.autoReconnectRetryCount < this.stationInfo.autoReconnectMaxRetries! ||
2319 this.stationInfo?.autoReconnectMaxRetries === -1
2320 ) {
2321 ++this.autoReconnectRetryCount
2322 const reconnectDelay =
2323 this.stationInfo?.reconnectExponentialDelay === true
2324 ? exponentialDelay(this.autoReconnectRetryCount)
2325 : secondsToMilliseconds(this.getConnectionTimeout())
2326 const reconnectDelayWithdraw = 1000
2327 const reconnectTimeout =
2328 reconnectDelay != null && reconnectDelay - reconnectDelayWithdraw > 0
2329 ? reconnectDelay - reconnectDelayWithdraw
2330 : 0
2331 logger.error(
2332 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2333 reconnectDelay,
2334 2
2335 )}ms, timeout ${reconnectTimeout}ms`
2336 )
2337 await sleep(reconnectDelay)
2338 logger.error(
2339 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2340 )
2341 this.openWSConnection(
2342 {
2343 handshakeTimeout: reconnectTimeout
2344 },
2345 { closeOpened: true }
2346 )
2347 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2348 logger.error(
2349 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2350 this.autoReconnectRetryCount
2351 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2352 )
2353 }
2354 }
2355 }