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