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