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