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