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