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