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