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