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