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