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