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