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