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