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