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