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