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