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