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