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