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