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