build(deps-dev): apply updates
[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
1d41bc6b 263 public 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(
1d41bc6b 804 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
66a7748d
JB
805 )
806 return
0a03f36c
JB
807 }
808
1d41bc6b 809 logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
db2336d9 810
feff11ec
JB
811 this.wsConnection = new WebSocket(
812 this.wsConnectionUrl,
9a77cc07 813 `ocpp${this.stationInfo?.ocppVersion}`,
66a7748d
JB
814 options
815 )
db2336d9
JB
816
817 // Handle WebSocket message
968f0e47
JB
818 this.wsConnection.on('message', data => {
819 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
820 })
db2336d9 821 // Handle WebSocket error
ba9a56a6 822 this.wsConnection.on('error', this.onError.bind(this))
db2336d9 823 // Handle WebSocket close
ba9a56a6 824 this.wsConnection.on('close', this.onClose.bind(this))
db2336d9 825 // Handle WebSocket open
968f0e47 826 this.wsConnection.on('open', () => {
5a15db90
JB
827 this.onOpen().catch(error =>
828 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
829 )
968f0e47 830 })
db2336d9 831 // Handle WebSocket ping
ba9a56a6 832 this.wsConnection.on('ping', this.onPing.bind(this))
db2336d9 833 // Handle WebSocket pong
ba9a56a6 834 this.wsConnection.on('pong', this.onPong.bind(this))
db2336d9
JB
835 }
836
66a7748d
JB
837 public closeWSConnection (): void {
838 if (this.isWebSocketConnectionOpened()) {
839 this.wsConnection?.close()
840 this.wsConnection = null
db2336d9
JB
841 }
842 }
843
5199f9fd
JB
844 public getAutomaticTransactionGeneratorConfiguration ():
845 | AutomaticTransactionGeneratorConfiguration
846 | undefined {
aa63c9b7 847 if (this.automaticTransactionGeneratorConfiguration == null) {
c7db8ecb 848 let automaticTransactionGeneratorConfiguration:
66a7748d
JB
849 | AutomaticTransactionGeneratorConfiguration
850 | undefined
851 const stationTemplate = this.getTemplateFromFile()
852 const stationConfiguration = this.getConfigurationFromFile()
c7db8ecb 853 if (
5398cecf 854 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
61854f7c 855 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
66a7748d 856 stationConfiguration?.automaticTransactionGenerator != null
c7db8ecb
JB
857 ) {
858 automaticTransactionGeneratorConfiguration =
5199f9fd 859 stationConfiguration.automaticTransactionGenerator
c7db8ecb 860 } else {
66a7748d 861 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
c7db8ecb
JB
862 }
863 this.automaticTransactionGeneratorConfiguration = {
864 ...Constants.DEFAULT_ATG_CONFIGURATION,
66a7748d
JB
865 ...automaticTransactionGeneratorConfiguration
866 }
ac7f79af 867 }
aa63c9b7 868 return this.automaticTransactionGeneratorConfiguration
ac7f79af
JB
869 }
870
66a7748d
JB
871 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
872 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
5ced7e80
JB
873 }
874
e054fc1c
JB
875 public startAutomaticTransactionGenerator (
876 connectorIds?: number[],
877 stopAbsoluteDuration?: boolean
878 ): void {
66a7748d 879 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
9bf0ef23 880 if (isNotEmptyArray(connectorIds)) {
5dc7c990 881 for (const connectorId of connectorIds) {
e054fc1c 882 this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
a5e9befc
JB
883 }
884 } else {
e054fc1c 885 this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
4f69be04 886 }
66a7748d
JB
887 this.saveAutomaticTransactionGeneratorConfiguration()
888 this.emit(ChargingStationEvents.updated)
4f69be04
JB
889 }
890
66a7748d 891 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
9bf0ef23 892 if (isNotEmptyArray(connectorIds)) {
5dc7c990 893 for (const connectorId of connectorIds) {
66a7748d 894 this.automaticTransactionGenerator?.stopConnector(connectorId)
a5e9befc
JB
895 }
896 } else {
66a7748d 897 this.automaticTransactionGenerator?.stop()
4f69be04 898 }
66a7748d
JB
899 this.saveAutomaticTransactionGeneratorConfiguration()
900 this.emit(ChargingStationEvents.updated)
4f69be04
JB
901 }
902
66a7748d 903 public async stopTransactionOnConnector (
5e3cb728 904 connectorId: number,
66a7748d 905 reason?: StopTransactionReason
5e3cb728 906 ): Promise<StopTransactionResponse> {
f938317f 907 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
5e3cb728 908 if (
5398cecf 909 this.stationInfo?.beginEndMeterValues === true &&
5199f9fd
JB
910 this.stationInfo.ocppStrictCompliance === true &&
911 this.stationInfo.outOfOrderEndMeterValues === false
5e3cb728 912 ) {
41f3983a 913 const transactionEndMeterValue = buildTransactionEndMeterValue(
5e3cb728
JB
914 this,
915 connectorId,
2466918c 916 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
66a7748d 917 )
5e3cb728
JB
918 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
919 this,
920 RequestCommand.METER_VALUES,
921 {
922 connectorId,
923 transactionId,
66a7748d
JB
924 meterValue: [transactionEndMeterValue]
925 }
926 )
5e3cb728 927 }
66a7748d
JB
928 return await this.ocppRequestService.requestHandler<
929 StopTransactionRequest,
930 StopTransactionResponse
931 >(this, RequestCommand.STOP_TRANSACTION, {
932 transactionId,
2466918c 933 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
aa63c9b7 934 ...(reason != null && { reason })
66a7748d 935 })
5e3cb728
JB
936 }
937
66a7748d 938 public getReserveConnectorZeroSupported (): boolean {
9bf0ef23 939 return convertToBoolean(
66a7748d
JB
940 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
941 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
942 )
24578c31
JB
943 }
944
66a7748d
JB
945 public async addReservation (reservation: Reservation): Promise<void> {
946 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
a807045b 947 if (reservationFound != null) {
66a7748d 948 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
d193a949 949 }
66a7748d
JB
950 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
951 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
041365be 952 await sendAndSetConnectorStatus(
d193a949 953 this,
ec94a3cf
JB
954 reservation.connectorId,
955 ConnectorStatusEnum.Reserved,
e1d9a0f4 956 undefined,
66a7748d
JB
957 { send: reservation.connectorId !== 0 }
958 )
24578c31
JB
959 }
960
66a7748d 961 public async removeReservation (
d193a949 962 reservation: Reservation,
66a7748d 963 reason: ReservationTerminationReason
d193a949 964 ): Promise<void> {
66a7748d
JB
965 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
966 const connector = this.getConnectorStatus(reservation.connectorId)!
d193a949 967 switch (reason) {
96d96b12 968 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
ec94a3cf 969 case ReservationTerminationReason.TRANSACTION_STARTED:
66a7748d
JB
970 delete connector.reservation
971 break
e74bc549
JB
972 case ReservationTerminationReason.RESERVATION_CANCELED:
973 case ReservationTerminationReason.REPLACE_EXISTING:
974 case ReservationTerminationReason.EXPIRED:
041365be 975 await sendAndSetConnectorStatus(
d193a949 976 this,
ec94a3cf
JB
977 reservation.connectorId,
978 ConnectorStatusEnum.Available,
e1d9a0f4 979 undefined,
66a7748d
JB
980 { send: reservation.connectorId !== 0 }
981 )
982 delete connector.reservation
983 break
b029e74e 984 default:
90aceaf6 985 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d 986 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
d193a949 987 }
24578c31
JB
988 }
989
66a7748d 990 public getReservationBy (
366f75f6 991 filterKey: ReservationKey,
66a7748d 992 value: number | string
3fa7f799 993 ): Reservation | undefined {
66dd3447 994 if (this.hasEvses) {
3fa7f799
JB
995 for (const evseStatus of this.evses.values()) {
996 for (const connectorStatus of evseStatus.connectors.values()) {
5199f9fd 997 if (connectorStatus.reservation?.[filterKey] === value) {
66a7748d 998 return connectorStatus.reservation
66dd3447
JB
999 }
1000 }
1001 }
1002 } else {
3fa7f799 1003 for (const connectorStatus of this.connectors.values()) {
5199f9fd 1004 if (connectorStatus.reservation?.[filterKey] === value) {
66a7748d 1005 return connectorStatus.reservation
66dd3447
JB
1006 }
1007 }
1008 }
d193a949
JB
1009 }
1010
66a7748d 1011 public isConnectorReservable (
e6948a57
JB
1012 reservationId: number,
1013 idTag?: string,
66a7748d 1014 connectorId?: number
e6948a57 1015 ): boolean {
66a7748d 1016 const reservation = this.getReservationBy('reservationId', reservationId)
300418e9 1017 const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
e6948a57 1018 if (arguments.length === 1) {
66a7748d 1019 return !reservationExists
e6948a57 1020 } else if (arguments.length > 1) {
300418e9
JB
1021 const userReservation =
1022 idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
e6948a57 1023 const userReservationExists =
300418e9
JB
1024 userReservation !== undefined && !hasReservationExpired(userReservation)
1025 const notConnectorZero = connectorId === undefined ? true : connectorId > 0
66a7748d 1026 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
e6948a57
JB
1027 return (
1028 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
66a7748d 1029 )
e6948a57 1030 }
66a7748d 1031 return false
e6948a57
JB
1032 }
1033
66a7748d 1034 private setIntervalFlushMessageBuffer (): void {
a807045b 1035 if (this.flushMessageBufferSetInterval == null) {
2a2ad81b 1036 this.flushMessageBufferSetInterval = setInterval(() => {
66a7748d
JB
1037 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1038 this.flushMessageBuffer()
2a2ad81b
JB
1039 }
1040 if (this.messageBuffer.size === 0) {
66a7748d 1041 this.clearIntervalFlushMessageBuffer()
2a2ad81b 1042 }
66a7748d 1043 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
2a2ad81b
JB
1044 }
1045 }
1046
66a7748d 1047 private clearIntervalFlushMessageBuffer (): void {
a807045b 1048 if (this.flushMessageBufferSetInterval != null) {
66a7748d
JB
1049 clearInterval(this.flushMessageBufferSetInterval)
1050 delete this.flushMessageBufferSetInterval
2a2ad81b
JB
1051 }
1052 }
1053
66a7748d
JB
1054 private getNumberOfReservableConnectors (): number {
1055 let numberOfReservableConnectors = 0
66dd3447 1056 if (this.hasEvses) {
3fa7f799 1057 for (const evseStatus of this.evses.values()) {
66a7748d 1058 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
66dd3447
JB
1059 }
1060 } else {
66a7748d 1061 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
66dd3447 1062 }
66a7748d 1063 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
66dd3447
JB
1064 }
1065
66a7748d 1066 private getNumberOfReservationsOnConnectorZero (): number {
6913d568 1067 if (
66a7748d
JB
1068 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1069 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
6913d568 1070 ) {
66a7748d 1071 return 1
66dd3447 1072 }
66a7748d 1073 return 0
24578c31
JB
1074 }
1075
66a7748d 1076 private flushMessageBuffer (): void {
8e242273 1077 if (this.messageBuffer.size > 0) {
7d3b0f64 1078 for (const message of this.messageBuffer.values()) {
66a7748d
JB
1079 let beginId: string | undefined
1080 let commandName: RequestCommand | undefined
1081 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1082 const isRequest = messageType === MessageType.CALL_MESSAGE
1431af78 1083 if (isRequest) {
66a7748d
JB
1084 [, , commandName] = JSON.parse(message) as OutgoingRequest
1085 beginId = PerformanceStatistics.beginMeasure(commandName)
1431af78 1086 }
d42379d8 1087 this.wsConnection?.send(message, (error?: Error) => {
66a7748d
JB
1088 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1089 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
aa63c9b7 1090 if (error == null) {
d42379d8 1091 logger.debug(
041365be 1092 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
66a7748d
JB
1093 messageType
1094 )} OCPP message sent '${JSON.stringify(message)}'`
1095 )
1096 this.messageBuffer.delete(message)
041365be
JB
1097 } else {
1098 logger.debug(
1099 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
66a7748d 1100 messageType
041365be 1101 )} OCPP message '${JSON.stringify(message)}' send failed:`,
66a7748d
JB
1102 error
1103 )
d42379d8 1104 }
66a7748d 1105 })
7d3b0f64 1106 }
77f00f84
JB
1107 }
1108 }
1109
66a7748d
JB
1110 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1111 let template: ChargingStationTemplate | undefined
5ad8570f 1112 try {
cda5d0fb 1113 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
66a7748d 1114 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
7c72977b 1115 } else {
66a7748d
JB
1116 const measureId = `${FileType.ChargingStationTemplate} read`
1117 const beginId = PerformanceStatistics.beginMeasure(measureId)
1118 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1119 PerformanceStatistics.endMeasure(measureId, beginId)
d972af76 1120 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
7c72977b 1121 .update(JSON.stringify(template))
66a7748d
JB
1122 .digest('hex')
1123 this.sharedLRUCache.setChargingStationTemplate(template)
1124 this.templateFileHash = template.templateHash
7c72977b 1125 }
5ad8570f 1126 } catch (error) {
fa5995d6 1127 handleFileException(
2484ac1e 1128 this.templateFile,
7164966d
JB
1129 FileType.ChargingStationTemplate,
1130 error as NodeJS.ErrnoException,
66a7748d
JB
1131 this.logPrefix()
1132 )
1133 }
1134 return template
1135 }
1136
1137 private getStationInfoFromTemplate (): ChargingStationInfo {
1138 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
97608fbd 1139 const stationTemplate = this.getTemplateFromFile()!
66a7748d
JB
1140 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1141 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1142 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
5199f9fd 1143 if (stationTemplate.Connectors != null) {
66a7748d
JB
1144 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1145 }
97608fbd 1146 const stationInfo = stationTemplateToStationInfo(stationTemplate)
66a7748d 1147 stationInfo.hashId = getHashId(this.index, stationTemplate)
244c1396 1148 stationInfo.autoStart = stationTemplate.autoStart ?? true
1fdb60b6 1149 stationInfo.templateName = parse(this.templateFile).name
66a7748d 1150 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
5199f9fd 1151 stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
66a7748d
JB
1152 createSerialNumber(stationTemplate, stationInfo)
1153 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
5199f9fd 1154 if (isNotEmptyArray(stationTemplate.power)) {
66a7748d 1155 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
cc6e8ab5 1156 stationInfo.maximumPower =
5199f9fd 1157 stationTemplate.powerUnit === PowerUnits.KILO_WATT
fa7bccf4 1158 ? stationTemplate.power[powerArrayRandomIndex] * 1000
66a7748d 1159 : stationTemplate.power[powerArrayRandomIndex]
5ad8570f 1160 } else {
cc6e8ab5 1161 stationInfo.maximumPower =
5199f9fd 1162 stationTemplate.powerUnit === PowerUnits.KILO_WATT
5dc7c990
JB
1163 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1164 stationTemplate.power! * 1000
66a7748d 1165 : stationTemplate.power
fa7bccf4 1166 }
66a7748d 1167 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
3637ca2c 1168 stationInfo.firmwareVersionPattern =
5199f9fd 1169 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
3637ca2c 1170 if (
9bf0ef23 1171 isNotEmptyString(stationInfo.firmwareVersion) &&
5dc7c990 1172 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
3637ca2c
JB
1173 ) {
1174 logger.warn(
1175 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1176 this.templateFile
66a7748d
JB
1177 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1178 )
3637ca2c 1179 }
598c886d 1180 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1181 {
598c886d 1182 versionUpgrade: {
66a7748d 1183 step: 1
598c886d 1184 },
66a7748d 1185 reset: true
15748260 1186 },
5199f9fd 1187 stationTemplate.firmwareUpgrade ?? {}
66a7748d 1188 )
aa63c9b7 1189 stationInfo.resetTime =
5199f9fd 1190 stationTemplate.resetTime != null
aa63c9b7 1191 ? secondsToMilliseconds(stationTemplate.resetTime)
7e3bde4f 1192 : Constants.DEFAULT_CHARGING_STATION_RESET_TIME
66a7748d 1193 return stationInfo
5ad8570f
JB
1194 }
1195
66a7748d
JB
1196 private getStationInfoFromFile (
1197 stationInfoPersistentConfiguration = true
78786898 1198 ): ChargingStationInfo | undefined {
66a7748d
JB
1199 let stationInfo: ChargingStationInfo | undefined
1200 if (stationInfoPersistentConfiguration) {
1201 stationInfo = this.getConfigurationFromFile()?.stationInfo
1202 if (stationInfo != null) {
5199f9fd 1203 delete stationInfo.infoHash
1fdb60b6
JB
1204 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1205 if (stationInfo.templateName == null) {
1206 stationInfo.templateName = parse(this.templateFile).name
1207 }
244c1396
JB
1208 if (stationInfo.autoStart == null) {
1209 stationInfo.autoStart = true
1210 }
f832e5df
JB
1211 }
1212 }
66a7748d 1213 return stationInfo
2484ac1e
JB
1214 }
1215
66a7748d
JB
1216 private getStationInfo (): ChargingStationInfo {
1217 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
97608fbd
JB
1218 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1219 const stationInfoFromFile = this.getStationInfoFromFile(
5199f9fd 1220 stationInfoFromTemplate.stationInfoPersistentConfiguration
66a7748d 1221 )
6b90dcca
JB
1222 // Priority:
1223 // 1. charging station info from template
1224 // 2. charging station info from configuration file
2466918c
JB
1225 if (
1226 stationInfoFromFile != null &&
1227 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1228 ) {
1229 return { ...defaultStationInfo, ...stationInfoFromFile }
f765beaa 1230 }
66a7748d 1231 stationInfoFromFile != null &&
fba11dc6 1232 propagateSerialNumber(
5199f9fd 1233 this.getTemplateFromFile(),
fec4d204 1234 stationInfoFromFile,
66a7748d
JB
1235 stationInfoFromTemplate
1236 )
1237 return { ...defaultStationInfo, ...stationInfoFromTemplate }
2484ac1e
JB
1238 }
1239
66a7748d 1240 private saveStationInfo (): void {
5398cecf 1241 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
66a7748d 1242 this.saveConfiguration()
ccb1d6e9 1243 }
2484ac1e
JB
1244 }
1245
66a7748d
JB
1246 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1247 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1248 logger.error(`${this.logPrefix()} ${errorMsg}`)
1249 throw new BaseError(errorMsg)
c0560973
JB
1250 }
1251
66a7748d
JB
1252 private initialize (): void {
1253 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1254 const stationTemplate = this.getTemplateFromFile()!
1255 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
d972af76
JB
1256 this.configurationFile = join(
1257 dirname(this.templateFile.replace('station-templates', 'configurations')),
66a7748d
JB
1258 `${getHashId(this.index, stationTemplate)}.json`
1259 )
1260 const stationConfiguration = this.getConfigurationFromFile()
a4f7c75f 1261 if (
5199f9fd 1262 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
66a7748d 1263 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
a4f7c75f 1264 ) {
66a7748d
JB
1265 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1266 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
a4f7c75f 1267 } else {
66a7748d 1268 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
a4f7c75f 1269 }
66a7748d 1270 this.stationInfo = this.getStationInfo()
3637ca2c
JB
1271 if (
1272 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1273 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1274 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1275 ) {
2466918c 1276 const patternGroup =
15748260 1277 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
5dc7c990
JB
1278 this.stationInfo.firmwareVersion.split('.').length
1279 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1280 .exec(this.stationInfo.firmwareVersion)
1281 ?.slice(1, patternGroup + 1)
aa63c9b7
JB
1282 if (match != null) {
1283 const patchLevelIndex = match.length - 1
1284 match[patchLevelIndex] = (
1285 convertToInt(match[patchLevelIndex]) +
1286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1287 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1288 ).toString()
1289 this.stationInfo.firmwareVersion = match.join('.')
77807350 1290 }
3637ca2c 1291 }
66a7748d
JB
1292 this.saveStationInfo()
1293 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
5199f9fd 1294 if (this.stationInfo.enableStatistics === true) {
6bccfcbc
JB
1295 this.performanceStatistics = PerformanceStatistics.getInstance(
1296 this.stationInfo.hashId,
2466918c 1297 this.stationInfo.chargingStationId,
66a7748d
JB
1298 this.configuredSupervisionUrl
1299 )
6bccfcbc 1300 }
2466918c
JB
1301 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1302 if (bootNotificationRequest == null) {
1303 const errorMsg = 'Error while creating boot notification request'
1304 logger.error(`${this.logPrefix()} ${errorMsg}`)
1305 throw new BaseError(errorMsg)
1306 }
1307 this.bootNotificationRequest = bootNotificationRequest
66a7748d 1308 this.powerDivider = this.getPowerDivider()
692f2f64 1309 // OCPP configuration
66a7748d
JB
1310 this.ocppConfiguration = this.getOcppConfiguration()
1311 this.initializeOcppConfiguration()
1312 this.initializeOcppServices()
5199f9fd 1313 if (this.stationInfo.autoRegister === true) {
692f2f64
JB
1314 this.bootNotificationResponse = {
1315 currentTime: new Date(),
be4c6702 1316 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
66a7748d
JB
1317 status: RegistrationStatusEnumType.ACCEPTED
1318 }
692f2f64 1319 }
147d0e0f
JB
1320 }
1321
66a7748d
JB
1322 private initializeOcppServices (): void {
1323 const ocppVersion = this.stationInfo?.ocppVersion
feff11ec
JB
1324 switch (ocppVersion) {
1325 case OCPPVersion.VERSION_16:
1326 this.ocppIncomingRequestService =
66a7748d 1327 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
feff11ec 1328 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
66a7748d
JB
1329 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1330 )
1331 break
feff11ec
JB
1332 case OCPPVersion.VERSION_20:
1333 case OCPPVersion.VERSION_201:
1334 this.ocppIncomingRequestService =
66a7748d 1335 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
feff11ec 1336 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
66a7748d
JB
1337 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1338 )
1339 break
feff11ec 1340 default:
66a7748d
JB
1341 this.handleUnsupportedVersion(ocppVersion)
1342 break
feff11ec
JB
1343 }
1344 }
1345
66a7748d 1346 private initializeOcppConfiguration (): void {
aa63c9b7 1347 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
66a7748d 1348 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
f0f65a62 1349 }
aa63c9b7 1350 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
66a7748d 1351 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
f0f65a62 1352 }
e7aeea18 1353 if (
4e3b1d6b 1354 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
5199f9fd 1355 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
5dc7c990 1356 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
e7aeea18 1357 ) {
f2d5e3d9 1358 addConfigurationKey(
17ac262c 1359 this,
5dc7c990 1360 this.stationInfo.supervisionUrlOcppKey,
fa7bccf4 1361 this.configuredSupervisionUrl.href,
66a7748d
JB
1362 { reboot: true }
1363 )
e6895390 1364 } else if (
4e3b1d6b 1365 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
5199f9fd 1366 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
5dc7c990 1367 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
e6895390 1368 ) {
5dc7c990 1369 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
12fc74d6 1370 }
cc6e8ab5 1371 if (
9bf0ef23 1372 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
5dc7c990 1373 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
cc6e8ab5 1374 ) {
f2d5e3d9 1375 addConfigurationKey(
17ac262c 1376 this,
5dc7c990 1377 this.stationInfo.amperageLimitationOcppKey,
66a7748d
JB
1378 // prettier-ignore
1379 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5dc7c990 1380 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
66a7748d 1381 )
cc6e8ab5 1382 }
aa63c9b7 1383 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
f2d5e3d9 1384 addConfigurationKey(
17ac262c 1385 this,
e7aeea18 1386 StandardParametersKey.SupportedFeatureProfiles,
66a7748d
JB
1387 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1388 )
e7aeea18 1389 }
f2d5e3d9 1390 addConfigurationKey(
17ac262c 1391 this,
e7aeea18
JB
1392 StandardParametersKey.NumberOfConnectors,
1393 this.getNumberOfConnectors().toString(),
a95873d8 1394 { readonly: true },
66a7748d
JB
1395 { overwrite: true }
1396 )
aa63c9b7 1397 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
f2d5e3d9 1398 addConfigurationKey(
17ac262c 1399 this,
e7aeea18 1400 StandardParametersKey.MeterValuesSampledData,
66a7748d
JB
1401 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1402 )
7abfea5f 1403 }
aa63c9b7 1404 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
66a7748d 1405 const connectorsPhaseRotation: string[] = []
28e78158
JB
1406 if (this.hasEvses) {
1407 for (const evseStatus of this.evses.values()) {
1408 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1409 connectorsPhaseRotation.push(
66a7748d
JB
1410 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1411 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1412 )
28e78158
JB
1413 }
1414 }
1415 } else {
1416 for (const connectorId of this.connectors.keys()) {
dd08d43d 1417 connectorsPhaseRotation.push(
66a7748d
JB
1418 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1419 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1420 )
7e1dc878
JB
1421 }
1422 }
f2d5e3d9 1423 addConfigurationKey(
17ac262c 1424 this,
e7aeea18 1425 StandardParametersKey.ConnectorPhaseRotation,
66a7748d
JB
1426 connectorsPhaseRotation.toString()
1427 )
7e1dc878 1428 }
aa63c9b7 1429 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
66a7748d 1430 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
36f6a92e 1431 }
17ac262c 1432 if (
aa63c9b7 1433 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
a807045b 1434 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
17ac262c 1435 ) {
66a7748d 1436 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
f2d5e3d9 1437 }
aa63c9b7 1438 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
f2d5e3d9 1439 addConfigurationKey(
17ac262c 1440 this,
e7aeea18 1441 StandardParametersKey.ConnectionTimeOut,
66a7748d
JB
1442 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1443 )
8bce55bf 1444 }
66a7748d 1445 this.saveOcppConfiguration()
073bd098
JB
1446 }
1447
66a7748d 1448 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
5199f9fd 1449 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
8df5ae48 1450 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
40615072 1451 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
8df5ae48 1452 }
5199f9fd 1453 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
a4f7c75f 1454 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
40615072 1455 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
66a7748d 1456 delete evseStatus.connectorsStatus
a4f7c75f 1457 this.evses.set(evseId, {
8df5ae48 1458 ...(evseStatus as EvseStatus),
a4f7c75f 1459 connectors: new Map<number, ConnectorStatus>(
66a7748d 1460 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 1461 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f 1462 connectorId,
66a7748d
JB
1463 connectorStatus
1464 ])
1465 )
1466 })
a4f7c75f 1467 }
5199f9fd 1468 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
66a7748d
JB
1469 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1470 logger.error(`${this.logPrefix()} ${errorMsg}`)
1471 throw new BaseError(errorMsg)
a4f7c75f 1472 } else {
66a7748d
JB
1473 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1474 logger.error(`${this.logPrefix()} ${errorMsg}`)
1475 throw new BaseError(errorMsg)
a4f7c75f
JB
1476 }
1477 }
1478
66a7748d 1479 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
5199f9fd 1480 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
66a7748d 1481 this.initializeConnectorsFromTemplate(stationTemplate)
5199f9fd 1482 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
66a7748d 1483 this.initializeEvsesFromTemplate(stationTemplate)
5199f9fd 1484 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
66a7748d
JB
1485 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1486 logger.error(`${this.logPrefix()} ${errorMsg}`)
1487 throw new BaseError(errorMsg)
ae25f265 1488 } else {
66a7748d
JB
1489 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1490 logger.error(`${this.logPrefix()} ${errorMsg}`)
1491 throw new BaseError(errorMsg)
ae25f265
JB
1492 }
1493 }
1494
66a7748d 1495 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
5199f9fd 1496 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
66a7748d
JB
1497 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1498 logger.error(`${this.logPrefix()} ${errorMsg}`)
1499 throw new BaseError(errorMsg)
3d25cc86 1500 }
5199f9fd 1501 if (stationTemplate.Connectors?.[0] == null) {
3d25cc86
JB
1502 logger.warn(
1503 `${this.logPrefix()} Charging station information from template ${
1504 this.templateFile
66a7748d
JB
1505 } with no connector id 0 configuration`
1506 )
3d25cc86 1507 }
5199f9fd 1508 if (stationTemplate.Connectors != null) {
cda5d0fb 1509 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
66a7748d 1510 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
d972af76 1511 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1512 .update(
5199f9fd 1513 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
cda5d0fb 1514 )
66a7748d 1515 .digest('hex')
3d25cc86 1516 const connectorsConfigChanged =
5199f9fd
JB
1517 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1518 if (this.connectors.size === 0 || connectorsConfigChanged) {
66a7748d
JB
1519 connectorsConfigChanged && this.connectors.clear()
1520 this.connectorsConfigurationHash = connectorsConfigHash
269196a8
JB
1521 if (templateMaxConnectors > 0) {
1522 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1523 if (
1524 connectorId === 0 &&
5199f9fd
JB
1525 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1526 (stationTemplate.Connectors[connectorId] == null ||
66a7748d 1527 !this.getUseConnectorId0(stationTemplate))
269196a8 1528 ) {
66a7748d 1529 continue
269196a8
JB
1530 }
1531 const templateConnectorId =
5199f9fd 1532 connectorId > 0 && stationTemplate.randomConnectors === true
9bf0ef23 1533 ? getRandomInteger(templateMaxAvailableConnectors, 1)
66a7748d 1534 : connectorId
5199f9fd 1535 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
fba11dc6 1536 checkStationInfoConnectorStatus(
ae25f265 1537 templateConnectorId,
04b1261c
JB
1538 connectorStatus,
1539 this.logPrefix(),
66a7748d
JB
1540 this.templateFile
1541 )
40615072 1542 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
3d25cc86 1543 }
66a7748d
JB
1544 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1545 this.saveConnectorsStatus()
ae25f265
JB
1546 } else {
1547 logger.warn(
1548 `${this.logPrefix()} Charging station information from template ${
1549 this.templateFile
66a7748d
JB
1550 } with no connectors configuration defined, cannot create connectors`
1551 )
3d25cc86
JB
1552 }
1553 }
1554 } else {
1555 logger.warn(
1556 `${this.logPrefix()} Charging station information from template ${
1557 this.templateFile
66a7748d
JB
1558 } with no connectors configuration defined, using already defined connectors`
1559 )
3d25cc86 1560 }
3d25cc86
JB
1561 }
1562
66a7748d 1563 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
5199f9fd 1564 if (stationTemplate.Evses == null && this.evses.size === 0) {
66a7748d
JB
1565 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1566 logger.error(`${this.logPrefix()} ${errorMsg}`)
1567 throw new BaseError(errorMsg)
2585c6e9 1568 }
5199f9fd 1569 if (stationTemplate.Evses?.[0] == null) {
2585c6e9
JB
1570 logger.warn(
1571 `${this.logPrefix()} Charging station information from template ${
1572 this.templateFile
66a7748d
JB
1573 } with no evse id 0 configuration`
1574 )
2585c6e9 1575 }
5199f9fd 1576 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
59a0f26d
JB
1577 logger.warn(
1578 `${this.logPrefix()} Charging station information from template ${
1579 this.templateFile
66a7748d
JB
1580 } with evse id 0 with no connector id 0 configuration`
1581 )
59a0f26d 1582 }
5199f9fd 1583 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
491dad29
JB
1584 logger.warn(
1585 `${this.logPrefix()} Charging station information from template ${
1586 this.templateFile
66a7748d
JB
1587 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1588 )
491dad29 1589 }
5199f9fd 1590 if (stationTemplate.Evses != null) {
d972af76 1591 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5199f9fd 1592 .update(JSON.stringify(stationTemplate.Evses))
66a7748d 1593 .digest('hex')
2585c6e9 1594 const evsesConfigChanged =
5199f9fd
JB
1595 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1596 if (this.evses.size === 0 || evsesConfigChanged) {
66a7748d
JB
1597 evsesConfigChanged && this.evses.clear()
1598 this.evsesConfigurationHash = evsesConfigHash
5199f9fd 1599 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
ae25f265 1600 if (templateMaxEvses > 0) {
eb979012 1601 for (const evseKey in stationTemplate.Evses) {
66a7748d 1602 const evseId = convertToInt(evseKey)
52952bf8 1603 this.evses.set(evseId, {
fba11dc6 1604 connectors: buildConnectorsMap(
5199f9fd 1605 stationTemplate.Evses[evseKey].Connectors,
ae25f265 1606 this.logPrefix(),
66a7748d 1607 this.templateFile
ae25f265 1608 ),
66a7748d
JB
1609 availability: AvailabilityType.Operative
1610 })
1611 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1612 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
ae25f265 1613 }
66a7748d 1614 this.saveEvsesStatus()
ae25f265
JB
1615 } else {
1616 logger.warn(
1617 `${this.logPrefix()} Charging station information from template ${
04b1261c 1618 this.templateFile
66a7748d
JB
1619 } with no evses configuration defined, cannot create evses`
1620 )
2585c6e9
JB
1621 }
1622 }
513db108
JB
1623 } else {
1624 logger.warn(
1625 `${this.logPrefix()} Charging station information from template ${
1626 this.templateFile
66a7748d
JB
1627 } with no evses configuration defined, using already defined evses`
1628 )
2585c6e9
JB
1629 }
1630 }
1631
66a7748d
JB
1632 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1633 let configuration: ChargingStationConfiguration | undefined
9bf0ef23 1634 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1635 try {
57adbebc
JB
1636 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1637 configuration = this.sharedLRUCache.getChargingStationConfiguration(
66a7748d
JB
1638 this.configurationFileHash
1639 )
7c72977b 1640 } else {
66a7748d
JB
1641 const measureId = `${FileType.ChargingStationConfiguration} read`
1642 const beginId = PerformanceStatistics.beginMeasure(measureId)
7c72977b 1643 configuration = JSON.parse(
66a7748d
JB
1644 readFileSync(this.configurationFile, 'utf8')
1645 ) as ChargingStationConfiguration
1646 PerformanceStatistics.endMeasure(measureId, beginId)
1647 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1648 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1649 this.configurationFileHash = configuration.configurationHash!
7c72977b 1650 }
073bd098 1651 } catch (error) {
fa5995d6 1652 handleFileException(
073bd098 1653 this.configurationFile,
7164966d
JB
1654 FileType.ChargingStationConfiguration,
1655 error as NodeJS.ErrnoException,
66a7748d
JB
1656 this.logPrefix()
1657 )
073bd098
JB
1658 }
1659 }
66a7748d 1660 return configuration
073bd098
JB
1661 }
1662
66a7748d 1663 private saveAutomaticTransactionGeneratorConfiguration (): void {
5398cecf 1664 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
66a7748d 1665 this.saveConfiguration()
5ced7e80 1666 }
ac7f79af
JB
1667 }
1668
66a7748d
JB
1669 private saveConnectorsStatus (): void {
1670 this.saveConfiguration()
52952bf8
JB
1671 }
1672
66a7748d
JB
1673 private saveEvsesStatus (): void {
1674 this.saveConfiguration()
52952bf8
JB
1675 }
1676
66a7748d 1677 private saveConfiguration (): void {
9bf0ef23 1678 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1679 try {
d972af76 1680 if (!existsSync(dirname(this.configurationFile))) {
66a7748d 1681 mkdirSync(dirname(this.configurationFile), { recursive: true })
073bd098 1682 }
2466918c 1683 const configurationFromFile = this.getConfigurationFromFile()
66a7748d 1684 let configurationData: ChargingStationConfiguration =
2466918c 1685 configurationFromFile != null
40615072 1686 ? clone<ChargingStationConfiguration>(configurationFromFile)
66a7748d 1687 : {}
5199f9fd 1688 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
66a7748d 1689 configurationData.stationInfo = this.stationInfo
5ced7e80 1690 } else {
66a7748d 1691 delete configurationData.stationInfo
52952bf8 1692 }
5398cecf
JB
1693 if (
1694 this.stationInfo?.ocppPersistentConfiguration === true &&
755a76d5 1695 Array.isArray(this.ocppConfiguration?.configurationKey)
5398cecf 1696 ) {
5199f9fd 1697 configurationData.configurationKey = this.ocppConfiguration.configurationKey
5ced7e80 1698 } else {
66a7748d 1699 delete configurationData.configurationKey
52952bf8 1700 }
179ed367
JB
1701 configurationData = merge<ChargingStationConfiguration>(
1702 configurationData,
66a7748d
JB
1703 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1704 )
5ced7e80 1705 if (
66a7748d
JB
1706 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1707 this.getAutomaticTransactionGeneratorConfiguration() == null
5ced7e80 1708 ) {
66a7748d 1709 delete configurationData.automaticTransactionGenerator
5ced7e80 1710 }
b1bbdae5 1711 if (this.connectors.size > 0) {
66a7748d 1712 configurationData.connectorsStatus = buildConnectorsStatus(this)
5ced7e80 1713 } else {
66a7748d 1714 delete configurationData.connectorsStatus
52952bf8 1715 }
b1bbdae5 1716 if (this.evses.size > 0) {
66a7748d 1717 configurationData.evsesStatus = buildEvsesStatus(this)
5ced7e80 1718 } else {
66a7748d 1719 delete configurationData.evsesStatus
52952bf8 1720 }
66a7748d 1721 delete configurationData.configurationHash
d972af76 1722 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1723 .update(
1724 JSON.stringify({
1725 stationInfo: configurationData.stationInfo,
1726 configurationKey: configurationData.configurationKey,
1727 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
8ab96efb 1728 ...(this.connectors.size > 0 && {
66a7748d 1729 connectorsStatus: configurationData.connectorsStatus
8ab96efb 1730 }),
66a7748d
JB
1731 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1732 } satisfies ChargingStationConfiguration)
5ced7e80 1733 )
66a7748d 1734 .digest('hex')
7c72977b 1735 if (this.configurationFileHash !== configurationHash) {
0ebf7c2e 1736 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
66a7748d
JB
1737 configurationData.configurationHash = configurationHash
1738 const measureId = `${FileType.ChargingStationConfiguration} write`
1739 const beginId = PerformanceStatistics.beginMeasure(measureId)
0ebf7c2e
JB
1740 writeFileSync(
1741 this.configurationFile,
4ed03b6e 1742 JSON.stringify(configurationData, undefined, 2),
66a7748d
JB
1743 'utf8'
1744 )
1745 PerformanceStatistics.endMeasure(measureId, beginId)
1746 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1747 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1748 this.configurationFileHash = configurationHash
a974c8e4 1749 }).catch(error => {
0ebf7c2e
JB
1750 handleFileException(
1751 this.configurationFile,
1752 FileType.ChargingStationConfiguration,
1753 error as NodeJS.ErrnoException,
66a7748d
JB
1754 this.logPrefix()
1755 )
1756 })
7c72977b
JB
1757 } else {
1758 logger.debug(
1759 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1760 this.configurationFile
66a7748d
JB
1761 }`
1762 )
2484ac1e 1763 }
2484ac1e 1764 } catch (error) {
fa5995d6 1765 handleFileException(
2484ac1e 1766 this.configurationFile,
7164966d
JB
1767 FileType.ChargingStationConfiguration,
1768 error as NodeJS.ErrnoException,
66a7748d
JB
1769 this.logPrefix()
1770 )
073bd098 1771 }
2484ac1e
JB
1772 } else {
1773 logger.error(
66a7748d
JB
1774 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1775 )
073bd098
JB
1776 }
1777 }
1778
66a7748d
JB
1779 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1780 return this.getTemplateFromFile()?.Configuration
2484ac1e
JB
1781 }
1782
66a7748d
JB
1783 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1784 const configurationKey = this.getConfigurationFromFile()?.configurationKey
9fe79a13 1785 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
66a7748d 1786 return { configurationKey }
648512ce 1787 }
66a7748d 1788 return undefined
7dde0b73
JB
1789 }
1790
66a7748d 1791 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
551e477c 1792 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
66a7748d
JB
1793 this.getOcppConfigurationFromFile()
1794 if (ocppConfiguration == null) {
1795 ocppConfiguration = this.getOcppConfigurationFromTemplate()
2484ac1e 1796 }
66a7748d 1797 return ocppConfiguration
2484ac1e
JB
1798 }
1799
66a7748d
JB
1800 private async onOpen (): Promise<void> {
1801 if (this.isWebSocketConnectionOpened()) {
5144f4d1 1802 logger.info(
1d41bc6b 1803 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
66a7748d
JB
1804 )
1805 let registrationRetryCount = 0
1806 if (!this.isRegistered()) {
5144f4d1 1807 // Send BootNotification
5144f4d1 1808 do {
f7f98c68 1809 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
66a7748d
JB
1810 BootNotificationRequest,
1811 BootNotificationResponse
8bfbc743 1812 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
1813 skipBufferingOnError: true
1814 })
01d2a2c7
JB
1815 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1816 if (this.bootNotificationResponse?.currentTime != null) {
1817 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1818 this.bootNotificationResponse.currentTime = convertToDate(
1819 this.bootNotificationResponse.currentTime
1820 )!
1821 }
66a7748d
JB
1822 if (!this.isRegistered()) {
1823 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
9bf0ef23 1824 await sleep(
5199f9fd 1825 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
73b78a1f 1826 this.bootNotificationResponse?.interval != null
be4c6702 1827 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
66a7748d
JB
1828 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1829 )
5144f4d1
JB
1830 }
1831 } while (
66a7748d
JB
1832 !this.isRegistered() &&
1833 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 1834 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
9a77cc07 1835 this.stationInfo?.registrationMaxRetries === -1)
66a7748d 1836 )
5144f4d1 1837 }
66a7748d
JB
1838 if (this.isRegistered()) {
1839 this.emit(ChargingStationEvents.registered)
1840 if (this.inAcceptedState()) {
1841 this.emit(ChargingStationEvents.accepted)
c0560973 1842 }
5144f4d1 1843 } else {
e054fc1c
JB
1844 if (this.inRejectedState()) {
1845 this.emit(ChargingStationEvents.rejected)
1846 }
5144f4d1 1847 logger.error(
a223d9be
JB
1848 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1849 this.stationInfo?.registrationMaxRetries
1850 })`
66a7748d 1851 )
caad9d6b 1852 }
2960841f 1853 this.wsConnectionRetryCount = 0
66a7748d 1854 this.emit(ChargingStationEvents.updated)
2e6f5966 1855 } else {
5144f4d1 1856 logger.warn(
1d41bc6b 1857 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
66a7748d 1858 )
2e6f5966 1859 }
2e6f5966
JB
1860 }
1861
ba9a56a6 1862 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
e054fc1c 1863 this.emit(ChargingStationEvents.disconnected)
d09085e9 1864 switch (code) {
6c65a295
JB
1865 // Normal close
1866 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1867 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1868 logger.info(
9bf0ef23 1869 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1870 code
1871 )}' and reason '${reason.toString()}'`
1872 )
2960841f 1873 this.wsConnectionRetryCount = 0
66a7748d 1874 break
6c65a295
JB
1875 // Abnormal close
1876 default:
e7aeea18 1877 logger.error(
9bf0ef23 1878 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1879 code
1880 )}' and reason '${reason.toString()}'`
1881 )
7c974155
JB
1882 this.started &&
1883 this.reconnect().catch(error =>
1884 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1885 )
66a7748d 1886 break
c0560973 1887 }
66a7748d 1888 this.emit(ChargingStationEvents.updated)
2e6f5966
JB
1889 }
1890
c510c989
JB
1891 private getCachedRequest (
1892 messageType: MessageType | undefined,
1893 messageId: string
1894 ): CachedRequest | undefined {
66a7748d
JB
1895 const cachedRequest = this.requests.get(messageId)
1896 if (Array.isArray(cachedRequest)) {
1897 return cachedRequest
56d09fd7
JB
1898 }
1899 throw new OCPPError(
1900 ErrorType.PROTOCOL_ERROR,
041365be 1901 `Cached request for message id ${messageId} ${getMessageTypeString(
66a7748d 1902 messageType
56d09fd7
JB
1903 )} is not an array`,
1904 undefined,
66a7748d
JB
1905 cachedRequest
1906 )
56d09fd7
JB
1907 }
1908
66a7748d
JB
1909 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1910 const [messageType, messageId, commandName, commandPayload] = request
9a77cc07 1911 if (this.stationInfo?.enableStatistics === true) {
66a7748d 1912 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
56d09fd7
JB
1913 }
1914 logger.debug(
1915 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
66a7748d
JB
1916 request
1917 )}`
1918 )
56d09fd7
JB
1919 // Process the message
1920 await this.ocppIncomingRequestService.incomingRequestHandler(
1921 this,
1922 messageId,
1923 commandName,
66a7748d
JB
1924 commandPayload
1925 )
1926 this.emit(ChargingStationEvents.updated)
56d09fd7
JB
1927 }
1928
66a7748d
JB
1929 private handleResponseMessage (response: Response): void {
1930 const [messageType, messageId, commandPayload] = response
1931 if (!this.requests.has(messageId)) {
56d09fd7
JB
1932 // Error
1933 throw new OCPPError(
1934 ErrorType.INTERNAL_ERROR,
1935 `Response for unknown message id ${messageId}`,
1936 undefined,
66a7748d
JB
1937 commandPayload
1938 )
56d09fd7
JB
1939 }
1940 // Respond
66a7748d 1941 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56d09fd7
JB
1942 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1943 messageType,
66a7748d
JB
1944 messageId
1945 )!
56d09fd7 1946 logger.debug(
5199f9fd
JB
1947 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1948 response
1949 )}`
66a7748d
JB
1950 )
1951 responseCallback(commandPayload, requestPayload)
56d09fd7
JB
1952 }
1953
66a7748d
JB
1954 private handleErrorMessage (errorResponse: ErrorResponse): void {
1955 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1956 if (!this.requests.has(messageId)) {
56d09fd7
JB
1957 // Error
1958 throw new OCPPError(
1959 ErrorType.INTERNAL_ERROR,
1960 `Error response for unknown message id ${messageId}`,
1961 undefined,
66a7748d
JB
1962 { errorType, errorMessage, errorDetails }
1963 )
56d09fd7 1964 }
66a7748d
JB
1965 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1966 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
56d09fd7 1967 logger.debug(
5199f9fd
JB
1968 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1969 errorResponse
1970 )}`
66a7748d
JB
1971 )
1972 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
56d09fd7
JB
1973 }
1974
66a7748d
JB
1975 private async onMessage (data: RawData): Promise<void> {
1976 let request: IncomingRequest | Response | ErrorResponse | undefined
1977 let messageType: MessageType | undefined
1978 let errorMsg: string
c0560973 1979 try {
e1d9a0f4 1980 // eslint-disable-next-line @typescript-eslint/no-base-to-string
66a7748d
JB
1981 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1982 if (Array.isArray(request)) {
1983 [messageType] = request
b3ec7bc1
JB
1984 // Check the type of message
1985 switch (messageType) {
1986 // Incoming Message
1987 case MessageType.CALL_MESSAGE:
66a7748d
JB
1988 await this.handleIncomingMessage(request as IncomingRequest)
1989 break
56d09fd7 1990 // Response Message
b3ec7bc1 1991 case MessageType.CALL_RESULT_MESSAGE:
66a7748d
JB
1992 this.handleResponseMessage(request as Response)
1993 break
a2d1c0f1
JB
1994 // Error Message
1995 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
1996 this.handleErrorMessage(request as ErrorResponse)
1997 break
56d09fd7 1998 // Unknown Message
b3ec7bc1
JB
1999 default:
2000 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d
JB
2001 errorMsg = `Wrong message type ${messageType}`
2002 logger.error(`${this.logPrefix()} ${errorMsg}`)
2003 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
b3ec7bc1 2004 }
47e22477 2005 } else {
e1d9a0f4
JB
2006 throw new OCPPError(
2007 ErrorType.PROTOCOL_ERROR,
2008 'Incoming message is not an array',
2009 undefined,
2010 {
66a7748d
JB
2011 request
2012 }
2013 )
47e22477 2014 }
c0560973 2015 } catch (error) {
c3c8ae3f
JB
2016 if (!Array.isArray(request)) {
2017 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2018 return
2019 }
66a7748d
JB
2020 let commandName: IncomingRequestCommand | undefined
2021 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2022 let errorCallback: ErrorCallback
c3c8ae3f 2023 const [, messageId] = request
13701f69
JB
2024 switch (messageType) {
2025 case MessageType.CALL_MESSAGE:
66a7748d 2026 [, , commandName] = request as IncomingRequest
13701f69 2027 // Send error
66a7748d
JB
2028 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2029 break
13701f69
JB
2030 case MessageType.CALL_RESULT_MESSAGE:
2031 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
2032 if (this.requests.has(messageId)) {
2033 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2034 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
13701f69 2035 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
66a7748d 2036 errorCallback(error as OCPPError, false)
13701f69
JB
2037 } else {
2038 // Remove the request from the cache in case of error at response handling
66a7748d 2039 this.requests.delete(messageId)
13701f69 2040 }
66a7748d 2041 break
ba7965c4 2042 }
66a7748d 2043 if (!(error instanceof OCPPError)) {
56d09fd7
JB
2044 logger.warn(
2045 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2046 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2047 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2048 }' message '${data.toString()}' handling is not an OCPPError:`,
66a7748d
JB
2049 error
2050 )
56d09fd7
JB
2051 }
2052 logger.error(
2053 `${this.logPrefix()} Incoming OCPP command '${
2054 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2055 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2056 }' message '${data.toString()}'${
c510c989
JB
2057 this.requests.has(messageId)
2058 ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
56d09fd7
JB
2059 : ''
2060 } processing error:`,
66a7748d
JB
2061 error
2062 )
c0560973 2063 }
2328be1e
JB
2064 }
2065
66a7748d
JB
2066 private onPing (): void {
2067 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
c0560973
JB
2068 }
2069
66a7748d
JB
2070 private onPong (): void {
2071 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
c0560973
JB
2072 }
2073
66a7748d
JB
2074 private onError (error: WSError): void {
2075 this.closeWSConnection()
2076 logger.error(`${this.logPrefix()} WebSocket error:`, error)
c0560973
JB
2077 }
2078
f938317f
JB
2079 private getEnergyActiveImportRegister (
2080 connectorStatus: ConnectorStatus | undefined,
2081 rounded = false
2082 ): number {
5398cecf 2083 if (this.stationInfo?.meteringPerTransaction === true) {
07989fad 2084 return (
66a7748d 2085 (rounded
f938317f
JB
2086 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2087 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2088 : undefined
2089 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
66a7748d 2090 )
07989fad
JB
2091 }
2092 return (
66a7748d 2093 (rounded
f938317f
JB
2094 ? connectorStatus?.energyActiveImportRegisterValue != null
2095 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2096 : undefined
2097 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
66a7748d 2098 )
07989fad
JB
2099 }
2100
66a7748d
JB
2101 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2102 return stationTemplate?.useConnectorId0 ?? true
8bce55bf
JB
2103 }
2104
66a7748d 2105 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
28e78158 2106 if (this.hasEvses) {
3fa7f799
JB
2107 for (const [evseId, evseStatus] of this.evses) {
2108 if (evseId === 0) {
66a7748d 2109 continue
3fa7f799 2110 }
28e78158
JB
2111 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2112 if (connectorStatus.transactionStarted === true) {
66a7748d 2113 await this.stopTransactionOnConnector(connectorId, reason)
28e78158
JB
2114 }
2115 }
2116 }
2117 } else {
2118 for (const connectorId of this.connectors.keys()) {
2119 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
66a7748d 2120 await this.stopTransactionOnConnector(connectorId, reason)
28e78158 2121 }
60ddad53
JB
2122 }
2123 }
2124 }
2125
1f761b9a 2126 // 0 for disabling
66a7748d 2127 private getConnectionTimeout (): number {
a807045b 2128 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
4e3b1d6b 2129 return convertToInt(
5199f9fd 2130 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
66a7748d
JB
2131 Constants.DEFAULT_CONNECTION_TIMEOUT
2132 )
291cb255 2133 }
66a7748d 2134 return Constants.DEFAULT_CONNECTION_TIMEOUT
3574dfd3
JB
2135 }
2136
66a7748d
JB
2137 private getPowerDivider (): number {
2138 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
be1e907c 2139 if (this.stationInfo?.powerSharedByConnectors === true) {
66a7748d 2140 powerDivider = this.getNumberOfRunningTransactions()
6ecb15e4 2141 }
66a7748d 2142 return powerDivider
6ecb15e4
JB
2143 }
2144
66a7748d
JB
2145 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 2147 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
fa7bccf4 2148 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2149 case CurrentType.AC:
2150 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2151 this.getNumberOfPhases(stationInfo),
b1bbdae5 2152 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
66a7748d
JB
2153 this.getVoltageOut(stationInfo)
2154 )
cc6e8ab5 2155 case CurrentType.DC:
66a7748d 2156 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
cc6e8ab5
JB
2157 }
2158 }
2159
66a7748d 2160 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
5199f9fd
JB
2161 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2162 return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC
5398cecf
JB
2163 }
2164
66a7748d 2165 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
74ed61d9 2166 return (
5199f9fd
JB
2167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2168 (stationInfo ?? this.stationInfo!).voltageOut ??
74ed61d9 2169 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
66a7748d 2170 )
5398cecf
JB
2171 }
2172
66a7748d 2173 private getAmperageLimitation (): number | undefined {
cc6e8ab5 2174 if (
9bf0ef23 2175 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
5dc7c990 2176 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
cc6e8ab5
JB
2177 ) {
2178 return (
5dc7c990
JB
2179 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2180 getAmperageLimitationUnitDivider(this.stationInfo)
66a7748d 2181 )
cc6e8ab5
JB
2182 }
2183 }
2184
e054fc1c 2185 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
b7f9e41d 2186 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2187 await this.ocppRequestService.requestHandler<
66a7748d
JB
2188 BootNotificationRequest,
2189 BootNotificationResponse
8bfbc743 2190 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
2191 skipBufferingOnError: true
2192 })
6114e6f1 2193 }
136c90ba 2194 // Start WebSocket ping
66a7748d 2195 this.startWebSocketPing()
5ad8570f 2196 // Start heartbeat
66a7748d 2197 this.startHeartbeat()
0a60c33c 2198 // Initialize connectors status
c3b83130
JB
2199 if (this.hasEvses) {
2200 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2201 if (evseId > 0) {
2202 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
66a7748d
JB
2203 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2204 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
4334db72 2205 }
c3b83130 2206 }
4334db72
JB
2207 }
2208 } else {
2209 for (const connectorId of this.connectors.keys()) {
2210 if (connectorId > 0) {
fba11dc6 2211 const connectorBootStatus = getBootConnectorStatus(
c3b83130
JB
2212 this,
2213 connectorId,
66a7748d
JB
2214 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2215 this.getConnectorStatus(connectorId)!
2216 )
2217 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
c3b83130
JB
2218 }
2219 }
5ad8570f 2220 }
5199f9fd 2221 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
c9a4f9ea 2222 await this.ocppRequestService.requestHandler<
66a7748d
JB
2223 FirmwareStatusNotificationRequest,
2224 FirmwareStatusNotificationResponse
c9a4f9ea 2225 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
66a7748d
JB
2226 status: FirmwareStatus.Installed
2227 })
2228 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
c9a4f9ea 2229 }
3637ca2c 2230
0a60c33c 2231 // Start the ATG
5199f9fd 2232 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
e054fc1c 2233 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
fa7bccf4 2234 }
66a7748d 2235 this.flushMessageBuffer()
fa7bccf4
JB
2236 }
2237
e054fc1c 2238 private internalStopMessageSequence (): void {
136c90ba 2239 // Stop WebSocket ping
66a7748d 2240 this.stopWebSocketPing()
79411696 2241 // Stop heartbeat
66a7748d 2242 this.stopHeartbeat()
9ff486f4 2243 // Stop the ATG
b20eb107 2244 if (this.automaticTransactionGenerator?.started === true) {
66a7748d 2245 this.stopAutomaticTransactionGenerator()
79411696 2246 }
e054fc1c
JB
2247 }
2248
2249 private async stopMessageSequence (
2250 reason?: StopTransactionReason,
7e3bde4f 2251 stopTransactions?: boolean
e054fc1c
JB
2252 ): Promise<void> {
2253 this.internalStopMessageSequence()
3e888c65 2254 // Stop ongoing transactions
66a7748d 2255 stopTransactions === true && (await this.stopRunningTransactions(reason))
039211f9
JB
2256 if (this.hasEvses) {
2257 for (const [evseId, evseStatus] of this.evses) {
2258 if (evseId > 0) {
2259 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
e054fc1c 2260 await sendAndSetConnectorStatus(
039211f9 2261 this,
e054fc1c
JB
2262 connectorId,
2263 ConnectorStatusEnum.Unavailable,
2264 evseId
66a7748d 2265 )
5199f9fd 2266 delete connectorStatus.status
039211f9
JB
2267 }
2268 }
2269 }
2270 } else {
2271 for (const connectorId of this.connectors.keys()) {
2272 if (connectorId > 0) {
e054fc1c 2273 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
66a7748d 2274 delete this.getConnectorStatus(connectorId)?.status
039211f9 2275 }
45c0ae82
JB
2276 }
2277 }
79411696
JB
2278 }
2279
66a7748d 2280 private startWebSocketPing (): void {
97608fbd 2281 const webSocketPingInterval =
a807045b 2282 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
4e3b1d6b 2283 ? convertToInt(
66a7748d
JB
2284 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2285 )
2286 : 0
2960841f
JB
2287 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2288 this.wsPingSetInterval = setInterval(() => {
66a7748d
JB
2289 if (this.isWebSocketConnectionOpened()) {
2290 this.wsConnection?.ping()
136c90ba 2291 }
66a7748d 2292 }, secondsToMilliseconds(webSocketPingInterval))
e7aeea18 2293 logger.info(
9bf0ef23 2294 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
66a7748d
JB
2295 webSocketPingInterval
2296 )}`
2297 )
2960841f 2298 } else if (this.wsPingSetInterval != null) {
e7aeea18 2299 logger.info(
9bf0ef23 2300 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
66a7748d
JB
2301 webSocketPingInterval
2302 )}`
2303 )
136c90ba 2304 } else {
e7aeea18 2305 logger.error(
66a7748d
JB
2306 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2307 )
136c90ba
JB
2308 }
2309 }
2310
66a7748d 2311 private stopWebSocketPing (): void {
2960841f
JB
2312 if (this.wsPingSetInterval != null) {
2313 clearInterval(this.wsPingSetInterval)
2314 delete this.wsPingSetInterval
136c90ba
JB
2315 }
2316 }
2317
66a7748d
JB
2318 private getConfiguredSupervisionUrl (): URL {
2319 let configuredSupervisionUrl: string
2320 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
9bf0ef23 2321 if (isNotEmptyArray(supervisionUrls)) {
66a7748d 2322 let configuredSupervisionUrlIndex: number
2dcfe98e 2323 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2324 case SupervisionUrlDistribution.RANDOM:
5dc7c990 2325 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
66a7748d 2326 break
a52a6446 2327 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2328 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2329 default:
66a7748d
JB
2330 !Object.values(SupervisionUrlDistribution).includes(
2331 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2332 Configuration.getSupervisionUrlDistribution()!
2333 ) &&
a52a6446 2334 logger.error(
e1d9a0f4 2335 // eslint-disable-next-line @typescript-eslint/no-base-to-string
a52a6446
JB
2336 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2337 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
66a7748d
JB
2338 }`
2339 )
5dc7c990 2340 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
66a7748d 2341 break
c0560973 2342 }
5dc7c990 2343 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
d5c3df49 2344 } else {
5dc7c990
JB
2345 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2346 configuredSupervisionUrl = supervisionUrls!
d5c3df49 2347 }
9bf0ef23 2348 if (isNotEmptyString(configuredSupervisionUrl)) {
66a7748d 2349 return new URL(configuredSupervisionUrl)
c0560973 2350 }
66a7748d
JB
2351 const errorMsg = 'No supervision url(s) configured'
2352 logger.error(`${this.logPrefix()} ${errorMsg}`)
5199f9fd 2353 throw new BaseError(errorMsg)
136c90ba
JB
2354 }
2355
66a7748d 2356 private stopHeartbeat (): void {
a807045b 2357 if (this.heartbeatSetInterval != null) {
66a7748d
JB
2358 clearInterval(this.heartbeatSetInterval)
2359 delete this.heartbeatSetInterval
7dde0b73 2360 }
5ad8570f
JB
2361 }
2362
66a7748d
JB
2363 private terminateWSConnection (): void {
2364 if (this.isWebSocketConnectionOpened()) {
2365 this.wsConnection?.terminate()
2366 this.wsConnection = null
55516218
JB
2367 }
2368 }
2369
66a7748d 2370 private async reconnect (): Promise<void> {
e7aeea18 2371 if (
66a7748d 2372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2960841f 2373 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
5398cecf 2374 this.stationInfo?.autoReconnectMaxRetries === -1
e7aeea18 2375 ) {
2960841f
JB
2376 this.wsConnectionRetried = true
2377 ++this.wsConnectionRetryCount
5398cecf
JB
2378 const reconnectDelay =
2379 this.stationInfo?.reconnectExponentialDelay === true
2960841f 2380 ? exponentialDelay(this.wsConnectionRetryCount)
66a7748d
JB
2381 : secondsToMilliseconds(this.getConnectionTimeout())
2382 const reconnectDelayWithdraw = 1000
1e080116 2383 const reconnectTimeout =
5199f9fd 2384 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
e7aeea18 2385 logger.error(
9bf0ef23 2386 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2387 reconnectDelay,
66a7748d
JB
2388 2
2389 )}ms, timeout ${reconnectTimeout}ms`
2390 )
2391 await sleep(reconnectDelay)
e7aeea18 2392 logger.error(
2960841f 2393 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
66a7748d 2394 )
e7aeea18 2395 this.openWSConnection(
59b6ed8d 2396 {
66a7748d 2397 handshakeTimeout: reconnectTimeout
59b6ed8d 2398 },
66a7748d
JB
2399 { closeOpened: true }
2400 )
5398cecf 2401 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
e7aeea18 2402 logger.error(
d56ea27c 2403 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2960841f 2404 this.wsConnectionRetryCount
66a7748d
JB
2405 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2406 )
5ad8570f
JB
2407 }
2408 }
7dde0b73 2409}