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