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