feat: add support for evses in all identified code paths
[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(),
da57964c 331 this.getAmperageLimitation() * this.getNumberOfConnectors()
cc6e8ab5 332 )
4160ae28 333 : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) /
fa7bccf4 334 this.powerDivider;
cc6e8ab5 335 }
fa7bccf4 336 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
15068be9
JB
337 const connectorChargingProfilesPowerLimit =
338 ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
ad8537a7
JB
339 return Math.min(
340 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
341 isNaN(connectorAmperageLimitationPowerLimit)
342 ? Infinity
343 : connectorAmperageLimitationPowerLimit,
15068be9 344 isNaN(connectorChargingProfilesPowerLimit) ? Infinity : connectorChargingProfilesPowerLimit
ad8537a7 345 );
cc6e8ab5
JB
346 }
347
6e0964c8 348 public getTransactionIdTag(transactionId: number): string | undefined {
28e78158
JB
349 if (this.hasEvses) {
350 for (const evseStatus of this.evses.values()) {
351 for (const connectorStatus of evseStatus.connectors.values()) {
352 if (connectorStatus.transactionId === transactionId) {
353 return connectorStatus.transactionIdTag;
354 }
355 }
356 }
357 } else {
358 for (const connectorId of this.connectors.keys()) {
359 if (
360 connectorId > 0 &&
361 this.getConnectorStatus(connectorId)?.transactionId === transactionId
362 ) {
363 return this.getConnectorStatus(connectorId)?.transactionIdTag;
364 }
c0560973
JB
365 }
366 }
367 }
368
ded57f02
JB
369 public getNumberOfRunningTransactions(): number {
370 let trxCount = 0;
371 if (this.hasEvses) {
372 for (const evseStatus of this.evses.values()) {
373 for (const connectorStatus of evseStatus.connectors.values()) {
374 if (connectorStatus.transactionStarted === true) {
375 ++trxCount;
376 }
377 }
378 }
379 } else {
380 for (const connectorId of this.connectors.keys()) {
381 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
382 ++trxCount;
383 }
384 }
385 }
386 return trxCount;
387 }
388
6ed92bc1 389 public getOutOfOrderEndMeterValues(): boolean {
ccb1d6e9 390 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
6ed92bc1
JB
391 }
392
393 public getBeginEndMeterValues(): boolean {
ccb1d6e9 394 return this.stationInfo?.beginEndMeterValues ?? false;
6ed92bc1
JB
395 }
396
397 public getMeteringPerTransaction(): boolean {
ccb1d6e9 398 return this.stationInfo?.meteringPerTransaction ?? true;
6ed92bc1
JB
399 }
400
fd0c36fa 401 public getTransactionDataMeterValues(): boolean {
ccb1d6e9 402 return this.stationInfo?.transactionDataMeterValues ?? false;
fd0c36fa
JB
403 }
404
9ccca265 405 public getMainVoltageMeterValues(): boolean {
ccb1d6e9 406 return this.stationInfo?.mainVoltageMeterValues ?? true;
9ccca265
JB
407 }
408
6b10669b 409 public getPhaseLineToLineVoltageMeterValues(): boolean {
ccb1d6e9 410 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
411 }
412
7bc31f9c 413 public getCustomValueLimitationMeterValues(): boolean {
ccb1d6e9 414 return this.stationInfo?.customValueLimitationMeterValues ?? true;
7bc31f9c
JB
415 }
416
f479a792 417 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
28e78158
JB
418 if (this.hasEvses) {
419 for (const evseStatus of this.evses.values()) {
420 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
421 if (connectorStatus.transactionId === transactionId) {
422 return connectorId;
423 }
424 }
425 }
426 } else {
427 for (const connectorId of this.connectors.keys()) {
428 if (
429 connectorId > 0 &&
430 this.getConnectorStatus(connectorId)?.transactionId === transactionId
431 ) {
432 return connectorId;
433 }
c0560973
JB
434 }
435 }
436 }
437
07989fad
JB
438 public getEnergyActiveImportRegisterByTransactionId(
439 transactionId: number,
18bf8274 440 rounded = false
07989fad
JB
441 ): number {
442 return this.getEnergyActiveImportRegister(
443 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
18bf8274 444 rounded
cbad1217 445 );
cbad1217
JB
446 }
447
18bf8274
JB
448 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
449 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
6ed92bc1
JB
450 }
451
c0560973 452 public getAuthorizeRemoteTxRequests(): boolean {
17ac262c
JB
453 const authorizeRemoteTxRequests = ChargingStationConfigurationUtils.getConfigurationKey(
454 this,
e7aeea18
JB
455 StandardParametersKey.AuthorizeRemoteTxRequests
456 );
457 return authorizeRemoteTxRequests
458 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
459 : false;
c0560973
JB
460 }
461
462 public getLocalAuthListEnabled(): boolean {
17ac262c
JB
463 const localAuthListEnabled = ChargingStationConfigurationUtils.getConfigurationKey(
464 this,
e7aeea18
JB
465 StandardParametersKey.LocalAuthListEnabled
466 );
c0560973
JB
467 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
468 }
469
8f953431
JB
470 public getHeartbeatInterval(): number {
471 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
472 this,
473 StandardParametersKey.HeartbeatInterval
474 );
475 if (HeartbeatInterval) {
476 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
477 }
478 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
479 this,
480 StandardParametersKey.HeartBeatInterval
481 );
482 if (HeartBeatInterval) {
483 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
484 }
485 this.stationInfo?.autoRegister === false &&
486 logger.warn(
487 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
488 Constants.DEFAULT_HEARTBEAT_INTERVAL
489 }`
490 );
491 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
492 }
493
269de583
JB
494 public setSupervisionUrl(url: string): void {
495 if (
496 this.getSupervisionUrlOcppConfiguration() &&
497 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey())
498 ) {
499 ChargingStationConfigurationUtils.setConfigurationKeyValue(
500 this,
501 this.getSupervisionUrlOcppKey(),
502 url
503 );
504 } else {
505 this.stationInfo.supervisionUrls = url;
506 this.saveStationInfo();
507 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
508 }
509 }
510
c0560973 511 public startHeartbeat(): void {
5b43123f 512 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
6a8329b4
JB
513 this.heartbeatSetInterval = setInterval(() => {
514 this.ocppRequestService
515 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 516 .catch((error) => {
6a8329b4
JB
517 logger.error(
518 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
519 error
520 );
521 });
c0560973 522 }, this.getHeartbeatInterval());
e7aeea18 523 logger.info(
44eb6026
JB
524 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
525 this.getHeartbeatInterval()
526 )}`
e7aeea18 527 );
c0560973 528 } else if (this.heartbeatSetInterval) {
e7aeea18 529 logger.info(
44eb6026
JB
530 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
531 this.getHeartbeatInterval()
532 )}`
e7aeea18 533 );
c0560973 534 } else {
e7aeea18 535 logger.error(
8f953431 536 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
e7aeea18 537 );
c0560973
JB
538 }
539 }
540
541 public restartHeartbeat(): void {
542 // Stop heartbeat
543 this.stopHeartbeat();
544 // Start heartbeat
545 this.startHeartbeat();
546 }
547
17ac262c
JB
548 public restartWebSocketPing(): void {
549 // Stop WebSocket ping
550 this.stopWebSocketPing();
551 // Start WebSocket ping
552 this.startWebSocketPing();
553 }
554
c0560973
JB
555 public startMeterValues(connectorId: number, interval: number): void {
556 if (connectorId === 0) {
e7aeea18 557 logger.error(
2585c6e9 558 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
e7aeea18 559 );
c0560973
JB
560 return;
561 }
734d790d 562 if (!this.getConnectorStatus(connectorId)) {
e7aeea18 563 logger.error(
2585c6e9 564 `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}`
e7aeea18 565 );
c0560973
JB
566 return;
567 }
5e3cb728 568 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18 569 logger.error(
2585c6e9 570 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
e7aeea18 571 );
c0560973 572 return;
e7aeea18 573 } else if (
5e3cb728 574 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
d812bdcb 575 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
e7aeea18
JB
576 ) {
577 logger.error(
2585c6e9 578 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
e7aeea18 579 );
c0560973
JB
580 return;
581 }
582 if (interval > 0) {
6a8329b4
JB
583 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => {
584 // FIXME: Implement OCPP version agnostic helpers
585 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
586 this,
587 connectorId,
588 this.getConnectorStatus(connectorId).transactionId,
589 interval
590 );
591 this.ocppRequestService
592 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
08f130a0 593 this,
f22266fd
JB
594 RequestCommand.METER_VALUES,
595 {
596 connectorId,
72092cfc 597 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
f22266fd
JB
598 meterValue: [meterValue],
599 }
6a8329b4 600 )
72092cfc 601 .catch((error) => {
6a8329b4
JB
602 logger.error(
603 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
604 error
605 );
606 });
607 }, interval);
c0560973 608 } else {
e7aeea18
JB
609 logger.error(
610 `${this.logPrefix()} Charging station ${
611 StandardParametersKey.MeterValueSampleInterval
910c3f0c 612 } configuration set to ${interval}, not sending MeterValues`
e7aeea18 613 );
c0560973
JB
614 }
615 }
616
04b1261c
JB
617 public stopMeterValues(connectorId: number) {
618 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
619 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
620 }
621 }
622
c0560973 623 public start(): void {
0d8852a5
JB
624 if (this.started === false) {
625 if (this.starting === false) {
626 this.starting = true;
ad774cec 627 if (this.getEnableStatistics() === true) {
551e477c 628 this.performanceStatistics?.start();
0d8852a5
JB
629 }
630 this.openWSConnection();
631 // Monitor charging station template file
632 this.templateFileWatcher = FileUtils.watchJsonFile(
0d8852a5 633 this.templateFile,
7164966d
JB
634 FileType.ChargingStationTemplate,
635 this.logPrefix(),
636 undefined,
0d8852a5 637 (event, filename): void => {
5a2a53cf 638 if (Utils.isNotEmptyString(filename) && event === 'change') {
0d8852a5
JB
639 try {
640 logger.debug(
641 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
642 this.templateFile
643 } file have changed, reload`
644 );
645 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
646 // Initialize
647 this.initialize();
648 // Restart the ATG
649 this.stopAutomaticTransactionGenerator();
650 if (
651 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
652 ) {
653 this.startAutomaticTransactionGenerator();
654 }
ad774cec 655 if (this.getEnableStatistics() === true) {
551e477c 656 this.performanceStatistics?.restart();
0d8852a5 657 } else {
551e477c 658 this.performanceStatistics?.stop();
0d8852a5
JB
659 }
660 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
661 } catch (error) {
662 logger.error(
663 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
664 error
665 );
950b1349 666 }
a95873d8 667 }
a95873d8 668 }
0d8852a5 669 );
56eb297e 670 this.started = true;
1895299d 671 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
0d8852a5
JB
672 this.starting = false;
673 } else {
674 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
675 }
950b1349 676 } else {
0d8852a5 677 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 678 }
c0560973
JB
679 }
680
60ddad53 681 public async stop(reason?: StopTransactionReason): Promise<void> {
0d8852a5
JB
682 if (this.started === true) {
683 if (this.stopping === false) {
684 this.stopping = true;
685 await this.stopMessageSequence(reason);
0d8852a5 686 this.closeWSConnection();
ad774cec 687 if (this.getEnableStatistics() === true) {
551e477c 688 this.performanceStatistics?.stop();
0d8852a5
JB
689 }
690 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 691 this.templateFileWatcher?.close();
0d8852a5 692 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
cdde2cfe 693 delete this.bootNotificationResponse;
0d8852a5 694 this.started = false;
1895299d 695 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
0d8852a5
JB
696 this.stopping = false;
697 } else {
698 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 699 }
950b1349 700 } else {
0d8852a5 701 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 702 }
c0560973
JB
703 }
704
60ddad53
JB
705 public async reset(reason?: StopTransactionReason): Promise<void> {
706 await this.stop(reason);
94ec7e96 707 await Utils.sleep(this.stationInfo.resetTime);
fa7bccf4 708 this.initialize();
94ec7e96
JB
709 this.start();
710 }
711
17ac262c
JB
712 public saveOcppConfiguration(): void {
713 if (this.getOcppPersistentConfiguration()) {
52952bf8 714 this.saveConfiguration({ stationInfo: false, connectors: false, evses: false });
e6895390
JB
715 }
716 }
717
72092cfc 718 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
17ac262c
JB
719 return ChargingStationConfigurationUtils.getConfigurationKey(
720 this,
721 StandardParametersKey.SupportedFeatureProfiles
72092cfc 722 )?.value?.includes(featureProfile);
68cb8b91
JB
723 }
724
8e242273
JB
725 public bufferMessage(message: string): void {
726 this.messageBuffer.add(message);
3ba2381e
JB
727 }
728
db2336d9 729 public openWSConnection(
abe9e9dd 730 options: WsOptions = this.stationInfo?.wsOptions ?? {},
db2336d9
JB
731 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
732 closeOpened: false,
733 terminateOpened: false,
734 }
735 ): void {
736 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
737 params.closeOpened = params?.closeOpened ?? false;
738 params.terminateOpened = params?.terminateOpened ?? false;
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()) {
52952bf8 1035 this.saveConfiguration({ ocppConfiguration: false, connectors: false, evses: false });
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()) {
1502 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, evses: false });
1503 }
1504 }
1505
1506 private saveEvsesStatus() {
1507 if (this.getOcppPersistentConfiguration()) {
1508 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, connectors: false });
1509 }
1510 }
1511
1512 private saveConfiguration(
1513 params: {
1514 stationInfo?: boolean;
1515 ocppConfiguration?: boolean;
1516 connectors?: boolean;
1517 evses?: boolean;
1518 } = { stationInfo: true, ocppConfiguration: true, connectors: true, evses: true }
1519 ): void {
2484ac1e 1520 if (this.configurationFile) {
52952bf8
JB
1521 params = {
1522 ...params,
1523 ...{ stationInfo: true, ocppConfiguration: true, connectors: true, evses: true },
1524 };
2484ac1e 1525 try {
2484ac1e
JB
1526 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1527 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1528 }
ccb1d6e9 1529 const configurationData: ChargingStationConfiguration =
abe9e9dd 1530 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
52952bf8
JB
1531 if (params.stationInfo && this.stationInfo) {
1532 configurationData.stationInfo = this.stationInfo;
1533 }
1534 if (params.ocppConfiguration && this.ocppConfiguration?.configurationKey) {
1535 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1536 }
1537 if (params.connectors && this.connectors.size > 0) {
1538 configurationData.connectorsStatus = [...this.connectors.values()].map(
1539 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1540 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1541 );
1542 }
1543 if (params.evses && this.evses.size > 0) {
1544 configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => {
1545 const status = {
1546 ...evseStatus,
1547 connectorsStatus: [...evseStatus.connectors.values()].map(
1548 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1549 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1550 ),
1551 };
1552 delete status.connectors;
1553 return status as EvseStatusConfiguration;
1554 });
1555 }
7c72977b
JB
1556 delete configurationData.configurationHash;
1557 const configurationHash = crypto
1558 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1559 .update(JSON.stringify(configurationData))
1560 .digest('hex');
1561 if (this.configurationFileHash !== configurationHash) {
1562 configurationData.configurationHash = configurationHash;
1563 const measureId = `${FileType.ChargingStationConfiguration} write`;
1564 const beginId = PerformanceStatistics.beginMeasure(measureId);
1565 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1566 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1567 fs.closeSync(fileDescriptor);
1568 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1569 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1570 this.configurationFileHash = configurationHash;
57adbebc 1571 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1572 } else {
1573 logger.debug(
1574 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1575 this.configurationFile
1576 }`
1577 );
2484ac1e 1578 }
2484ac1e
JB
1579 } catch (error) {
1580 FileUtils.handleFileException(
2484ac1e 1581 this.configurationFile,
7164966d
JB
1582 FileType.ChargingStationConfiguration,
1583 error as NodeJS.ErrnoException,
1584 this.logPrefix()
073bd098
JB
1585 );
1586 }
2484ac1e
JB
1587 } else {
1588 logger.error(
01efc60a 1589 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1590 );
073bd098
JB
1591 }
1592 }
1593
551e477c
JB
1594 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1595 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1596 }
1597
551e477c
JB
1598 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1599 let configuration: ChargingStationConfiguration | undefined;
23290150 1600 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1601 const configurationFromFile = this.getConfigurationFromFile();
1602 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1603 }
648512ce
JB
1604 if (!Utils.isNullOrUndefined(configuration)) {
1605 delete configuration.stationInfo;
1606 delete configuration.configurationHash;
1607 }
073bd098 1608 return configuration;
7dde0b73
JB
1609 }
1610
551e477c
JB
1611 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1612 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1613 this.getOcppConfigurationFromFile();
2484ac1e
JB
1614 if (!ocppConfiguration) {
1615 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1616 }
1617 return ocppConfiguration;
1618 }
1619
c0560973 1620 private async onOpen(): Promise<void> {
56eb297e 1621 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1622 logger.info(
1623 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1624 );
ed6cfcff 1625 if (this.isRegistered() === false) {
5144f4d1
JB
1626 // Send BootNotification
1627 let registrationRetryCount = 0;
1628 do {
f7f98c68 1629 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1630 BootNotificationRequest,
1631 BootNotificationResponse
8bfbc743
JB
1632 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1633 skipBufferingOnError: true,
1634 });
ed6cfcff 1635 if (this.isRegistered() === false) {
5144f4d1
JB
1636 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1637 await Utils.sleep(
1895299d 1638 this?.bootNotificationResponse?.interval
5144f4d1
JB
1639 ? this.bootNotificationResponse.interval * 1000
1640 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1641 );
1642 }
1643 } while (
ed6cfcff 1644 this.isRegistered() === false &&
5144f4d1
JB
1645 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1646 this.getRegistrationMaxRetries() === -1)
1647 );
1648 }
ed6cfcff 1649 if (this.isRegistered() === true) {
23290150 1650 if (this.isInAcceptedState() === true) {
94bb24d5 1651 await this.startMessageSequence();
c0560973 1652 }
5144f4d1
JB
1653 } else {
1654 logger.error(
1655 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1656 );
caad9d6b 1657 }
5144f4d1 1658 this.wsConnectionRestarted = false;
aa428a31 1659 this.autoReconnectRetryCount = 0;
1895299d 1660 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1661 } else {
5144f4d1
JB
1662 logger.warn(
1663 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1664 );
2e6f5966 1665 }
2e6f5966
JB
1666 }
1667
ef7d8c21 1668 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1669 switch (code) {
6c65a295
JB
1670 // Normal close
1671 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1672 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1673 logger.info(
5e3cb728 1674 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1675 code
ef7d8c21 1676 )}' and reason '${reason.toString()}'`
e7aeea18 1677 );
c0560973
JB
1678 this.autoReconnectRetryCount = 0;
1679 break;
6c65a295
JB
1680 // Abnormal close
1681 default:
e7aeea18 1682 logger.error(
5e3cb728 1683 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1684 code
ef7d8c21 1685 )}' and reason '${reason.toString()}'`
e7aeea18 1686 );
56eb297e 1687 this.started === true && (await this.reconnect());
c0560973
JB
1688 break;
1689 }
1895299d 1690 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1691 }
1692
56d09fd7
JB
1693 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1694 const cachedRequest = this.requests.get(messageId);
1695 if (Array.isArray(cachedRequest) === true) {
1696 return cachedRequest;
1697 }
1698 throw new OCPPError(
1699 ErrorType.PROTOCOL_ERROR,
1700 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1701 messageType
1702 )} is not an array`,
1703 undefined,
617cad0c 1704 cachedRequest as JsonType
56d09fd7
JB
1705 );
1706 }
1707
1708 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1709 const [messageType, messageId, commandName, commandPayload] = request;
1710 if (this.getEnableStatistics() === true) {
1711 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1712 }
1713 logger.debug(
1714 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1715 request
1716 )}`
1717 );
1718 // Process the message
1719 await this.ocppIncomingRequestService.incomingRequestHandler(
1720 this,
1721 messageId,
1722 commandName,
1723 commandPayload
1724 );
1725 }
1726
1727 private handleResponseMessage(response: Response): void {
1728 const [messageType, messageId, commandPayload] = response;
1729 if (this.requests.has(messageId) === false) {
1730 // Error
1731 throw new OCPPError(
1732 ErrorType.INTERNAL_ERROR,
1733 `Response for unknown message id ${messageId}`,
1734 undefined,
1735 commandPayload
1736 );
1737 }
1738 // Respond
1739 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1740 messageType,
1741 messageId
1742 );
1743 logger.debug(
1744 `${this.logPrefix()} << Command '${
1745 requestCommandName ?? Constants.UNKNOWN_COMMAND
1746 }' received response payload: ${JSON.stringify(response)}`
1747 );
1748 responseCallback(commandPayload, requestPayload);
1749 }
1750
1751 private handleErrorMessage(errorResponse: ErrorResponse): void {
1752 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1753 if (this.requests.has(messageId) === false) {
1754 // Error
1755 throw new OCPPError(
1756 ErrorType.INTERNAL_ERROR,
1757 `Error response for unknown message id ${messageId}`,
1758 undefined,
1759 { errorType, errorMessage, errorDetails }
1760 );
1761 }
1762 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1763 logger.debug(
1764 `${this.logPrefix()} << Command '${
1765 requestCommandName ?? Constants.UNKNOWN_COMMAND
1766 }' received error response payload: ${JSON.stringify(errorResponse)}`
1767 );
1768 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1769 }
1770
ef7d8c21 1771 private async onMessage(data: RawData): Promise<void> {
56d09fd7 1772 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 1773 let messageType: number;
ded57f02 1774 let errorMsg: string;
c0560973 1775 try {
56d09fd7 1776 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1777 if (Array.isArray(request) === true) {
56d09fd7 1778 [messageType] = request;
b3ec7bc1
JB
1779 // Check the type of message
1780 switch (messageType) {
1781 // Incoming Message
1782 case MessageType.CALL_MESSAGE:
56d09fd7 1783 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1784 break;
56d09fd7 1785 // Response Message
b3ec7bc1 1786 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1787 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1788 break;
1789 // Error Message
1790 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1791 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1792 break;
56d09fd7 1793 // Unknown Message
b3ec7bc1
JB
1794 default:
1795 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
1796 errorMsg = `Wrong message type ${messageType}`;
1797 logger.error(`${this.logPrefix()} ${errorMsg}`);
1798 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 1799 }
1895299d 1800 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1801 } else {
53e5fd67 1802 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1803 request,
ac54a9bb 1804 });
47e22477 1805 }
c0560973 1806 } catch (error) {
56d09fd7
JB
1807 let commandName: IncomingRequestCommand;
1808 let requestCommandName: RequestCommand | IncomingRequestCommand;
1809 let errorCallback: ErrorCallback;
1810 const [, messageId] = request;
13701f69
JB
1811 switch (messageType) {
1812 case MessageType.CALL_MESSAGE:
56d09fd7 1813 [, , commandName] = request as IncomingRequest;
13701f69 1814 // Send error
56d09fd7 1815 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1816 break;
1817 case MessageType.CALL_RESULT_MESSAGE:
1818 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
1819 if (this.requests.has(messageId) === true) {
1820 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
1821 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1822 errorCallback(error as OCPPError, false);
1823 } else {
1824 // Remove the request from the cache in case of error at response handling
1825 this.requests.delete(messageId);
1826 }
de4cb8b6 1827 break;
ba7965c4 1828 }
56d09fd7
JB
1829 if (error instanceof OCPPError === false) {
1830 logger.warn(
1831 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1832 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1833 }' message '${data.toString()}' handling is not an OCPPError:`,
1834 error
1835 );
1836 }
1837 logger.error(
1838 `${this.logPrefix()} Incoming OCPP command '${
1839 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1840 }' message '${data.toString()}'${
1841 messageType !== MessageType.CALL_MESSAGE
1842 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1843 : ''
1844 } processing error:`,
1845 error
1846 );
c0560973 1847 }
2328be1e
JB
1848 }
1849
c0560973 1850 private onPing(): void {
44eb6026 1851 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1852 }
1853
1854 private onPong(): void {
44eb6026 1855 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1856 }
1857
9534e74e 1858 private onError(error: WSError): void {
bcc9c3c0 1859 this.closeWSConnection();
44eb6026 1860 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1861 }
1862
18bf8274 1863 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1864 if (this.getMeteringPerTransaction() === true) {
07989fad 1865 return (
18bf8274 1866 (rounded === true
07989fad
JB
1867 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1868 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1869 );
1870 }
1871 return (
18bf8274 1872 (rounded === true
07989fad
JB
1873 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1874 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1875 );
1876 }
1877
bb83b5ed 1878 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4 1879 const localStationInfo = stationInfo ?? this.stationInfo;
a14885a3 1880 return localStationInfo?.useConnectorId0 ?? true;
8bce55bf
JB
1881 }
1882
60ddad53 1883 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158
JB
1884 if (this.hasEvses) {
1885 for (const evseStatus of this.evses.values()) {
1886 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1887 if (connectorStatus.transactionStarted === true) {
1888 await this.stopTransactionOnConnector(connectorId, reason);
1889 }
1890 }
1891 }
1892 } else {
1893 for (const connectorId of this.connectors.keys()) {
1894 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1895 await this.stopTransactionOnConnector(connectorId, reason);
1896 }
60ddad53
JB
1897 }
1898 }
1899 }
1900
1f761b9a 1901 // 0 for disabling
c72f6634 1902 private getConnectionTimeout(): number {
17ac262c
JB
1903 if (
1904 ChargingStationConfigurationUtils.getConfigurationKey(
1905 this,
1906 StandardParametersKey.ConnectionTimeOut
1907 )
1908 ) {
e7aeea18 1909 return (
17ac262c
JB
1910 parseInt(
1911 ChargingStationConfigurationUtils.getConfigurationKey(
1912 this,
1913 StandardParametersKey.ConnectionTimeOut
1914 ).value
1915 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1916 );
291cb255 1917 }
291cb255 1918 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1919 }
1920
1f761b9a 1921 // -1 for unlimited, 0 for disabling
72092cfc 1922 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1923 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1924 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1925 }
1926 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1927 return Configuration.getAutoReconnectMaxRetries();
1928 }
1929 return -1;
1930 }
1931
ec977daf 1932 // 0 for disabling
72092cfc 1933 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1934 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1935 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1936 }
1937 return -1;
1938 }
1939
c0560973
JB
1940 private getPowerDivider(): number {
1941 let powerDivider = this.getNumberOfConnectors();
fa7bccf4 1942 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1943 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1944 }
1945 return powerDivider;
1946 }
1947
fa7bccf4
JB
1948 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1949 const maximumPower = this.getMaximumPower(stationInfo);
1950 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1951 case CurrentType.AC:
1952 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1953 this.getNumberOfPhases(stationInfo),
ad8537a7 1954 maximumPower / this.getNumberOfConnectors(),
fa7bccf4 1955 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1956 );
1957 case CurrentType.DC:
fa7bccf4 1958 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1959 }
1960 }
1961
cc6e8ab5
JB
1962 private getAmperageLimitation(): number | undefined {
1963 if (
5a2a53cf 1964 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1965 ChargingStationConfigurationUtils.getConfigurationKey(
1966 this,
1967 this.stationInfo.amperageLimitationOcppKey
1968 )
cc6e8ab5
JB
1969 ) {
1970 return (
1971 Utils.convertToInt(
17ac262c
JB
1972 ChargingStationConfigurationUtils.getConfigurationKey(
1973 this,
1974 this.stationInfo.amperageLimitationOcppKey
72092cfc 1975 )?.value
17ac262c 1976 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1977 );
1978 }
1979 }
1980
c0560973 1981 private async startMessageSequence(): Promise<void> {
b7f9e41d 1982 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1983 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1984 BootNotificationRequest,
1985 BootNotificationResponse
8bfbc743
JB
1986 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1987 skipBufferingOnError: true,
1988 });
6114e6f1 1989 }
136c90ba 1990 // Start WebSocket ping
c0560973 1991 this.startWebSocketPing();
5ad8570f 1992 // Start heartbeat
c0560973 1993 this.startHeartbeat();
0a60c33c 1994 // Initialize connectors status
c3b83130
JB
1995 if (this.hasEvses) {
1996 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
1997 if (evseId > 0) {
1998 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1999 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2000 this,
2001 connectorId,
2002 connectorStatus
2003 );
2004 await OCPPServiceUtils.sendAndSetConnectorStatus(
2005 this,
2006 connectorId,
2007 connectorBootStatus
2008 );
2009 }
c3b83130 2010 }
4334db72
JB
2011 }
2012 } else {
2013 for (const connectorId of this.connectors.keys()) {
2014 if (connectorId > 0) {
c3b83130
JB
2015 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2016 this,
2017 connectorId,
4334db72 2018 this.getConnectorStatus(connectorId)
c3b83130
JB
2019 );
2020 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2021 }
2022 }
5ad8570f 2023 }
c9a4f9ea
JB
2024 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2025 await this.ocppRequestService.requestHandler<
2026 FirmwareStatusNotificationRequest,
2027 FirmwareStatusNotificationResponse
2028 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2029 status: FirmwareStatus.Installed,
2030 });
2031 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2032 }
3637ca2c 2033
0a60c33c 2034 // Start the ATG
60ddad53 2035 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 2036 this.startAutomaticTransactionGenerator();
fa7bccf4 2037 }
aa428a31 2038 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2039 }
2040
e7aeea18
JB
2041 private async stopMessageSequence(
2042 reason: StopTransactionReason = StopTransactionReason.NONE
2043 ): Promise<void> {
136c90ba 2044 // Stop WebSocket ping
c0560973 2045 this.stopWebSocketPing();
79411696 2046 // Stop heartbeat
c0560973 2047 this.stopHeartbeat();
fa7bccf4 2048 // Stop ongoing transactions
b20eb107 2049 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
2050 this.stopAutomaticTransactionGenerator();
2051 } else {
2052 await this.stopRunningTransactions(reason);
79411696 2053 }
039211f9
JB
2054 if (this.hasEvses) {
2055 for (const [evseId, evseStatus] of this.evses) {
2056 if (evseId > 0) {
2057 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2058 await this.ocppRequestService.requestHandler<
2059 StatusNotificationRequest,
2060 StatusNotificationResponse
2061 >(
2062 this,
2063 RequestCommand.STATUS_NOTIFICATION,
2064 OCPPServiceUtils.buildStatusNotificationRequest(
2065 this,
2066 connectorId,
2067 ConnectorStatusEnum.Unavailable
2068 )
2069 );
2070 delete connectorStatus?.status;
2071 }
2072 }
2073 }
2074 } else {
2075 for (const connectorId of this.connectors.keys()) {
2076 if (connectorId > 0) {
2077 await this.ocppRequestService.requestHandler<
2078 StatusNotificationRequest,
2079 StatusNotificationResponse
2080 >(
6e939d9e 2081 this,
039211f9
JB
2082 RequestCommand.STATUS_NOTIFICATION,
2083 OCPPServiceUtils.buildStatusNotificationRequest(
2084 this,
2085 connectorId,
2086 ConnectorStatusEnum.Unavailable
2087 )
2088 );
2089 delete this.getConnectorStatus(connectorId)?.status;
2090 }
45c0ae82
JB
2091 }
2092 }
79411696
JB
2093 }
2094
c0560973 2095 private startWebSocketPing(): void {
17ac262c
JB
2096 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2097 this,
e7aeea18
JB
2098 StandardParametersKey.WebSocketPingInterval
2099 )
2100 ? Utils.convertToInt(
17ac262c
JB
2101 ChargingStationConfigurationUtils.getConfigurationKey(
2102 this,
2103 StandardParametersKey.WebSocketPingInterval
72092cfc 2104 )?.value
e7aeea18 2105 )
9cd3dfb0 2106 : 0;
ad2f27c3
JB
2107 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2108 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2109 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2110 this.wsConnection?.ping();
136c90ba
JB
2111 }
2112 }, webSocketPingInterval * 1000);
e7aeea18 2113 logger.info(
44eb6026
JB
2114 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2115 webSocketPingInterval
2116 )}`
e7aeea18 2117 );
ad2f27c3 2118 } else if (this.webSocketPingSetInterval) {
e7aeea18 2119 logger.info(
44eb6026
JB
2120 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2121 webSocketPingInterval
2122 )}`
e7aeea18 2123 );
136c90ba 2124 } else {
e7aeea18 2125 logger.error(
8f953431 2126 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
e7aeea18 2127 );
136c90ba
JB
2128 }
2129 }
2130
c0560973 2131 private stopWebSocketPing(): void {
ad2f27c3
JB
2132 if (this.webSocketPingSetInterval) {
2133 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2134 delete this.webSocketPingSetInterval;
136c90ba
JB
2135 }
2136 }
2137
1f5df42a 2138 private getConfiguredSupervisionUrl(): URL {
72092cfc 2139 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 2140 if (Utils.isNotEmptyArray(supervisionUrls)) {
269de583 2141 let configuredSupervisionUrlIndex: number;
2dcfe98e 2142 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2143 case SupervisionUrlDistribution.RANDOM:
269de583 2144 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2dcfe98e 2145 break;
a52a6446 2146 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2147 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2148 default:
a52a6446
JB
2149 Object.values(SupervisionUrlDistribution).includes(
2150 Configuration.getSupervisionUrlDistribution()
2151 ) === false &&
2152 logger.error(
2153 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2154 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2155 }`
2156 );
269de583 2157 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 2158 break;
c0560973 2159 }
269de583 2160 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
c0560973 2161 }
57939a9d 2162 return new URL(supervisionUrls as string);
136c90ba
JB
2163 }
2164
c0560973 2165 private stopHeartbeat(): void {
ad2f27c3
JB
2166 if (this.heartbeatSetInterval) {
2167 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2168 delete this.heartbeatSetInterval;
7dde0b73 2169 }
5ad8570f
JB
2170 }
2171
55516218 2172 private terminateWSConnection(): void {
56eb297e 2173 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2174 this.wsConnection?.terminate();
55516218
JB
2175 this.wsConnection = null;
2176 }
2177 }
2178
c72f6634 2179 private getReconnectExponentialDelay(): boolean {
a14885a3 2180 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2181 }
2182
aa428a31 2183 private async reconnect(): Promise<void> {
7874b0b1
JB
2184 // Stop WebSocket ping
2185 this.stopWebSocketPing();
136c90ba 2186 // Stop heartbeat
c0560973 2187 this.stopHeartbeat();
5ad8570f 2188 // Stop the ATG if needed
6d9876e7 2189 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2190 this.stopAutomaticTransactionGenerator();
ad2f27c3 2191 }
e7aeea18
JB
2192 if (
2193 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2194 this.getAutoReconnectMaxRetries() === -1
2195 ) {
ad2f27c3 2196 this.autoReconnectRetryCount++;
e7aeea18
JB
2197 const reconnectDelay = this.getReconnectExponentialDelay()
2198 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2199 : this.getConnectionTimeout() * 1000;
1e080116
JB
2200 const reconnectDelayWithdraw = 1000;
2201 const reconnectTimeout =
2202 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2203 ? reconnectDelay - reconnectDelayWithdraw
2204 : 0;
e7aeea18 2205 logger.error(
d56ea27c 2206 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2207 reconnectDelay,
2208 2
2209 )}ms, timeout ${reconnectTimeout}ms`
2210 );
032d6efc 2211 await Utils.sleep(reconnectDelay);
e7aeea18 2212 logger.error(
44eb6026 2213 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2214 );
2215 this.openWSConnection(
59b6ed8d 2216 {
abe9e9dd 2217 ...(this.stationInfo?.wsOptions ?? {}),
59b6ed8d
JB
2218 handshakeTimeout: reconnectTimeout,
2219 },
1e080116 2220 { closeOpened: true }
e7aeea18 2221 );
265e4266 2222 this.wsConnectionRestarted = true;
c0560973 2223 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2224 logger.error(
d56ea27c 2225 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2226 this.autoReconnectRetryCount
d56ea27c 2227 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2228 );
5ad8570f
JB
2229 }
2230 }
2231
551e477c
JB
2232 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2233 | AutomaticTransactionGeneratorConfiguration
2234 | undefined {
2235 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
fa7bccf4 2236 }
7dde0b73 2237}