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