docs: refine code comments
[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 {
ab29e682 1842 await this.ocppRequestService.requestHandler<
66a7748d
JB
1843 BootNotificationRequest,
1844 BootNotificationResponse
8bfbc743 1845 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
1846 skipBufferingOnError: true
1847 })
01d2a2c7
JB
1848 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1849 if (this.bootNotificationResponse?.currentTime != null) {
1850 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1851 this.bootNotificationResponse.currentTime = convertToDate(
1852 this.bootNotificationResponse.currentTime
1853 )!
1854 }
66a7748d
JB
1855 if (!this.isRegistered()) {
1856 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
9bf0ef23 1857 await sleep(
5199f9fd 1858 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
73b78a1f 1859 this.bootNotificationResponse?.interval != null
be4c6702 1860 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
66a7748d
JB
1861 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1862 )
5144f4d1
JB
1863 }
1864 } while (
66a7748d
JB
1865 !this.isRegistered() &&
1866 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 1867 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
9a77cc07 1868 this.stationInfo?.registrationMaxRetries === -1)
66a7748d 1869 )
5144f4d1 1870 }
50539084 1871 if (!this.isRegistered()) {
5144f4d1 1872 logger.error(
a223d9be
JB
1873 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1874 this.stationInfo?.registrationMaxRetries
1875 })`
66a7748d 1876 )
caad9d6b 1877 }
2960841f 1878 this.wsConnectionRetryCount = 0
66a7748d 1879 this.emit(ChargingStationEvents.updated)
2e6f5966 1880 } else {
5144f4d1 1881 logger.warn(
1d41bc6b 1882 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
66a7748d 1883 )
2e6f5966 1884 }
2e6f5966
JB
1885 }
1886
ba9a56a6 1887 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
e054fc1c 1888 this.emit(ChargingStationEvents.disconnected)
5c0e9352 1889 this.emit(ChargingStationEvents.updated)
d09085e9 1890 switch (code) {
6c65a295
JB
1891 // Normal close
1892 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1893 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1894 logger.info(
9bf0ef23 1895 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1896 code
1897 )}' and reason '${reason.toString()}'`
1898 )
2960841f 1899 this.wsConnectionRetryCount = 0
66a7748d 1900 break
6c65a295
JB
1901 // Abnormal close
1902 default:
e7aeea18 1903 logger.error(
9bf0ef23 1904 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1905 code
1906 )}' and reason '${reason.toString()}'`
1907 )
7c974155 1908 this.started &&
5c0e9352
JB
1909 this.reconnect()
1910 .then(() => {
1911 this.emit(ChargingStationEvents.updated)
1912 })
ea32ea05
JB
1913 .catch((error: unknown) =>
1914 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1915 )
66a7748d 1916 break
c0560973 1917 }
2e6f5966
JB
1918 }
1919
c510c989
JB
1920 private getCachedRequest (
1921 messageType: MessageType | undefined,
1922 messageId: string
1923 ): CachedRequest | undefined {
66a7748d
JB
1924 const cachedRequest = this.requests.get(messageId)
1925 if (Array.isArray(cachedRequest)) {
1926 return cachedRequest
56d09fd7
JB
1927 }
1928 throw new OCPPError(
1929 ErrorType.PROTOCOL_ERROR,
041365be 1930 `Cached request for message id ${messageId} ${getMessageTypeString(
66a7748d 1931 messageType
56d09fd7
JB
1932 )} is not an array`,
1933 undefined,
66a7748d
JB
1934 cachedRequest
1935 )
56d09fd7
JB
1936 }
1937
66a7748d
JB
1938 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1939 const [messageType, messageId, commandName, commandPayload] = request
9a77cc07 1940 if (this.stationInfo?.enableStatistics === true) {
66a7748d 1941 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
56d09fd7
JB
1942 }
1943 logger.debug(
1944 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
66a7748d
JB
1945 request
1946 )}`
1947 )
56d09fd7
JB
1948 // Process the message
1949 await this.ocppIncomingRequestService.incomingRequestHandler(
1950 this,
1951 messageId,
1952 commandName,
66a7748d
JB
1953 commandPayload
1954 )
1955 this.emit(ChargingStationEvents.updated)
56d09fd7
JB
1956 }
1957
66a7748d
JB
1958 private handleResponseMessage (response: Response): void {
1959 const [messageType, messageId, commandPayload] = response
1960 if (!this.requests.has(messageId)) {
56d09fd7
JB
1961 // Error
1962 throw new OCPPError(
1963 ErrorType.INTERNAL_ERROR,
1964 `Response for unknown message id ${messageId}`,
1965 undefined,
66a7748d
JB
1966 commandPayload
1967 )
56d09fd7
JB
1968 }
1969 // Respond
66a7748d 1970 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56d09fd7
JB
1971 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1972 messageType,
66a7748d
JB
1973 messageId
1974 )!
56d09fd7 1975 logger.debug(
5199f9fd
JB
1976 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1977 response
1978 )}`
66a7748d
JB
1979 )
1980 responseCallback(commandPayload, requestPayload)
56d09fd7
JB
1981 }
1982
66a7748d
JB
1983 private handleErrorMessage (errorResponse: ErrorResponse): void {
1984 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1985 if (!this.requests.has(messageId)) {
56d09fd7
JB
1986 // Error
1987 throw new OCPPError(
1988 ErrorType.INTERNAL_ERROR,
1989 `Error response for unknown message id ${messageId}`,
1990 undefined,
66a7748d
JB
1991 { errorType, errorMessage, errorDetails }
1992 )
56d09fd7 1993 }
66a7748d
JB
1994 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1995 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
56d09fd7 1996 logger.debug(
5199f9fd
JB
1997 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1998 errorResponse
1999 )}`
66a7748d
JB
2000 )
2001 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
56d09fd7
JB
2002 }
2003
66a7748d
JB
2004 private async onMessage (data: RawData): Promise<void> {
2005 let request: IncomingRequest | Response | ErrorResponse | undefined
2006 let messageType: MessageType | undefined
2007 let errorMsg: string
c0560973 2008 try {
e1d9a0f4 2009 // eslint-disable-next-line @typescript-eslint/no-base-to-string
66a7748d
JB
2010 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
2011 if (Array.isArray(request)) {
2012 [messageType] = request
b3ec7bc1
JB
2013 // Check the type of message
2014 switch (messageType) {
2015 // Incoming Message
2016 case MessageType.CALL_MESSAGE:
66a7748d
JB
2017 await this.handleIncomingMessage(request as IncomingRequest)
2018 break
56d09fd7 2019 // Response Message
b3ec7bc1 2020 case MessageType.CALL_RESULT_MESSAGE:
66a7748d
JB
2021 this.handleResponseMessage(request as Response)
2022 break
a2d1c0f1
JB
2023 // Error Message
2024 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
2025 this.handleErrorMessage(request as ErrorResponse)
2026 break
56d09fd7 2027 // Unknown Message
b3ec7bc1
JB
2028 default:
2029 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d
JB
2030 errorMsg = `Wrong message type ${messageType}`
2031 logger.error(`${this.logPrefix()} ${errorMsg}`)
2032 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
b3ec7bc1 2033 }
47e22477 2034 } else {
e1d9a0f4
JB
2035 throw new OCPPError(
2036 ErrorType.PROTOCOL_ERROR,
2037 'Incoming message is not an array',
2038 undefined,
2039 {
66a7748d
JB
2040 request
2041 }
2042 )
47e22477 2043 }
c0560973 2044 } catch (error) {
c3c8ae3f
JB
2045 if (!Array.isArray(request)) {
2046 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2047 return
2048 }
66a7748d
JB
2049 let commandName: IncomingRequestCommand | undefined
2050 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2051 let errorCallback: ErrorCallback
c3c8ae3f 2052 const [, messageId] = request
13701f69
JB
2053 switch (messageType) {
2054 case MessageType.CALL_MESSAGE:
66a7748d 2055 [, , commandName] = request as IncomingRequest
13701f69 2056 // Send error
66a7748d
JB
2057 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2058 break
13701f69
JB
2059 case MessageType.CALL_RESULT_MESSAGE:
2060 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
2061 if (this.requests.has(messageId)) {
2062 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2063 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
13701f69 2064 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
66a7748d 2065 errorCallback(error as OCPPError, false)
13701f69
JB
2066 } else {
2067 // Remove the request from the cache in case of error at response handling
66a7748d 2068 this.requests.delete(messageId)
13701f69 2069 }
66a7748d 2070 break
ba7965c4 2071 }
66a7748d 2072 if (!(error instanceof OCPPError)) {
56d09fd7
JB
2073 logger.warn(
2074 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1feac591 2075 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
e1d9a0f4 2076 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2077 }' message '${data.toString()}' handling is not an OCPPError:`,
66a7748d
JB
2078 error
2079 )
56d09fd7
JB
2080 }
2081 logger.error(
2082 `${this.logPrefix()} Incoming OCPP command '${
1feac591 2083 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
e1d9a0f4 2084 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2085 }' message '${data.toString()}'${
c510c989 2086 this.requests.has(messageId)
48847bc0
JB
2087 ? ` matching cached request '${JSON.stringify(
2088 this.getCachedRequest(messageType, messageId)
2089 )}'`
56d09fd7
JB
2090 : ''
2091 } processing error:`,
66a7748d
JB
2092 error
2093 )
c0560973 2094 }
2328be1e
JB
2095 }
2096
66a7748d
JB
2097 private onPing (): void {
2098 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
c0560973
JB
2099 }
2100
66a7748d
JB
2101 private onPong (): void {
2102 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
c0560973
JB
2103 }
2104
66a7748d
JB
2105 private onError (error: WSError): void {
2106 this.closeWSConnection()
2107 logger.error(`${this.logPrefix()} WebSocket error:`, error)
c0560973
JB
2108 }
2109
f938317f
JB
2110 private getEnergyActiveImportRegister (
2111 connectorStatus: ConnectorStatus | undefined,
2112 rounded = false
2113 ): number {
5398cecf 2114 if (this.stationInfo?.meteringPerTransaction === true) {
07989fad 2115 return (
66a7748d 2116 (rounded
f938317f
JB
2117 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2118 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2119 : undefined
2120 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
66a7748d 2121 )
07989fad
JB
2122 }
2123 return (
66a7748d 2124 (rounded
f938317f
JB
2125 ? connectorStatus?.energyActiveImportRegisterValue != null
2126 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2127 : undefined
2128 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
66a7748d 2129 )
07989fad
JB
2130 }
2131
66a7748d 2132 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
1feac591
JB
2133 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2134 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
8bce55bf
JB
2135 }
2136
66a7748d 2137 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
28e78158 2138 if (this.hasEvses) {
3fa7f799
JB
2139 for (const [evseId, evseStatus] of this.evses) {
2140 if (evseId === 0) {
66a7748d 2141 continue
3fa7f799 2142 }
28e78158
JB
2143 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2144 if (connectorStatus.transactionStarted === true) {
66a7748d 2145 await this.stopTransactionOnConnector(connectorId, reason)
28e78158
JB
2146 }
2147 }
2148 }
2149 } else {
2150 for (const connectorId of this.connectors.keys()) {
2151 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
66a7748d 2152 await this.stopTransactionOnConnector(connectorId, reason)
28e78158 2153 }
60ddad53
JB
2154 }
2155 }
2156 }
2157
1f761b9a 2158 // 0 for disabling
66a7748d 2159 private getConnectionTimeout (): number {
a807045b 2160 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
4e3b1d6b 2161 return convertToInt(
5199f9fd 2162 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
66a7748d
JB
2163 Constants.DEFAULT_CONNECTION_TIMEOUT
2164 )
291cb255 2165 }
66a7748d 2166 return Constants.DEFAULT_CONNECTION_TIMEOUT
3574dfd3
JB
2167 }
2168
66a7748d
JB
2169 private getPowerDivider (): number {
2170 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
be1e907c 2171 if (this.stationInfo?.powerSharedByConnectors === true) {
66a7748d 2172 powerDivider = this.getNumberOfRunningTransactions()
6ecb15e4 2173 }
66a7748d 2174 return powerDivider
6ecb15e4
JB
2175 }
2176
66a7748d
JB
2177 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2178 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 2179 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
fa7bccf4 2180 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2181 case CurrentType.AC:
2182 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2183 this.getNumberOfPhases(stationInfo),
b1bbdae5 2184 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
66a7748d
JB
2185 this.getVoltageOut(stationInfo)
2186 )
cc6e8ab5 2187 case CurrentType.DC:
66a7748d 2188 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
cc6e8ab5
JB
2189 }
2190 }
2191
66a7748d 2192 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
1feac591
JB
2193 return (
2194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2195 (stationInfo ?? this.stationInfo!).currentOutType ??
2196 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2197 Constants.DEFAULT_STATION_INFO.currentOutType!
2198 )
5398cecf
JB
2199 }
2200
66a7748d 2201 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
74ed61d9 2202 return (
5199f9fd
JB
2203 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2204 (stationInfo ?? this.stationInfo!).voltageOut ??
74ed61d9 2205 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
66a7748d 2206 )
5398cecf
JB
2207 }
2208
66a7748d 2209 private getAmperageLimitation (): number | undefined {
cc6e8ab5 2210 if (
9bf0ef23 2211 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
5dc7c990 2212 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
cc6e8ab5
JB
2213 ) {
2214 return (
5dc7c990
JB
2215 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2216 getAmperageLimitationUnitDivider(this.stationInfo)
66a7748d 2217 )
cc6e8ab5
JB
2218 }
2219 }
2220
e054fc1c 2221 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
b7f9e41d 2222 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2223 await this.ocppRequestService.requestHandler<
66a7748d
JB
2224 BootNotificationRequest,
2225 BootNotificationResponse
8bfbc743 2226 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
2227 skipBufferingOnError: true
2228 })
6114e6f1 2229 }
136c90ba 2230 // Start WebSocket ping
99100f9c
JB
2231 if (this.wsPingSetInterval == null) {
2232 this.startWebSocketPing()
2233 }
5ad8570f 2234 // Start heartbeat
d627f8ef
JB
2235 if (this.heartbeatSetInterval == null) {
2236 this.startHeartbeat()
d627f8ef 2237 }
0a60c33c 2238 // Initialize connectors status
c3b83130
JB
2239 if (this.hasEvses) {
2240 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2241 if (evseId > 0) {
2242 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
a9671b9e
JB
2243 await sendAndSetConnectorStatus(
2244 this,
2245 connectorId,
2246 getBootConnectorStatus(this, connectorId, connectorStatus),
2247 evseId
2248 )
4334db72 2249 }
c3b83130 2250 }
4334db72
JB
2251 }
2252 } else {
2253 for (const connectorId of this.connectors.keys()) {
2254 if (connectorId > 0) {
a9671b9e 2255 await sendAndSetConnectorStatus(
c3b83130
JB
2256 this,
2257 connectorId,
a9671b9e
JB
2258 getBootConnectorStatus(
2259 this,
2260 connectorId,
2261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2262 this.getConnectorStatus(connectorId)!
2263 )
66a7748d 2264 )
c3b83130
JB
2265 }
2266 }
5ad8570f 2267 }
5199f9fd 2268 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
c9a4f9ea 2269 await this.ocppRequestService.requestHandler<
66a7748d
JB
2270 FirmwareStatusNotificationRequest,
2271 FirmwareStatusNotificationResponse
c9a4f9ea 2272 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
66a7748d
JB
2273 status: FirmwareStatus.Installed
2274 })
2275 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
c9a4f9ea 2276 }
3637ca2c 2277
0a60c33c 2278 // Start the ATG
5199f9fd 2279 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
e054fc1c 2280 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
fa7bccf4 2281 }
66a7748d 2282 this.flushMessageBuffer()
fa7bccf4
JB
2283 }
2284
e054fc1c 2285 private internalStopMessageSequence (): void {
136c90ba 2286 // Stop WebSocket ping
66a7748d 2287 this.stopWebSocketPing()
79411696 2288 // Stop heartbeat
66a7748d 2289 this.stopHeartbeat()
9ff486f4 2290 // Stop the ATG
b20eb107 2291 if (this.automaticTransactionGenerator?.started === true) {
66a7748d 2292 this.stopAutomaticTransactionGenerator()
79411696 2293 }
e054fc1c
JB
2294 }
2295
2296 private async stopMessageSequence (
2297 reason?: StopTransactionReason,
7e3bde4f 2298 stopTransactions?: boolean
e054fc1c
JB
2299 ): Promise<void> {
2300 this.internalStopMessageSequence()
3e888c65 2301 // Stop ongoing transactions
66a7748d 2302 stopTransactions === true && (await this.stopRunningTransactions(reason))
039211f9
JB
2303 if (this.hasEvses) {
2304 for (const [evseId, evseStatus] of this.evses) {
2305 if (evseId > 0) {
2306 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
e054fc1c 2307 await sendAndSetConnectorStatus(
039211f9 2308 this,
e054fc1c
JB
2309 connectorId,
2310 ConnectorStatusEnum.Unavailable,
2311 evseId
66a7748d 2312 )
5199f9fd 2313 delete connectorStatus.status
039211f9
JB
2314 }
2315 }
2316 }
2317 } else {
2318 for (const connectorId of this.connectors.keys()) {
2319 if (connectorId > 0) {
e054fc1c 2320 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
66a7748d 2321 delete this.getConnectorStatus(connectorId)?.status
039211f9 2322 }
45c0ae82
JB
2323 }
2324 }
79411696
JB
2325 }
2326
99100f9c
JB
2327 private getWebSocketPingInterval (): number {
2328 return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2329 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2330 : 0
2331 }
2332
66a7748d 2333 private startWebSocketPing (): void {
99100f9c 2334 const webSocketPingInterval = this.getWebSocketPingInterval()
2960841f
JB
2335 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2336 this.wsPingSetInterval = setInterval(() => {
66a7748d
JB
2337 if (this.isWebSocketConnectionOpened()) {
2338 this.wsConnection?.ping()
136c90ba 2339 }
66a7748d 2340 }, secondsToMilliseconds(webSocketPingInterval))
e7aeea18 2341 logger.info(
9bf0ef23 2342 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
66a7748d
JB
2343 webSocketPingInterval
2344 )}`
2345 )
2960841f 2346 } else if (this.wsPingSetInterval != null) {
e7aeea18 2347 logger.info(
9bf0ef23 2348 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
66a7748d
JB
2349 webSocketPingInterval
2350 )}`
2351 )
136c90ba 2352 } else {
e7aeea18 2353 logger.error(
66a7748d
JB
2354 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2355 )
136c90ba
JB
2356 }
2357 }
2358
66a7748d 2359 private stopWebSocketPing (): void {
2960841f
JB
2360 if (this.wsPingSetInterval != null) {
2361 clearInterval(this.wsPingSetInterval)
2362 delete this.wsPingSetInterval
136c90ba
JB
2363 }
2364 }
2365
66a7748d
JB
2366 private getConfiguredSupervisionUrl (): URL {
2367 let configuredSupervisionUrl: string
2368 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
9bf0ef23 2369 if (isNotEmptyArray(supervisionUrls)) {
66a7748d 2370 let configuredSupervisionUrlIndex: number
2dcfe98e 2371 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2372 case SupervisionUrlDistribution.RANDOM:
5dc7c990 2373 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
66a7748d 2374 break
a52a6446 2375 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2376 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2377 default:
66a7748d
JB
2378 !Object.values(SupervisionUrlDistribution).includes(
2379 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2380 Configuration.getSupervisionUrlDistribution()!
2381 ) &&
ae61fa2f 2382 logger.warn(
e1d9a0f4 2383 // eslint-disable-next-line @typescript-eslint/no-base-to-string
81c74884 2384 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
a52a6446 2385 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
81c74884 2386 }'`
66a7748d 2387 )
5dc7c990 2388 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
66a7748d 2389 break
c0560973 2390 }
5dc7c990 2391 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
d5c3df49 2392 } else {
5dc7c990
JB
2393 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2394 configuredSupervisionUrl = supervisionUrls!
d5c3df49 2395 }
9bf0ef23 2396 if (isNotEmptyString(configuredSupervisionUrl)) {
66a7748d 2397 return new URL(configuredSupervisionUrl)
c0560973 2398 }
66a7748d
JB
2399 const errorMsg = 'No supervision url(s) configured'
2400 logger.error(`${this.logPrefix()} ${errorMsg}`)
5199f9fd 2401 throw new BaseError(errorMsg)
136c90ba
JB
2402 }
2403
66a7748d 2404 private stopHeartbeat (): void {
a807045b 2405 if (this.heartbeatSetInterval != null) {
66a7748d
JB
2406 clearInterval(this.heartbeatSetInterval)
2407 delete this.heartbeatSetInterval
7dde0b73 2408 }
5ad8570f
JB
2409 }
2410
66a7748d
JB
2411 private terminateWSConnection (): void {
2412 if (this.isWebSocketConnectionOpened()) {
2413 this.wsConnection?.terminate()
2414 this.wsConnection = null
55516218
JB
2415 }
2416 }
2417
66a7748d 2418 private async reconnect (): Promise<void> {
e7aeea18 2419 if (
66a7748d 2420 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2960841f 2421 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
5398cecf 2422 this.stationInfo?.autoReconnectMaxRetries === -1
e7aeea18 2423 ) {
2960841f
JB
2424 this.wsConnectionRetried = true
2425 ++this.wsConnectionRetryCount
5398cecf
JB
2426 const reconnectDelay =
2427 this.stationInfo?.reconnectExponentialDelay === true
2960841f 2428 ? exponentialDelay(this.wsConnectionRetryCount)
66a7748d
JB
2429 : secondsToMilliseconds(this.getConnectionTimeout())
2430 const reconnectDelayWithdraw = 1000
1e080116 2431 const reconnectTimeout =
5199f9fd 2432 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
e7aeea18 2433 logger.error(
9bf0ef23 2434 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2435 reconnectDelay,
66a7748d
JB
2436 2
2437 )}ms, timeout ${reconnectTimeout}ms`
2438 )
2439 await sleep(reconnectDelay)
e7aeea18 2440 logger.error(
2960841f 2441 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
66a7748d 2442 )
e7aeea18 2443 this.openWSConnection(
59b6ed8d 2444 {
66a7748d 2445 handshakeTimeout: reconnectTimeout
59b6ed8d 2446 },
66a7748d
JB
2447 { closeOpened: true }
2448 )
5398cecf 2449 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
e7aeea18 2450 logger.error(
4c6f3565 2451 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
66a7748d 2452 )
5ad8570f
JB
2453 }
2454 }
7dde0b73 2455}