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