fix: make stationInfo attributes writable again
[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
551e477c
JB
1135 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1136 let stationInfo: ChargingStationInfo | undefined;
5398cecf 1137 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
f832e5df
JB
1138 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1139 if (stationInfo) {
1140 delete stationInfo?.infoHash;
1141 }
1142 }
f765beaa 1143 return stationInfo;
2484ac1e
JB
1144 }
1145
33276ba6
JB
1146 private getStationInfo(): ChargingStationInfo {
1147 const defaultStationInfo: Partial<ChargingStationInfo> = {
1148 enableStatistics: false,
1149 remoteAuthorization: true,
1150 currentOutType: CurrentType.AC,
1151 mainVoltageMeterValues: true,
1152 phaseLineToLineVoltageMeterValues: false,
1153 customValueLimitationMeterValues: true,
1154 ocppStrictCompliance: true,
1155 outOfOrderEndMeterValues: false,
1156 beginEndMeterValues: false,
1157 meteringPerTransaction: true,
1158 transactionDataMeterValues: false,
1159 supervisionUrlOcppConfiguration: false,
1160 supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
1161 ocppVersion: OCPPVersion.VERSION_16,
1162 ocppPersistentConfiguration: true,
1163 stationInfoPersistentConfiguration: true,
1164 automaticTransactionGeneratorPersistentConfiguration: true,
1165 autoReconnectMaxRetries: -1,
1166 registrationMaxRetries: -1,
1167 reconnectExponentialDelay: false,
1168 stopTransactionsOnStopped: true,
1169 };
2484ac1e 1170 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1171 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1172 // Priority:
1173 // 1. charging station info from template
1174 // 2. charging station info from configuration file
f765beaa 1175 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
33276ba6 1176 return { ...defaultStationInfo, ...stationInfoFromFile! };
f765beaa 1177 }
fec4d204 1178 stationInfoFromFile &&
fba11dc6 1179 propagateSerialNumber(
e1d9a0f4 1180 this.getTemplateFromFile()!,
fec4d204 1181 stationInfoFromFile,
5edd8ba0 1182 stationInfoFromTemplate,
fec4d204 1183 );
33276ba6 1184 return { ...defaultStationInfo, ...stationInfoFromTemplate };
2484ac1e
JB
1185 }
1186
1187 private saveStationInfo(): void {
5398cecf 1188 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
b1bbdae5 1189 this.saveConfiguration();
ccb1d6e9 1190 }
2484ac1e
JB
1191 }
1192
3e888c65 1193 private handleUnsupportedVersion(version: OCPPVersion | undefined) {
944d4529 1194 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
ded57f02
JB
1195 logger.error(`${this.logPrefix()} ${errorMsg}`);
1196 throw new BaseError(errorMsg);
c0560973
JB
1197 }
1198
2484ac1e 1199 private initialize(): void {
e1d9a0f4 1200 const stationTemplate = this.getTemplateFromFile()!;
fba11dc6 1201 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
d972af76
JB
1202 this.configurationFile = join(
1203 dirname(this.templateFile.replace('station-templates', 'configurations')),
5edd8ba0 1204 `${getHashId(this.index, stationTemplate)}.json`,
0642c3d2 1205 );
61854f7c 1206 const stationConfiguration = this.getConfigurationFromFile();
a4f7c75f 1207 if (
61854f7c 1208 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
e1d9a0f4 1209 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
61854f7c 1210 (stationConfiguration?.connectorsStatus || stationConfiguration?.evsesStatus)
a4f7c75f 1211 ) {
61854f7c 1212 this.initializeConnectorsOrEvsesFromFile(stationConfiguration);
a4f7c75f
JB
1213 } else {
1214 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1215 }
33276ba6 1216 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1217 if (
1218 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1219 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1220 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1221 ) {
d812bdcb 1222 const patternGroup: number | undefined =
15748260 1223 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1224 this.stationInfo.firmwareVersion?.split('.').length;
e1d9a0f4
JB
1225 const match = this.stationInfo
1226 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1227 .slice(1, patternGroup! + 1);
3637ca2c 1228 const patchLevelIndex = match.length - 1;
5d280aae 1229 match[patchLevelIndex] = (
9bf0ef23 1230 convertToInt(match[patchLevelIndex]) +
e1d9a0f4 1231 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
5d280aae 1232 ).toString();
72092cfc 1233 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1234 }
6bccfcbc 1235 this.saveStationInfo();
6bccfcbc 1236 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
5398cecf 1237 if (this.stationInfo?.enableStatistics === true) {
6bccfcbc
JB
1238 this.performanceStatistics = PerformanceStatistics.getInstance(
1239 this.stationInfo.hashId,
e1d9a0f4 1240 this.stationInfo.chargingStationId!,
5edd8ba0 1241 this.configuredSupervisionUrl,
6bccfcbc
JB
1242 );
1243 }
fba11dc6 1244 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
692f2f64
JB
1245 this.powerDivider = this.getPowerDivider();
1246 // OCPP configuration
1247 this.ocppConfiguration = this.getOcppConfiguration();
1248 this.initializeOcppConfiguration();
1249 this.initializeOcppServices();
3e888c65
JB
1250 this.once(ChargingStationEvents.accepted, () => {
1251 this.startMessageSequence().catch((error) => {
1252 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error);
1253 });
1254 });
692f2f64
JB
1255 if (this.stationInfo?.autoRegister === true) {
1256 this.bootNotificationResponse = {
1257 currentTime: new Date(),
be4c6702 1258 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
692f2f64
JB
1259 status: RegistrationStatusEnumType.ACCEPTED,
1260 };
1261 }
147d0e0f
JB
1262 }
1263
feff11ec 1264 private initializeOcppServices(): void {
9a77cc07 1265 const ocppVersion = this.stationInfo?.ocppVersion;
feff11ec
JB
1266 switch (ocppVersion) {
1267 case OCPPVersion.VERSION_16:
1268 this.ocppIncomingRequestService =
1269 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1270 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
5edd8ba0 1271 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
feff11ec
JB
1272 );
1273 break;
1274 case OCPPVersion.VERSION_20:
1275 case OCPPVersion.VERSION_201:
1276 this.ocppIncomingRequestService =
1277 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1278 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
5edd8ba0 1279 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
feff11ec
JB
1280 );
1281 break;
1282 default:
1283 this.handleUnsupportedVersion(ocppVersion);
1284 break;
1285 }
1286 }
1287
2484ac1e 1288 private initializeOcppConfiguration(): void {
4e3b1d6b 1289 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.HeartbeatInterval))) {
f2d5e3d9 1290 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
f0f65a62 1291 }
4e3b1d6b 1292 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.HeartBeatInterval))) {
f2d5e3d9 1293 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
f0f65a62 1294 }
e7aeea18 1295 if (
4e3b1d6b 1296 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
5398cecf 1297 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
4e3b1d6b 1298 isNullOrUndefined(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!))
e7aeea18 1299 ) {
f2d5e3d9 1300 addConfigurationKey(
17ac262c 1301 this,
5398cecf 1302 this.stationInfo.supervisionUrlOcppKey!,
fa7bccf4 1303 this.configuredSupervisionUrl.href,
5edd8ba0 1304 { reboot: true },
e7aeea18 1305 );
e6895390 1306 } else if (
4e3b1d6b 1307 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
5398cecf 1308 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
bc0f4d8f 1309 !isNullOrUndefined(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!))
e6895390 1310 ) {
5398cecf 1311 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false });
12fc74d6 1312 }
cc6e8ab5 1313 if (
9bf0ef23 1314 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
4e3b1d6b 1315 isNullOrUndefined(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!))
cc6e8ab5 1316 ) {
f2d5e3d9 1317 addConfigurationKey(
17ac262c 1318 this,
e1d9a0f4 1319 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 1320 (
e1d9a0f4 1321 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
5edd8ba0 1322 ).toString(),
cc6e8ab5
JB
1323 );
1324 }
4e3b1d6b
JB
1325 if (
1326 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles))
1327 ) {
f2d5e3d9 1328 addConfigurationKey(
17ac262c 1329 this,
e7aeea18 1330 StandardParametersKey.SupportedFeatureProfiles,
5edd8ba0 1331 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
e7aeea18
JB
1332 );
1333 }
f2d5e3d9 1334 addConfigurationKey(
17ac262c 1335 this,
e7aeea18
JB
1336 StandardParametersKey.NumberOfConnectors,
1337 this.getNumberOfConnectors().toString(),
a95873d8 1338 { readonly: true },
5edd8ba0 1339 { overwrite: true },
e7aeea18 1340 );
4e3b1d6b
JB
1341 if (
1342 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData))
1343 ) {
f2d5e3d9 1344 addConfigurationKey(
17ac262c 1345 this,
e7aeea18 1346 StandardParametersKey.MeterValuesSampledData,
5edd8ba0 1347 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
e7aeea18 1348 );
7abfea5f 1349 }
4e3b1d6b
JB
1350 if (
1351 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation))
1352 ) {
dd08d43d 1353 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1354 if (this.hasEvses) {
1355 for (const evseStatus of this.evses.values()) {
1356 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1357 connectorsPhaseRotation.push(
e1d9a0f4 1358 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1359 );
28e78158
JB
1360 }
1361 }
1362 } else {
1363 for (const connectorId of this.connectors.keys()) {
dd08d43d 1364 connectorsPhaseRotation.push(
e1d9a0f4 1365 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1366 );
7e1dc878
JB
1367 }
1368 }
f2d5e3d9 1369 addConfigurationKey(
17ac262c 1370 this,
e7aeea18 1371 StandardParametersKey.ConnectorPhaseRotation,
5edd8ba0 1372 connectorsPhaseRotation.toString(),
e7aeea18 1373 );
7e1dc878 1374 }
4e3b1d6b
JB
1375 if (
1376 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests))
1377 ) {
f2d5e3d9 1378 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
36f6a92e 1379 }
17ac262c 1380 if (
4e3b1d6b 1381 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled)) &&
f2d5e3d9
JB
1382 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1383 SupportedFeatureProfiles.LocalAuthListManagement,
17ac262c
JB
1384 )
1385 ) {
f2d5e3d9
JB
1386 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1387 }
4e3b1d6b 1388 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut))) {
f2d5e3d9 1389 addConfigurationKey(
17ac262c 1390 this,
e7aeea18 1391 StandardParametersKey.ConnectionTimeOut,
5edd8ba0 1392 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
e7aeea18 1393 );
8bce55bf 1394 }
2484ac1e 1395 this.saveOcppConfiguration();
073bd098
JB
1396 }
1397
a4f7c75f
JB
1398 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1399 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
8df5ae48 1400 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
9bf0ef23 1401 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
8df5ae48 1402 }
a4f7c75f
JB
1403 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1404 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
9bf0ef23 1405 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
a4f7c75f
JB
1406 delete evseStatus.connectorsStatus;
1407 this.evses.set(evseId, {
8df5ae48 1408 ...(evseStatus as EvseStatus),
a4f7c75f 1409 connectors: new Map<number, ConnectorStatus>(
e1d9a0f4 1410 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f
JB
1411 connectorId,
1412 connectorStatus,
5edd8ba0 1413 ]),
a4f7c75f
JB
1414 ),
1415 });
1416 }
1417 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1418 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1419 logger.error(`${this.logPrefix()} ${errorMsg}`);
1420 throw new BaseError(errorMsg);
1421 } else {
1422 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1423 logger.error(`${this.logPrefix()} ${errorMsg}`);
1424 throw new BaseError(errorMsg);
1425 }
1426 }
1427
34eeb1fb 1428 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
cda5d0fb 1429 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
34eeb1fb 1430 this.initializeConnectorsFromTemplate(stationTemplate);
cda5d0fb 1431 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
34eeb1fb 1432 this.initializeEvsesFromTemplate(stationTemplate);
cda5d0fb 1433 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
ae25f265
JB
1434 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1435 logger.error(`${this.logPrefix()} ${errorMsg}`);
1436 throw new BaseError(errorMsg);
1437 } else {
1438 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1439 logger.error(`${this.logPrefix()} ${errorMsg}`);
1440 throw new BaseError(errorMsg);
1441 }
1442 }
1443
34eeb1fb 1444 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1445 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1446 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1447 logger.error(`${this.logPrefix()} ${errorMsg}`);
1448 throw new BaseError(errorMsg);
3d25cc86 1449 }
e1d9a0f4 1450 if (!stationTemplate?.Connectors?.[0]) {
3d25cc86
JB
1451 logger.warn(
1452 `${this.logPrefix()} Charging station information from template ${
1453 this.templateFile
5edd8ba0 1454 } with no connector id 0 configuration`,
3d25cc86
JB
1455 );
1456 }
cda5d0fb
JB
1457 if (stationTemplate?.Connectors) {
1458 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
fba11dc6 1459 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
d972af76 1460 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1461 .update(
5edd8ba0 1462 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
cda5d0fb 1463 )
3d25cc86
JB
1464 .digest('hex');
1465 const connectorsConfigChanged =
1466 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1467 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1468 connectorsConfigChanged && this.connectors.clear();
1469 this.connectorsConfigurationHash = connectorsConfigHash;
269196a8
JB
1470 if (templateMaxConnectors > 0) {
1471 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1472 if (
1473 connectorId === 0 &&
1c9de2b9 1474 (!stationTemplate?.Connectors?.[connectorId] ||
cda5d0fb 1475 this.getUseConnectorId0(stationTemplate) === false)
269196a8
JB
1476 ) {
1477 continue;
1478 }
1479 const templateConnectorId =
cda5d0fb 1480 connectorId > 0 && stationTemplate?.randomConnectors
9bf0ef23 1481 ? getRandomInteger(templateMaxAvailableConnectors, 1)
269196a8 1482 : connectorId;
cda5d0fb 1483 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
fba11dc6 1484 checkStationInfoConnectorStatus(
ae25f265 1485 templateConnectorId,
04b1261c
JB
1486 connectorStatus,
1487 this.logPrefix(),
5edd8ba0 1488 this.templateFile,
04b1261c 1489 );
9bf0ef23 1490 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1491 }
fba11dc6 1492 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
52952bf8 1493 this.saveConnectorsStatus();
ae25f265
JB
1494 } else {
1495 logger.warn(
1496 `${this.logPrefix()} Charging station information from template ${
1497 this.templateFile
5edd8ba0 1498 } with no connectors configuration defined, cannot create connectors`,
ae25f265 1499 );
3d25cc86
JB
1500 }
1501 }
1502 } else {
1503 logger.warn(
1504 `${this.logPrefix()} Charging station information from template ${
1505 this.templateFile
5edd8ba0 1506 } with no connectors configuration defined, using already defined connectors`,
3d25cc86
JB
1507 );
1508 }
3d25cc86
JB
1509 }
1510
34eeb1fb 1511 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1512 if (!stationTemplate?.Evses && this.evses.size === 0) {
ded57f02
JB
1513 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1514 logger.error(`${this.logPrefix()} ${errorMsg}`);
1515 throw new BaseError(errorMsg);
2585c6e9 1516 }
e1d9a0f4 1517 if (!stationTemplate?.Evses?.[0]) {
2585c6e9
JB
1518 logger.warn(
1519 `${this.logPrefix()} Charging station information from template ${
1520 this.templateFile
5edd8ba0 1521 } with no evse id 0 configuration`,
2585c6e9
JB
1522 );
1523 }
e1d9a0f4 1524 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
59a0f26d
JB
1525 logger.warn(
1526 `${this.logPrefix()} Charging station information from template ${
1527 this.templateFile
5edd8ba0 1528 } with evse id 0 with no connector id 0 configuration`,
59a0f26d
JB
1529 );
1530 }
491dad29
JB
1531 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1532 logger.warn(
1533 `${this.logPrefix()} Charging station information from template ${
1534 this.templateFile
1535 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
1536 );
1537 }
cda5d0fb 1538 if (stationTemplate?.Evses) {
d972af76 1539 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
ba01a213 1540 .update(JSON.stringify(stationTemplate?.Evses))
2585c6e9
JB
1541 .digest('hex');
1542 const evsesConfigChanged =
1543 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1544 if (this.evses?.size === 0 || evsesConfigChanged) {
1545 evsesConfigChanged && this.evses.clear();
1546 this.evsesConfigurationHash = evsesConfigHash;
fba11dc6 1547 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
ae25f265 1548 if (templateMaxEvses > 0) {
eb979012
JB
1549 for (const evseKey in stationTemplate.Evses) {
1550 const evseId = convertToInt(evseKey);
52952bf8 1551 this.evses.set(evseId, {
fba11dc6 1552 connectors: buildConnectorsMap(
eb979012 1553 stationTemplate?.Evses[evseKey]?.Connectors,
ae25f265 1554 this.logPrefix(),
5edd8ba0 1555 this.templateFile,
ae25f265
JB
1556 ),
1557 availability: AvailabilityType.Operative,
1558 });
e1d9a0f4 1559 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
ae25f265 1560 }
52952bf8 1561 this.saveEvsesStatus();
ae25f265
JB
1562 } else {
1563 logger.warn(
1564 `${this.logPrefix()} Charging station information from template ${
04b1261c 1565 this.templateFile
5edd8ba0 1566 } with no evses configuration defined, cannot create evses`,
04b1261c 1567 );
2585c6e9
JB
1568 }
1569 }
513db108
JB
1570 } else {
1571 logger.warn(
1572 `${this.logPrefix()} Charging station information from template ${
1573 this.templateFile
5edd8ba0 1574 } with no evses configuration defined, using already defined evses`,
513db108 1575 );
2585c6e9
JB
1576 }
1577 }
1578
551e477c
JB
1579 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1580 let configuration: ChargingStationConfiguration | undefined;
9bf0ef23 1581 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1582 try {
57adbebc
JB
1583 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1584 configuration = this.sharedLRUCache.getChargingStationConfiguration(
5edd8ba0 1585 this.configurationFileHash,
57adbebc 1586 );
7c72977b
JB
1587 } else {
1588 const measureId = `${FileType.ChargingStationConfiguration} read`;
1589 const beginId = PerformanceStatistics.beginMeasure(measureId);
1590 configuration = JSON.parse(
5edd8ba0 1591 readFileSync(this.configurationFile, 'utf8'),
7c72977b
JB
1592 ) as ChargingStationConfiguration;
1593 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1594 this.sharedLRUCache.setChargingStationConfiguration(configuration);
e1d9a0f4 1595 this.configurationFileHash = configuration.configurationHash!;
7c72977b 1596 }
073bd098 1597 } catch (error) {
fa5995d6 1598 handleFileException(
073bd098 1599 this.configurationFile,
7164966d
JB
1600 FileType.ChargingStationConfiguration,
1601 error as NodeJS.ErrnoException,
5edd8ba0 1602 this.logPrefix(),
073bd098
JB
1603 );
1604 }
1605 }
1606 return configuration;
1607 }
1608
cb60061f 1609 private saveAutomaticTransactionGeneratorConfiguration(): void {
5398cecf 1610 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
5ced7e80
JB
1611 this.saveConfiguration();
1612 }
ac7f79af
JB
1613 }
1614
52952bf8 1615 private saveConnectorsStatus() {
7446de3b 1616 this.saveConfiguration();
52952bf8
JB
1617 }
1618
1619 private saveEvsesStatus() {
7446de3b 1620 this.saveConfiguration();
52952bf8
JB
1621 }
1622
179ed367 1623 private saveConfiguration(): void {
9bf0ef23 1624 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1625 try {
d972af76
JB
1626 if (!existsSync(dirname(this.configurationFile))) {
1627 mkdirSync(dirname(this.configurationFile), { recursive: true });
073bd098 1628 }
ae8ceef3
JB
1629 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1630 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1631 : {};
5398cecf 1632 if (this.stationInfo?.stationInfoPersistentConfiguration === true && this.stationInfo) {
52952bf8 1633 configurationData.stationInfo = this.stationInfo;
5ced7e80
JB
1634 } else {
1635 delete configurationData.stationInfo;
52952bf8 1636 }
5398cecf
JB
1637 if (
1638 this.stationInfo?.ocppPersistentConfiguration === true &&
1639 this.ocppConfiguration?.configurationKey
1640 ) {
52952bf8 1641 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
5ced7e80
JB
1642 } else {
1643 delete configurationData.configurationKey;
52952bf8 1644 }
179ed367
JB
1645 configurationData = merge<ChargingStationConfiguration>(
1646 configurationData,
5edd8ba0 1647 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
179ed367 1648 );
5ced7e80 1649 if (
5398cecf 1650 !this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ||
5ced7e80
JB
1651 !this.getAutomaticTransactionGeneratorConfiguration()
1652 ) {
1653 delete configurationData.automaticTransactionGenerator;
1654 }
b1bbdae5 1655 if (this.connectors.size > 0) {
179ed367 1656 configurationData.connectorsStatus = buildConnectorsStatus(this);
5ced7e80
JB
1657 } else {
1658 delete configurationData.connectorsStatus;
52952bf8 1659 }
b1bbdae5 1660 if (this.evses.size > 0) {
179ed367 1661 configurationData.evsesStatus = buildEvsesStatus(this);
5ced7e80
JB
1662 } else {
1663 delete configurationData.evsesStatus;
52952bf8 1664 }
7c72977b 1665 delete configurationData.configurationHash;
d972af76 1666 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1667 .update(
1668 JSON.stringify({
1669 stationInfo: configurationData.stationInfo,
1670 configurationKey: configurationData.configurationKey,
1671 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
8ab96efb
JB
1672 ...(this.connectors.size > 0 && {
1673 connectorsStatus: configurationData.connectorsStatus,
1674 }),
1675 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }),
5edd8ba0 1676 } as ChargingStationConfiguration),
5ced7e80 1677 )
7c72977b
JB
1678 .digest('hex');
1679 if (this.configurationFileHash !== configurationHash) {
0ebf7c2e
JB
1680 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1681 configurationData.configurationHash = configurationHash;
1682 const measureId = `${FileType.ChargingStationConfiguration} write`;
1683 const beginId = PerformanceStatistics.beginMeasure(measureId);
1684 writeFileSync(
1685 this.configurationFile,
4ed03b6e 1686 JSON.stringify(configurationData, undefined, 2),
0ebf7c2e
JB
1687 'utf8',
1688 );
1689 PerformanceStatistics.endMeasure(measureId, beginId);
1690 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1691 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1692 this.configurationFileHash = configurationHash;
1693 }).catch((error) => {
1694 handleFileException(
1695 this.configurationFile,
1696 FileType.ChargingStationConfiguration,
1697 error as NodeJS.ErrnoException,
1698 this.logPrefix(),
1699 );
1700 });
7c72977b
JB
1701 } else {
1702 logger.debug(
1703 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1704 this.configurationFile
5edd8ba0 1705 }`,
7c72977b 1706 );
2484ac1e 1707 }
2484ac1e 1708 } catch (error) {
fa5995d6 1709 handleFileException(
2484ac1e 1710 this.configurationFile,
7164966d
JB
1711 FileType.ChargingStationConfiguration,
1712 error as NodeJS.ErrnoException,
5edd8ba0 1713 this.logPrefix(),
073bd098
JB
1714 );
1715 }
2484ac1e
JB
1716 } else {
1717 logger.error(
5edd8ba0 1718 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
2484ac1e 1719 );
073bd098
JB
1720 }
1721 }
1722
551e477c
JB
1723 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1724 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1725 }
1726
551e477c 1727 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
60655b26 1728 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
9fe79a13 1729 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
60655b26 1730 return { configurationKey };
648512ce 1731 }
60655b26 1732 return undefined;
7dde0b73
JB
1733 }
1734
551e477c
JB
1735 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1736 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1737 this.getOcppConfigurationFromFile();
2484ac1e
JB
1738 if (!ocppConfiguration) {
1739 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1740 }
1741 return ocppConfiguration;
1742 }
1743
c0560973 1744 private async onOpen(): Promise<void> {
56eb297e 1745 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1 1746 logger.info(
5edd8ba0 1747 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
5144f4d1 1748 );
f23be6aa 1749 let registrationRetryCount = 0;
ed6cfcff 1750 if (this.isRegistered() === false) {
5144f4d1 1751 // Send BootNotification
5144f4d1 1752 do {
f7f98c68 1753 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1754 BootNotificationRequest,
1755 BootNotificationResponse
8bfbc743
JB
1756 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1757 skipBufferingOnError: true,
1758 });
ed6cfcff 1759 if (this.isRegistered() === false) {
5398cecf 1760 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount;
9bf0ef23 1761 await sleep(
1895299d 1762 this?.bootNotificationResponse?.interval
be4c6702 1763 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
5edd8ba0 1764 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
5144f4d1
JB
1765 );
1766 }
1767 } while (
ed6cfcff 1768 this.isRegistered() === false &&
5398cecf 1769 (registrationRetryCount <= this.stationInfo.registrationMaxRetries! ||
9a77cc07 1770 this.stationInfo?.registrationMaxRetries === -1)
5144f4d1
JB
1771 );
1772 }
ed6cfcff 1773 if (this.isRegistered() === true) {
db54d2e0 1774 this.emit(ChargingStationEvents.registered);
f7c2994d 1775 if (this.inAcceptedState() === true) {
db54d2e0 1776 this.emit(ChargingStationEvents.accepted);
c0560973 1777 }
5144f4d1
JB
1778 } else {
1779 logger.error(
f23be6aa 1780 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${this
5398cecf 1781 .stationInfo?.registrationMaxRetries})`,
5144f4d1 1782 );
caad9d6b 1783 }
aa428a31 1784 this.autoReconnectRetryCount = 0;
db54d2e0 1785 this.emit(ChargingStationEvents.updated);
2e6f5966 1786 } else {
5144f4d1 1787 logger.warn(
5edd8ba0 1788 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
e7aeea18 1789 );
2e6f5966 1790 }
2e6f5966
JB
1791 }
1792
9e9ddf70 1793 private async onClose(code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
d09085e9 1794 switch (code) {
6c65a295
JB
1795 // Normal close
1796 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1797 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1798 logger.info(
9bf0ef23 1799 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1800 code,
1801 )}' and reason '${reason.toString()}'`,
e7aeea18 1802 );
c0560973
JB
1803 this.autoReconnectRetryCount = 0;
1804 break;
6c65a295
JB
1805 // Abnormal close
1806 default:
e7aeea18 1807 logger.error(
9bf0ef23 1808 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1809 code,
1810 )}' and reason '${reason.toString()}'`,
e7aeea18 1811 );
56eb297e 1812 this.started === true && (await this.reconnect());
c0560973
JB
1813 break;
1814 }
db54d2e0 1815 this.emit(ChargingStationEvents.updated);
2e6f5966
JB
1816 }
1817
56d09fd7
JB
1818 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1819 const cachedRequest = this.requests.get(messageId);
1820 if (Array.isArray(cachedRequest) === true) {
1821 return cachedRequest;
1822 }
1823 throw new OCPPError(
1824 ErrorType.PROTOCOL_ERROR,
1825 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0 1826 messageType,
56d09fd7
JB
1827 )} is not an array`,
1828 undefined,
5edd8ba0 1829 cachedRequest as JsonType,
56d09fd7
JB
1830 );
1831 }
1832
1833 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1834 const [messageType, messageId, commandName, commandPayload] = request;
9a77cc07 1835 if (this.stationInfo?.enableStatistics === true) {
56d09fd7
JB
1836 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1837 }
1838 logger.debug(
1839 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
5edd8ba0
JB
1840 request,
1841 )}`,
56d09fd7
JB
1842 );
1843 // Process the message
1844 await this.ocppIncomingRequestService.incomingRequestHandler(
1845 this,
1846 messageId,
1847 commandName,
5edd8ba0 1848 commandPayload,
56d09fd7 1849 );
8baae8ee 1850 this.emit(ChargingStationEvents.updated);
56d09fd7
JB
1851 }
1852
1853 private handleResponseMessage(response: Response): void {
1854 const [messageType, messageId, commandPayload] = response;
1855 if (this.requests.has(messageId) === false) {
1856 // Error
1857 throw new OCPPError(
1858 ErrorType.INTERNAL_ERROR,
1859 `Response for unknown message id ${messageId}`,
1860 undefined,
5edd8ba0 1861 commandPayload,
56d09fd7
JB
1862 );
1863 }
1864 // Respond
1865 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1866 messageType,
5edd8ba0 1867 messageId,
e1d9a0f4 1868 )!;
56d09fd7
JB
1869 logger.debug(
1870 `${this.logPrefix()} << Command '${
1871 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1872 }' received response payload: ${JSON.stringify(response)}`,
56d09fd7
JB
1873 );
1874 responseCallback(commandPayload, requestPayload);
1875 }
1876
1877 private handleErrorMessage(errorResponse: ErrorResponse): void {
1878 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1879 if (this.requests.has(messageId) === false) {
1880 // Error
1881 throw new OCPPError(
1882 ErrorType.INTERNAL_ERROR,
1883 `Error response for unknown message id ${messageId}`,
1884 undefined,
5edd8ba0 1885 { errorType, errorMessage, errorDetails },
56d09fd7
JB
1886 );
1887 }
e1d9a0f4 1888 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
56d09fd7
JB
1889 logger.debug(
1890 `${this.logPrefix()} << Command '${
1891 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1892 }' received error response payload: ${JSON.stringify(errorResponse)}`,
56d09fd7
JB
1893 );
1894 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1895 }
1896
ef7d8c21 1897 private async onMessage(data: RawData): Promise<void> {
e1d9a0f4 1898 let request: IncomingRequest | Response | ErrorResponse | undefined;
9e9ddf70 1899 let messageType: MessageType | undefined;
ded57f02 1900 let errorMsg: string;
c0560973 1901 try {
e1d9a0f4 1902 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1903 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1904 if (Array.isArray(request) === true) {
56d09fd7 1905 [messageType] = request;
b3ec7bc1
JB
1906 // Check the type of message
1907 switch (messageType) {
1908 // Incoming Message
1909 case MessageType.CALL_MESSAGE:
56d09fd7 1910 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1911 break;
56d09fd7 1912 // Response Message
b3ec7bc1 1913 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1914 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1915 break;
1916 // Error Message
1917 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1918 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1919 break;
56d09fd7 1920 // Unknown Message
b3ec7bc1
JB
1921 default:
1922 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
1923 errorMsg = `Wrong message type ${messageType}`;
1924 logger.error(`${this.logPrefix()} ${errorMsg}`);
1925 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 1926 }
47e22477 1927 } else {
e1d9a0f4
JB
1928 throw new OCPPError(
1929 ErrorType.PROTOCOL_ERROR,
1930 'Incoming message is not an array',
1931 undefined,
1932 {
1933 request,
1934 },
1935 );
47e22477 1936 }
c0560973 1937 } catch (error) {
e1d9a0f4
JB
1938 let commandName: IncomingRequestCommand | undefined;
1939 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
56d09fd7 1940 let errorCallback: ErrorCallback;
e1d9a0f4 1941 const [, messageId] = request!;
13701f69
JB
1942 switch (messageType) {
1943 case MessageType.CALL_MESSAGE:
56d09fd7 1944 [, , commandName] = request as IncomingRequest;
13701f69 1945 // Send error
56d09fd7 1946 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1947 break;
1948 case MessageType.CALL_RESULT_MESSAGE:
1949 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1950 if (this.requests.has(messageId) === true) {
e1d9a0f4 1951 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
13701f69
JB
1952 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1953 errorCallback(error as OCPPError, false);
1954 } else {
1955 // Remove the request from the cache in case of error at response handling
1956 this.requests.delete(messageId);
1957 }
de4cb8b6 1958 break;
ba7965c4 1959 }
56d09fd7
JB
1960 if (error instanceof OCPPError === false) {
1961 logger.warn(
1962 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1963 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1964 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1965 }' message '${data.toString()}' handling is not an OCPPError:`,
5edd8ba0 1966 error,
56d09fd7
JB
1967 );
1968 }
1969 logger.error(
1970 `${this.logPrefix()} Incoming OCPP command '${
1971 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1972 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7
JB
1973 }' message '${data.toString()}'${
1974 messageType !== MessageType.CALL_MESSAGE
1975 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1976 : ''
1977 } processing error:`,
5edd8ba0 1978 error,
56d09fd7 1979 );
c0560973 1980 }
2328be1e
JB
1981 }
1982
c0560973 1983 private onPing(): void {
44eb6026 1984 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1985 }
1986
1987 private onPong(): void {
44eb6026 1988 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1989 }
1990
9534e74e 1991 private onError(error: WSError): void {
bcc9c3c0 1992 this.closeWSConnection();
44eb6026 1993 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1994 }
1995
18bf8274 1996 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
5398cecf 1997 if (this.stationInfo?.meteringPerTransaction === true) {
07989fad 1998 return (
18bf8274 1999 (rounded === true
e1d9a0f4 2000 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
07989fad
JB
2001 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2002 );
2003 }
2004 return (
18bf8274 2005 (rounded === true
e1d9a0f4 2006 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
07989fad
JB
2007 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2008 );
2009 }
2010
cda5d0fb
JB
2011 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2012 return stationTemplate?.useConnectorId0 ?? true;
8bce55bf
JB
2013 }
2014
9ff486f4 2015 private async stopRunningTransactions(reason?: StopTransactionReason): Promise<void> {
28e78158 2016 if (this.hasEvses) {
3fa7f799
JB
2017 for (const [evseId, evseStatus] of this.evses) {
2018 if (evseId === 0) {
2019 continue;
2020 }
28e78158
JB
2021 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2022 if (connectorStatus.transactionStarted === true) {
2023 await this.stopTransactionOnConnector(connectorId, reason);
2024 }
2025 }
2026 }
2027 } else {
2028 for (const connectorId of this.connectors.keys()) {
2029 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2030 await this.stopTransactionOnConnector(connectorId, reason);
2031 }
60ddad53
JB
2032 }
2033 }
2034 }
2035
1f761b9a 2036 // 0 for disabling
c72f6634 2037 private getConnectionTimeout(): number {
4e3b1d6b
JB
2038 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) !== undefined) {
2039 return convertToInt(
2040 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value! ??
2041 Constants.DEFAULT_CONNECTION_TIMEOUT,
e7aeea18 2042 );
291cb255 2043 }
291cb255 2044 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
2045 }
2046
c0560973 2047 private getPowerDivider(): number {
b1bbdae5 2048 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
be1e907c 2049 if (this.stationInfo?.powerSharedByConnectors === true) {
c0560973 2050 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
2051 }
2052 return powerDivider;
2053 }
2054
fa7bccf4
JB
2055 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2056 const maximumPower = this.getMaximumPower(stationInfo);
2057 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2058 case CurrentType.AC:
2059 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2060 this.getNumberOfPhases(stationInfo),
b1bbdae5 2061 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
5edd8ba0 2062 this.getVoltageOut(stationInfo),
cc6e8ab5
JB
2063 );
2064 case CurrentType.DC:
fa7bccf4 2065 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
2066 }
2067 }
2068
5398cecf
JB
2069 private getMaximumPower(stationInfo?: ChargingStationInfo): number {
2070 return (stationInfo ?? this.stationInfo).maximumPower!;
2071 }
2072
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}