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