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