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