fix: fix return at getting connector status 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(),
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
6ed92bc1 369 public getOutOfOrderEndMeterValues(): boolean {
ccb1d6e9 370 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
6ed92bc1
JB
371 }
372
373 public getBeginEndMeterValues(): boolean {
ccb1d6e9 374 return this.stationInfo?.beginEndMeterValues ?? false;
6ed92bc1
JB
375 }
376
377 public getMeteringPerTransaction(): boolean {
ccb1d6e9 378 return this.stationInfo?.meteringPerTransaction ?? true;
6ed92bc1
JB
379 }
380
fd0c36fa 381 public getTransactionDataMeterValues(): boolean {
ccb1d6e9 382 return this.stationInfo?.transactionDataMeterValues ?? false;
fd0c36fa
JB
383 }
384
9ccca265 385 public getMainVoltageMeterValues(): boolean {
ccb1d6e9 386 return this.stationInfo?.mainVoltageMeterValues ?? true;
9ccca265
JB
387 }
388
6b10669b 389 public getPhaseLineToLineVoltageMeterValues(): boolean {
ccb1d6e9 390 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
391 }
392
7bc31f9c 393 public getCustomValueLimitationMeterValues(): boolean {
ccb1d6e9 394 return this.stationInfo?.customValueLimitationMeterValues ?? true;
7bc31f9c
JB
395 }
396
f479a792 397 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
28e78158
JB
398 if (this.hasEvses) {
399 for (const evseStatus of this.evses.values()) {
400 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
401 if (connectorStatus.transactionId === transactionId) {
402 return connectorId;
403 }
404 }
405 }
406 } else {
407 for (const connectorId of this.connectors.keys()) {
408 if (
409 connectorId > 0 &&
410 this.getConnectorStatus(connectorId)?.transactionId === transactionId
411 ) {
412 return connectorId;
413 }
c0560973
JB
414 }
415 }
416 }
417
07989fad
JB
418 public getEnergyActiveImportRegisterByTransactionId(
419 transactionId: number,
18bf8274 420 rounded = false
07989fad
JB
421 ): number {
422 return this.getEnergyActiveImportRegister(
423 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
18bf8274 424 rounded
cbad1217 425 );
cbad1217
JB
426 }
427
18bf8274
JB
428 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
429 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
6ed92bc1
JB
430 }
431
c0560973 432 public getAuthorizeRemoteTxRequests(): boolean {
17ac262c
JB
433 const authorizeRemoteTxRequests = ChargingStationConfigurationUtils.getConfigurationKey(
434 this,
e7aeea18
JB
435 StandardParametersKey.AuthorizeRemoteTxRequests
436 );
437 return authorizeRemoteTxRequests
438 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
439 : false;
c0560973
JB
440 }
441
442 public getLocalAuthListEnabled(): boolean {
17ac262c
JB
443 const localAuthListEnabled = ChargingStationConfigurationUtils.getConfigurationKey(
444 this,
e7aeea18
JB
445 StandardParametersKey.LocalAuthListEnabled
446 );
c0560973
JB
447 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
448 }
449
8f953431
JB
450 public getHeartbeatInterval(): number {
451 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
452 this,
453 StandardParametersKey.HeartbeatInterval
454 );
455 if (HeartbeatInterval) {
456 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
457 }
458 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
459 this,
460 StandardParametersKey.HeartBeatInterval
461 );
462 if (HeartBeatInterval) {
463 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
464 }
465 this.stationInfo?.autoRegister === false &&
466 logger.warn(
467 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
468 Constants.DEFAULT_HEARTBEAT_INTERVAL
469 }`
470 );
471 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
472 }
473
269de583
JB
474 public setSupervisionUrl(url: string): void {
475 if (
476 this.getSupervisionUrlOcppConfiguration() &&
477 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey())
478 ) {
479 ChargingStationConfigurationUtils.setConfigurationKeyValue(
480 this,
481 this.getSupervisionUrlOcppKey(),
482 url
483 );
484 } else {
485 this.stationInfo.supervisionUrls = url;
486 this.saveStationInfo();
487 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
488 }
489 }
490
c0560973 491 public startHeartbeat(): void {
5b43123f 492 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
6a8329b4
JB
493 this.heartbeatSetInterval = setInterval(() => {
494 this.ocppRequestService
495 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 496 .catch((error) => {
6a8329b4
JB
497 logger.error(
498 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
499 error
500 );
501 });
c0560973 502 }, this.getHeartbeatInterval());
e7aeea18 503 logger.info(
44eb6026
JB
504 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
505 this.getHeartbeatInterval()
506 )}`
e7aeea18 507 );
c0560973 508 } else if (this.heartbeatSetInterval) {
e7aeea18 509 logger.info(
44eb6026
JB
510 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
511 this.getHeartbeatInterval()
512 )}`
e7aeea18 513 );
c0560973 514 } else {
e7aeea18 515 logger.error(
8f953431 516 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
e7aeea18 517 );
c0560973
JB
518 }
519 }
520
521 public restartHeartbeat(): void {
522 // Stop heartbeat
523 this.stopHeartbeat();
524 // Start heartbeat
525 this.startHeartbeat();
526 }
527
17ac262c
JB
528 public restartWebSocketPing(): void {
529 // Stop WebSocket ping
530 this.stopWebSocketPing();
531 // Start WebSocket ping
532 this.startWebSocketPing();
533 }
534
c0560973
JB
535 public startMeterValues(connectorId: number, interval: number): void {
536 if (connectorId === 0) {
e7aeea18 537 logger.error(
2585c6e9 538 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
e7aeea18 539 );
c0560973
JB
540 return;
541 }
734d790d 542 if (!this.getConnectorStatus(connectorId)) {
e7aeea18 543 logger.error(
2585c6e9 544 `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}`
e7aeea18 545 );
c0560973
JB
546 return;
547 }
5e3cb728 548 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18 549 logger.error(
2585c6e9 550 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
e7aeea18 551 );
c0560973 552 return;
e7aeea18 553 } else if (
5e3cb728 554 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
d812bdcb 555 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
e7aeea18
JB
556 ) {
557 logger.error(
2585c6e9 558 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
e7aeea18 559 );
c0560973
JB
560 return;
561 }
562 if (interval > 0) {
6a8329b4
JB
563 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => {
564 // FIXME: Implement OCPP version agnostic helpers
565 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
566 this,
567 connectorId,
568 this.getConnectorStatus(connectorId).transactionId,
569 interval
570 );
571 this.ocppRequestService
572 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
08f130a0 573 this,
f22266fd
JB
574 RequestCommand.METER_VALUES,
575 {
576 connectorId,
72092cfc 577 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
f22266fd
JB
578 meterValue: [meterValue],
579 }
6a8329b4 580 )
72092cfc 581 .catch((error) => {
6a8329b4
JB
582 logger.error(
583 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
584 error
585 );
586 });
587 }, interval);
c0560973 588 } else {
e7aeea18
JB
589 logger.error(
590 `${this.logPrefix()} Charging station ${
591 StandardParametersKey.MeterValueSampleInterval
910c3f0c 592 } configuration set to ${interval}, not sending MeterValues`
e7aeea18 593 );
c0560973
JB
594 }
595 }
596
04b1261c
JB
597 public stopMeterValues(connectorId: number) {
598 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
599 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
600 }
601 }
602
c0560973 603 public start(): void {
0d8852a5
JB
604 if (this.started === false) {
605 if (this.starting === false) {
606 this.starting = true;
ad774cec 607 if (this.getEnableStatistics() === true) {
551e477c 608 this.performanceStatistics?.start();
0d8852a5
JB
609 }
610 this.openWSConnection();
611 // Monitor charging station template file
612 this.templateFileWatcher = FileUtils.watchJsonFile(
0d8852a5 613 this.templateFile,
7164966d
JB
614 FileType.ChargingStationTemplate,
615 this.logPrefix(),
616 undefined,
0d8852a5 617 (event, filename): void => {
5a2a53cf 618 if (Utils.isNotEmptyString(filename) && event === 'change') {
0d8852a5
JB
619 try {
620 logger.debug(
621 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
622 this.templateFile
623 } file have changed, reload`
624 );
625 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
626 // Initialize
627 this.initialize();
628 // Restart the ATG
629 this.stopAutomaticTransactionGenerator();
630 if (
631 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
632 ) {
633 this.startAutomaticTransactionGenerator();
634 }
ad774cec 635 if (this.getEnableStatistics() === true) {
551e477c 636 this.performanceStatistics?.restart();
0d8852a5 637 } else {
551e477c 638 this.performanceStatistics?.stop();
0d8852a5
JB
639 }
640 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
641 } catch (error) {
642 logger.error(
643 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
644 error
645 );
950b1349 646 }
a95873d8 647 }
a95873d8 648 }
0d8852a5 649 );
56eb297e 650 this.started = true;
1895299d 651 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
0d8852a5
JB
652 this.starting = false;
653 } else {
654 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
655 }
950b1349 656 } else {
0d8852a5 657 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 658 }
c0560973
JB
659 }
660
60ddad53 661 public async stop(reason?: StopTransactionReason): Promise<void> {
0d8852a5
JB
662 if (this.started === true) {
663 if (this.stopping === false) {
664 this.stopping = true;
665 await this.stopMessageSequence(reason);
0d8852a5 666 this.closeWSConnection();
ad774cec 667 if (this.getEnableStatistics() === true) {
551e477c 668 this.performanceStatistics?.stop();
0d8852a5
JB
669 }
670 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 671 this.templateFileWatcher?.close();
0d8852a5 672 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
cdde2cfe 673 delete this.bootNotificationResponse;
0d8852a5 674 this.started = false;
1895299d 675 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
0d8852a5
JB
676 this.stopping = false;
677 } else {
678 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 679 }
950b1349 680 } else {
0d8852a5 681 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 682 }
c0560973
JB
683 }
684
60ddad53
JB
685 public async reset(reason?: StopTransactionReason): Promise<void> {
686 await this.stop(reason);
94ec7e96 687 await Utils.sleep(this.stationInfo.resetTime);
fa7bccf4 688 this.initialize();
94ec7e96
JB
689 this.start();
690 }
691
17ac262c
JB
692 public saveOcppConfiguration(): void {
693 if (this.getOcppPersistentConfiguration()) {
52952bf8 694 this.saveConfiguration({ stationInfo: false, connectors: false, evses: false });
e6895390
JB
695 }
696 }
697
72092cfc 698 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
17ac262c
JB
699 return ChargingStationConfigurationUtils.getConfigurationKey(
700 this,
701 StandardParametersKey.SupportedFeatureProfiles
72092cfc 702 )?.value?.includes(featureProfile);
68cb8b91
JB
703 }
704
8e242273
JB
705 public bufferMessage(message: string): void {
706 this.messageBuffer.add(message);
3ba2381e
JB
707 }
708
db2336d9 709 public openWSConnection(
abe9e9dd 710 options: WsOptions = this.stationInfo?.wsOptions ?? {},
db2336d9
JB
711 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
712 closeOpened: false,
713 terminateOpened: false,
714 }
715 ): void {
716 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
717 params.closeOpened = params?.closeOpened ?? false;
718 params.terminateOpened = params?.terminateOpened ?? false;
cbf9b878 719 if (this.started === false && this.starting === false) {
d1c6c833
JB
720 logger.warn(
721 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
722 );
723 return;
724 }
db2336d9
JB
725 if (
726 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
727 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
728 ) {
729 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
730 }
731 if (params?.closeOpened) {
732 this.closeWSConnection();
733 }
734 if (params?.terminateOpened) {
735 this.terminateWSConnection();
736 }
db2336d9 737
56eb297e 738 if (this.isWebSocketConnectionOpened() === true) {
0a03f36c
JB
739 logger.warn(
740 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
741 );
742 return;
743 }
744
db2336d9 745 logger.info(
0a03f36c 746 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
db2336d9
JB
747 );
748
feff11ec
JB
749 this.wsConnection = new WebSocket(
750 this.wsConnectionUrl,
751 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
752 options
753 );
db2336d9
JB
754
755 // Handle WebSocket message
756 this.wsConnection.on(
757 'message',
758 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
759 );
760 // Handle WebSocket error
761 this.wsConnection.on(
762 'error',
763 this.onError.bind(this) as (this: WebSocket, error: Error) => void
764 );
765 // Handle WebSocket close
766 this.wsConnection.on(
767 'close',
768 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
769 );
770 // Handle WebSocket open
771 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
772 // Handle WebSocket ping
773 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
774 // Handle WebSocket pong
775 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
776 }
777
778 public closeWSConnection(): void {
56eb297e 779 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 780 this.wsConnection?.close();
db2336d9
JB
781 this.wsConnection = null;
782 }
783 }
784
8f879946
JB
785 public startAutomaticTransactionGenerator(
786 connectorIds?: number[],
787 automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
788 ): void {
789 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
790 automaticTransactionGeneratorConfiguration ??
4f69be04 791 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
8f879946
JB
792 this
793 );
53ac516c 794 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc 795 for (const connectorId of connectorIds) {
551e477c 796 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
797 }
798 } else {
551e477c 799 this.automaticTransactionGenerator?.start();
4f69be04 800 }
1895299d 801 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
802 }
803
a5e9befc 804 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
53ac516c 805 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc
JB
806 for (const connectorId of connectorIds) {
807 this.automaticTransactionGenerator?.stopConnector(connectorId);
808 }
809 } else {
810 this.automaticTransactionGenerator?.stop();
4f69be04 811 }
1895299d 812 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
813 }
814
5e3cb728
JB
815 public async stopTransactionOnConnector(
816 connectorId: number,
817 reason = StopTransactionReason.NONE
818 ): Promise<StopTransactionResponse> {
72092cfc 819 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 820 if (
c7e8e0a2
JB
821 this.getBeginEndMeterValues() === true &&
822 this.getOcppStrictCompliance() === true &&
823 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
824 ) {
825 // FIXME: Implement OCPP version agnostic helpers
826 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
827 this,
828 connectorId,
829 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
830 );
831 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
832 this,
833 RequestCommand.METER_VALUES,
834 {
835 connectorId,
836 transactionId,
837 meterValue: [transactionEndMeterValue],
838 }
839 );
840 }
841 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
842 this,
843 RequestCommand.STOP_TRANSACTION,
844 {
845 transactionId,
846 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
5e3cb728
JB
847 reason,
848 }
849 );
850 }
851
f90c1757 852 private flushMessageBuffer(): void {
8e242273 853 if (this.messageBuffer.size > 0) {
7d3b0f64 854 for (const message of this.messageBuffer.values()) {
1431af78
JB
855 let beginId: string;
856 let commandName: RequestCommand;
8ca6874c 857 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
858 const isRequest = messageType === MessageType.CALL_MESSAGE;
859 if (isRequest) {
860 [, , commandName] = JSON.parse(message) as OutgoingRequest;
861 beginId = PerformanceStatistics.beginMeasure(commandName);
862 }
72092cfc 863 this.wsConnection?.send(message);
1431af78 864 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
8ca6874c
JB
865 logger.debug(
866 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
867 messageType
868 )} payload sent: ${message}`
869 );
8e242273 870 this.messageBuffer.delete(message);
7d3b0f64 871 }
77f00f84
JB
872 }
873 }
874
1f5df42a
JB
875 private getSupervisionUrlOcppConfiguration(): boolean {
876 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
877 }
878
e8e865ea 879 private getSupervisionUrlOcppKey(): string {
6dad8e21 880 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
881 }
882
72092cfc
JB
883 private getTemplateFromFile(): ChargingStationTemplate | undefined {
884 let template: ChargingStationTemplate;
5ad8570f 885 try {
57adbebc
JB
886 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
887 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
7c72977b
JB
888 } else {
889 const measureId = `${FileType.ChargingStationTemplate} read`;
890 const beginId = PerformanceStatistics.beginMeasure(measureId);
891 template = JSON.parse(
892 fs.readFileSync(this.templateFile, 'utf8')
893 ) as ChargingStationTemplate;
894 PerformanceStatistics.endMeasure(measureId, beginId);
895 template.templateHash = crypto
896 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
897 .update(JSON.stringify(template))
898 .digest('hex');
57adbebc 899 this.sharedLRUCache.setChargingStationTemplate(template);
7c72977b 900 }
5ad8570f 901 } catch (error) {
e7aeea18 902 FileUtils.handleFileException(
2484ac1e 903 this.templateFile,
7164966d
JB
904 FileType.ChargingStationTemplate,
905 error as NodeJS.ErrnoException,
906 this.logPrefix()
e7aeea18 907 );
5ad8570f 908 }
2484ac1e
JB
909 return template;
910 }
911
7a3a2ebb 912 private getStationInfoFromTemplate(): ChargingStationInfo {
72092cfc 913 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
fa7bccf4 914 if (Utils.isNullOrUndefined(stationTemplate)) {
eaad6e5c 915 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
ccb1d6e9
JB
916 logger.error(`${this.logPrefix()} ${errorMsg}`);
917 throw new BaseError(errorMsg);
94ec7e96 918 }
fa7bccf4 919 if (Utils.isEmptyObject(stationTemplate)) {
ccb1d6e9
JB
920 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
921 logger.error(`${this.logPrefix()} ${errorMsg}`);
922 throw new BaseError(errorMsg);
94ec7e96 923 }
ae5020a3
JB
924 ChargingStationUtils.warnTemplateKeysDeprecation(
925 this.templateFile,
926 stationTemplate,
927 this.logPrefix()
928 );
fa7bccf4
JB
929 const stationInfo: ChargingStationInfo =
930 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
51c83d6f 931 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
fa7bccf4
JB
932 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
933 this.index,
934 stationTemplate
935 );
72092cfc 936 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fa7bccf4 937 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
53ac516c 938 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
551e477c 939 stationTemplate.power = stationTemplate.power as number[];
fa7bccf4 940 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
cc6e8ab5 941 stationInfo.maximumPower =
72092cfc 942 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
943 ? stationTemplate.power[powerArrayRandomIndex] * 1000
944 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 945 } else {
551e477c 946 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 947 stationInfo.maximumPower =
72092cfc 948 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
949 ? stationTemplate.power * 1000
950 : stationTemplate.power;
951 }
3637ca2c 952 stationInfo.firmwareVersionPattern =
72092cfc 953 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 954 if (
5a2a53cf 955 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
3637ca2c
JB
956 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
957 ) {
958 logger.warn(
959 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
960 this.templateFile
961 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
962 );
963 }
598c886d 964 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 965 {
598c886d
JB
966 versionUpgrade: {
967 step: 1,
968 },
15748260
JB
969 reset: true,
970 },
abe9e9dd 971 stationTemplate?.firmwareUpgrade ?? {}
15748260 972 );
d812bdcb 973 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
fa7bccf4 974 ? stationTemplate.resetTime * 1000
e7aeea18 975 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
ae25f265
JB
976 // Initialize evses or connectors if needed (FIXME: should be factored out)
977 this.initializeConnectorsOrEvses(stationInfo);
fa7bccf4
JB
978 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
979 ChargingStationUtils.createStationInfoHash(stationInfo);
9ac86a7e 980 return stationInfo;
5ad8570f
JB
981 }
982
551e477c
JB
983 private getStationInfoFromFile(): ChargingStationInfo | undefined {
984 let stationInfo: ChargingStationInfo | undefined;
fa7bccf4 985 this.getStationInfoPersistentConfiguration() &&
551e477c 986 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
fa7bccf4 987 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
f765beaa 988 return stationInfo;
2484ac1e
JB
989 }
990
991 private getStationInfo(): ChargingStationInfo {
992 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 993 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
994 // Priority:
995 // 1. charging station info from template
996 // 2. charging station info from configuration file
997 // 3. charging station info attribute
f765beaa 998 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
01efc60a
JB
999 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
1000 return this.stationInfo;
1001 }
2484ac1e 1002 return stationInfoFromFile;
f765beaa 1003 }
fec4d204
JB
1004 stationInfoFromFile &&
1005 ChargingStationUtils.propagateSerialNumber(
1006 this.getTemplateFromFile(),
1007 stationInfoFromFile,
1008 stationInfoFromTemplate
1009 );
01efc60a 1010 return stationInfoFromTemplate;
2484ac1e
JB
1011 }
1012
1013 private saveStationInfo(): void {
ccb1d6e9 1014 if (this.getStationInfoPersistentConfiguration()) {
52952bf8 1015 this.saveConfiguration({ ocppConfiguration: false, connectors: false, evses: false });
ccb1d6e9 1016 }
2484ac1e
JB
1017 }
1018
e8e865ea 1019 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
1020 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1021 }
1022
1023 private getStationInfoPersistentConfiguration(): boolean {
1024 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
1025 }
1026
c0560973 1027 private handleUnsupportedVersion(version: OCPPVersion) {
fc040c43
JB
1028 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1029 logger.error(`${this.logPrefix()} ${errMsg}`);
6c8f5d90 1030 throw new BaseError(errMsg);
c0560973
JB
1031 }
1032
2484ac1e 1033 private initialize(): void {
fa7bccf4 1034 this.configurationFile = path.join(
ee5f26a2 1035 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
44eb6026 1036 `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json`
0642c3d2 1037 );
b44b779a 1038 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1039 if (
1040 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
5a2a53cf
JB
1041 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1042 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1043 ) {
d812bdcb 1044 const patternGroup: number | undefined =
15748260 1045 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1046 this.stationInfo.firmwareVersion?.split('.').length;
72092cfc
JB
1047 const match = this.stationInfo?.firmwareVersion
1048 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1049 ?.slice(1, patternGroup + 1);
3637ca2c 1050 const patchLevelIndex = match.length - 1;
5d280aae 1051 match[patchLevelIndex] = (
07c52a72
JB
1052 Utils.convertToInt(match[patchLevelIndex]) +
1053 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
5d280aae 1054 ).toString();
72092cfc 1055 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1056 }
6bccfcbc 1057 this.saveStationInfo();
ae25f265 1058 // Avoid duplication of connectors or evses related information in RAM
6bccfcbc 1059 delete this.stationInfo?.Connectors;
ae25f265 1060 delete this.stationInfo?.Evses;
6bccfcbc
JB
1061 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1062 if (this.getEnableStatistics() === true) {
1063 this.performanceStatistics = PerformanceStatistics.getInstance(
1064 this.stationInfo.hashId,
1065 this.stationInfo.chargingStationId,
1066 this.configuredSupervisionUrl
1067 );
1068 }
692f2f64
JB
1069 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1070 this.stationInfo
1071 );
1072 this.powerDivider = this.getPowerDivider();
1073 // OCPP configuration
1074 this.ocppConfiguration = this.getOcppConfiguration();
1075 this.initializeOcppConfiguration();
1076 this.initializeOcppServices();
1077 if (this.stationInfo?.autoRegister === true) {
1078 this.bootNotificationResponse = {
1079 currentTime: new Date(),
1080 interval: this.getHeartbeatInterval() / 1000,
1081 status: RegistrationStatusEnumType.ACCEPTED,
1082 };
1083 }
147d0e0f
JB
1084 }
1085
feff11ec
JB
1086 private initializeOcppServices(): void {
1087 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1088 switch (ocppVersion) {
1089 case OCPPVersion.VERSION_16:
1090 this.ocppIncomingRequestService =
1091 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1092 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1093 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1094 );
1095 break;
1096 case OCPPVersion.VERSION_20:
1097 case OCPPVersion.VERSION_201:
1098 this.ocppIncomingRequestService =
1099 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1100 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1101 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1102 );
1103 break;
1104 default:
1105 this.handleUnsupportedVersion(ocppVersion);
1106 break;
1107 }
1108 }
1109
2484ac1e 1110 private initializeOcppConfiguration(): void {
17ac262c
JB
1111 if (
1112 !ChargingStationConfigurationUtils.getConfigurationKey(
1113 this,
1114 StandardParametersKey.HeartbeatInterval
1115 )
1116 ) {
1117 ChargingStationConfigurationUtils.addConfigurationKey(
1118 this,
1119 StandardParametersKey.HeartbeatInterval,
1120 '0'
1121 );
f0f65a62 1122 }
17ac262c
JB
1123 if (
1124 !ChargingStationConfigurationUtils.getConfigurationKey(
1125 this,
1126 StandardParametersKey.HeartBeatInterval
1127 )
1128 ) {
1129 ChargingStationConfigurationUtils.addConfigurationKey(
1130 this,
1131 StandardParametersKey.HeartBeatInterval,
1132 '0',
1133 { visible: false }
1134 );
f0f65a62 1135 }
e7aeea18
JB
1136 if (
1137 this.getSupervisionUrlOcppConfiguration() &&
269de583 1138 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1139 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1140 ) {
17ac262c
JB
1141 ChargingStationConfigurationUtils.addConfigurationKey(
1142 this,
a59737e3 1143 this.getSupervisionUrlOcppKey(),
fa7bccf4 1144 this.configuredSupervisionUrl.href,
e7aeea18
JB
1145 { reboot: true }
1146 );
e6895390
JB
1147 } else if (
1148 !this.getSupervisionUrlOcppConfiguration() &&
269de583 1149 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1150 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1151 ) {
17ac262c
JB
1152 ChargingStationConfigurationUtils.deleteConfigurationKey(
1153 this,
1154 this.getSupervisionUrlOcppKey(),
1155 { save: false }
1156 );
12fc74d6 1157 }
cc6e8ab5 1158 if (
5a2a53cf 1159 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1160 !ChargingStationConfigurationUtils.getConfigurationKey(
1161 this,
1162 this.stationInfo.amperageLimitationOcppKey
1163 )
cc6e8ab5 1164 ) {
17ac262c
JB
1165 ChargingStationConfigurationUtils.addConfigurationKey(
1166 this,
cc6e8ab5 1167 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1168 (
1169 this.stationInfo.maximumAmperage *
1170 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1171 ).toString()
cc6e8ab5
JB
1172 );
1173 }
17ac262c
JB
1174 if (
1175 !ChargingStationConfigurationUtils.getConfigurationKey(
1176 this,
1177 StandardParametersKey.SupportedFeatureProfiles
1178 )
1179 ) {
1180 ChargingStationConfigurationUtils.addConfigurationKey(
1181 this,
e7aeea18 1182 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1183 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1184 );
1185 }
17ac262c
JB
1186 ChargingStationConfigurationUtils.addConfigurationKey(
1187 this,
e7aeea18
JB
1188 StandardParametersKey.NumberOfConnectors,
1189 this.getNumberOfConnectors().toString(),
a95873d8
JB
1190 { readonly: true },
1191 { overwrite: true }
e7aeea18 1192 );
17ac262c
JB
1193 if (
1194 !ChargingStationConfigurationUtils.getConfigurationKey(
1195 this,
1196 StandardParametersKey.MeterValuesSampledData
1197 )
1198 ) {
1199 ChargingStationConfigurationUtils.addConfigurationKey(
1200 this,
e7aeea18
JB
1201 StandardParametersKey.MeterValuesSampledData,
1202 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1203 );
7abfea5f 1204 }
17ac262c
JB
1205 if (
1206 !ChargingStationConfigurationUtils.getConfigurationKey(
1207 this,
1208 StandardParametersKey.ConnectorPhaseRotation
1209 )
1210 ) {
dd08d43d 1211 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1212 if (this.hasEvses) {
1213 for (const evseStatus of this.evses.values()) {
1214 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d
JB
1215 connectorsPhaseRotation.push(
1216 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1217 );
28e78158
JB
1218 }
1219 }
1220 } else {
1221 for (const connectorId of this.connectors.keys()) {
dd08d43d
JB
1222 connectorsPhaseRotation.push(
1223 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1224 );
7e1dc878
JB
1225 }
1226 }
17ac262c
JB
1227 ChargingStationConfigurationUtils.addConfigurationKey(
1228 this,
e7aeea18 1229 StandardParametersKey.ConnectorPhaseRotation,
dd08d43d 1230 connectorsPhaseRotation.toString()
e7aeea18 1231 );
7e1dc878 1232 }
e7aeea18 1233 if (
17ac262c
JB
1234 !ChargingStationConfigurationUtils.getConfigurationKey(
1235 this,
1236 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1237 )
1238 ) {
17ac262c
JB
1239 ChargingStationConfigurationUtils.addConfigurationKey(
1240 this,
1241 StandardParametersKey.AuthorizeRemoteTxRequests,
1242 'true'
1243 );
36f6a92e 1244 }
17ac262c
JB
1245 if (
1246 !ChargingStationConfigurationUtils.getConfigurationKey(
1247 this,
1248 StandardParametersKey.LocalAuthListEnabled
1249 ) &&
1250 ChargingStationConfigurationUtils.getConfigurationKey(
1251 this,
1252 StandardParametersKey.SupportedFeatureProfiles
72092cfc 1253 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1254 ) {
1255 ChargingStationConfigurationUtils.addConfigurationKey(
1256 this,
1257 StandardParametersKey.LocalAuthListEnabled,
1258 'false'
1259 );
1260 }
1261 if (
1262 !ChargingStationConfigurationUtils.getConfigurationKey(
1263 this,
1264 StandardParametersKey.ConnectionTimeOut
1265 )
1266 ) {
1267 ChargingStationConfigurationUtils.addConfigurationKey(
1268 this,
e7aeea18
JB
1269 StandardParametersKey.ConnectionTimeOut,
1270 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1271 );
8bce55bf 1272 }
2484ac1e 1273 this.saveOcppConfiguration();
073bd098
JB
1274 }
1275
ae25f265
JB
1276 private initializeConnectorsOrEvses(stationInfo: ChargingStationInfo) {
1277 if (stationInfo?.Connectors && !stationInfo?.Evses) {
1278 this.initializeConnectors(stationInfo);
1279 } else if (stationInfo?.Evses && !stationInfo?.Connectors) {
1280 this.initializeEvses(stationInfo);
1281 } else if (stationInfo?.Evses && stationInfo?.Connectors) {
1282 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1283 logger.error(`${this.logPrefix()} ${errorMsg}`);
1284 throw new BaseError(errorMsg);
1285 } else {
1286 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1287 logger.error(`${this.logPrefix()} ${errorMsg}`);
1288 throw new BaseError(errorMsg);
1289 }
1290 }
1291
1292 private initializeConnectors(stationInfo: ChargingStationInfo): void {
3d25cc86 1293 if (!stationInfo?.Connectors && this.connectors.size === 0) {
fc040c43
JB
1294 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1295 logger.error(`${this.logPrefix()} ${logMsg}`);
3d25cc86
JB
1296 throw new BaseError(logMsg);
1297 }
1298 if (!stationInfo?.Connectors[0]) {
1299 logger.warn(
1300 `${this.logPrefix()} Charging station information from template ${
1301 this.templateFile
2585c6e9 1302 } with no connector id 0 configuration`
3d25cc86
JB
1303 );
1304 }
1305 if (stationInfo?.Connectors) {
ae25f265
JB
1306 const configuredMaxConnectors =
1307 ChargingStationUtils.getConfiguredNumberOfConnectors(stationInfo);
1308 ChargingStationUtils.checkConfiguredMaxConnectors(
1309 configuredMaxConnectors,
1310 this.templateFile,
1311 this.logPrefix()
1312 );
3d25cc86
JB
1313 const connectorsConfigHash = crypto
1314 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
14ecae6a 1315 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
3d25cc86
JB
1316 .digest('hex');
1317 const connectorsConfigChanged =
1318 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1319 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1320 connectorsConfigChanged && this.connectors.clear();
1321 this.connectorsConfigurationHash = connectorsConfigHash;
a78ef5ed 1322 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
ae25f265 1323 stationInfo.Connectors
a78ef5ed 1324 );
ae25f265
JB
1325 ChargingStationUtils.checkTemplateMaxConnectors(
1326 templateMaxConnectors,
1327 this.templateFile,
1328 this.logPrefix()
1329 );
269196a8
JB
1330 const templateMaxAvailableConnectors = stationInfo?.Connectors[0]
1331 ? templateMaxConnectors - 1
1332 : templateMaxConnectors;
ae25f265 1333 if (
269196a8 1334 configuredMaxConnectors > templateMaxAvailableConnectors &&
ae25f265
JB
1335 !stationInfo?.randomConnectors
1336 ) {
1337 logger.warn(
1338 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
1339 this.templateFile
1340 }, forcing random connector configurations affectation`
1341 );
1342 stationInfo.randomConnectors = true;
1343 }
269196a8
JB
1344 if (templateMaxConnectors > 0) {
1345 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1346 if (
1347 connectorId === 0 &&
1348 (!stationInfo?.Connectors[connectorId] ||
1349 this.getUseConnectorId0(stationInfo) === false)
1350 ) {
1351 continue;
1352 }
1353 const templateConnectorId =
1354 connectorId > 0 && stationInfo?.randomConnectors
1355 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1356 : connectorId;
ae25f265 1357 const connectorStatus = stationInfo?.Connectors[templateConnectorId];
04b1261c 1358 ChargingStationUtils.checkStationInfoConnectorStatus(
ae25f265 1359 templateConnectorId,
04b1261c
JB
1360 connectorStatus,
1361 this.logPrefix(),
1362 this.templateFile
1363 );
ae25f265 1364 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1365 }
52952bf8
JB
1366 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1367 this.saveConnectorsStatus();
ae25f265
JB
1368 } else {
1369 logger.warn(
1370 `${this.logPrefix()} Charging station information from template ${
1371 this.templateFile
1372 } with no connectors configuration defined, cannot create connectors`
1373 );
3d25cc86
JB
1374 }
1375 }
1376 } else {
1377 logger.warn(
1378 `${this.logPrefix()} Charging station information from template ${
1379 this.templateFile
1380 } with no connectors configuration defined, using already defined connectors`
1381 );
1382 }
3d25cc86
JB
1383 }
1384
2585c6e9
JB
1385 private initializeEvses(stationInfo: ChargingStationInfo): void {
1386 if (!stationInfo?.Evses && this.evses.size === 0) {
1387 const logMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
ae25f265
JB
1388 logger.error(`${this.logPrefix()} ${logMsg}`);
1389 throw new BaseError(logMsg);
2585c6e9
JB
1390 }
1391 if (!stationInfo?.Evses[0]) {
1392 logger.warn(
1393 `${this.logPrefix()} Charging station information from template ${
1394 this.templateFile
1395 } with no evse id 0 configuration`
1396 );
1397 }
59a0f26d
JB
1398 if (!stationInfo?.Evses[0]?.Connectors[0]) {
1399 logger.warn(
1400 `${this.logPrefix()} Charging station information from template ${
1401 this.templateFile
1402 } with evse id 0 with no connector id 0 configuration`
1403 );
1404 }
2585c6e9
JB
1405 if (stationInfo?.Evses) {
1406 const evsesConfigHash = crypto
1407 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1408 .update(`${JSON.stringify(stationInfo?.Evses)}`)
1409 .digest('hex');
1410 const evsesConfigChanged =
1411 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1412 if (this.evses?.size === 0 || evsesConfigChanged) {
1413 evsesConfigChanged && this.evses.clear();
1414 this.evsesConfigurationHash = evsesConfigHash;
ae25f265
JB
1415 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationInfo?.Evses);
1416 if (templateMaxEvses > 0) {
1417 for (const evse in stationInfo.Evses) {
52952bf8
JB
1418 const evseId = Utils.convertToInt(evse);
1419 this.evses.set(evseId, {
ae25f265
JB
1420 connectors: ChargingStationUtils.buildConnectorsMap(
1421 stationInfo?.Evses[evse]?.Connectors,
1422 this.logPrefix(),
1423 this.templateFile
1424 ),
1425 availability: AvailabilityType.Operative,
1426 });
1427 ChargingStationUtils.initializeConnectorsMapStatus(
52952bf8 1428 this.evses.get(evseId)?.connectors,
ae25f265
JB
1429 this.logPrefix()
1430 );
1431 }
52952bf8 1432 this.saveEvsesStatus();
ae25f265
JB
1433 } else {
1434 logger.warn(
1435 `${this.logPrefix()} Charging station information from template ${
04b1261c 1436 this.templateFile
ae25f265 1437 } with no evses configuration defined, cannot create evses`
04b1261c 1438 );
2585c6e9
JB
1439 }
1440 }
513db108
JB
1441 } else {
1442 logger.warn(
1443 `${this.logPrefix()} Charging station information from template ${
1444 this.templateFile
1445 } with no evses configuration defined, using already defined evses`
1446 );
2585c6e9
JB
1447 }
1448 }
1449
551e477c
JB
1450 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1451 let configuration: ChargingStationConfiguration | undefined;
2484ac1e 1452 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1453 try {
57adbebc
JB
1454 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1455 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1456 this.configurationFileHash
1457 );
7c72977b
JB
1458 } else {
1459 const measureId = `${FileType.ChargingStationConfiguration} read`;
1460 const beginId = PerformanceStatistics.beginMeasure(measureId);
1461 configuration = JSON.parse(
1462 fs.readFileSync(this.configurationFile, 'utf8')
1463 ) as ChargingStationConfiguration;
1464 PerformanceStatistics.endMeasure(measureId, beginId);
1465 this.configurationFileHash = configuration.configurationHash;
57adbebc 1466 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1467 }
073bd098
JB
1468 } catch (error) {
1469 FileUtils.handleFileException(
073bd098 1470 this.configurationFile,
7164966d
JB
1471 FileType.ChargingStationConfiguration,
1472 error as NodeJS.ErrnoException,
1473 this.logPrefix()
073bd098
JB
1474 );
1475 }
1476 }
1477 return configuration;
1478 }
1479
52952bf8
JB
1480 private saveConnectorsStatus() {
1481 if (this.getOcppPersistentConfiguration()) {
1482 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, evses: false });
1483 }
1484 }
1485
1486 private saveEvsesStatus() {
1487 if (this.getOcppPersistentConfiguration()) {
1488 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, connectors: false });
1489 }
1490 }
1491
1492 private saveConfiguration(
1493 params: {
1494 stationInfo?: boolean;
1495 ocppConfiguration?: boolean;
1496 connectors?: boolean;
1497 evses?: boolean;
1498 } = { stationInfo: true, ocppConfiguration: true, connectors: true, evses: true }
1499 ): void {
2484ac1e 1500 if (this.configurationFile) {
52952bf8
JB
1501 params = {
1502 ...params,
1503 ...{ stationInfo: true, ocppConfiguration: true, connectors: true, evses: true },
1504 };
2484ac1e 1505 try {
2484ac1e
JB
1506 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1507 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1508 }
ccb1d6e9 1509 const configurationData: ChargingStationConfiguration =
abe9e9dd 1510 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
52952bf8
JB
1511 if (params.stationInfo && this.stationInfo) {
1512 configurationData.stationInfo = this.stationInfo;
1513 }
1514 if (params.ocppConfiguration && this.ocppConfiguration?.configurationKey) {
1515 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1516 }
1517 if (params.connectors && this.connectors.size > 0) {
1518 configurationData.connectorsStatus = [...this.connectors.values()].map(
1519 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1520 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1521 );
1522 }
1523 if (params.evses && this.evses.size > 0) {
1524 configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => {
1525 const status = {
1526 ...evseStatus,
1527 connectorsStatus: [...evseStatus.connectors.values()].map(
1528 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1529 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1530 ),
1531 };
1532 delete status.connectors;
1533 return status as EvseStatusConfiguration;
1534 });
1535 }
7c72977b
JB
1536 delete configurationData.configurationHash;
1537 const configurationHash = crypto
1538 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1539 .update(JSON.stringify(configurationData))
1540 .digest('hex');
1541 if (this.configurationFileHash !== configurationHash) {
1542 configurationData.configurationHash = configurationHash;
1543 const measureId = `${FileType.ChargingStationConfiguration} write`;
1544 const beginId = PerformanceStatistics.beginMeasure(measureId);
1545 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1546 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1547 fs.closeSync(fileDescriptor);
1548 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1549 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1550 this.configurationFileHash = configurationHash;
57adbebc 1551 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1552 } else {
1553 logger.debug(
1554 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1555 this.configurationFile
1556 }`
1557 );
2484ac1e 1558 }
2484ac1e
JB
1559 } catch (error) {
1560 FileUtils.handleFileException(
2484ac1e 1561 this.configurationFile,
7164966d
JB
1562 FileType.ChargingStationConfiguration,
1563 error as NodeJS.ErrnoException,
1564 this.logPrefix()
073bd098
JB
1565 );
1566 }
2484ac1e
JB
1567 } else {
1568 logger.error(
01efc60a 1569 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1570 );
073bd098
JB
1571 }
1572 }
1573
551e477c
JB
1574 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1575 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1576 }
1577
551e477c
JB
1578 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1579 let configuration: ChargingStationConfiguration | undefined;
23290150 1580 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1581 const configurationFromFile = this.getConfigurationFromFile();
1582 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1583 }
648512ce
JB
1584 if (!Utils.isNullOrUndefined(configuration)) {
1585 delete configuration.stationInfo;
1586 delete configuration.configurationHash;
1587 }
073bd098 1588 return configuration;
7dde0b73
JB
1589 }
1590
551e477c
JB
1591 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1592 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1593 this.getOcppConfigurationFromFile();
2484ac1e
JB
1594 if (!ocppConfiguration) {
1595 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1596 }
1597 return ocppConfiguration;
1598 }
1599
c0560973 1600 private async onOpen(): Promise<void> {
56eb297e 1601 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1602 logger.info(
1603 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1604 );
ed6cfcff 1605 if (this.isRegistered() === false) {
5144f4d1
JB
1606 // Send BootNotification
1607 let registrationRetryCount = 0;
1608 do {
f7f98c68 1609 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1610 BootNotificationRequest,
1611 BootNotificationResponse
8bfbc743
JB
1612 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1613 skipBufferingOnError: true,
1614 });
ed6cfcff 1615 if (this.isRegistered() === false) {
5144f4d1
JB
1616 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1617 await Utils.sleep(
1895299d 1618 this?.bootNotificationResponse?.interval
5144f4d1
JB
1619 ? this.bootNotificationResponse.interval * 1000
1620 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1621 );
1622 }
1623 } while (
ed6cfcff 1624 this.isRegistered() === false &&
5144f4d1
JB
1625 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1626 this.getRegistrationMaxRetries() === -1)
1627 );
1628 }
ed6cfcff 1629 if (this.isRegistered() === true) {
23290150 1630 if (this.isInAcceptedState() === true) {
94bb24d5 1631 await this.startMessageSequence();
c0560973 1632 }
5144f4d1
JB
1633 } else {
1634 logger.error(
1635 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1636 );
caad9d6b 1637 }
5144f4d1 1638 this.wsConnectionRestarted = false;
aa428a31 1639 this.autoReconnectRetryCount = 0;
1895299d 1640 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1641 } else {
5144f4d1
JB
1642 logger.warn(
1643 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1644 );
2e6f5966 1645 }
2e6f5966
JB
1646 }
1647
ef7d8c21 1648 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1649 switch (code) {
6c65a295
JB
1650 // Normal close
1651 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1652 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1653 logger.info(
5e3cb728 1654 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1655 code
ef7d8c21 1656 )}' and reason '${reason.toString()}'`
e7aeea18 1657 );
c0560973
JB
1658 this.autoReconnectRetryCount = 0;
1659 break;
6c65a295
JB
1660 // Abnormal close
1661 default:
e7aeea18 1662 logger.error(
5e3cb728 1663 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1664 code
ef7d8c21 1665 )}' and reason '${reason.toString()}'`
e7aeea18 1666 );
56eb297e 1667 this.started === true && (await this.reconnect());
c0560973
JB
1668 break;
1669 }
1895299d 1670 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1671 }
1672
56d09fd7
JB
1673 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1674 const cachedRequest = this.requests.get(messageId);
1675 if (Array.isArray(cachedRequest) === true) {
1676 return cachedRequest;
1677 }
1678 throw new OCPPError(
1679 ErrorType.PROTOCOL_ERROR,
1680 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1681 messageType
1682 )} is not an array`,
1683 undefined,
617cad0c 1684 cachedRequest as JsonType
56d09fd7
JB
1685 );
1686 }
1687
1688 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1689 const [messageType, messageId, commandName, commandPayload] = request;
1690 if (this.getEnableStatistics() === true) {
1691 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1692 }
1693 logger.debug(
1694 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1695 request
1696 )}`
1697 );
1698 // Process the message
1699 await this.ocppIncomingRequestService.incomingRequestHandler(
1700 this,
1701 messageId,
1702 commandName,
1703 commandPayload
1704 );
1705 }
1706
1707 private handleResponseMessage(response: Response): void {
1708 const [messageType, messageId, commandPayload] = response;
1709 if (this.requests.has(messageId) === false) {
1710 // Error
1711 throw new OCPPError(
1712 ErrorType.INTERNAL_ERROR,
1713 `Response for unknown message id ${messageId}`,
1714 undefined,
1715 commandPayload
1716 );
1717 }
1718 // Respond
1719 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1720 messageType,
1721 messageId
1722 );
1723 logger.debug(
1724 `${this.logPrefix()} << Command '${
1725 requestCommandName ?? Constants.UNKNOWN_COMMAND
1726 }' received response payload: ${JSON.stringify(response)}`
1727 );
1728 responseCallback(commandPayload, requestPayload);
1729 }
1730
1731 private handleErrorMessage(errorResponse: ErrorResponse): void {
1732 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1733 if (this.requests.has(messageId) === false) {
1734 // Error
1735 throw new OCPPError(
1736 ErrorType.INTERNAL_ERROR,
1737 `Error response for unknown message id ${messageId}`,
1738 undefined,
1739 { errorType, errorMessage, errorDetails }
1740 );
1741 }
1742 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1743 logger.debug(
1744 `${this.logPrefix()} << Command '${
1745 requestCommandName ?? Constants.UNKNOWN_COMMAND
1746 }' received error response payload: ${JSON.stringify(errorResponse)}`
1747 );
1748 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1749 }
1750
ef7d8c21 1751 private async onMessage(data: RawData): Promise<void> {
56d09fd7 1752 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 1753 let messageType: number;
c0560973
JB
1754 let errMsg: string;
1755 try {
56d09fd7 1756 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1757 if (Array.isArray(request) === true) {
56d09fd7 1758 [messageType] = request;
b3ec7bc1
JB
1759 // Check the type of message
1760 switch (messageType) {
1761 // Incoming Message
1762 case MessageType.CALL_MESSAGE:
56d09fd7 1763 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1764 break;
56d09fd7 1765 // Response Message
b3ec7bc1 1766 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1767 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1768 break;
1769 // Error Message
1770 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1771 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1772 break;
56d09fd7 1773 // Unknown Message
b3ec7bc1
JB
1774 default:
1775 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fc040c43
JB
1776 errMsg = `Wrong message type ${messageType}`;
1777 logger.error(`${this.logPrefix()} ${errMsg}`);
b3ec7bc1
JB
1778 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1779 }
1895299d 1780 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1781 } else {
53e5fd67 1782 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1783 request,
ac54a9bb 1784 });
47e22477 1785 }
c0560973 1786 } catch (error) {
56d09fd7
JB
1787 let commandName: IncomingRequestCommand;
1788 let requestCommandName: RequestCommand | IncomingRequestCommand;
1789 let errorCallback: ErrorCallback;
1790 const [, messageId] = request;
13701f69
JB
1791 switch (messageType) {
1792 case MessageType.CALL_MESSAGE:
56d09fd7 1793 [, , commandName] = request as IncomingRequest;
13701f69 1794 // Send error
56d09fd7 1795 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1796 break;
1797 case MessageType.CALL_RESULT_MESSAGE:
1798 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
1799 if (this.requests.has(messageId) === true) {
1800 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
1801 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1802 errorCallback(error as OCPPError, false);
1803 } else {
1804 // Remove the request from the cache in case of error at response handling
1805 this.requests.delete(messageId);
1806 }
de4cb8b6 1807 break;
ba7965c4 1808 }
56d09fd7
JB
1809 if (error instanceof OCPPError === false) {
1810 logger.warn(
1811 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1812 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1813 }' message '${data.toString()}' handling is not an OCPPError:`,
1814 error
1815 );
1816 }
1817 logger.error(
1818 `${this.logPrefix()} Incoming OCPP command '${
1819 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1820 }' message '${data.toString()}'${
1821 messageType !== MessageType.CALL_MESSAGE
1822 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1823 : ''
1824 } processing error:`,
1825 error
1826 );
c0560973 1827 }
2328be1e
JB
1828 }
1829
c0560973 1830 private onPing(): void {
44eb6026 1831 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1832 }
1833
1834 private onPong(): void {
44eb6026 1835 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1836 }
1837
9534e74e 1838 private onError(error: WSError): void {
bcc9c3c0 1839 this.closeWSConnection();
44eb6026 1840 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1841 }
1842
18bf8274 1843 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1844 if (this.getMeteringPerTransaction() === true) {
07989fad 1845 return (
18bf8274 1846 (rounded === true
07989fad
JB
1847 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1848 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1849 );
1850 }
1851 return (
18bf8274 1852 (rounded === true
07989fad
JB
1853 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1854 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1855 );
1856 }
1857
bb83b5ed 1858 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4 1859 const localStationInfo = stationInfo ?? this.stationInfo;
a14885a3 1860 return localStationInfo?.useConnectorId0 ?? true;
8bce55bf
JB
1861 }
1862
60ddad53
JB
1863 private getNumberOfRunningTransactions(): number {
1864 let trxCount = 0;
28e78158
JB
1865 if (this.hasEvses) {
1866 for (const evseStatus of this.evses.values()) {
1867 for (const connectorStatus of evseStatus.connectors.values()) {
1868 if (connectorStatus.transactionStarted === true) {
1869 trxCount++;
1870 }
1871 }
1872 }
1873 } else {
1874 for (const connectorId of this.connectors.keys()) {
1875 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1876 trxCount++;
1877 }
60ddad53
JB
1878 }
1879 }
1880 return trxCount;
1881 }
1882
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}