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