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