fix: add sanity check at evses initialization
[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 }
59a0f26d
JB
1414 if (!stationInfo?.Evses[0]?.Connectors[0]) {
1415 logger.warn(
1416 `${this.logPrefix()} Charging station information from template ${
1417 this.templateFile
1418 } with evse id 0 with no connector id 0 configuration`
1419 );
1420 }
2585c6e9
JB
1421 if (stationInfo?.Evses) {
1422 const evsesConfigHash = crypto
1423 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1424 .update(`${JSON.stringify(stationInfo?.Evses)}`)
1425 .digest('hex');
1426 const evsesConfigChanged =
1427 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1428 if (this.evses?.size === 0 || evsesConfigChanged) {
1429 evsesConfigChanged && this.evses.clear();
1430 this.evsesConfigurationHash = evsesConfigHash;
ae25f265
JB
1431 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationInfo?.Evses);
1432 if (templateMaxEvses > 0) {
1433 for (const evse in stationInfo.Evses) {
1434 this.evses.set(Utils.convertToInt(evse), {
1435 connectors: ChargingStationUtils.buildConnectorsMap(
1436 stationInfo?.Evses[evse]?.Connectors,
1437 this.logPrefix(),
1438 this.templateFile
1439 ),
1440 availability: AvailabilityType.Operative,
1441 });
1442 ChargingStationUtils.initializeConnectorsMapStatus(
1443 this.evses.get(Utils.convertToInt(evse))?.connectors,
1444 this.logPrefix()
1445 );
1446 }
1447 } else {
1448 logger.warn(
1449 `${this.logPrefix()} Charging station information from template ${
04b1261c 1450 this.templateFile
ae25f265 1451 } with no evses configuration defined, cannot create evses`
04b1261c 1452 );
2585c6e9
JB
1453 }
1454 }
513db108
JB
1455 } else {
1456 logger.warn(
1457 `${this.logPrefix()} Charging station information from template ${
1458 this.templateFile
1459 } with no evses configuration defined, using already defined evses`
1460 );
2585c6e9
JB
1461 }
1462 }
1463
551e477c
JB
1464 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1465 let configuration: ChargingStationConfiguration | undefined;
2484ac1e 1466 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1467 try {
57adbebc
JB
1468 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1469 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1470 this.configurationFileHash
1471 );
7c72977b
JB
1472 } else {
1473 const measureId = `${FileType.ChargingStationConfiguration} read`;
1474 const beginId = PerformanceStatistics.beginMeasure(measureId);
1475 configuration = JSON.parse(
1476 fs.readFileSync(this.configurationFile, 'utf8')
1477 ) as ChargingStationConfiguration;
1478 PerformanceStatistics.endMeasure(measureId, beginId);
1479 this.configurationFileHash = configuration.configurationHash;
57adbebc 1480 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1481 }
073bd098
JB
1482 } catch (error) {
1483 FileUtils.handleFileException(
073bd098 1484 this.configurationFile,
7164966d
JB
1485 FileType.ChargingStationConfiguration,
1486 error as NodeJS.ErrnoException,
1487 this.logPrefix()
073bd098
JB
1488 );
1489 }
1490 }
1491 return configuration;
1492 }
1493
7c72977b 1494 private saveConfiguration(): void {
2484ac1e
JB
1495 if (this.configurationFile) {
1496 try {
2484ac1e
JB
1497 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1498 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1499 }
ccb1d6e9 1500 const configurationData: ChargingStationConfiguration =
abe9e9dd 1501 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
7c72977b
JB
1502 this.ocppConfiguration?.configurationKey &&
1503 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1504 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1505 delete configurationData.configurationHash;
1506 const configurationHash = crypto
1507 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1508 .update(JSON.stringify(configurationData))
1509 .digest('hex');
1510 if (this.configurationFileHash !== configurationHash) {
1511 configurationData.configurationHash = configurationHash;
1512 const measureId = `${FileType.ChargingStationConfiguration} write`;
1513 const beginId = PerformanceStatistics.beginMeasure(measureId);
1514 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1515 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1516 fs.closeSync(fileDescriptor);
1517 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1518 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1519 this.configurationFileHash = configurationHash;
57adbebc 1520 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1521 } else {
1522 logger.debug(
1523 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1524 this.configurationFile
1525 }`
1526 );
2484ac1e 1527 }
2484ac1e
JB
1528 } catch (error) {
1529 FileUtils.handleFileException(
2484ac1e 1530 this.configurationFile,
7164966d
JB
1531 FileType.ChargingStationConfiguration,
1532 error as NodeJS.ErrnoException,
1533 this.logPrefix()
073bd098
JB
1534 );
1535 }
2484ac1e
JB
1536 } else {
1537 logger.error(
01efc60a 1538 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1539 );
073bd098
JB
1540 }
1541 }
1542
551e477c
JB
1543 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1544 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1545 }
1546
551e477c
JB
1547 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1548 let configuration: ChargingStationConfiguration | undefined;
23290150 1549 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1550 const configurationFromFile = this.getConfigurationFromFile();
1551 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1552 }
648512ce
JB
1553 if (!Utils.isNullOrUndefined(configuration)) {
1554 delete configuration.stationInfo;
1555 delete configuration.configurationHash;
1556 }
073bd098 1557 return configuration;
7dde0b73
JB
1558 }
1559
551e477c
JB
1560 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1561 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1562 this.getOcppConfigurationFromFile();
2484ac1e
JB
1563 if (!ocppConfiguration) {
1564 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1565 }
1566 return ocppConfiguration;
1567 }
1568
c0560973 1569 private async onOpen(): Promise<void> {
56eb297e 1570 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1571 logger.info(
1572 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1573 );
ed6cfcff 1574 if (this.isRegistered() === false) {
5144f4d1
JB
1575 // Send BootNotification
1576 let registrationRetryCount = 0;
1577 do {
f7f98c68 1578 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1579 BootNotificationRequest,
1580 BootNotificationResponse
8bfbc743
JB
1581 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1582 skipBufferingOnError: true,
1583 });
ed6cfcff 1584 if (this.isRegistered() === false) {
5144f4d1
JB
1585 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1586 await Utils.sleep(
1895299d 1587 this?.bootNotificationResponse?.interval
5144f4d1
JB
1588 ? this.bootNotificationResponse.interval * 1000
1589 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1590 );
1591 }
1592 } while (
ed6cfcff 1593 this.isRegistered() === false &&
5144f4d1
JB
1594 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1595 this.getRegistrationMaxRetries() === -1)
1596 );
1597 }
ed6cfcff 1598 if (this.isRegistered() === true) {
23290150 1599 if (this.isInAcceptedState() === true) {
94bb24d5 1600 await this.startMessageSequence();
c0560973 1601 }
5144f4d1
JB
1602 } else {
1603 logger.error(
1604 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1605 );
caad9d6b 1606 }
5144f4d1 1607 this.wsConnectionRestarted = false;
aa428a31 1608 this.autoReconnectRetryCount = 0;
1895299d 1609 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1610 } else {
5144f4d1
JB
1611 logger.warn(
1612 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1613 );
2e6f5966 1614 }
2e6f5966
JB
1615 }
1616
ef7d8c21 1617 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1618 switch (code) {
6c65a295
JB
1619 // Normal close
1620 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1621 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1622 logger.info(
5e3cb728 1623 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1624 code
ef7d8c21 1625 )}' and reason '${reason.toString()}'`
e7aeea18 1626 );
c0560973
JB
1627 this.autoReconnectRetryCount = 0;
1628 break;
6c65a295
JB
1629 // Abnormal close
1630 default:
e7aeea18 1631 logger.error(
5e3cb728 1632 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1633 code
ef7d8c21 1634 )}' and reason '${reason.toString()}'`
e7aeea18 1635 );
56eb297e 1636 this.started === true && (await this.reconnect());
c0560973
JB
1637 break;
1638 }
1895299d 1639 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1640 }
1641
56d09fd7
JB
1642 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1643 const cachedRequest = this.requests.get(messageId);
1644 if (Array.isArray(cachedRequest) === true) {
1645 return cachedRequest;
1646 }
1647 throw new OCPPError(
1648 ErrorType.PROTOCOL_ERROR,
1649 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1650 messageType
1651 )} is not an array`,
1652 undefined,
617cad0c 1653 cachedRequest as JsonType
56d09fd7
JB
1654 );
1655 }
1656
1657 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1658 const [messageType, messageId, commandName, commandPayload] = request;
1659 if (this.getEnableStatistics() === true) {
1660 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1661 }
1662 logger.debug(
1663 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1664 request
1665 )}`
1666 );
1667 // Process the message
1668 await this.ocppIncomingRequestService.incomingRequestHandler(
1669 this,
1670 messageId,
1671 commandName,
1672 commandPayload
1673 );
1674 }
1675
1676 private handleResponseMessage(response: Response): void {
1677 const [messageType, messageId, commandPayload] = response;
1678 if (this.requests.has(messageId) === false) {
1679 // Error
1680 throw new OCPPError(
1681 ErrorType.INTERNAL_ERROR,
1682 `Response for unknown message id ${messageId}`,
1683 undefined,
1684 commandPayload
1685 );
1686 }
1687 // Respond
1688 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1689 messageType,
1690 messageId
1691 );
1692 logger.debug(
1693 `${this.logPrefix()} << Command '${
1694 requestCommandName ?? Constants.UNKNOWN_COMMAND
1695 }' received response payload: ${JSON.stringify(response)}`
1696 );
1697 responseCallback(commandPayload, requestPayload);
1698 }
1699
1700 private handleErrorMessage(errorResponse: ErrorResponse): void {
1701 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1702 if (this.requests.has(messageId) === false) {
1703 // Error
1704 throw new OCPPError(
1705 ErrorType.INTERNAL_ERROR,
1706 `Error response for unknown message id ${messageId}`,
1707 undefined,
1708 { errorType, errorMessage, errorDetails }
1709 );
1710 }
1711 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1712 logger.debug(
1713 `${this.logPrefix()} << Command '${
1714 requestCommandName ?? Constants.UNKNOWN_COMMAND
1715 }' received error response payload: ${JSON.stringify(errorResponse)}`
1716 );
1717 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1718 }
1719
ef7d8c21 1720 private async onMessage(data: RawData): Promise<void> {
56d09fd7 1721 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 1722 let messageType: number;
c0560973
JB
1723 let errMsg: string;
1724 try {
56d09fd7 1725 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1726 if (Array.isArray(request) === true) {
56d09fd7 1727 [messageType] = request;
b3ec7bc1
JB
1728 // Check the type of message
1729 switch (messageType) {
1730 // Incoming Message
1731 case MessageType.CALL_MESSAGE:
56d09fd7 1732 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1733 break;
56d09fd7 1734 // Response Message
b3ec7bc1 1735 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1736 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1737 break;
1738 // Error Message
1739 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1740 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1741 break;
56d09fd7 1742 // Unknown Message
b3ec7bc1
JB
1743 default:
1744 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fc040c43
JB
1745 errMsg = `Wrong message type ${messageType}`;
1746 logger.error(`${this.logPrefix()} ${errMsg}`);
b3ec7bc1
JB
1747 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1748 }
1895299d 1749 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1750 } else {
53e5fd67 1751 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1752 request,
ac54a9bb 1753 });
47e22477 1754 }
c0560973 1755 } catch (error) {
56d09fd7
JB
1756 let commandName: IncomingRequestCommand;
1757 let requestCommandName: RequestCommand | IncomingRequestCommand;
1758 let errorCallback: ErrorCallback;
1759 const [, messageId] = request;
13701f69
JB
1760 switch (messageType) {
1761 case MessageType.CALL_MESSAGE:
56d09fd7 1762 [, , commandName] = request as IncomingRequest;
13701f69 1763 // Send error
56d09fd7 1764 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1765 break;
1766 case MessageType.CALL_RESULT_MESSAGE:
1767 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
1768 if (this.requests.has(messageId) === true) {
1769 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
1770 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1771 errorCallback(error as OCPPError, false);
1772 } else {
1773 // Remove the request from the cache in case of error at response handling
1774 this.requests.delete(messageId);
1775 }
de4cb8b6 1776 break;
ba7965c4 1777 }
56d09fd7
JB
1778 if (error instanceof OCPPError === false) {
1779 logger.warn(
1780 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1781 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1782 }' message '${data.toString()}' handling is not an OCPPError:`,
1783 error
1784 );
1785 }
1786 logger.error(
1787 `${this.logPrefix()} Incoming OCPP command '${
1788 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1789 }' message '${data.toString()}'${
1790 messageType !== MessageType.CALL_MESSAGE
1791 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1792 : ''
1793 } processing error:`,
1794 error
1795 );
c0560973 1796 }
2328be1e
JB
1797 }
1798
c0560973 1799 private onPing(): void {
44eb6026 1800 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1801 }
1802
1803 private onPong(): void {
44eb6026 1804 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1805 }
1806
9534e74e 1807 private onError(error: WSError): void {
bcc9c3c0 1808 this.closeWSConnection();
44eb6026 1809 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1810 }
1811
18bf8274 1812 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1813 if (this.getMeteringPerTransaction() === true) {
07989fad 1814 return (
18bf8274 1815 (rounded === true
07989fad
JB
1816 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1817 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1818 );
1819 }
1820 return (
18bf8274 1821 (rounded === true
07989fad
JB
1822 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1823 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1824 );
1825 }
1826
bb83b5ed 1827 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4 1828 const localStationInfo = stationInfo ?? this.stationInfo;
a14885a3 1829 return localStationInfo?.useConnectorId0 ?? true;
8bce55bf
JB
1830 }
1831
60ddad53
JB
1832 private getNumberOfRunningTransactions(): number {
1833 let trxCount = 0;
28e78158
JB
1834 if (this.hasEvses) {
1835 for (const evseStatus of this.evses.values()) {
1836 for (const connectorStatus of evseStatus.connectors.values()) {
1837 if (connectorStatus.transactionStarted === true) {
1838 trxCount++;
1839 }
1840 }
1841 }
1842 } else {
1843 for (const connectorId of this.connectors.keys()) {
1844 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1845 trxCount++;
1846 }
60ddad53
JB
1847 }
1848 }
1849 return trxCount;
1850 }
1851
1852 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158
JB
1853 if (this.hasEvses) {
1854 for (const evseStatus of this.evses.values()) {
1855 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1856 if (connectorStatus.transactionStarted === true) {
1857 await this.stopTransactionOnConnector(connectorId, reason);
1858 }
1859 }
1860 }
1861 } else {
1862 for (const connectorId of this.connectors.keys()) {
1863 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1864 await this.stopTransactionOnConnector(connectorId, reason);
1865 }
60ddad53
JB
1866 }
1867 }
1868 }
1869
1f761b9a 1870 // 0 for disabling
c72f6634 1871 private getConnectionTimeout(): number {
17ac262c
JB
1872 if (
1873 ChargingStationConfigurationUtils.getConfigurationKey(
1874 this,
1875 StandardParametersKey.ConnectionTimeOut
1876 )
1877 ) {
e7aeea18 1878 return (
17ac262c
JB
1879 parseInt(
1880 ChargingStationConfigurationUtils.getConfigurationKey(
1881 this,
1882 StandardParametersKey.ConnectionTimeOut
1883 ).value
1884 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1885 );
291cb255 1886 }
291cb255 1887 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1888 }
1889
1f761b9a 1890 // -1 for unlimited, 0 for disabling
72092cfc 1891 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1892 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1893 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1894 }
1895 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1896 return Configuration.getAutoReconnectMaxRetries();
1897 }
1898 return -1;
1899 }
1900
ec977daf 1901 // 0 for disabling
72092cfc 1902 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1903 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1904 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1905 }
1906 return -1;
1907 }
1908
c0560973
JB
1909 private getPowerDivider(): number {
1910 let powerDivider = this.getNumberOfConnectors();
fa7bccf4 1911 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1912 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1913 }
1914 return powerDivider;
1915 }
1916
fa7bccf4
JB
1917 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1918 const maximumPower = this.getMaximumPower(stationInfo);
1919 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1920 case CurrentType.AC:
1921 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1922 this.getNumberOfPhases(stationInfo),
ad8537a7 1923 maximumPower / this.getNumberOfConnectors(),
fa7bccf4 1924 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1925 );
1926 case CurrentType.DC:
fa7bccf4 1927 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1928 }
1929 }
1930
cc6e8ab5
JB
1931 private getAmperageLimitation(): number | undefined {
1932 if (
5a2a53cf 1933 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1934 ChargingStationConfigurationUtils.getConfigurationKey(
1935 this,
1936 this.stationInfo.amperageLimitationOcppKey
1937 )
cc6e8ab5
JB
1938 ) {
1939 return (
1940 Utils.convertToInt(
17ac262c
JB
1941 ChargingStationConfigurationUtils.getConfigurationKey(
1942 this,
1943 this.stationInfo.amperageLimitationOcppKey
72092cfc 1944 )?.value
17ac262c 1945 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1946 );
1947 }
1948 }
1949
c0560973 1950 private async startMessageSequence(): Promise<void> {
b7f9e41d 1951 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1952 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1953 BootNotificationRequest,
1954 BootNotificationResponse
8bfbc743
JB
1955 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1956 skipBufferingOnError: true,
1957 });
6114e6f1 1958 }
136c90ba 1959 // Start WebSocket ping
c0560973 1960 this.startWebSocketPing();
5ad8570f 1961 // Start heartbeat
c0560973 1962 this.startHeartbeat();
0a60c33c 1963 // Initialize connectors status
734d790d 1964 for (const connectorId of this.connectors.keys()) {
72092cfc 1965 let connectorStatus: ConnectorStatusEnum | undefined;
734d790d 1966 if (connectorId === 0) {
593cf3f9 1967 continue;
e7aeea18 1968 } else if (
56eb297e
JB
1969 !this.getConnectorStatus(connectorId)?.status &&
1970 (this.isChargingStationAvailable() === false ||
1789ba2c 1971 this.isConnectorAvailable(connectorId) === false)
e7aeea18 1972 ) {
721646e9 1973 connectorStatus = ConnectorStatusEnum.Unavailable;
45c0ae82
JB
1974 } else if (
1975 !this.getConnectorStatus(connectorId)?.status &&
1976 this.getConnectorStatus(connectorId)?.bootStatus
1977 ) {
1978 // Set boot status in template at startup
72092cfc 1979 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
56eb297e
JB
1980 } else if (this.getConnectorStatus(connectorId)?.status) {
1981 // Set previous status at startup
72092cfc 1982 connectorStatus = this.getConnectorStatus(connectorId)?.status;
5ad8570f 1983 } else {
56eb297e 1984 // Set default status
721646e9 1985 connectorStatus = ConnectorStatusEnum.Available;
5ad8570f 1986 }
48b75072 1987 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorStatus);
5ad8570f 1988 }
c9a4f9ea
JB
1989 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1990 await this.ocppRequestService.requestHandler<
1991 FirmwareStatusNotificationRequest,
1992 FirmwareStatusNotificationResponse
1993 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1994 status: FirmwareStatus.Installed,
1995 });
1996 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 1997 }
3637ca2c 1998
0a60c33c 1999 // Start the ATG
60ddad53 2000 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 2001 this.startAutomaticTransactionGenerator();
fa7bccf4 2002 }
aa428a31 2003 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2004 }
2005
e7aeea18
JB
2006 private async stopMessageSequence(
2007 reason: StopTransactionReason = StopTransactionReason.NONE
2008 ): Promise<void> {
136c90ba 2009 // Stop WebSocket ping
c0560973 2010 this.stopWebSocketPing();
79411696 2011 // Stop heartbeat
c0560973 2012 this.stopHeartbeat();
fa7bccf4 2013 // Stop ongoing transactions
b20eb107 2014 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
2015 this.stopAutomaticTransactionGenerator();
2016 } else {
2017 await this.stopRunningTransactions(reason);
79411696 2018 }
45c0ae82
JB
2019 for (const connectorId of this.connectors.keys()) {
2020 if (connectorId > 0) {
2021 await this.ocppRequestService.requestHandler<
2022 StatusNotificationRequest,
2023 StatusNotificationResponse
6e939d9e
JB
2024 >(
2025 this,
2026 RequestCommand.STATUS_NOTIFICATION,
2027 OCPPServiceUtils.buildStatusNotificationRequest(
2028 this,
2029 connectorId,
721646e9 2030 ConnectorStatusEnum.Unavailable
6e939d9e
JB
2031 )
2032 );
cdde2cfe 2033 delete this.getConnectorStatus(connectorId)?.status;
45c0ae82
JB
2034 }
2035 }
79411696
JB
2036 }
2037
c0560973 2038 private startWebSocketPing(): void {
17ac262c
JB
2039 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2040 this,
e7aeea18
JB
2041 StandardParametersKey.WebSocketPingInterval
2042 )
2043 ? Utils.convertToInt(
17ac262c
JB
2044 ChargingStationConfigurationUtils.getConfigurationKey(
2045 this,
2046 StandardParametersKey.WebSocketPingInterval
72092cfc 2047 )?.value
e7aeea18 2048 )
9cd3dfb0 2049 : 0;
ad2f27c3
JB
2050 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2051 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2052 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2053 this.wsConnection?.ping();
136c90ba
JB
2054 }
2055 }, webSocketPingInterval * 1000);
e7aeea18 2056 logger.info(
44eb6026
JB
2057 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2058 webSocketPingInterval
2059 )}`
e7aeea18 2060 );
ad2f27c3 2061 } else if (this.webSocketPingSetInterval) {
e7aeea18 2062 logger.info(
44eb6026
JB
2063 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2064 webSocketPingInterval
2065 )}`
e7aeea18 2066 );
136c90ba 2067 } else {
e7aeea18 2068 logger.error(
8f953431 2069 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
e7aeea18 2070 );
136c90ba
JB
2071 }
2072 }
2073
c0560973 2074 private stopWebSocketPing(): void {
ad2f27c3
JB
2075 if (this.webSocketPingSetInterval) {
2076 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2077 delete this.webSocketPingSetInterval;
136c90ba
JB
2078 }
2079 }
2080
1f5df42a 2081 private getConfiguredSupervisionUrl(): URL {
72092cfc 2082 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 2083 if (Utils.isNotEmptyArray(supervisionUrls)) {
269de583 2084 let configuredSupervisionUrlIndex: number;
2dcfe98e 2085 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2086 case SupervisionUrlDistribution.RANDOM:
269de583 2087 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2dcfe98e 2088 break;
a52a6446 2089 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2090 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2091 default:
a52a6446
JB
2092 Object.values(SupervisionUrlDistribution).includes(
2093 Configuration.getSupervisionUrlDistribution()
2094 ) === false &&
2095 logger.error(
2096 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2097 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2098 }`
2099 );
269de583 2100 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 2101 break;
c0560973 2102 }
269de583 2103 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
c0560973 2104 }
57939a9d 2105 return new URL(supervisionUrls as string);
136c90ba
JB
2106 }
2107
c0560973 2108 private stopHeartbeat(): void {
ad2f27c3
JB
2109 if (this.heartbeatSetInterval) {
2110 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2111 delete this.heartbeatSetInterval;
7dde0b73 2112 }
5ad8570f
JB
2113 }
2114
55516218 2115 private terminateWSConnection(): void {
56eb297e 2116 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2117 this.wsConnection?.terminate();
55516218
JB
2118 this.wsConnection = null;
2119 }
2120 }
2121
c72f6634 2122 private getReconnectExponentialDelay(): boolean {
a14885a3 2123 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2124 }
2125
aa428a31 2126 private async reconnect(): Promise<void> {
7874b0b1
JB
2127 // Stop WebSocket ping
2128 this.stopWebSocketPing();
136c90ba 2129 // Stop heartbeat
c0560973 2130 this.stopHeartbeat();
5ad8570f 2131 // Stop the ATG if needed
6d9876e7 2132 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2133 this.stopAutomaticTransactionGenerator();
ad2f27c3 2134 }
e7aeea18
JB
2135 if (
2136 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2137 this.getAutoReconnectMaxRetries() === -1
2138 ) {
ad2f27c3 2139 this.autoReconnectRetryCount++;
e7aeea18
JB
2140 const reconnectDelay = this.getReconnectExponentialDelay()
2141 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2142 : this.getConnectionTimeout() * 1000;
1e080116
JB
2143 const reconnectDelayWithdraw = 1000;
2144 const reconnectTimeout =
2145 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2146 ? reconnectDelay - reconnectDelayWithdraw
2147 : 0;
e7aeea18 2148 logger.error(
d56ea27c 2149 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2150 reconnectDelay,
2151 2
2152 )}ms, timeout ${reconnectTimeout}ms`
2153 );
032d6efc 2154 await Utils.sleep(reconnectDelay);
e7aeea18 2155 logger.error(
44eb6026 2156 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2157 );
2158 this.openWSConnection(
59b6ed8d 2159 {
abe9e9dd 2160 ...(this.stationInfo?.wsOptions ?? {}),
59b6ed8d
JB
2161 handshakeTimeout: reconnectTimeout,
2162 },
1e080116 2163 { closeOpened: true }
e7aeea18 2164 );
265e4266 2165 this.wsConnectionRestarted = true;
c0560973 2166 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2167 logger.error(
d56ea27c 2168 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2169 this.autoReconnectRetryCount
d56ea27c 2170 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2171 );
5ad8570f
JB
2172 }
2173 }
2174
551e477c
JB
2175 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2176 | AutomaticTransactionGeneratorConfiguration
2177 | undefined {
2178 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
fa7bccf4 2179 }
7dde0b73 2180}