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