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