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