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