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