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