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