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