fix: fix electrical specs calculation with evses
[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;
ae25f265
JB
996 // Initialize evses or connectors if needed (FIXME: should be factored out)
997 this.initializeConnectorsOrEvses(stationInfo);
fa7bccf4
JB
998 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
999 ChargingStationUtils.createStationInfoHash(stationInfo);
9ac86a7e 1000 return stationInfo;
5ad8570f
JB
1001 }
1002
551e477c
JB
1003 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1004 let stationInfo: ChargingStationInfo | undefined;
fa7bccf4 1005 this.getStationInfoPersistentConfiguration() &&
551e477c 1006 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
fa7bccf4 1007 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
f765beaa 1008 return stationInfo;
2484ac1e
JB
1009 }
1010
1011 private getStationInfo(): ChargingStationInfo {
1012 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1013 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1014 // Priority:
1015 // 1. charging station info from template
1016 // 2. charging station info from configuration file
1017 // 3. charging station info attribute
f765beaa 1018 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
01efc60a
JB
1019 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
1020 return this.stationInfo;
1021 }
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();
ae25f265 1078 // Avoid duplication of connectors or evses related information in RAM
6bccfcbc 1079 delete this.stationInfo?.Connectors;
ae25f265 1080 delete this.stationInfo?.Evses;
6bccfcbc
JB
1081 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1082 if (this.getEnableStatistics() === true) {
1083 this.performanceStatistics = PerformanceStatistics.getInstance(
1084 this.stationInfo.hashId,
1085 this.stationInfo.chargingStationId,
1086 this.configuredSupervisionUrl
1087 );
1088 }
692f2f64
JB
1089 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1090 this.stationInfo
1091 );
1092 this.powerDivider = this.getPowerDivider();
1093 // OCPP configuration
1094 this.ocppConfiguration = this.getOcppConfiguration();
1095 this.initializeOcppConfiguration();
1096 this.initializeOcppServices();
1097 if (this.stationInfo?.autoRegister === true) {
1098 this.bootNotificationResponse = {
1099 currentTime: new Date(),
1100 interval: this.getHeartbeatInterval() / 1000,
1101 status: RegistrationStatusEnumType.ACCEPTED,
1102 };
1103 }
147d0e0f
JB
1104 }
1105
feff11ec
JB
1106 private initializeOcppServices(): void {
1107 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1108 switch (ocppVersion) {
1109 case OCPPVersion.VERSION_16:
1110 this.ocppIncomingRequestService =
1111 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1112 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1113 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1114 );
1115 break;
1116 case OCPPVersion.VERSION_20:
1117 case OCPPVersion.VERSION_201:
1118 this.ocppIncomingRequestService =
1119 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1120 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1121 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1122 );
1123 break;
1124 default:
1125 this.handleUnsupportedVersion(ocppVersion);
1126 break;
1127 }
1128 }
1129
2484ac1e 1130 private initializeOcppConfiguration(): void {
17ac262c
JB
1131 if (
1132 !ChargingStationConfigurationUtils.getConfigurationKey(
1133 this,
1134 StandardParametersKey.HeartbeatInterval
1135 )
1136 ) {
1137 ChargingStationConfigurationUtils.addConfigurationKey(
1138 this,
1139 StandardParametersKey.HeartbeatInterval,
1140 '0'
1141 );
f0f65a62 1142 }
17ac262c
JB
1143 if (
1144 !ChargingStationConfigurationUtils.getConfigurationKey(
1145 this,
1146 StandardParametersKey.HeartBeatInterval
1147 )
1148 ) {
1149 ChargingStationConfigurationUtils.addConfigurationKey(
1150 this,
1151 StandardParametersKey.HeartBeatInterval,
1152 '0',
1153 { visible: false }
1154 );
f0f65a62 1155 }
e7aeea18
JB
1156 if (
1157 this.getSupervisionUrlOcppConfiguration() &&
269de583 1158 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1159 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1160 ) {
17ac262c
JB
1161 ChargingStationConfigurationUtils.addConfigurationKey(
1162 this,
a59737e3 1163 this.getSupervisionUrlOcppKey(),
fa7bccf4 1164 this.configuredSupervisionUrl.href,
e7aeea18
JB
1165 { reboot: true }
1166 );
e6895390
JB
1167 } else if (
1168 !this.getSupervisionUrlOcppConfiguration() &&
269de583 1169 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1170 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1171 ) {
17ac262c
JB
1172 ChargingStationConfigurationUtils.deleteConfigurationKey(
1173 this,
1174 this.getSupervisionUrlOcppKey(),
1175 { save: false }
1176 );
12fc74d6 1177 }
cc6e8ab5 1178 if (
5a2a53cf 1179 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1180 !ChargingStationConfigurationUtils.getConfigurationKey(
1181 this,
1182 this.stationInfo.amperageLimitationOcppKey
1183 )
cc6e8ab5 1184 ) {
17ac262c
JB
1185 ChargingStationConfigurationUtils.addConfigurationKey(
1186 this,
cc6e8ab5 1187 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1188 (
1189 this.stationInfo.maximumAmperage *
1190 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1191 ).toString()
cc6e8ab5
JB
1192 );
1193 }
17ac262c
JB
1194 if (
1195 !ChargingStationConfigurationUtils.getConfigurationKey(
1196 this,
1197 StandardParametersKey.SupportedFeatureProfiles
1198 )
1199 ) {
1200 ChargingStationConfigurationUtils.addConfigurationKey(
1201 this,
e7aeea18 1202 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1203 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1204 );
1205 }
17ac262c
JB
1206 ChargingStationConfigurationUtils.addConfigurationKey(
1207 this,
e7aeea18
JB
1208 StandardParametersKey.NumberOfConnectors,
1209 this.getNumberOfConnectors().toString(),
a95873d8
JB
1210 { readonly: true },
1211 { overwrite: true }
e7aeea18 1212 );
17ac262c
JB
1213 if (
1214 !ChargingStationConfigurationUtils.getConfigurationKey(
1215 this,
1216 StandardParametersKey.MeterValuesSampledData
1217 )
1218 ) {
1219 ChargingStationConfigurationUtils.addConfigurationKey(
1220 this,
e7aeea18
JB
1221 StandardParametersKey.MeterValuesSampledData,
1222 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1223 );
7abfea5f 1224 }
17ac262c
JB
1225 if (
1226 !ChargingStationConfigurationUtils.getConfigurationKey(
1227 this,
1228 StandardParametersKey.ConnectorPhaseRotation
1229 )
1230 ) {
dd08d43d 1231 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1232 if (this.hasEvses) {
1233 for (const evseStatus of this.evses.values()) {
1234 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d
JB
1235 connectorsPhaseRotation.push(
1236 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1237 );
28e78158
JB
1238 }
1239 }
1240 } else {
1241 for (const connectorId of this.connectors.keys()) {
dd08d43d
JB
1242 connectorsPhaseRotation.push(
1243 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1244 );
7e1dc878
JB
1245 }
1246 }
17ac262c
JB
1247 ChargingStationConfigurationUtils.addConfigurationKey(
1248 this,
e7aeea18 1249 StandardParametersKey.ConnectorPhaseRotation,
dd08d43d 1250 connectorsPhaseRotation.toString()
e7aeea18 1251 );
7e1dc878 1252 }
e7aeea18 1253 if (
17ac262c
JB
1254 !ChargingStationConfigurationUtils.getConfigurationKey(
1255 this,
1256 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1257 )
1258 ) {
17ac262c
JB
1259 ChargingStationConfigurationUtils.addConfigurationKey(
1260 this,
1261 StandardParametersKey.AuthorizeRemoteTxRequests,
1262 'true'
1263 );
36f6a92e 1264 }
17ac262c
JB
1265 if (
1266 !ChargingStationConfigurationUtils.getConfigurationKey(
1267 this,
1268 StandardParametersKey.LocalAuthListEnabled
1269 ) &&
1270 ChargingStationConfigurationUtils.getConfigurationKey(
1271 this,
1272 StandardParametersKey.SupportedFeatureProfiles
72092cfc 1273 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1274 ) {
1275 ChargingStationConfigurationUtils.addConfigurationKey(
1276 this,
1277 StandardParametersKey.LocalAuthListEnabled,
1278 'false'
1279 );
1280 }
1281 if (
1282 !ChargingStationConfigurationUtils.getConfigurationKey(
1283 this,
1284 StandardParametersKey.ConnectionTimeOut
1285 )
1286 ) {
1287 ChargingStationConfigurationUtils.addConfigurationKey(
1288 this,
e7aeea18
JB
1289 StandardParametersKey.ConnectionTimeOut,
1290 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1291 );
8bce55bf 1292 }
2484ac1e 1293 this.saveOcppConfiguration();
073bd098
JB
1294 }
1295
ae25f265
JB
1296 private initializeConnectorsOrEvses(stationInfo: ChargingStationInfo) {
1297 if (stationInfo?.Connectors && !stationInfo?.Evses) {
1298 this.initializeConnectors(stationInfo);
1299 } else if (stationInfo?.Evses && !stationInfo?.Connectors) {
1300 this.initializeEvses(stationInfo);
1301 } else if (stationInfo?.Evses && stationInfo?.Connectors) {
1302 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1303 logger.error(`${this.logPrefix()} ${errorMsg}`);
1304 throw new BaseError(errorMsg);
1305 } else {
1306 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1307 logger.error(`${this.logPrefix()} ${errorMsg}`);
1308 throw new BaseError(errorMsg);
1309 }
1310 }
1311
1312 private initializeConnectors(stationInfo: ChargingStationInfo): void {
3d25cc86 1313 if (!stationInfo?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1314 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1315 logger.error(`${this.logPrefix()} ${errorMsg}`);
1316 throw new BaseError(errorMsg);
3d25cc86
JB
1317 }
1318 if (!stationInfo?.Connectors[0]) {
1319 logger.warn(
1320 `${this.logPrefix()} Charging station information from template ${
1321 this.templateFile
2585c6e9 1322 } with no connector id 0 configuration`
3d25cc86
JB
1323 );
1324 }
1325 if (stationInfo?.Connectors) {
ae25f265
JB
1326 const configuredMaxConnectors =
1327 ChargingStationUtils.getConfiguredNumberOfConnectors(stationInfo);
1328 ChargingStationUtils.checkConfiguredMaxConnectors(
1329 configuredMaxConnectors,
1330 this.templateFile,
1331 this.logPrefix()
1332 );
3d25cc86
JB
1333 const connectorsConfigHash = crypto
1334 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
14ecae6a 1335 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
3d25cc86
JB
1336 .digest('hex');
1337 const connectorsConfigChanged =
1338 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1339 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1340 connectorsConfigChanged && this.connectors.clear();
1341 this.connectorsConfigurationHash = connectorsConfigHash;
a78ef5ed 1342 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
ae25f265 1343 stationInfo.Connectors
a78ef5ed 1344 );
ae25f265
JB
1345 ChargingStationUtils.checkTemplateMaxConnectors(
1346 templateMaxConnectors,
1347 this.templateFile,
1348 this.logPrefix()
1349 );
269196a8
JB
1350 const templateMaxAvailableConnectors = stationInfo?.Connectors[0]
1351 ? templateMaxConnectors - 1
1352 : templateMaxConnectors;
ae25f265 1353 if (
269196a8 1354 configuredMaxConnectors > templateMaxAvailableConnectors &&
ae25f265
JB
1355 !stationInfo?.randomConnectors
1356 ) {
1357 logger.warn(
1358 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
1359 this.templateFile
1360 }, forcing random connector configurations affectation`
1361 );
1362 stationInfo.randomConnectors = true;
1363 }
269196a8
JB
1364 if (templateMaxConnectors > 0) {
1365 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1366 if (
1367 connectorId === 0 &&
1368 (!stationInfo?.Connectors[connectorId] ||
1369 this.getUseConnectorId0(stationInfo) === false)
1370 ) {
1371 continue;
1372 }
1373 const templateConnectorId =
1374 connectorId > 0 && stationInfo?.randomConnectors
1375 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1376 : connectorId;
ae25f265 1377 const connectorStatus = stationInfo?.Connectors[templateConnectorId];
04b1261c 1378 ChargingStationUtils.checkStationInfoConnectorStatus(
ae25f265 1379 templateConnectorId,
04b1261c
JB
1380 connectorStatus,
1381 this.logPrefix(),
1382 this.templateFile
1383 );
ae25f265 1384 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1385 }
52952bf8
JB
1386 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1387 this.saveConnectorsStatus();
ae25f265
JB
1388 } else {
1389 logger.warn(
1390 `${this.logPrefix()} Charging station information from template ${
1391 this.templateFile
1392 } with no connectors configuration defined, cannot create connectors`
1393 );
3d25cc86
JB
1394 }
1395 }
1396 } else {
1397 logger.warn(
1398 `${this.logPrefix()} Charging station information from template ${
1399 this.templateFile
1400 } with no connectors configuration defined, using already defined connectors`
1401 );
1402 }
3d25cc86
JB
1403 }
1404
2585c6e9
JB
1405 private initializeEvses(stationInfo: ChargingStationInfo): void {
1406 if (!stationInfo?.Evses && this.evses.size === 0) {
ded57f02
JB
1407 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1408 logger.error(`${this.logPrefix()} ${errorMsg}`);
1409 throw new BaseError(errorMsg);
2585c6e9
JB
1410 }
1411 if (!stationInfo?.Evses[0]) {
1412 logger.warn(
1413 `${this.logPrefix()} Charging station information from template ${
1414 this.templateFile
1415 } with no evse id 0 configuration`
1416 );
1417 }
59a0f26d
JB
1418 if (!stationInfo?.Evses[0]?.Connectors[0]) {
1419 logger.warn(
1420 `${this.logPrefix()} Charging station information from template ${
1421 this.templateFile
1422 } with evse id 0 with no connector id 0 configuration`
1423 );
1424 }
2585c6e9
JB
1425 if (stationInfo?.Evses) {
1426 const evsesConfigHash = crypto
1427 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1428 .update(`${JSON.stringify(stationInfo?.Evses)}`)
1429 .digest('hex');
1430 const evsesConfigChanged =
1431 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1432 if (this.evses?.size === 0 || evsesConfigChanged) {
1433 evsesConfigChanged && this.evses.clear();
1434 this.evsesConfigurationHash = evsesConfigHash;
ae25f265
JB
1435 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationInfo?.Evses);
1436 if (templateMaxEvses > 0) {
1437 for (const evse in stationInfo.Evses) {
52952bf8
JB
1438 const evseId = Utils.convertToInt(evse);
1439 this.evses.set(evseId, {
ae25f265
JB
1440 connectors: ChargingStationUtils.buildConnectorsMap(
1441 stationInfo?.Evses[evse]?.Connectors,
1442 this.logPrefix(),
1443 this.templateFile
1444 ),
1445 availability: AvailabilityType.Operative,
1446 });
1447 ChargingStationUtils.initializeConnectorsMapStatus(
52952bf8 1448 this.evses.get(evseId)?.connectors,
ae25f265
JB
1449 this.logPrefix()
1450 );
1451 }
52952bf8 1452 this.saveEvsesStatus();
ae25f265
JB
1453 } else {
1454 logger.warn(
1455 `${this.logPrefix()} Charging station information from template ${
04b1261c 1456 this.templateFile
ae25f265 1457 } with no evses configuration defined, cannot create evses`
04b1261c 1458 );
2585c6e9
JB
1459 }
1460 }
513db108
JB
1461 } else {
1462 logger.warn(
1463 `${this.logPrefix()} Charging station information from template ${
1464 this.templateFile
1465 } with no evses configuration defined, using already defined evses`
1466 );
2585c6e9
JB
1467 }
1468 }
1469
551e477c
JB
1470 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1471 let configuration: ChargingStationConfiguration | undefined;
2484ac1e 1472 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1473 try {
57adbebc
JB
1474 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1475 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1476 this.configurationFileHash
1477 );
7c72977b
JB
1478 } else {
1479 const measureId = `${FileType.ChargingStationConfiguration} read`;
1480 const beginId = PerformanceStatistics.beginMeasure(measureId);
1481 configuration = JSON.parse(
1482 fs.readFileSync(this.configurationFile, 'utf8')
1483 ) as ChargingStationConfiguration;
1484 PerformanceStatistics.endMeasure(measureId, beginId);
1485 this.configurationFileHash = configuration.configurationHash;
57adbebc 1486 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1487 }
073bd098
JB
1488 } catch (error) {
1489 FileUtils.handleFileException(
073bd098 1490 this.configurationFile,
7164966d
JB
1491 FileType.ChargingStationConfiguration,
1492 error as NodeJS.ErrnoException,
1493 this.logPrefix()
073bd098
JB
1494 );
1495 }
1496 }
1497 return configuration;
1498 }
1499
52952bf8
JB
1500 private saveConnectorsStatus() {
1501 if (this.getOcppPersistentConfiguration()) {
b1bbdae5 1502 this.saveConfiguration();
52952bf8
JB
1503 }
1504 }
1505
1506 private saveEvsesStatus() {
1507 if (this.getOcppPersistentConfiguration()) {
b1bbdae5 1508 this.saveConfiguration();
52952bf8
JB
1509 }
1510 }
1511
b1bbdae5 1512 private saveConfiguration(): void {
2484ac1e
JB
1513 if (this.configurationFile) {
1514 try {
2484ac1e
JB
1515 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1516 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1517 }
ccb1d6e9 1518 const configurationData: ChargingStationConfiguration =
abe9e9dd 1519 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
b1bbdae5 1520 if (this.stationInfo) {
52952bf8
JB
1521 configurationData.stationInfo = this.stationInfo;
1522 }
b1bbdae5 1523 if (this.ocppConfiguration?.configurationKey) {
52952bf8
JB
1524 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1525 }
b1bbdae5 1526 if (this.connectors.size > 0) {
52952bf8
JB
1527 configurationData.connectorsStatus = [...this.connectors.values()].map(
1528 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1529 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1530 );
1531 }
b1bbdae5 1532 if (this.evses.size > 0) {
52952bf8
JB
1533 configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => {
1534 const status = {
1535 ...evseStatus,
1536 connectorsStatus: [...evseStatus.connectors.values()].map(
1537 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1538 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1539 ),
1540 };
1541 delete status.connectors;
1542 return status as EvseStatusConfiguration;
1543 });
1544 }
7c72977b
JB
1545 delete configurationData.configurationHash;
1546 const configurationHash = crypto
1547 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1548 .update(JSON.stringify(configurationData))
1549 .digest('hex');
1550 if (this.configurationFileHash !== configurationHash) {
1551 configurationData.configurationHash = configurationHash;
1552 const measureId = `${FileType.ChargingStationConfiguration} write`;
1553 const beginId = PerformanceStatistics.beginMeasure(measureId);
1554 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1555 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1556 fs.closeSync(fileDescriptor);
1557 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1558 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1559 this.configurationFileHash = configurationHash;
57adbebc 1560 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1561 } else {
1562 logger.debug(
1563 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1564 this.configurationFile
1565 }`
1566 );
2484ac1e 1567 }
2484ac1e
JB
1568 } catch (error) {
1569 FileUtils.handleFileException(
2484ac1e 1570 this.configurationFile,
7164966d
JB
1571 FileType.ChargingStationConfiguration,
1572 error as NodeJS.ErrnoException,
1573 this.logPrefix()
073bd098
JB
1574 );
1575 }
2484ac1e
JB
1576 } else {
1577 logger.error(
01efc60a 1578 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1579 );
073bd098
JB
1580 }
1581 }
1582
551e477c
JB
1583 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1584 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1585 }
1586
551e477c
JB
1587 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1588 let configuration: ChargingStationConfiguration | undefined;
23290150 1589 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1590 const configurationFromFile = this.getConfigurationFromFile();
1591 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1592 }
648512ce
JB
1593 if (!Utils.isNullOrUndefined(configuration)) {
1594 delete configuration.stationInfo;
1595 delete configuration.configurationHash;
1596 }
073bd098 1597 return configuration;
7dde0b73
JB
1598 }
1599
551e477c
JB
1600 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1601 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1602 this.getOcppConfigurationFromFile();
2484ac1e
JB
1603 if (!ocppConfiguration) {
1604 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1605 }
1606 return ocppConfiguration;
1607 }
1608
c0560973 1609 private async onOpen(): Promise<void> {
56eb297e 1610 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1611 logger.info(
1612 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1613 );
ed6cfcff 1614 if (this.isRegistered() === false) {
5144f4d1
JB
1615 // Send BootNotification
1616 let registrationRetryCount = 0;
1617 do {
f7f98c68 1618 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1619 BootNotificationRequest,
1620 BootNotificationResponse
8bfbc743
JB
1621 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1622 skipBufferingOnError: true,
1623 });
ed6cfcff 1624 if (this.isRegistered() === false) {
5144f4d1
JB
1625 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1626 await Utils.sleep(
1895299d 1627 this?.bootNotificationResponse?.interval
5144f4d1
JB
1628 ? this.bootNotificationResponse.interval * 1000
1629 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1630 );
1631 }
1632 } while (
ed6cfcff 1633 this.isRegistered() === false &&
5144f4d1
JB
1634 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1635 this.getRegistrationMaxRetries() === -1)
1636 );
1637 }
ed6cfcff 1638 if (this.isRegistered() === true) {
23290150 1639 if (this.isInAcceptedState() === true) {
94bb24d5 1640 await this.startMessageSequence();
c0560973 1641 }
5144f4d1
JB
1642 } else {
1643 logger.error(
1644 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1645 );
caad9d6b 1646 }
5144f4d1 1647 this.wsConnectionRestarted = false;
aa428a31 1648 this.autoReconnectRetryCount = 0;
1895299d 1649 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1650 } else {
5144f4d1
JB
1651 logger.warn(
1652 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1653 );
2e6f5966 1654 }
2e6f5966
JB
1655 }
1656
ef7d8c21 1657 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1658 switch (code) {
6c65a295
JB
1659 // Normal close
1660 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1661 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1662 logger.info(
5e3cb728 1663 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1664 code
ef7d8c21 1665 )}' and reason '${reason.toString()}'`
e7aeea18 1666 );
c0560973
JB
1667 this.autoReconnectRetryCount = 0;
1668 break;
6c65a295
JB
1669 // Abnormal close
1670 default:
e7aeea18 1671 logger.error(
5e3cb728 1672 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1673 code
ef7d8c21 1674 )}' and reason '${reason.toString()}'`
e7aeea18 1675 );
56eb297e 1676 this.started === true && (await this.reconnect());
c0560973
JB
1677 break;
1678 }
1895299d 1679 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1680 }
1681
56d09fd7
JB
1682 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1683 const cachedRequest = this.requests.get(messageId);
1684 if (Array.isArray(cachedRequest) === true) {
1685 return cachedRequest;
1686 }
1687 throw new OCPPError(
1688 ErrorType.PROTOCOL_ERROR,
1689 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1690 messageType
1691 )} is not an array`,
1692 undefined,
617cad0c 1693 cachedRequest as JsonType
56d09fd7
JB
1694 );
1695 }
1696
1697 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1698 const [messageType, messageId, commandName, commandPayload] = request;
1699 if (this.getEnableStatistics() === true) {
1700 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1701 }
1702 logger.debug(
1703 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1704 request
1705 )}`
1706 );
1707 // Process the message
1708 await this.ocppIncomingRequestService.incomingRequestHandler(
1709 this,
1710 messageId,
1711 commandName,
1712 commandPayload
1713 );
1714 }
1715
1716 private handleResponseMessage(response: Response): void {
1717 const [messageType, messageId, commandPayload] = response;
1718 if (this.requests.has(messageId) === false) {
1719 // Error
1720 throw new OCPPError(
1721 ErrorType.INTERNAL_ERROR,
1722 `Response for unknown message id ${messageId}`,
1723 undefined,
1724 commandPayload
1725 );
1726 }
1727 // Respond
1728 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1729 messageType,
1730 messageId
1731 );
1732 logger.debug(
1733 `${this.logPrefix()} << Command '${
1734 requestCommandName ?? Constants.UNKNOWN_COMMAND
1735 }' received response payload: ${JSON.stringify(response)}`
1736 );
1737 responseCallback(commandPayload, requestPayload);
1738 }
1739
1740 private handleErrorMessage(errorResponse: ErrorResponse): void {
1741 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1742 if (this.requests.has(messageId) === false) {
1743 // Error
1744 throw new OCPPError(
1745 ErrorType.INTERNAL_ERROR,
1746 `Error response for unknown message id ${messageId}`,
1747 undefined,
1748 { errorType, errorMessage, errorDetails }
1749 );
1750 }
1751 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1752 logger.debug(
1753 `${this.logPrefix()} << Command '${
1754 requestCommandName ?? Constants.UNKNOWN_COMMAND
1755 }' received error response payload: ${JSON.stringify(errorResponse)}`
1756 );
1757 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1758 }
1759
ef7d8c21 1760 private async onMessage(data: RawData): Promise<void> {
56d09fd7 1761 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 1762 let messageType: number;
ded57f02 1763 let errorMsg: string;
c0560973 1764 try {
56d09fd7 1765 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1766 if (Array.isArray(request) === true) {
56d09fd7 1767 [messageType] = request;
b3ec7bc1
JB
1768 // Check the type of message
1769 switch (messageType) {
1770 // Incoming Message
1771 case MessageType.CALL_MESSAGE:
56d09fd7 1772 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1773 break;
56d09fd7 1774 // Response Message
b3ec7bc1 1775 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1776 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1777 break;
1778 // Error Message
1779 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1780 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1781 break;
56d09fd7 1782 // Unknown Message
b3ec7bc1
JB
1783 default:
1784 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
1785 errorMsg = `Wrong message type ${messageType}`;
1786 logger.error(`${this.logPrefix()} ${errorMsg}`);
1787 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 1788 }
1895299d 1789 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1790 } else {
53e5fd67 1791 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1792 request,
ac54a9bb 1793 });
47e22477 1794 }
c0560973 1795 } catch (error) {
56d09fd7
JB
1796 let commandName: IncomingRequestCommand;
1797 let requestCommandName: RequestCommand | IncomingRequestCommand;
1798 let errorCallback: ErrorCallback;
1799 const [, messageId] = request;
13701f69
JB
1800 switch (messageType) {
1801 case MessageType.CALL_MESSAGE:
56d09fd7 1802 [, , commandName] = request as IncomingRequest;
13701f69 1803 // Send error
56d09fd7 1804 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1805 break;
1806 case MessageType.CALL_RESULT_MESSAGE:
1807 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
1808 if (this.requests.has(messageId) === true) {
1809 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
1810 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1811 errorCallback(error as OCPPError, false);
1812 } else {
1813 // Remove the request from the cache in case of error at response handling
1814 this.requests.delete(messageId);
1815 }
de4cb8b6 1816 break;
ba7965c4 1817 }
56d09fd7
JB
1818 if (error instanceof OCPPError === false) {
1819 logger.warn(
1820 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1821 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1822 }' message '${data.toString()}' handling is not an OCPPError:`,
1823 error
1824 );
1825 }
1826 logger.error(
1827 `${this.logPrefix()} Incoming OCPP command '${
1828 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1829 }' message '${data.toString()}'${
1830 messageType !== MessageType.CALL_MESSAGE
1831 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1832 : ''
1833 } processing error:`,
1834 error
1835 );
c0560973 1836 }
2328be1e
JB
1837 }
1838
c0560973 1839 private onPing(): void {
44eb6026 1840 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1841 }
1842
1843 private onPong(): void {
44eb6026 1844 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1845 }
1846
9534e74e 1847 private onError(error: WSError): void {
bcc9c3c0 1848 this.closeWSConnection();
44eb6026 1849 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1850 }
1851
18bf8274 1852 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1853 if (this.getMeteringPerTransaction() === true) {
07989fad 1854 return (
18bf8274 1855 (rounded === true
07989fad
JB
1856 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1857 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1858 );
1859 }
1860 return (
18bf8274 1861 (rounded === true
07989fad
JB
1862 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1863 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1864 );
1865 }
1866
bb83b5ed 1867 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
b1bbdae5 1868 return (stationInfo ?? this.stationInfo)?.useConnectorId0 ?? true;
8bce55bf
JB
1869 }
1870
60ddad53 1871 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158
JB
1872 if (this.hasEvses) {
1873 for (const evseStatus of this.evses.values()) {
1874 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1875 if (connectorStatus.transactionStarted === true) {
1876 await this.stopTransactionOnConnector(connectorId, reason);
1877 }
1878 }
1879 }
1880 } else {
1881 for (const connectorId of this.connectors.keys()) {
1882 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1883 await this.stopTransactionOnConnector(connectorId, reason);
1884 }
60ddad53
JB
1885 }
1886 }
1887 }
1888
1f761b9a 1889 // 0 for disabling
c72f6634 1890 private getConnectionTimeout(): number {
17ac262c
JB
1891 if (
1892 ChargingStationConfigurationUtils.getConfigurationKey(
1893 this,
1894 StandardParametersKey.ConnectionTimeOut
1895 )
1896 ) {
e7aeea18 1897 return (
17ac262c
JB
1898 parseInt(
1899 ChargingStationConfigurationUtils.getConfigurationKey(
1900 this,
1901 StandardParametersKey.ConnectionTimeOut
1902 ).value
1903 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1904 );
291cb255 1905 }
291cb255 1906 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1907 }
1908
1f761b9a 1909 // -1 for unlimited, 0 for disabling
72092cfc 1910 private getAutoReconnectMaxRetries(): number | undefined {
b1bbdae5
JB
1911 return (
1912 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
1913 );
3574dfd3
JB
1914 }
1915
ec977daf 1916 // 0 for disabling
72092cfc 1917 private getRegistrationMaxRetries(): number | undefined {
b1bbdae5 1918 return this.stationInfo.registrationMaxRetries ?? -1;
32a1eb7a
JB
1919 }
1920
c0560973 1921 private getPowerDivider(): number {
b1bbdae5 1922 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
fa7bccf4 1923 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1924 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1925 }
1926 return powerDivider;
1927 }
1928
fa7bccf4
JB
1929 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1930 const maximumPower = this.getMaximumPower(stationInfo);
1931 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1932 case CurrentType.AC:
1933 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1934 this.getNumberOfPhases(stationInfo),
b1bbdae5 1935 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
fa7bccf4 1936 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1937 );
1938 case CurrentType.DC:
fa7bccf4 1939 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1940 }
1941 }
1942
cc6e8ab5
JB
1943 private getAmperageLimitation(): number | undefined {
1944 if (
5a2a53cf 1945 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1946 ChargingStationConfigurationUtils.getConfigurationKey(
1947 this,
1948 this.stationInfo.amperageLimitationOcppKey
1949 )
cc6e8ab5
JB
1950 ) {
1951 return (
1952 Utils.convertToInt(
17ac262c
JB
1953 ChargingStationConfigurationUtils.getConfigurationKey(
1954 this,
1955 this.stationInfo.amperageLimitationOcppKey
72092cfc 1956 )?.value
17ac262c 1957 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1958 );
1959 }
1960 }
1961
c0560973 1962 private async startMessageSequence(): Promise<void> {
b7f9e41d 1963 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1964 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1965 BootNotificationRequest,
1966 BootNotificationResponse
8bfbc743
JB
1967 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1968 skipBufferingOnError: true,
1969 });
6114e6f1 1970 }
136c90ba 1971 // Start WebSocket ping
c0560973 1972 this.startWebSocketPing();
5ad8570f 1973 // Start heartbeat
c0560973 1974 this.startHeartbeat();
0a60c33c 1975 // Initialize connectors status
c3b83130
JB
1976 if (this.hasEvses) {
1977 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
1978 if (evseId > 0) {
1979 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1980 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
1981 this,
1982 connectorId,
1983 connectorStatus
1984 );
1985 await OCPPServiceUtils.sendAndSetConnectorStatus(
1986 this,
1987 connectorId,
1988 connectorBootStatus
1989 );
1990 }
c3b83130 1991 }
4334db72
JB
1992 }
1993 } else {
1994 for (const connectorId of this.connectors.keys()) {
1995 if (connectorId > 0) {
c3b83130
JB
1996 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
1997 this,
1998 connectorId,
4334db72 1999 this.getConnectorStatus(connectorId)
c3b83130
JB
2000 );
2001 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2002 }
2003 }
5ad8570f 2004 }
c9a4f9ea
JB
2005 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2006 await this.ocppRequestService.requestHandler<
2007 FirmwareStatusNotificationRequest,
2008 FirmwareStatusNotificationResponse
2009 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2010 status: FirmwareStatus.Installed,
2011 });
2012 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2013 }
3637ca2c 2014
0a60c33c 2015 // Start the ATG
60ddad53 2016 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 2017 this.startAutomaticTransactionGenerator();
fa7bccf4 2018 }
aa428a31 2019 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2020 }
2021
e7aeea18
JB
2022 private async stopMessageSequence(
2023 reason: StopTransactionReason = StopTransactionReason.NONE
2024 ): Promise<void> {
136c90ba 2025 // Stop WebSocket ping
c0560973 2026 this.stopWebSocketPing();
79411696 2027 // Stop heartbeat
c0560973 2028 this.stopHeartbeat();
fa7bccf4 2029 // Stop ongoing transactions
b20eb107 2030 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
2031 this.stopAutomaticTransactionGenerator();
2032 } else {
2033 await this.stopRunningTransactions(reason);
79411696 2034 }
039211f9
JB
2035 if (this.hasEvses) {
2036 for (const [evseId, evseStatus] of this.evses) {
2037 if (evseId > 0) {
2038 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2039 await this.ocppRequestService.requestHandler<
2040 StatusNotificationRequest,
2041 StatusNotificationResponse
2042 >(
2043 this,
2044 RequestCommand.STATUS_NOTIFICATION,
2045 OCPPServiceUtils.buildStatusNotificationRequest(
2046 this,
2047 connectorId,
2048 ConnectorStatusEnum.Unavailable
2049 )
2050 );
2051 delete connectorStatus?.status;
2052 }
2053 }
2054 }
2055 } else {
2056 for (const connectorId of this.connectors.keys()) {
2057 if (connectorId > 0) {
2058 await this.ocppRequestService.requestHandler<
2059 StatusNotificationRequest,
2060 StatusNotificationResponse
2061 >(
6e939d9e 2062 this,
039211f9
JB
2063 RequestCommand.STATUS_NOTIFICATION,
2064 OCPPServiceUtils.buildStatusNotificationRequest(
2065 this,
2066 connectorId,
2067 ConnectorStatusEnum.Unavailable
2068 )
2069 );
2070 delete this.getConnectorStatus(connectorId)?.status;
2071 }
45c0ae82
JB
2072 }
2073 }
79411696
JB
2074 }
2075
c0560973 2076 private startWebSocketPing(): void {
17ac262c
JB
2077 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2078 this,
e7aeea18
JB
2079 StandardParametersKey.WebSocketPingInterval
2080 )
2081 ? Utils.convertToInt(
17ac262c
JB
2082 ChargingStationConfigurationUtils.getConfigurationKey(
2083 this,
2084 StandardParametersKey.WebSocketPingInterval
72092cfc 2085 )?.value
e7aeea18 2086 )
9cd3dfb0 2087 : 0;
ad2f27c3
JB
2088 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2089 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2090 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2091 this.wsConnection?.ping();
136c90ba
JB
2092 }
2093 }, webSocketPingInterval * 1000);
e7aeea18 2094 logger.info(
44eb6026
JB
2095 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2096 webSocketPingInterval
2097 )}`
e7aeea18 2098 );
ad2f27c3 2099 } else if (this.webSocketPingSetInterval) {
e7aeea18 2100 logger.info(
44eb6026
JB
2101 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2102 webSocketPingInterval
2103 )}`
e7aeea18 2104 );
136c90ba 2105 } else {
e7aeea18 2106 logger.error(
8f953431 2107 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
e7aeea18 2108 );
136c90ba
JB
2109 }
2110 }
2111
c0560973 2112 private stopWebSocketPing(): void {
ad2f27c3
JB
2113 if (this.webSocketPingSetInterval) {
2114 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2115 delete this.webSocketPingSetInterval;
136c90ba
JB
2116 }
2117 }
2118
1f5df42a 2119 private getConfiguredSupervisionUrl(): URL {
72092cfc 2120 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 2121 if (Utils.isNotEmptyArray(supervisionUrls)) {
269de583 2122 let configuredSupervisionUrlIndex: number;
2dcfe98e 2123 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2124 case SupervisionUrlDistribution.RANDOM:
269de583 2125 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2dcfe98e 2126 break;
a52a6446 2127 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2128 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2129 default:
a52a6446
JB
2130 Object.values(SupervisionUrlDistribution).includes(
2131 Configuration.getSupervisionUrlDistribution()
2132 ) === false &&
2133 logger.error(
2134 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2135 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2136 }`
2137 );
269de583 2138 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 2139 break;
c0560973 2140 }
269de583 2141 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
c0560973 2142 }
57939a9d 2143 return new URL(supervisionUrls as string);
136c90ba
JB
2144 }
2145
c0560973 2146 private stopHeartbeat(): void {
ad2f27c3
JB
2147 if (this.heartbeatSetInterval) {
2148 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2149 delete this.heartbeatSetInterval;
7dde0b73 2150 }
5ad8570f
JB
2151 }
2152
55516218 2153 private terminateWSConnection(): void {
56eb297e 2154 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2155 this.wsConnection?.terminate();
55516218
JB
2156 this.wsConnection = null;
2157 }
2158 }
2159
c72f6634 2160 private getReconnectExponentialDelay(): boolean {
a14885a3 2161 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2162 }
2163
aa428a31 2164 private async reconnect(): Promise<void> {
7874b0b1
JB
2165 // Stop WebSocket ping
2166 this.stopWebSocketPing();
136c90ba 2167 // Stop heartbeat
c0560973 2168 this.stopHeartbeat();
5ad8570f 2169 // Stop the ATG if needed
6d9876e7 2170 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2171 this.stopAutomaticTransactionGenerator();
ad2f27c3 2172 }
e7aeea18
JB
2173 if (
2174 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2175 this.getAutoReconnectMaxRetries() === -1
2176 ) {
ad2f27c3 2177 this.autoReconnectRetryCount++;
e7aeea18
JB
2178 const reconnectDelay = this.getReconnectExponentialDelay()
2179 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2180 : this.getConnectionTimeout() * 1000;
1e080116
JB
2181 const reconnectDelayWithdraw = 1000;
2182 const reconnectTimeout =
2183 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2184 ? reconnectDelay - reconnectDelayWithdraw
2185 : 0;
e7aeea18 2186 logger.error(
d56ea27c 2187 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2188 reconnectDelay,
2189 2
2190 )}ms, timeout ${reconnectTimeout}ms`
2191 );
032d6efc 2192 await Utils.sleep(reconnectDelay);
e7aeea18 2193 logger.error(
44eb6026 2194 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2195 );
2196 this.openWSConnection(
59b6ed8d 2197 {
abe9e9dd 2198 ...(this.stationInfo?.wsOptions ?? {}),
59b6ed8d
JB
2199 handshakeTimeout: reconnectTimeout,
2200 },
1e080116 2201 { closeOpened: true }
e7aeea18 2202 );
265e4266 2203 this.wsConnectionRestarted = true;
c0560973 2204 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2205 logger.error(
d56ea27c 2206 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2207 this.autoReconnectRetryCount
d56ea27c 2208 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2209 );
5ad8570f
JB
2210 }
2211 }
2212
551e477c
JB
2213 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2214 | AutomaticTransactionGeneratorConfiguration
2215 | undefined {
2216 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
fa7bccf4 2217 }
7dde0b73 2218}