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