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