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