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