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