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