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