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