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