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