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