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