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