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