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