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