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