build(deps-dev): apply updates
[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
JB
1039 );
1040 // eslint-disable-next-line @typescript-eslint/no-misused-promises
178956d8 1041 this.reservationExpirationSetInterval = setInterval(async (): Promise<void> => {
3fa7f799 1042 const now = new Date();
66dd3447 1043 if (this.hasEvses) {
3fa7f799
JB
1044 for (const evseStatus of this.evses.values()) {
1045 for (const connectorStatus of evseStatus.connectors.values()) {
e1d9a0f4 1046 if (connectorStatus.reservation!.expiryDate < now) {
ec9f36cc 1047 await this.removeReservation(
e1d9a0f4 1048 connectorStatus.reservation!,
5edd8ba0 1049 ReservationTerminationReason.EXPIRED,
ec9f36cc 1050 );
66dd3447
JB
1051 }
1052 }
1053 }
1054 } else {
3fa7f799 1055 for (const connectorStatus of this.connectors.values()) {
e1d9a0f4 1056 if (connectorStatus.reservation!.expiryDate < now) {
ec9f36cc 1057 await this.removeReservation(
e1d9a0f4 1058 connectorStatus.reservation!,
5edd8ba0 1059 ReservationTerminationReason.EXPIRED,
ec9f36cc 1060 );
d193a949
JB
1061 }
1062 }
24578c31 1063 }
d193a949
JB
1064 }, interval);
1065 }
1066
1067 public restartReservationExpiryDateSetInterval(): void {
178956d8
JB
1068 this.stopReservationExpirationSetInterval();
1069 this.startReservationExpirationSetInterval();
d193a949
JB
1070 }
1071
1072 public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
3fa7f799 1073 return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag;
d193a949
JB
1074 }
1075
1076 public isConnectorReservable(
1077 reservationId: number,
66dd3447 1078 idTag?: string,
5edd8ba0 1079 connectorId?: number,
d193a949 1080 ): boolean {
ea5d5eef 1081 const [alreadyExists] = this.doesReservationExists({ id: reservationId });
d193a949
JB
1082 if (alreadyExists) {
1083 return alreadyExists;
24578c31 1084 }
9bf0ef23 1085 const userReservedAlready = isUndefined(
e1d9a0f4 1086 this.getReservationBy(ReservationFilterKey.ID_TAG, idTag!),
66dd3447
JB
1087 )
1088 ? false
1089 : true;
e1d9a0f4 1090 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
d193a949
JB
1091 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1092 return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
1093 }
1094
1095 private getNumberOfReservableConnectors(): number {
1096 let reservableConnectors = 0;
66dd3447 1097 if (this.hasEvses) {
3fa7f799 1098 for (const evseStatus of this.evses.values()) {
fba11dc6 1099 reservableConnectors += countReservableConnectors(evseStatus.connectors);
66dd3447
JB
1100 }
1101 } else {
fba11dc6 1102 reservableConnectors = countReservableConnectors(this.connectors);
66dd3447
JB
1103 }
1104 return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
1105 }
1106
d193a949 1107 private getNumberOfReservationsOnConnectorZero(): number {
66dd3447 1108 let numberOfReservations = 0;
3fa7f799
JB
1109 if (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) {
1110 ++numberOfReservations;
66dd3447
JB
1111 } else if (this.connectors.get(0)?.reservation) {
1112 ++numberOfReservations;
1113 }
1114 return numberOfReservations;
24578c31
JB
1115 }
1116
f90c1757 1117 private flushMessageBuffer(): void {
8e242273 1118 if (this.messageBuffer.size > 0) {
7d3b0f64 1119 for (const message of this.messageBuffer.values()) {
e1d9a0f4
JB
1120 let beginId: string | undefined;
1121 let commandName: RequestCommand | undefined;
8ca6874c 1122 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
1123 const isRequest = messageType === MessageType.CALL_MESSAGE;
1124 if (isRequest) {
1125 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1126 beginId = PerformanceStatistics.beginMeasure(commandName);
1127 }
72092cfc 1128 this.wsConnection?.send(message);
e1d9a0f4 1129 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!);
8ca6874c
JB
1130 logger.debug(
1131 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0
JB
1132 messageType,
1133 )} payload sent: ${message}`,
8ca6874c 1134 );
8e242273 1135 this.messageBuffer.delete(message);
7d3b0f64 1136 }
77f00f84
JB
1137 }
1138 }
1139
1f5df42a
JB
1140 private getSupervisionUrlOcppConfiguration(): boolean {
1141 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
1142 }
1143
178956d8
JB
1144 private stopReservationExpirationSetInterval(): void {
1145 if (this.reservationExpirationSetInterval) {
1146 clearInterval(this.reservationExpirationSetInterval);
d193a949 1147 }
24578c31
JB
1148 }
1149
e8e865ea 1150 private getSupervisionUrlOcppKey(): string {
6dad8e21 1151 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
1152 }
1153
72092cfc 1154 private getTemplateFromFile(): ChargingStationTemplate | undefined {
e1d9a0f4 1155 let template: ChargingStationTemplate | undefined;
5ad8570f 1156 try {
cda5d0fb
JB
1157 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1158 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
7c72977b
JB
1159 } else {
1160 const measureId = `${FileType.ChargingStationTemplate} read`;
1161 const beginId = PerformanceStatistics.beginMeasure(measureId);
d972af76 1162 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
7c72977b 1163 PerformanceStatistics.endMeasure(measureId, beginId);
d972af76 1164 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
7c72977b
JB
1165 .update(JSON.stringify(template))
1166 .digest('hex');
57adbebc 1167 this.sharedLRUCache.setChargingStationTemplate(template);
cda5d0fb 1168 this.templateFileHash = template.templateHash;
7c72977b 1169 }
5ad8570f 1170 } catch (error) {
fa5995d6 1171 handleFileException(
2484ac1e 1172 this.templateFile,
7164966d
JB
1173 FileType.ChargingStationTemplate,
1174 error as NodeJS.ErrnoException,
5edd8ba0 1175 this.logPrefix(),
e7aeea18 1176 );
5ad8570f 1177 }
2484ac1e
JB
1178 return template;
1179 }
1180
7a3a2ebb 1181 private getStationInfoFromTemplate(): ChargingStationInfo {
e1d9a0f4 1182 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
fba11dc6
JB
1183 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
1184 warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1185 if (stationTemplate?.Connectors) {
fba11dc6 1186 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1187 }
fba11dc6
JB
1188 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
1189 stationInfo.hashId = getHashId(this.index, stationTemplate);
1190 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
72092cfc 1191 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fba11dc6 1192 createSerialNumber(stationTemplate, stationInfo);
9bf0ef23 1193 if (isNotEmptyArray(stationTemplate?.power)) {
551e477c 1194 stationTemplate.power = stationTemplate.power as number[];
9bf0ef23 1195 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
cc6e8ab5 1196 stationInfo.maximumPower =
72092cfc 1197 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1198 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1199 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 1200 } else {
551e477c 1201 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 1202 stationInfo.maximumPower =
72092cfc 1203 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1204 ? stationTemplate.power * 1000
1205 : stationTemplate.power;
1206 }
3637ca2c 1207 stationInfo.firmwareVersionPattern =
72092cfc 1208 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 1209 if (
9bf0ef23 1210 isNotEmptyString(stationInfo.firmwareVersion) &&
e1d9a0f4 1211 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) === false
3637ca2c
JB
1212 ) {
1213 logger.warn(
1214 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1215 this.templateFile
5edd8ba0 1216 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
3637ca2c
JB
1217 );
1218 }
598c886d 1219 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1220 {
598c886d
JB
1221 versionUpgrade: {
1222 step: 1,
1223 },
15748260
JB
1224 reset: true,
1225 },
5edd8ba0 1226 stationTemplate?.firmwareUpgrade ?? {},
15748260 1227 );
9bf0ef23 1228 stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
e1d9a0f4 1229 ? stationTemplate.resetTime! * 1000
e7aeea18 1230 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
fa7bccf4 1231 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
9ac86a7e 1232 return stationInfo;
5ad8570f
JB
1233 }
1234
551e477c
JB
1235 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1236 let stationInfo: ChargingStationInfo | undefined;
f832e5df
JB
1237 if (this.getStationInfoPersistentConfiguration()) {
1238 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1239 if (stationInfo) {
1240 delete stationInfo?.infoHash;
1241 }
1242 }
f765beaa 1243 return stationInfo;
2484ac1e
JB
1244 }
1245
1246 private getStationInfo(): ChargingStationInfo {
1247 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1248 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1249 // Priority:
1250 // 1. charging station info from template
1251 // 2. charging station info from configuration file
f765beaa 1252 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
e1d9a0f4 1253 return stationInfoFromFile!;
f765beaa 1254 }
fec4d204 1255 stationInfoFromFile &&
fba11dc6 1256 propagateSerialNumber(
e1d9a0f4 1257 this.getTemplateFromFile()!,
fec4d204 1258 stationInfoFromFile,
5edd8ba0 1259 stationInfoFromTemplate,
fec4d204 1260 );
01efc60a 1261 return stationInfoFromTemplate;
2484ac1e
JB
1262 }
1263
1264 private saveStationInfo(): void {
ccb1d6e9 1265 if (this.getStationInfoPersistentConfiguration()) {
b1bbdae5 1266 this.saveConfiguration();
ccb1d6e9 1267 }
2484ac1e
JB
1268 }
1269
e8e865ea 1270 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
1271 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1272 }
1273
1274 private getStationInfoPersistentConfiguration(): boolean {
1275 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
1276 }
1277
5ced7e80
JB
1278 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1279 return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
1280 }
1281
c0560973 1282 private handleUnsupportedVersion(version: OCPPVersion) {
66dd3447
JB
1283 const errorMsg = `Unsupported protocol version '${version}' configured
1284 in template file ${this.templateFile}`;
ded57f02
JB
1285 logger.error(`${this.logPrefix()} ${errorMsg}`);
1286 throw new BaseError(errorMsg);
c0560973
JB
1287 }
1288
2484ac1e 1289 private initialize(): void {
e1d9a0f4 1290 const stationTemplate = this.getTemplateFromFile()!;
fba11dc6 1291 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
d972af76
JB
1292 this.configurationFile = join(
1293 dirname(this.templateFile.replace('station-templates', 'configurations')),
5edd8ba0 1294 `${getHashId(this.index, stationTemplate)}.json`,
0642c3d2 1295 );
a4f7c75f 1296 const chargingStationConfiguration = this.getConfigurationFromFile();
a4f7c75f 1297 if (
ba01a213 1298 chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
e1d9a0f4 1299 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
a4f7c75f
JB
1300 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1301 ) {
1302 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1303 } else {
1304 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1305 }
b44b779a 1306 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1307 if (
1308 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1309 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1310 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1311 ) {
d812bdcb 1312 const patternGroup: number | undefined =
15748260 1313 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1314 this.stationInfo.firmwareVersion?.split('.').length;
e1d9a0f4
JB
1315 const match = this.stationInfo
1316 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1317 .slice(1, patternGroup! + 1);
3637ca2c 1318 const patchLevelIndex = match.length - 1;
5d280aae 1319 match[patchLevelIndex] = (
9bf0ef23 1320 convertToInt(match[patchLevelIndex]) +
e1d9a0f4 1321 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
5d280aae 1322 ).toString();
72092cfc 1323 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1324 }
6bccfcbc 1325 this.saveStationInfo();
6bccfcbc
JB
1326 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1327 if (this.getEnableStatistics() === true) {
1328 this.performanceStatistics = PerformanceStatistics.getInstance(
1329 this.stationInfo.hashId,
e1d9a0f4 1330 this.stationInfo.chargingStationId!,
5edd8ba0 1331 this.configuredSupervisionUrl,
6bccfcbc
JB
1332 );
1333 }
fba11dc6 1334 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
692f2f64
JB
1335 this.powerDivider = this.getPowerDivider();
1336 // OCPP configuration
1337 this.ocppConfiguration = this.getOcppConfiguration();
1338 this.initializeOcppConfiguration();
1339 this.initializeOcppServices();
1340 if (this.stationInfo?.autoRegister === true) {
1341 this.bootNotificationResponse = {
1342 currentTime: new Date(),
1343 interval: this.getHeartbeatInterval() / 1000,
1344 status: RegistrationStatusEnumType.ACCEPTED,
1345 };
1346 }
147d0e0f
JB
1347 }
1348
feff11ec
JB
1349 private initializeOcppServices(): void {
1350 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1351 switch (ocppVersion) {
1352 case OCPPVersion.VERSION_16:
1353 this.ocppIncomingRequestService =
1354 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1355 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
5edd8ba0 1356 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
feff11ec
JB
1357 );
1358 break;
1359 case OCPPVersion.VERSION_20:
1360 case OCPPVersion.VERSION_201:
1361 this.ocppIncomingRequestService =
1362 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1363 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
5edd8ba0 1364 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
feff11ec
JB
1365 );
1366 break;
1367 default:
1368 this.handleUnsupportedVersion(ocppVersion);
1369 break;
1370 }
1371 }
1372
2484ac1e 1373 private initializeOcppConfiguration(): void {
17ac262c
JB
1374 if (
1375 !ChargingStationConfigurationUtils.getConfigurationKey(
1376 this,
5edd8ba0 1377 StandardParametersKey.HeartbeatInterval,
17ac262c
JB
1378 )
1379 ) {
1380 ChargingStationConfigurationUtils.addConfigurationKey(
1381 this,
1382 StandardParametersKey.HeartbeatInterval,
5edd8ba0 1383 '0',
17ac262c 1384 );
f0f65a62 1385 }
17ac262c
JB
1386 if (
1387 !ChargingStationConfigurationUtils.getConfigurationKey(
1388 this,
5edd8ba0 1389 StandardParametersKey.HeartBeatInterval,
17ac262c
JB
1390 )
1391 ) {
1392 ChargingStationConfigurationUtils.addConfigurationKey(
1393 this,
1394 StandardParametersKey.HeartBeatInterval,
1395 '0',
5edd8ba0 1396 { visible: false },
17ac262c 1397 );
f0f65a62 1398 }
e7aeea18
JB
1399 if (
1400 this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1401 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1402 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1403 ) {
17ac262c
JB
1404 ChargingStationConfigurationUtils.addConfigurationKey(
1405 this,
a59737e3 1406 this.getSupervisionUrlOcppKey(),
fa7bccf4 1407 this.configuredSupervisionUrl.href,
5edd8ba0 1408 { reboot: true },
e7aeea18 1409 );
e6895390
JB
1410 } else if (
1411 !this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1412 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1413 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1414 ) {
17ac262c
JB
1415 ChargingStationConfigurationUtils.deleteConfigurationKey(
1416 this,
1417 this.getSupervisionUrlOcppKey(),
5edd8ba0 1418 { save: false },
17ac262c 1419 );
12fc74d6 1420 }
cc6e8ab5 1421 if (
9bf0ef23 1422 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1423 !ChargingStationConfigurationUtils.getConfigurationKey(
1424 this,
e1d9a0f4 1425 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 1426 )
cc6e8ab5 1427 ) {
17ac262c
JB
1428 ChargingStationConfigurationUtils.addConfigurationKey(
1429 this,
e1d9a0f4 1430 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 1431 (
e1d9a0f4 1432 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
5edd8ba0 1433 ).toString(),
cc6e8ab5
JB
1434 );
1435 }
17ac262c
JB
1436 if (
1437 !ChargingStationConfigurationUtils.getConfigurationKey(
1438 this,
5edd8ba0 1439 StandardParametersKey.SupportedFeatureProfiles,
17ac262c
JB
1440 )
1441 ) {
1442 ChargingStationConfigurationUtils.addConfigurationKey(
1443 this,
e7aeea18 1444 StandardParametersKey.SupportedFeatureProfiles,
5edd8ba0 1445 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
e7aeea18
JB
1446 );
1447 }
17ac262c
JB
1448 ChargingStationConfigurationUtils.addConfigurationKey(
1449 this,
e7aeea18
JB
1450 StandardParametersKey.NumberOfConnectors,
1451 this.getNumberOfConnectors().toString(),
a95873d8 1452 { readonly: true },
5edd8ba0 1453 { overwrite: true },
e7aeea18 1454 );
17ac262c
JB
1455 if (
1456 !ChargingStationConfigurationUtils.getConfigurationKey(
1457 this,
5edd8ba0 1458 StandardParametersKey.MeterValuesSampledData,
17ac262c
JB
1459 )
1460 ) {
1461 ChargingStationConfigurationUtils.addConfigurationKey(
1462 this,
e7aeea18 1463 StandardParametersKey.MeterValuesSampledData,
5edd8ba0 1464 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
e7aeea18 1465 );
7abfea5f 1466 }
17ac262c
JB
1467 if (
1468 !ChargingStationConfigurationUtils.getConfigurationKey(
1469 this,
5edd8ba0 1470 StandardParametersKey.ConnectorPhaseRotation,
17ac262c
JB
1471 )
1472 ) {
dd08d43d 1473 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1474 if (this.hasEvses) {
1475 for (const evseStatus of this.evses.values()) {
1476 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1477 connectorsPhaseRotation.push(
e1d9a0f4 1478 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1479 );
28e78158
JB
1480 }
1481 }
1482 } else {
1483 for (const connectorId of this.connectors.keys()) {
dd08d43d 1484 connectorsPhaseRotation.push(
e1d9a0f4 1485 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1486 );
7e1dc878
JB
1487 }
1488 }
17ac262c
JB
1489 ChargingStationConfigurationUtils.addConfigurationKey(
1490 this,
e7aeea18 1491 StandardParametersKey.ConnectorPhaseRotation,
5edd8ba0 1492 connectorsPhaseRotation.toString(),
e7aeea18 1493 );
7e1dc878 1494 }
e7aeea18 1495 if (
17ac262c
JB
1496 !ChargingStationConfigurationUtils.getConfigurationKey(
1497 this,
5edd8ba0 1498 StandardParametersKey.AuthorizeRemoteTxRequests,
e7aeea18
JB
1499 )
1500 ) {
17ac262c
JB
1501 ChargingStationConfigurationUtils.addConfigurationKey(
1502 this,
1503 StandardParametersKey.AuthorizeRemoteTxRequests,
5edd8ba0 1504 'true',
17ac262c 1505 );
36f6a92e 1506 }
17ac262c
JB
1507 if (
1508 !ChargingStationConfigurationUtils.getConfigurationKey(
1509 this,
5edd8ba0 1510 StandardParametersKey.LocalAuthListEnabled,
17ac262c
JB
1511 ) &&
1512 ChargingStationConfigurationUtils.getConfigurationKey(
1513 this,
5edd8ba0 1514 StandardParametersKey.SupportedFeatureProfiles,
72092cfc 1515 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1516 ) {
1517 ChargingStationConfigurationUtils.addConfigurationKey(
1518 this,
1519 StandardParametersKey.LocalAuthListEnabled,
5edd8ba0 1520 'false',
17ac262c
JB
1521 );
1522 }
1523 if (
1524 !ChargingStationConfigurationUtils.getConfigurationKey(
1525 this,
5edd8ba0 1526 StandardParametersKey.ConnectionTimeOut,
17ac262c
JB
1527 )
1528 ) {
1529 ChargingStationConfigurationUtils.addConfigurationKey(
1530 this,
e7aeea18 1531 StandardParametersKey.ConnectionTimeOut,
5edd8ba0 1532 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
e7aeea18 1533 );
8bce55bf 1534 }
2484ac1e 1535 this.saveOcppConfiguration();
073bd098
JB
1536 }
1537
a4f7c75f
JB
1538 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1539 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
8df5ae48 1540 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
9bf0ef23 1541 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
8df5ae48 1542 }
a4f7c75f
JB
1543 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1544 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
9bf0ef23 1545 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
a4f7c75f
JB
1546 delete evseStatus.connectorsStatus;
1547 this.evses.set(evseId, {
8df5ae48 1548 ...(evseStatus as EvseStatus),
a4f7c75f 1549 connectors: new Map<number, ConnectorStatus>(
e1d9a0f4 1550 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f
JB
1551 connectorId,
1552 connectorStatus,
5edd8ba0 1553 ]),
a4f7c75f
JB
1554 ),
1555 });
1556 }
1557 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1558 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1559 logger.error(`${this.logPrefix()} ${errorMsg}`);
1560 throw new BaseError(errorMsg);
1561 } else {
1562 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1563 logger.error(`${this.logPrefix()} ${errorMsg}`);
1564 throw new BaseError(errorMsg);
1565 }
1566 }
1567
34eeb1fb 1568 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
cda5d0fb 1569 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
34eeb1fb 1570 this.initializeConnectorsFromTemplate(stationTemplate);
cda5d0fb 1571 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
34eeb1fb 1572 this.initializeEvsesFromTemplate(stationTemplate);
cda5d0fb 1573 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
ae25f265
JB
1574 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1575 logger.error(`${this.logPrefix()} ${errorMsg}`);
1576 throw new BaseError(errorMsg);
1577 } else {
1578 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1579 logger.error(`${this.logPrefix()} ${errorMsg}`);
1580 throw new BaseError(errorMsg);
1581 }
1582 }
1583
34eeb1fb 1584 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1585 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1586 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1587 logger.error(`${this.logPrefix()} ${errorMsg}`);
1588 throw new BaseError(errorMsg);
3d25cc86 1589 }
e1d9a0f4 1590 if (!stationTemplate?.Connectors?.[0]) {
3d25cc86
JB
1591 logger.warn(
1592 `${this.logPrefix()} Charging station information from template ${
1593 this.templateFile
5edd8ba0 1594 } with no connector id 0 configuration`,
3d25cc86
JB
1595 );
1596 }
cda5d0fb
JB
1597 if (stationTemplate?.Connectors) {
1598 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
fba11dc6 1599 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
d972af76 1600 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1601 .update(
5edd8ba0 1602 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
cda5d0fb 1603 )
3d25cc86
JB
1604 .digest('hex');
1605 const connectorsConfigChanged =
1606 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1607 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1608 connectorsConfigChanged && this.connectors.clear();
1609 this.connectorsConfigurationHash = connectorsConfigHash;
269196a8
JB
1610 if (templateMaxConnectors > 0) {
1611 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1612 if (
1613 connectorId === 0 &&
cda5d0fb
JB
1614 (!stationTemplate?.Connectors[connectorId] ||
1615 this.getUseConnectorId0(stationTemplate) === false)
269196a8
JB
1616 ) {
1617 continue;
1618 }
1619 const templateConnectorId =
cda5d0fb 1620 connectorId > 0 && stationTemplate?.randomConnectors
9bf0ef23 1621 ? getRandomInteger(templateMaxAvailableConnectors, 1)
269196a8 1622 : connectorId;
cda5d0fb 1623 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
fba11dc6 1624 checkStationInfoConnectorStatus(
ae25f265 1625 templateConnectorId,
04b1261c
JB
1626 connectorStatus,
1627 this.logPrefix(),
5edd8ba0 1628 this.templateFile,
04b1261c 1629 );
9bf0ef23 1630 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1631 }
fba11dc6 1632 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
52952bf8 1633 this.saveConnectorsStatus();
ae25f265
JB
1634 } else {
1635 logger.warn(
1636 `${this.logPrefix()} Charging station information from template ${
1637 this.templateFile
5edd8ba0 1638 } with no connectors configuration defined, cannot create connectors`,
ae25f265 1639 );
3d25cc86
JB
1640 }
1641 }
1642 } else {
1643 logger.warn(
1644 `${this.logPrefix()} Charging station information from template ${
1645 this.templateFile
5edd8ba0 1646 } with no connectors configuration defined, using already defined connectors`,
3d25cc86
JB
1647 );
1648 }
3d25cc86
JB
1649 }
1650
34eeb1fb 1651 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1652 if (!stationTemplate?.Evses && this.evses.size === 0) {
ded57f02
JB
1653 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1654 logger.error(`${this.logPrefix()} ${errorMsg}`);
1655 throw new BaseError(errorMsg);
2585c6e9 1656 }
e1d9a0f4 1657 if (!stationTemplate?.Evses?.[0]) {
2585c6e9
JB
1658 logger.warn(
1659 `${this.logPrefix()} Charging station information from template ${
1660 this.templateFile
5edd8ba0 1661 } with no evse id 0 configuration`,
2585c6e9
JB
1662 );
1663 }
e1d9a0f4 1664 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
59a0f26d
JB
1665 logger.warn(
1666 `${this.logPrefix()} Charging station information from template ${
1667 this.templateFile
5edd8ba0 1668 } with evse id 0 with no connector id 0 configuration`,
59a0f26d
JB
1669 );
1670 }
cda5d0fb 1671 if (stationTemplate?.Evses) {
d972af76 1672 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
ba01a213 1673 .update(JSON.stringify(stationTemplate?.Evses))
2585c6e9
JB
1674 .digest('hex');
1675 const evsesConfigChanged =
1676 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1677 if (this.evses?.size === 0 || evsesConfigChanged) {
1678 evsesConfigChanged && this.evses.clear();
1679 this.evsesConfigurationHash = evsesConfigHash;
fba11dc6 1680 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
ae25f265 1681 if (templateMaxEvses > 0) {
cda5d0fb 1682 for (const evse in stationTemplate.Evses) {
9bf0ef23 1683 const evseId = convertToInt(evse);
52952bf8 1684 this.evses.set(evseId, {
fba11dc6 1685 connectors: buildConnectorsMap(
cda5d0fb 1686 stationTemplate?.Evses[evse]?.Connectors,
ae25f265 1687 this.logPrefix(),
5edd8ba0 1688 this.templateFile,
ae25f265
JB
1689 ),
1690 availability: AvailabilityType.Operative,
1691 });
e1d9a0f4 1692 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
ae25f265 1693 }
52952bf8 1694 this.saveEvsesStatus();
ae25f265
JB
1695 } else {
1696 logger.warn(
1697 `${this.logPrefix()} Charging station information from template ${
04b1261c 1698 this.templateFile
5edd8ba0 1699 } with no evses configuration defined, cannot create evses`,
04b1261c 1700 );
2585c6e9
JB
1701 }
1702 }
513db108
JB
1703 } else {
1704 logger.warn(
1705 `${this.logPrefix()} Charging station information from template ${
1706 this.templateFile
5edd8ba0 1707 } with no evses configuration defined, using already defined evses`,
513db108 1708 );
2585c6e9
JB
1709 }
1710 }
1711
551e477c
JB
1712 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1713 let configuration: ChargingStationConfiguration | undefined;
9bf0ef23 1714 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1715 try {
57adbebc
JB
1716 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1717 configuration = this.sharedLRUCache.getChargingStationConfiguration(
5edd8ba0 1718 this.configurationFileHash,
57adbebc 1719 );
7c72977b
JB
1720 } else {
1721 const measureId = `${FileType.ChargingStationConfiguration} read`;
1722 const beginId = PerformanceStatistics.beginMeasure(measureId);
1723 configuration = JSON.parse(
5edd8ba0 1724 readFileSync(this.configurationFile, 'utf8'),
7c72977b
JB
1725 ) as ChargingStationConfiguration;
1726 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1727 this.sharedLRUCache.setChargingStationConfiguration(configuration);
e1d9a0f4 1728 this.configurationFileHash = configuration.configurationHash!;
7c72977b 1729 }
073bd098 1730 } catch (error) {
fa5995d6 1731 handleFileException(
073bd098 1732 this.configurationFile,
7164966d
JB
1733 FileType.ChargingStationConfiguration,
1734 error as NodeJS.ErrnoException,
5edd8ba0 1735 this.logPrefix(),
073bd098
JB
1736 );
1737 }
1738 }
1739 return configuration;
1740 }
1741
cb60061f 1742 private saveAutomaticTransactionGeneratorConfiguration(): void {
5ced7e80
JB
1743 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1744 this.saveConfiguration();
1745 }
ac7f79af
JB
1746 }
1747
52952bf8 1748 private saveConnectorsStatus() {
7446de3b 1749 this.saveConfiguration();
52952bf8
JB
1750 }
1751
1752 private saveEvsesStatus() {
7446de3b 1753 this.saveConfiguration();
52952bf8
JB
1754 }
1755
179ed367 1756 private saveConfiguration(): void {
9bf0ef23 1757 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1758 try {
d972af76
JB
1759 if (!existsSync(dirname(this.configurationFile))) {
1760 mkdirSync(dirname(this.configurationFile), { recursive: true });
073bd098 1761 }
ac7f79af 1762 let configurationData: ChargingStationConfiguration =
e1d9a0f4 1763 cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!) ?? {};
34eeb1fb 1764 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
52952bf8 1765 configurationData.stationInfo = this.stationInfo;
5ced7e80
JB
1766 } else {
1767 delete configurationData.stationInfo;
52952bf8 1768 }
34eeb1fb 1769 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
52952bf8 1770 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
5ced7e80
JB
1771 } else {
1772 delete configurationData.configurationKey;
52952bf8 1773 }
179ed367
JB
1774 configurationData = merge<ChargingStationConfiguration>(
1775 configurationData,
5edd8ba0 1776 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
179ed367 1777 );
5ced7e80
JB
1778 if (
1779 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1780 !this.getAutomaticTransactionGeneratorConfiguration()
1781 ) {
1782 delete configurationData.automaticTransactionGenerator;
1783 }
b1bbdae5 1784 if (this.connectors.size > 0) {
179ed367 1785 configurationData.connectorsStatus = buildConnectorsStatus(this);
5ced7e80
JB
1786 } else {
1787 delete configurationData.connectorsStatus;
52952bf8 1788 }
b1bbdae5 1789 if (this.evses.size > 0) {
179ed367 1790 configurationData.evsesStatus = buildEvsesStatus(this);
5ced7e80
JB
1791 } else {
1792 delete configurationData.evsesStatus;
52952bf8 1793 }
7c72977b 1794 delete configurationData.configurationHash;
d972af76 1795 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1796 .update(
1797 JSON.stringify({
1798 stationInfo: configurationData.stationInfo,
1799 configurationKey: configurationData.configurationKey,
1800 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
5edd8ba0 1801 } as ChargingStationConfiguration),
5ced7e80 1802 )
7c72977b
JB
1803 .digest('hex');
1804 if (this.configurationFileHash !== configurationHash) {
dd485b56 1805 AsyncLock.acquire(AsyncLockType.configuration)
1227a6f1
JB
1806 .then(() => {
1807 configurationData.configurationHash = configurationHash;
1808 const measureId = `${FileType.ChargingStationConfiguration} write`;
1809 const beginId = PerformanceStatistics.beginMeasure(measureId);
d972af76
JB
1810 const fileDescriptor = openSync(this.configurationFile, 'w');
1811 writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1812 closeSync(fileDescriptor);
1227a6f1
JB
1813 PerformanceStatistics.endMeasure(measureId, beginId);
1814 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1815 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1816 this.configurationFileHash = configurationHash;
1817 })
1818 .catch((error) => {
fa5995d6 1819 handleFileException(
1227a6f1
JB
1820 this.configurationFile,
1821 FileType.ChargingStationConfiguration,
1822 error as NodeJS.ErrnoException,
5edd8ba0 1823 this.logPrefix(),
1227a6f1
JB
1824 );
1825 })
1826 .finally(() => {
dd485b56 1827 AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
1227a6f1 1828 });
7c72977b
JB
1829 } else {
1830 logger.debug(
1831 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1832 this.configurationFile
5edd8ba0 1833 }`,
7c72977b 1834 );
2484ac1e 1835 }
2484ac1e 1836 } catch (error) {
fa5995d6 1837 handleFileException(
2484ac1e 1838 this.configurationFile,
7164966d
JB
1839 FileType.ChargingStationConfiguration,
1840 error as NodeJS.ErrnoException,
5edd8ba0 1841 this.logPrefix(),
073bd098
JB
1842 );
1843 }
2484ac1e
JB
1844 } else {
1845 logger.error(
5edd8ba0 1846 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
2484ac1e 1847 );
073bd098
JB
1848 }
1849 }
1850
551e477c
JB
1851 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1852 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1853 }
1854
551e477c 1855 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
60655b26
JB
1856 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1857 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1858 return { configurationKey };
648512ce 1859 }
60655b26 1860 return undefined;
7dde0b73
JB
1861 }
1862
551e477c
JB
1863 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1864 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1865 this.getOcppConfigurationFromFile();
2484ac1e
JB
1866 if (!ocppConfiguration) {
1867 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1868 }
1869 return ocppConfiguration;
1870 }
1871
c0560973 1872 private async onOpen(): Promise<void> {
56eb297e 1873 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1 1874 logger.info(
5edd8ba0 1875 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
5144f4d1 1876 );
ed6cfcff 1877 if (this.isRegistered() === false) {
5144f4d1
JB
1878 // Send BootNotification
1879 let registrationRetryCount = 0;
1880 do {
f7f98c68 1881 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1882 BootNotificationRequest,
1883 BootNotificationResponse
8bfbc743
JB
1884 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1885 skipBufferingOnError: true,
1886 });
ed6cfcff 1887 if (this.isRegistered() === false) {
1fe0632a 1888 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
9bf0ef23 1889 await sleep(
1895299d 1890 this?.bootNotificationResponse?.interval
5144f4d1 1891 ? this.bootNotificationResponse.interval * 1000
5edd8ba0 1892 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
5144f4d1
JB
1893 );
1894 }
1895 } while (
ed6cfcff 1896 this.isRegistered() === false &&
e1d9a0f4 1897 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
5144f4d1
JB
1898 this.getRegistrationMaxRetries() === -1)
1899 );
1900 }
ed6cfcff 1901 if (this.isRegistered() === true) {
f7c2994d 1902 if (this.inAcceptedState() === true) {
94bb24d5 1903 await this.startMessageSequence();
c0560973 1904 }
5144f4d1
JB
1905 } else {
1906 logger.error(
5edd8ba0 1907 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
5144f4d1 1908 );
caad9d6b 1909 }
5144f4d1 1910 this.wsConnectionRestarted = false;
aa428a31 1911 this.autoReconnectRetryCount = 0;
c8faabc8 1912 parentPort?.postMessage(buildUpdatedMessage(this));
2e6f5966 1913 } else {
5144f4d1 1914 logger.warn(
5edd8ba0 1915 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
e7aeea18 1916 );
2e6f5966 1917 }
2e6f5966
JB
1918 }
1919
ef7d8c21 1920 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1921 switch (code) {
6c65a295
JB
1922 // Normal close
1923 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1924 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1925 logger.info(
9bf0ef23 1926 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1927 code,
1928 )}' and reason '${reason.toString()}'`,
e7aeea18 1929 );
c0560973
JB
1930 this.autoReconnectRetryCount = 0;
1931 break;
6c65a295
JB
1932 // Abnormal close
1933 default:
e7aeea18 1934 logger.error(
9bf0ef23 1935 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1936 code,
1937 )}' and reason '${reason.toString()}'`,
e7aeea18 1938 );
56eb297e 1939 this.started === true && (await this.reconnect());
c0560973
JB
1940 break;
1941 }
c8faabc8 1942 parentPort?.postMessage(buildUpdatedMessage(this));
2e6f5966
JB
1943 }
1944
56d09fd7
JB
1945 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1946 const cachedRequest = this.requests.get(messageId);
1947 if (Array.isArray(cachedRequest) === true) {
1948 return cachedRequest;
1949 }
1950 throw new OCPPError(
1951 ErrorType.PROTOCOL_ERROR,
1952 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0 1953 messageType,
56d09fd7
JB
1954 )} is not an array`,
1955 undefined,
5edd8ba0 1956 cachedRequest as JsonType,
56d09fd7
JB
1957 );
1958 }
1959
1960 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1961 const [messageType, messageId, commandName, commandPayload] = request;
1962 if (this.getEnableStatistics() === true) {
1963 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1964 }
1965 logger.debug(
1966 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
5edd8ba0
JB
1967 request,
1968 )}`,
56d09fd7
JB
1969 );
1970 // Process the message
1971 await this.ocppIncomingRequestService.incomingRequestHandler(
1972 this,
1973 messageId,
1974 commandName,
5edd8ba0 1975 commandPayload,
56d09fd7
JB
1976 );
1977 }
1978
1979 private handleResponseMessage(response: Response): void {
1980 const [messageType, messageId, commandPayload] = response;
1981 if (this.requests.has(messageId) === false) {
1982 // Error
1983 throw new OCPPError(
1984 ErrorType.INTERNAL_ERROR,
1985 `Response for unknown message id ${messageId}`,
1986 undefined,
5edd8ba0 1987 commandPayload,
56d09fd7
JB
1988 );
1989 }
1990 // Respond
1991 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1992 messageType,
5edd8ba0 1993 messageId,
e1d9a0f4 1994 )!;
56d09fd7
JB
1995 logger.debug(
1996 `${this.logPrefix()} << Command '${
1997 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1998 }' received response payload: ${JSON.stringify(response)}`,
56d09fd7
JB
1999 );
2000 responseCallback(commandPayload, requestPayload);
2001 }
2002
2003 private handleErrorMessage(errorResponse: ErrorResponse): void {
2004 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
2005 if (this.requests.has(messageId) === false) {
2006 // Error
2007 throw new OCPPError(
2008 ErrorType.INTERNAL_ERROR,
2009 `Error response for unknown message id ${messageId}`,
2010 undefined,
5edd8ba0 2011 { errorType, errorMessage, errorDetails },
56d09fd7
JB
2012 );
2013 }
e1d9a0f4 2014 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
56d09fd7
JB
2015 logger.debug(
2016 `${this.logPrefix()} << Command '${
2017 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 2018 }' received error response payload: ${JSON.stringify(errorResponse)}`,
56d09fd7
JB
2019 );
2020 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
2021 }
2022
ef7d8c21 2023 private async onMessage(data: RawData): Promise<void> {
e1d9a0f4
JB
2024 let request: IncomingRequest | Response | ErrorResponse | undefined;
2025 let messageType: number | undefined;
ded57f02 2026 let errorMsg: string;
c0560973 2027 try {
e1d9a0f4 2028 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2029 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 2030 if (Array.isArray(request) === true) {
56d09fd7 2031 [messageType] = request;
b3ec7bc1
JB
2032 // Check the type of message
2033 switch (messageType) {
2034 // Incoming Message
2035 case MessageType.CALL_MESSAGE:
56d09fd7 2036 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 2037 break;
56d09fd7 2038 // Response Message
b3ec7bc1 2039 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 2040 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
2041 break;
2042 // Error Message
2043 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 2044 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 2045 break;
56d09fd7 2046 // Unknown Message
b3ec7bc1
JB
2047 default:
2048 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
2049 errorMsg = `Wrong message type ${messageType}`;
2050 logger.error(`${this.logPrefix()} ${errorMsg}`);
2051 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 2052 }
c8faabc8 2053 parentPort?.postMessage(buildUpdatedMessage(this));
47e22477 2054 } else {
e1d9a0f4
JB
2055 throw new OCPPError(
2056 ErrorType.PROTOCOL_ERROR,
2057 'Incoming message is not an array',
2058 undefined,
2059 {
2060 request,
2061 },
2062 );
47e22477 2063 }
c0560973 2064 } catch (error) {
e1d9a0f4
JB
2065 let commandName: IncomingRequestCommand | undefined;
2066 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
56d09fd7 2067 let errorCallback: ErrorCallback;
e1d9a0f4 2068 const [, messageId] = request!;
13701f69
JB
2069 switch (messageType) {
2070 case MessageType.CALL_MESSAGE:
56d09fd7 2071 [, , commandName] = request as IncomingRequest;
13701f69 2072 // Send error
56d09fd7 2073 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
2074 break;
2075 case MessageType.CALL_RESULT_MESSAGE:
2076 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 2077 if (this.requests.has(messageId) === true) {
e1d9a0f4 2078 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
13701f69
JB
2079 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2080 errorCallback(error as OCPPError, false);
2081 } else {
2082 // Remove the request from the cache in case of error at response handling
2083 this.requests.delete(messageId);
2084 }
de4cb8b6 2085 break;
ba7965c4 2086 }
56d09fd7
JB
2087 if (error instanceof OCPPError === false) {
2088 logger.warn(
2089 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2090 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2091 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2092 }' message '${data.toString()}' handling is not an OCPPError:`,
5edd8ba0 2093 error,
56d09fd7
JB
2094 );
2095 }
2096 logger.error(
2097 `${this.logPrefix()} Incoming OCPP command '${
2098 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2099 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7
JB
2100 }' message '${data.toString()}'${
2101 messageType !== MessageType.CALL_MESSAGE
2102 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2103 : ''
2104 } processing error:`,
5edd8ba0 2105 error,
56d09fd7 2106 );
c0560973 2107 }
2328be1e
JB
2108 }
2109
c0560973 2110 private onPing(): void {
44eb6026 2111 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
2112 }
2113
2114 private onPong(): void {
44eb6026 2115 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
2116 }
2117
9534e74e 2118 private onError(error: WSError): void {
bcc9c3c0 2119 this.closeWSConnection();
44eb6026 2120 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
2121 }
2122
18bf8274 2123 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 2124 if (this.getMeteringPerTransaction() === true) {
07989fad 2125 return (
18bf8274 2126 (rounded === true
e1d9a0f4 2127 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
07989fad
JB
2128 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2129 );
2130 }
2131 return (
18bf8274 2132 (rounded === true
e1d9a0f4 2133 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
07989fad
JB
2134 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2135 );
2136 }
2137
cda5d0fb
JB
2138 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2139 return stationTemplate?.useConnectorId0 ?? true;
8bce55bf
JB
2140 }
2141
60ddad53 2142 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158 2143 if (this.hasEvses) {
3fa7f799
JB
2144 for (const [evseId, evseStatus] of this.evses) {
2145 if (evseId === 0) {
2146 continue;
2147 }
28e78158
JB
2148 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2149 if (connectorStatus.transactionStarted === true) {
2150 await this.stopTransactionOnConnector(connectorId, reason);
2151 }
2152 }
2153 }
2154 } else {
2155 for (const connectorId of this.connectors.keys()) {
2156 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2157 await this.stopTransactionOnConnector(connectorId, reason);
2158 }
60ddad53
JB
2159 }
2160 }
2161 }
2162
1f761b9a 2163 // 0 for disabling
c72f6634 2164 private getConnectionTimeout(): number {
17ac262c
JB
2165 if (
2166 ChargingStationConfigurationUtils.getConfigurationKey(
2167 this,
5edd8ba0 2168 StandardParametersKey.ConnectionTimeOut,
17ac262c
JB
2169 )
2170 ) {
e7aeea18 2171 return (
17ac262c
JB
2172 parseInt(
2173 ChargingStationConfigurationUtils.getConfigurationKey(
2174 this,
5edd8ba0 2175 StandardParametersKey.ConnectionTimeOut,
e1d9a0f4 2176 )!.value!,
17ac262c 2177 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 2178 );
291cb255 2179 }
291cb255 2180 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
2181 }
2182
1f761b9a 2183 // -1 for unlimited, 0 for disabling
72092cfc 2184 private getAutoReconnectMaxRetries(): number | undefined {
b1bbdae5
JB
2185 return (
2186 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
2187 );
3574dfd3
JB
2188 }
2189
ec977daf 2190 // 0 for disabling
72092cfc 2191 private getRegistrationMaxRetries(): number | undefined {
b1bbdae5 2192 return this.stationInfo.registrationMaxRetries ?? -1;
32a1eb7a
JB
2193 }
2194
c0560973 2195 private getPowerDivider(): number {
b1bbdae5 2196 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
fa7bccf4 2197 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 2198 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
2199 }
2200 return powerDivider;
2201 }
2202
fa7bccf4
JB
2203 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2204 const maximumPower = this.getMaximumPower(stationInfo);
2205 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2206 case CurrentType.AC:
2207 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2208 this.getNumberOfPhases(stationInfo),
b1bbdae5 2209 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
5edd8ba0 2210 this.getVoltageOut(stationInfo),
cc6e8ab5
JB
2211 );
2212 case CurrentType.DC:
fa7bccf4 2213 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
2214 }
2215 }
2216
cc6e8ab5
JB
2217 private getAmperageLimitation(): number | undefined {
2218 if (
9bf0ef23 2219 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
2220 ChargingStationConfigurationUtils.getConfigurationKey(
2221 this,
e1d9a0f4 2222 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 2223 )
cc6e8ab5
JB
2224 ) {
2225 return (
9bf0ef23 2226 convertToInt(
17ac262c
JB
2227 ChargingStationConfigurationUtils.getConfigurationKey(
2228 this,
e1d9a0f4 2229 this.stationInfo.amperageLimitationOcppKey!,
5edd8ba0 2230 )?.value,
fba11dc6 2231 ) / getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
2232 );
2233 }
2234 }
2235
c0560973 2236 private async startMessageSequence(): Promise<void> {
b7f9e41d 2237 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2238 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
2239 BootNotificationRequest,
2240 BootNotificationResponse
8bfbc743
JB
2241 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2242 skipBufferingOnError: true,
2243 });
6114e6f1 2244 }
136c90ba 2245 // Start WebSocket ping
c0560973 2246 this.startWebSocketPing();
5ad8570f 2247 // Start heartbeat
c0560973 2248 this.startHeartbeat();
0a60c33c 2249 // Initialize connectors status
c3b83130
JB
2250 if (this.hasEvses) {
2251 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2252 if (evseId > 0) {
2253 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
fba11dc6 2254 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
4334db72
JB
2255 await OCPPServiceUtils.sendAndSetConnectorStatus(
2256 this,
2257 connectorId,
12f26d4a 2258 connectorBootStatus,
5edd8ba0 2259 evseId,
4334db72
JB
2260 );
2261 }
c3b83130 2262 }
4334db72
JB
2263 }
2264 } else {
2265 for (const connectorId of this.connectors.keys()) {
2266 if (connectorId > 0) {
fba11dc6 2267 const connectorBootStatus = getBootConnectorStatus(
c3b83130
JB
2268 this,
2269 connectorId,
e1d9a0f4 2270 this.getConnectorStatus(connectorId)!,
c3b83130
JB
2271 );
2272 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2273 }
2274 }
5ad8570f 2275 }
c9a4f9ea
JB
2276 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2277 await this.ocppRequestService.requestHandler<
2278 FirmwareStatusNotificationRequest,
2279 FirmwareStatusNotificationResponse
2280 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2281 status: FirmwareStatus.Installed,
2282 });
2283 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2284 }
3637ca2c 2285
0a60c33c 2286 // Start the ATG
ac7f79af 2287 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
4f69be04 2288 this.startAutomaticTransactionGenerator();
fa7bccf4 2289 }
aa428a31 2290 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2291 }
2292
e7aeea18 2293 private async stopMessageSequence(
5edd8ba0 2294 reason: StopTransactionReason = StopTransactionReason.NONE,
e7aeea18 2295 ): Promise<void> {
136c90ba 2296 // Stop WebSocket ping
c0560973 2297 this.stopWebSocketPing();
79411696 2298 // Stop heartbeat
c0560973 2299 this.stopHeartbeat();
fa7bccf4 2300 // Stop ongoing transactions
b20eb107 2301 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
2302 this.stopAutomaticTransactionGenerator();
2303 } else {
2304 await this.stopRunningTransactions(reason);
79411696 2305 }
039211f9
JB
2306 if (this.hasEvses) {
2307 for (const [evseId, evseStatus] of this.evses) {
2308 if (evseId > 0) {
2309 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2310 await this.ocppRequestService.requestHandler<
2311 StatusNotificationRequest,
2312 StatusNotificationResponse
2313 >(
2314 this,
2315 RequestCommand.STATUS_NOTIFICATION,
2316 OCPPServiceUtils.buildStatusNotificationRequest(
2317 this,
2318 connectorId,
12f26d4a 2319 ConnectorStatusEnum.Unavailable,
5edd8ba0
JB
2320 evseId,
2321 ),
039211f9
JB
2322 );
2323 delete connectorStatus?.status;
2324 }
2325 }
2326 }
2327 } else {
2328 for (const connectorId of this.connectors.keys()) {
2329 if (connectorId > 0) {
2330 await this.ocppRequestService.requestHandler<
2331 StatusNotificationRequest,
2332 StatusNotificationResponse
2333 >(
6e939d9e 2334 this,
039211f9
JB
2335 RequestCommand.STATUS_NOTIFICATION,
2336 OCPPServiceUtils.buildStatusNotificationRequest(
2337 this,
2338 connectorId,
5edd8ba0
JB
2339 ConnectorStatusEnum.Unavailable,
2340 ),
039211f9
JB
2341 );
2342 delete this.getConnectorStatus(connectorId)?.status;
2343 }
45c0ae82
JB
2344 }
2345 }
79411696
JB
2346 }
2347
c0560973 2348 private startWebSocketPing(): void {
17ac262c
JB
2349 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2350 this,
5edd8ba0 2351 StandardParametersKey.WebSocketPingInterval,
e7aeea18 2352 )
9bf0ef23 2353 ? convertToInt(
17ac262c
JB
2354 ChargingStationConfigurationUtils.getConfigurationKey(
2355 this,
5edd8ba0
JB
2356 StandardParametersKey.WebSocketPingInterval,
2357 )?.value,
e7aeea18 2358 )
9cd3dfb0 2359 : 0;
ad2f27c3
JB
2360 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2361 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2362 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2363 this.wsConnection?.ping();
136c90ba
JB
2364 }
2365 }, webSocketPingInterval * 1000);
e7aeea18 2366 logger.info(
9bf0ef23 2367 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
5edd8ba0
JB
2368 webSocketPingInterval,
2369 )}`,
e7aeea18 2370 );
ad2f27c3 2371 } else if (this.webSocketPingSetInterval) {
e7aeea18 2372 logger.info(
9bf0ef23 2373 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
5edd8ba0
JB
2374 webSocketPingInterval,
2375 )}`,
e7aeea18 2376 );
136c90ba 2377 } else {
e7aeea18 2378 logger.error(
5edd8ba0 2379 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
e7aeea18 2380 );
136c90ba
JB
2381 }
2382 }
2383
c0560973 2384 private stopWebSocketPing(): void {
ad2f27c3
JB
2385 if (this.webSocketPingSetInterval) {
2386 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2387 delete this.webSocketPingSetInterval;
136c90ba
JB
2388 }
2389 }
2390
1f5df42a 2391 private getConfiguredSupervisionUrl(): URL {
d5c3df49 2392 let configuredSupervisionUrl: string;
72092cfc 2393 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
9bf0ef23 2394 if (isNotEmptyArray(supervisionUrls)) {
269de583 2395 let configuredSupervisionUrlIndex: number;
2dcfe98e 2396 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2397 case SupervisionUrlDistribution.RANDOM:
e1d9a0f4
JB
2398 configuredSupervisionUrlIndex = Math.floor(
2399 secureRandom() * (supervisionUrls as string[]).length,
2400 );
2dcfe98e 2401 break;
a52a6446 2402 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2403 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2404 default:
a52a6446 2405 Object.values(SupervisionUrlDistribution).includes(
e1d9a0f4 2406 Configuration.getSupervisionUrlDistribution()!,
a52a6446
JB
2407 ) === false &&
2408 logger.error(
e1d9a0f4 2409 // eslint-disable-next-line @typescript-eslint/no-base-to-string
a52a6446
JB
2410 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2411 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
5edd8ba0 2412 }`,
a52a6446 2413 );
e1d9a0f4 2414 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2dcfe98e 2415 break;
c0560973 2416 }
e1d9a0f4 2417 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
d5c3df49
JB
2418 } else {
2419 configuredSupervisionUrl = supervisionUrls as string;
2420 }
9bf0ef23 2421 if (isNotEmptyString(configuredSupervisionUrl)) {
d5c3df49 2422 return new URL(configuredSupervisionUrl);
c0560973 2423 }
49c508b0 2424 const errorMsg = 'No supervision url(s) configured';
7f77d16f
JB
2425 logger.error(`${this.logPrefix()} ${errorMsg}`);
2426 throw new BaseError(`${errorMsg}`);
136c90ba
JB
2427 }
2428
c0560973 2429 private stopHeartbeat(): void {
ad2f27c3
JB
2430 if (this.heartbeatSetInterval) {
2431 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2432 delete this.heartbeatSetInterval;
7dde0b73 2433 }
5ad8570f
JB
2434 }
2435
55516218 2436 private terminateWSConnection(): void {
56eb297e 2437 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2438 this.wsConnection?.terminate();
55516218
JB
2439 this.wsConnection = null;
2440 }
2441 }
2442
c72f6634 2443 private getReconnectExponentialDelay(): boolean {
a14885a3 2444 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2445 }
2446
aa428a31 2447 private async reconnect(): Promise<void> {
7874b0b1
JB
2448 // Stop WebSocket ping
2449 this.stopWebSocketPing();
136c90ba 2450 // Stop heartbeat
c0560973 2451 this.stopHeartbeat();
5ad8570f 2452 // Stop the ATG if needed
ac7f79af 2453 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
fa7bccf4 2454 this.stopAutomaticTransactionGenerator();
ad2f27c3 2455 }
e7aeea18 2456 if (
e1d9a0f4 2457 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
e7aeea18
JB
2458 this.getAutoReconnectMaxRetries() === -1
2459 ) {
1fe0632a 2460 ++this.autoReconnectRetryCount;
e7aeea18 2461 const reconnectDelay = this.getReconnectExponentialDelay()
9bf0ef23 2462 ? exponentialDelay(this.autoReconnectRetryCount)
e7aeea18 2463 : this.getConnectionTimeout() * 1000;
1e080116
JB
2464 const reconnectDelayWithdraw = 1000;
2465 const reconnectTimeout =
2466 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2467 ? reconnectDelay - reconnectDelayWithdraw
2468 : 0;
e7aeea18 2469 logger.error(
9bf0ef23 2470 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2471 reconnectDelay,
5edd8ba0
JB
2472 2,
2473 )}ms, timeout ${reconnectTimeout}ms`,
e7aeea18 2474 );
9bf0ef23 2475 await sleep(reconnectDelay);
e7aeea18 2476 logger.error(
5edd8ba0 2477 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
e7aeea18
JB
2478 );
2479 this.openWSConnection(
59b6ed8d 2480 {
abe9e9dd 2481 ...(this.stationInfo?.wsOptions ?? {}),
59b6ed8d
JB
2482 handshakeTimeout: reconnectTimeout,
2483 },
5edd8ba0 2484 { closeOpened: true },
e7aeea18 2485 );
265e4266 2486 this.wsConnectionRestarted = true;
c0560973 2487 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2488 logger.error(
d56ea27c 2489 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2490 this.autoReconnectRetryCount
5edd8ba0 2491 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
e7aeea18 2492 );
5ad8570f
JB
2493 }
2494 }
7dde0b73 2495}