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