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