56adcc9cc13d4e351087b2e25895ed78cf716d40
[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 reservationFound = this.getReservationBy(
947 ReservationFilterKey.RESERVATION_ID,
948 reservation.reservationId,
949 );
950 if (reservationFound) {
951 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING);
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 startReservationExpirationSetInterval(customInterval?: number): void {
1012 const interval =
1013 customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
1014 if (interval > 0) {
1015 logger.info(
1016 `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
1017 interval,
1018 )}`,
1019 );
1020 this.reservationExpirationSetInterval = setInterval((): void => {
1021 const currentDate = new Date();
1022 if (this.hasEvses) {
1023 for (const evseStatus of this.evses.values()) {
1024 for (const connectorStatus of evseStatus.connectors.values()) {
1025 if (
1026 connectorStatus.reservation &&
1027 connectorStatus.reservation.expiryDate < currentDate
1028 ) {
1029 this.removeReservation(
1030 connectorStatus.reservation,
1031 ReservationTerminationReason.EXPIRED,
1032 ).catch(Constants.EMPTY_FUNCTION);
1033 }
1034 }
1035 }
1036 } else {
1037 for (const connectorStatus of this.connectors.values()) {
1038 if (
1039 connectorStatus.reservation &&
1040 connectorStatus.reservation.expiryDate < currentDate
1041 ) {
1042 this.removeReservation(
1043 connectorStatus.reservation,
1044 ReservationTerminationReason.EXPIRED,
1045 ).catch(Constants.EMPTY_FUNCTION);
1046 }
1047 }
1048 }
1049 }, interval);
1050 }
1051 }
1052
1053 public restartReservationExpiryDateSetInterval(): void {
1054 this.stopReservationExpirationSetInterval();
1055 this.startReservationExpirationSetInterval();
1056 }
1057
1058 public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
1059 return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag;
1060 }
1061
1062 public isConnectorReservable(
1063 reservationId: number,
1064 idTag?: string,
1065 connectorId?: number,
1066 ): boolean {
1067 const reservation = this.getReservationBy(ReservationFilterKey.RESERVATION_ID, reservationId);
1068 const userReservedAlready =
1069 !isUndefined(idTag) && isUndefined(this.getReservationBy(ReservationFilterKey.ID_TAG, idTag!))
1070 ? false
1071 : true;
1072 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
1073 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1074 return (
1075 isUndefined(reservation) &&
1076 !userReservedAlready &&
1077 notConnectorZero &&
1078 freeConnectorsAvailable
1079 );
1080 }
1081
1082 private getNumberOfReservableConnectors(): number {
1083 let reservableConnectors = 0;
1084 if (this.hasEvses) {
1085 for (const evseStatus of this.evses.values()) {
1086 reservableConnectors += countReservableConnectors(evseStatus.connectors);
1087 }
1088 } else {
1089 reservableConnectors = countReservableConnectors(this.connectors);
1090 }
1091 return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
1092 }
1093
1094 private getNumberOfReservationsOnConnectorZero(): number {
1095 let numberOfReservations = 0;
1096 if (
1097 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1098 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) ||
1099 (!this.hasEvses && this.connectors.get(0)?.reservation)
1100 ) {
1101 ++numberOfReservations;
1102 }
1103 return numberOfReservations;
1104 }
1105
1106 private flushMessageBuffer(): void {
1107 if (this.messageBuffer.size > 0) {
1108 for (const message of this.messageBuffer.values()) {
1109 let beginId: string | undefined;
1110 let commandName: RequestCommand | undefined;
1111 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1112 const isRequest = messageType === MessageType.CALL_MESSAGE;
1113 if (isRequest) {
1114 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1115 beginId = PerformanceStatistics.beginMeasure(commandName);
1116 }
1117 this.wsConnection?.send(message);
1118 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!);
1119 logger.debug(
1120 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1121 messageType,
1122 )} payload sent: ${message}`,
1123 );
1124 this.messageBuffer.delete(message);
1125 }
1126 }
1127 }
1128
1129 private getSupervisionUrlOcppConfiguration(): boolean {
1130 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
1131 }
1132
1133 private stopReservationExpirationSetInterval(): void {
1134 if (this.reservationExpirationSetInterval) {
1135 clearInterval(this.reservationExpirationSetInterval);
1136 }
1137 }
1138
1139 private getSupervisionUrlOcppKey(): string {
1140 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
1141 }
1142
1143 private getTemplateFromFile(): ChargingStationTemplate | undefined {
1144 let template: ChargingStationTemplate | undefined;
1145 try {
1146 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1147 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
1148 } else {
1149 const measureId = `${FileType.ChargingStationTemplate} read`;
1150 const beginId = PerformanceStatistics.beginMeasure(measureId);
1151 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
1152 PerformanceStatistics.endMeasure(measureId, beginId);
1153 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1154 .update(JSON.stringify(template))
1155 .digest('hex');
1156 this.sharedLRUCache.setChargingStationTemplate(template);
1157 this.templateFileHash = template.templateHash;
1158 }
1159 } catch (error) {
1160 handleFileException(
1161 this.templateFile,
1162 FileType.ChargingStationTemplate,
1163 error as NodeJS.ErrnoException,
1164 this.logPrefix(),
1165 );
1166 }
1167 return template;
1168 }
1169
1170 private getStationInfoFromTemplate(): ChargingStationInfo {
1171 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
1172 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
1173 warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
1174 if (stationTemplate?.Connectors) {
1175 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
1176 }
1177 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
1178 stationInfo.hashId = getHashId(this.index, stationTemplate);
1179 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
1180 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
1181 createSerialNumber(stationTemplate, stationInfo);
1182 if (isNotEmptyArray(stationTemplate?.power)) {
1183 stationTemplate.power = stationTemplate.power as number[];
1184 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
1185 stationInfo.maximumPower =
1186 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
1187 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1188 : stationTemplate.power[powerArrayRandomIndex];
1189 } else {
1190 stationTemplate.power = stationTemplate?.power as number;
1191 stationInfo.maximumPower =
1192 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
1193 ? stationTemplate.power * 1000
1194 : stationTemplate.power;
1195 }
1196 stationInfo.firmwareVersionPattern =
1197 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
1198 if (
1199 isNotEmptyString(stationInfo.firmwareVersion) &&
1200 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) === false
1201 ) {
1202 logger.warn(
1203 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1204 this.templateFile
1205 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
1206 );
1207 }
1208 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1209 {
1210 versionUpgrade: {
1211 step: 1,
1212 },
1213 reset: true,
1214 },
1215 stationTemplate?.firmwareUpgrade ?? {},
1216 );
1217 stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
1218 ? secondsToMilliseconds(stationTemplate.resetTime!)
1219 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
1220 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
1221 return stationInfo;
1222 }
1223
1224 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1225 let stationInfo: ChargingStationInfo | undefined;
1226 if (this.getStationInfoPersistentConfiguration()) {
1227 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1228 if (stationInfo) {
1229 delete stationInfo?.infoHash;
1230 }
1231 }
1232 return stationInfo;
1233 }
1234
1235 private getStationInfo(): ChargingStationInfo {
1236 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
1237 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
1238 // Priority:
1239 // 1. charging station info from template
1240 // 2. charging station info from configuration file
1241 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
1242 return stationInfoFromFile!;
1243 }
1244 stationInfoFromFile &&
1245 propagateSerialNumber(
1246 this.getTemplateFromFile()!,
1247 stationInfoFromFile,
1248 stationInfoFromTemplate,
1249 );
1250 return stationInfoFromTemplate;
1251 }
1252
1253 private saveStationInfo(): void {
1254 if (this.getStationInfoPersistentConfiguration()) {
1255 this.saveConfiguration();
1256 }
1257 }
1258
1259 private getOcppPersistentConfiguration(): boolean {
1260 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1261 }
1262
1263 private getStationInfoPersistentConfiguration(): boolean {
1264 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
1265 }
1266
1267 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1268 return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
1269 }
1270
1271 private handleUnsupportedVersion(version: OCPPVersion) {
1272 const errorMsg = `Unsupported protocol version '${version}' configured
1273 in template file ${this.templateFile}`;
1274 logger.error(`${this.logPrefix()} ${errorMsg}`);
1275 throw new BaseError(errorMsg);
1276 }
1277
1278 private initialize(): void {
1279 const stationTemplate = this.getTemplateFromFile()!;
1280 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
1281 this.configurationFile = join(
1282 dirname(this.templateFile.replace('station-templates', 'configurations')),
1283 `${getHashId(this.index, stationTemplate)}.json`,
1284 );
1285 const chargingStationConfiguration = this.getConfigurationFromFile();
1286 if (
1287 chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
1288 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1289 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1290 ) {
1291 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1292 } else {
1293 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1294 }
1295 this.stationInfo = this.getStationInfo();
1296 if (
1297 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1298 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1299 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1300 ) {
1301 const patternGroup: number | undefined =
1302 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1303 this.stationInfo.firmwareVersion?.split('.').length;
1304 const match = this.stationInfo
1305 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1306 .slice(1, patternGroup! + 1);
1307 const patchLevelIndex = match.length - 1;
1308 match[patchLevelIndex] = (
1309 convertToInt(match[patchLevelIndex]) +
1310 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1311 ).toString();
1312 this.stationInfo.firmwareVersion = match?.join('.');
1313 }
1314 this.saveStationInfo();
1315 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1316 if (this.getEnableStatistics() === true) {
1317 this.performanceStatistics = PerformanceStatistics.getInstance(
1318 this.stationInfo.hashId,
1319 this.stationInfo.chargingStationId!,
1320 this.configuredSupervisionUrl,
1321 );
1322 }
1323 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
1324 this.powerDivider = this.getPowerDivider();
1325 // OCPP configuration
1326 this.ocppConfiguration = this.getOcppConfiguration();
1327 this.initializeOcppConfiguration();
1328 this.initializeOcppServices();
1329 if (this.stationInfo?.autoRegister === true) {
1330 this.bootNotificationResponse = {
1331 currentTime: new Date(),
1332 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1333 status: RegistrationStatusEnumType.ACCEPTED,
1334 };
1335 }
1336 }
1337
1338 private initializeOcppServices(): void {
1339 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1340 switch (ocppVersion) {
1341 case OCPPVersion.VERSION_16:
1342 this.ocppIncomingRequestService =
1343 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1344 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1345 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
1346 );
1347 break;
1348 case OCPPVersion.VERSION_20:
1349 case OCPPVersion.VERSION_201:
1350 this.ocppIncomingRequestService =
1351 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1352 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1353 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
1354 );
1355 break;
1356 default:
1357 this.handleUnsupportedVersion(ocppVersion);
1358 break;
1359 }
1360 }
1361
1362 private initializeOcppConfiguration(): void {
1363 if (!getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)) {
1364 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
1365 }
1366 if (!getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)) {
1367 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
1368 }
1369 if (
1370 this.getSupervisionUrlOcppConfiguration() &&
1371 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1372 !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1373 ) {
1374 addConfigurationKey(
1375 this,
1376 this.getSupervisionUrlOcppKey(),
1377 this.configuredSupervisionUrl.href,
1378 { reboot: true },
1379 );
1380 } else if (
1381 !this.getSupervisionUrlOcppConfiguration() &&
1382 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1383 getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1384 ) {
1385 deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save: false });
1386 }
1387 if (
1388 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1389 !getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
1390 ) {
1391 addConfigurationKey(
1392 this,
1393 this.stationInfo.amperageLimitationOcppKey!,
1394 (
1395 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
1396 ).toString(),
1397 );
1398 }
1399 if (!getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)) {
1400 addConfigurationKey(
1401 this,
1402 StandardParametersKey.SupportedFeatureProfiles,
1403 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
1404 );
1405 }
1406 addConfigurationKey(
1407 this,
1408 StandardParametersKey.NumberOfConnectors,
1409 this.getNumberOfConnectors().toString(),
1410 { readonly: true },
1411 { overwrite: true },
1412 );
1413 if (!getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData)) {
1414 addConfigurationKey(
1415 this,
1416 StandardParametersKey.MeterValuesSampledData,
1417 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
1418 );
1419 }
1420 if (!getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation)) {
1421 const connectorsPhaseRotation: string[] = [];
1422 if (this.hasEvses) {
1423 for (const evseStatus of this.evses.values()) {
1424 for (const connectorId of evseStatus.connectors.keys()) {
1425 connectorsPhaseRotation.push(
1426 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1427 );
1428 }
1429 }
1430 } else {
1431 for (const connectorId of this.connectors.keys()) {
1432 connectorsPhaseRotation.push(
1433 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1434 );
1435 }
1436 }
1437 addConfigurationKey(
1438 this,
1439 StandardParametersKey.ConnectorPhaseRotation,
1440 connectorsPhaseRotation.toString(),
1441 );
1442 }
1443 if (!getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests)) {
1444 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
1445 }
1446 if (
1447 !getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) &&
1448 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1449 SupportedFeatureProfiles.LocalAuthListManagement,
1450 )
1451 ) {
1452 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1453 }
1454 if (!getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
1455 addConfigurationKey(
1456 this,
1457 StandardParametersKey.ConnectionTimeOut,
1458 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
1459 );
1460 }
1461 this.saveOcppConfiguration();
1462 }
1463
1464 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1465 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
1466 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1467 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1468 }
1469 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1470 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1471 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
1472 delete evseStatus.connectorsStatus;
1473 this.evses.set(evseId, {
1474 ...(evseStatus as EvseStatus),
1475 connectors: new Map<number, ConnectorStatus>(
1476 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1477 connectorId,
1478 connectorStatus,
1479 ]),
1480 ),
1481 });
1482 }
1483 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1484 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1485 logger.error(`${this.logPrefix()} ${errorMsg}`);
1486 throw new BaseError(errorMsg);
1487 } else {
1488 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1489 logger.error(`${this.logPrefix()} ${errorMsg}`);
1490 throw new BaseError(errorMsg);
1491 }
1492 }
1493
1494 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
1495 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
1496 this.initializeConnectorsFromTemplate(stationTemplate);
1497 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
1498 this.initializeEvsesFromTemplate(stationTemplate);
1499 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
1500 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1501 logger.error(`${this.logPrefix()} ${errorMsg}`);
1502 throw new BaseError(errorMsg);
1503 } else {
1504 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1505 logger.error(`${this.logPrefix()} ${errorMsg}`);
1506 throw new BaseError(errorMsg);
1507 }
1508 }
1509
1510 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
1511 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
1512 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1513 logger.error(`${this.logPrefix()} ${errorMsg}`);
1514 throw new BaseError(errorMsg);
1515 }
1516 if (!stationTemplate?.Connectors?.[0]) {
1517 logger.warn(
1518 `${this.logPrefix()} Charging station information from template ${
1519 this.templateFile
1520 } with no connector id 0 configuration`,
1521 );
1522 }
1523 if (stationTemplate?.Connectors) {
1524 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1525 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
1526 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1527 .update(
1528 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
1529 )
1530 .digest('hex');
1531 const connectorsConfigChanged =
1532 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1533 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1534 connectorsConfigChanged && this.connectors.clear();
1535 this.connectorsConfigurationHash = connectorsConfigHash;
1536 if (templateMaxConnectors > 0) {
1537 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1538 if (
1539 connectorId === 0 &&
1540 (!stationTemplate?.Connectors[connectorId] ||
1541 this.getUseConnectorId0(stationTemplate) === false)
1542 ) {
1543 continue;
1544 }
1545 const templateConnectorId =
1546 connectorId > 0 && stationTemplate?.randomConnectors
1547 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1548 : connectorId;
1549 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
1550 checkStationInfoConnectorStatus(
1551 templateConnectorId,
1552 connectorStatus,
1553 this.logPrefix(),
1554 this.templateFile,
1555 );
1556 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1557 }
1558 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1559 this.saveConnectorsStatus();
1560 } else {
1561 logger.warn(
1562 `${this.logPrefix()} Charging station information from template ${
1563 this.templateFile
1564 } with no connectors configuration defined, cannot create connectors`,
1565 );
1566 }
1567 }
1568 } else {
1569 logger.warn(
1570 `${this.logPrefix()} Charging station information from template ${
1571 this.templateFile
1572 } with no connectors configuration defined, using already defined connectors`,
1573 );
1574 }
1575 }
1576
1577 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
1578 if (!stationTemplate?.Evses && this.evses.size === 0) {
1579 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1580 logger.error(`${this.logPrefix()} ${errorMsg}`);
1581 throw new BaseError(errorMsg);
1582 }
1583 if (!stationTemplate?.Evses?.[0]) {
1584 logger.warn(
1585 `${this.logPrefix()} Charging station information from template ${
1586 this.templateFile
1587 } with no evse id 0 configuration`,
1588 );
1589 }
1590 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
1591 logger.warn(
1592 `${this.logPrefix()} Charging station information from template ${
1593 this.templateFile
1594 } with evse id 0 with no connector id 0 configuration`,
1595 );
1596 }
1597 if (stationTemplate?.Evses) {
1598 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1599 .update(JSON.stringify(stationTemplate?.Evses))
1600 .digest('hex');
1601 const evsesConfigChanged =
1602 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1603 if (this.evses?.size === 0 || evsesConfigChanged) {
1604 evsesConfigChanged && this.evses.clear();
1605 this.evsesConfigurationHash = evsesConfigHash;
1606 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
1607 if (templateMaxEvses > 0) {
1608 for (const evse in stationTemplate.Evses) {
1609 const evseId = convertToInt(evse);
1610 this.evses.set(evseId, {
1611 connectors: buildConnectorsMap(
1612 stationTemplate?.Evses[evse]?.Connectors,
1613 this.logPrefix(),
1614 this.templateFile,
1615 ),
1616 availability: AvailabilityType.Operative,
1617 });
1618 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
1619 }
1620 this.saveEvsesStatus();
1621 } else {
1622 logger.warn(
1623 `${this.logPrefix()} Charging station information from template ${
1624 this.templateFile
1625 } with no evses configuration defined, cannot create evses`,
1626 );
1627 }
1628 }
1629 } else {
1630 logger.warn(
1631 `${this.logPrefix()} Charging station information from template ${
1632 this.templateFile
1633 } with no evses configuration defined, using already defined evses`,
1634 );
1635 }
1636 }
1637
1638 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1639 let configuration: ChargingStationConfiguration | undefined;
1640 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1641 try {
1642 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1643 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1644 this.configurationFileHash,
1645 );
1646 } else {
1647 const measureId = `${FileType.ChargingStationConfiguration} read`;
1648 const beginId = PerformanceStatistics.beginMeasure(measureId);
1649 configuration = JSON.parse(
1650 readFileSync(this.configurationFile, 'utf8'),
1651 ) as ChargingStationConfiguration;
1652 PerformanceStatistics.endMeasure(measureId, beginId);
1653 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1654 this.configurationFileHash = configuration.configurationHash!;
1655 }
1656 } catch (error) {
1657 handleFileException(
1658 this.configurationFile,
1659 FileType.ChargingStationConfiguration,
1660 error as NodeJS.ErrnoException,
1661 this.logPrefix(),
1662 );
1663 }
1664 }
1665 return configuration;
1666 }
1667
1668 private saveAutomaticTransactionGeneratorConfiguration(): void {
1669 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1670 this.saveConfiguration();
1671 }
1672 }
1673
1674 private saveConnectorsStatus() {
1675 this.saveConfiguration();
1676 }
1677
1678 private saveEvsesStatus() {
1679 this.saveConfiguration();
1680 }
1681
1682 private saveConfiguration(): void {
1683 if (isNotEmptyString(this.configurationFile)) {
1684 try {
1685 if (!existsSync(dirname(this.configurationFile))) {
1686 mkdirSync(dirname(this.configurationFile), { recursive: true });
1687 }
1688 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1689 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1690 : {};
1691 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
1692 configurationData.stationInfo = this.stationInfo;
1693 } else {
1694 delete configurationData.stationInfo;
1695 }
1696 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
1697 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1698 } else {
1699 delete configurationData.configurationKey;
1700 }
1701 configurationData = merge<ChargingStationConfiguration>(
1702 configurationData,
1703 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
1704 );
1705 if (
1706 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1707 !this.getAutomaticTransactionGeneratorConfiguration()
1708 ) {
1709 delete configurationData.automaticTransactionGenerator;
1710 }
1711 if (this.connectors.size > 0) {
1712 configurationData.connectorsStatus = buildConnectorsStatus(this);
1713 } else {
1714 delete configurationData.connectorsStatus;
1715 }
1716 if (this.evses.size > 0) {
1717 configurationData.evsesStatus = buildEvsesStatus(this);
1718 } else {
1719 delete configurationData.evsesStatus;
1720 }
1721 delete configurationData.configurationHash;
1722 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1723 .update(
1724 JSON.stringify({
1725 stationInfo: configurationData.stationInfo,
1726 configurationKey: configurationData.configurationKey,
1727 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1728 } as ChargingStationConfiguration),
1729 )
1730 .digest('hex');
1731 if (this.configurationFileHash !== configurationHash) {
1732 AsyncLock.acquire(AsyncLockType.configuration)
1733 .then(() => {
1734 configurationData.configurationHash = configurationHash;
1735 const measureId = `${FileType.ChargingStationConfiguration} write`;
1736 const beginId = PerformanceStatistics.beginMeasure(measureId);
1737 const fileDescriptor = openSync(this.configurationFile, 'w');
1738 writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1739 closeSync(fileDescriptor);
1740 PerformanceStatistics.endMeasure(measureId, beginId);
1741 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1742 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1743 this.configurationFileHash = configurationHash;
1744 })
1745 .catch((error) => {
1746 handleFileException(
1747 this.configurationFile,
1748 FileType.ChargingStationConfiguration,
1749 error as NodeJS.ErrnoException,
1750 this.logPrefix(),
1751 );
1752 })
1753 .finally(() => {
1754 AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
1755 });
1756 } else {
1757 logger.debug(
1758 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1759 this.configurationFile
1760 }`,
1761 );
1762 }
1763 } catch (error) {
1764 handleFileException(
1765 this.configurationFile,
1766 FileType.ChargingStationConfiguration,
1767 error as NodeJS.ErrnoException,
1768 this.logPrefix(),
1769 );
1770 }
1771 } else {
1772 logger.error(
1773 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
1774 );
1775 }
1776 }
1777
1778 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1779 return this.getTemplateFromFile()?.Configuration;
1780 }
1781
1782 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1783 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1784 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1785 return { configurationKey };
1786 }
1787 return undefined;
1788 }
1789
1790 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1791 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1792 this.getOcppConfigurationFromFile();
1793 if (!ocppConfiguration) {
1794 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1795 }
1796 return ocppConfiguration;
1797 }
1798
1799 private async onOpen(): Promise<void> {
1800 if (this.isWebSocketConnectionOpened() === true) {
1801 logger.info(
1802 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
1803 );
1804 if (this.isRegistered() === false) {
1805 // Send BootNotification
1806 let registrationRetryCount = 0;
1807 do {
1808 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1809 BootNotificationRequest,
1810 BootNotificationResponse
1811 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1812 skipBufferingOnError: true,
1813 });
1814 if (this.isRegistered() === false) {
1815 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
1816 await sleep(
1817 this?.bootNotificationResponse?.interval
1818 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1819 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
1820 );
1821 }
1822 } while (
1823 this.isRegistered() === false &&
1824 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
1825 this.getRegistrationMaxRetries() === -1)
1826 );
1827 }
1828 if (this.isRegistered() === true) {
1829 if (this.inAcceptedState() === true) {
1830 await this.startMessageSequence();
1831 }
1832 } else {
1833 logger.error(
1834 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
1835 );
1836 }
1837 this.wsConnectionRestarted = false;
1838 this.autoReconnectRetryCount = 0;
1839 parentPort?.postMessage(buildUpdatedMessage(this));
1840 } else {
1841 logger.warn(
1842 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
1843 );
1844 }
1845 }
1846
1847 private async onClose(code: number, reason: Buffer): Promise<void> {
1848 switch (code) {
1849 // Normal close
1850 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1851 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1852 logger.info(
1853 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1854 code,
1855 )}' and reason '${reason.toString()}'`,
1856 );
1857 this.autoReconnectRetryCount = 0;
1858 break;
1859 // Abnormal close
1860 default:
1861 logger.error(
1862 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1863 code,
1864 )}' and reason '${reason.toString()}'`,
1865 );
1866 this.started === true && (await this.reconnect());
1867 break;
1868 }
1869 parentPort?.postMessage(buildUpdatedMessage(this));
1870 }
1871
1872 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1873 const cachedRequest = this.requests.get(messageId);
1874 if (Array.isArray(cachedRequest) === true) {
1875 return cachedRequest;
1876 }
1877 throw new OCPPError(
1878 ErrorType.PROTOCOL_ERROR,
1879 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1880 messageType,
1881 )} is not an array`,
1882 undefined,
1883 cachedRequest as JsonType,
1884 );
1885 }
1886
1887 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1888 const [messageType, messageId, commandName, commandPayload] = request;
1889 if (this.getEnableStatistics() === true) {
1890 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1891 }
1892 logger.debug(
1893 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1894 request,
1895 )}`,
1896 );
1897 // Process the message
1898 await this.ocppIncomingRequestService.incomingRequestHandler(
1899 this,
1900 messageId,
1901 commandName,
1902 commandPayload,
1903 );
1904 }
1905
1906 private handleResponseMessage(response: Response): void {
1907 const [messageType, messageId, commandPayload] = response;
1908 if (this.requests.has(messageId) === false) {
1909 // Error
1910 throw new OCPPError(
1911 ErrorType.INTERNAL_ERROR,
1912 `Response for unknown message id ${messageId}`,
1913 undefined,
1914 commandPayload,
1915 );
1916 }
1917 // Respond
1918 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1919 messageType,
1920 messageId,
1921 )!;
1922 logger.debug(
1923 `${this.logPrefix()} << Command '${
1924 requestCommandName ?? Constants.UNKNOWN_COMMAND
1925 }' received response payload: ${JSON.stringify(response)}`,
1926 );
1927 responseCallback(commandPayload, requestPayload);
1928 }
1929
1930 private handleErrorMessage(errorResponse: ErrorResponse): void {
1931 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1932 if (this.requests.has(messageId) === false) {
1933 // Error
1934 throw new OCPPError(
1935 ErrorType.INTERNAL_ERROR,
1936 `Error response for unknown message id ${messageId}`,
1937 undefined,
1938 { errorType, errorMessage, errorDetails },
1939 );
1940 }
1941 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1942 logger.debug(
1943 `${this.logPrefix()} << Command '${
1944 requestCommandName ?? Constants.UNKNOWN_COMMAND
1945 }' received error response payload: ${JSON.stringify(errorResponse)}`,
1946 );
1947 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1948 }
1949
1950 private async onMessage(data: RawData): Promise<void> {
1951 let request: IncomingRequest | Response | ErrorResponse | undefined;
1952 let messageType: number | undefined;
1953 let errorMsg: string;
1954 try {
1955 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1956 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1957 if (Array.isArray(request) === true) {
1958 [messageType] = request;
1959 // Check the type of message
1960 switch (messageType) {
1961 // Incoming Message
1962 case MessageType.CALL_MESSAGE:
1963 await this.handleIncomingMessage(request as IncomingRequest);
1964 break;
1965 // Response Message
1966 case MessageType.CALL_RESULT_MESSAGE:
1967 this.handleResponseMessage(request as Response);
1968 break;
1969 // Error Message
1970 case MessageType.CALL_ERROR_MESSAGE:
1971 this.handleErrorMessage(request as ErrorResponse);
1972 break;
1973 // Unknown Message
1974 default:
1975 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1976 errorMsg = `Wrong message type ${messageType}`;
1977 logger.error(`${this.logPrefix()} ${errorMsg}`);
1978 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1979 }
1980 parentPort?.postMessage(buildUpdatedMessage(this));
1981 } else {
1982 throw new OCPPError(
1983 ErrorType.PROTOCOL_ERROR,
1984 'Incoming message is not an array',
1985 undefined,
1986 {
1987 request,
1988 },
1989 );
1990 }
1991 } catch (error) {
1992 let commandName: IncomingRequestCommand | undefined;
1993 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
1994 let errorCallback: ErrorCallback;
1995 const [, messageId] = request!;
1996 switch (messageType) {
1997 case MessageType.CALL_MESSAGE:
1998 [, , commandName] = request as IncomingRequest;
1999 // Send error
2000 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
2001 break;
2002 case MessageType.CALL_RESULT_MESSAGE:
2003 case MessageType.CALL_ERROR_MESSAGE:
2004 if (this.requests.has(messageId) === true) {
2005 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
2006 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2007 errorCallback(error as OCPPError, false);
2008 } else {
2009 // Remove the request from the cache in case of error at response handling
2010 this.requests.delete(messageId);
2011 }
2012 break;
2013 }
2014 if (error instanceof OCPPError === false) {
2015 logger.warn(
2016 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2017 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2018 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2019 }' message '${data.toString()}' handling is not an OCPPError:`,
2020 error,
2021 );
2022 }
2023 logger.error(
2024 `${this.logPrefix()} Incoming OCPP command '${
2025 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2026 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2027 }' message '${data.toString()}'${
2028 messageType !== MessageType.CALL_MESSAGE
2029 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2030 : ''
2031 } processing error:`,
2032 error,
2033 );
2034 }
2035 }
2036
2037 private onPing(): void {
2038 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2039 }
2040
2041 private onPong(): void {
2042 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2043 }
2044
2045 private onError(error: WSError): void {
2046 this.closeWSConnection();
2047 logger.error(`${this.logPrefix()} WebSocket error:`, error);
2048 }
2049
2050 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
2051 if (this.getMeteringPerTransaction() === true) {
2052 return (
2053 (rounded === true
2054 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
2055 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2056 );
2057 }
2058 return (
2059 (rounded === true
2060 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
2061 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2062 );
2063 }
2064
2065 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2066 return stationTemplate?.useConnectorId0 ?? true;
2067 }
2068
2069 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
2070 if (this.hasEvses) {
2071 for (const [evseId, evseStatus] of this.evses) {
2072 if (evseId === 0) {
2073 continue;
2074 }
2075 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2076 if (connectorStatus.transactionStarted === true) {
2077 await this.stopTransactionOnConnector(connectorId, reason);
2078 }
2079 }
2080 }
2081 } else {
2082 for (const connectorId of this.connectors.keys()) {
2083 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2084 await this.stopTransactionOnConnector(connectorId, reason);
2085 }
2086 }
2087 }
2088 }
2089
2090 // 0 for disabling
2091 private getConnectionTimeout(): number {
2092 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
2093 return (
2094 parseInt(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value!) ??
2095 Constants.DEFAULT_CONNECTION_TIMEOUT
2096 );
2097 }
2098 return Constants.DEFAULT_CONNECTION_TIMEOUT;
2099 }
2100
2101 // -1 for unlimited, 0 for disabling
2102 private getAutoReconnectMaxRetries(): number | undefined {
2103 return (
2104 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
2105 );
2106 }
2107
2108 // 0 for disabling
2109 private getRegistrationMaxRetries(): number | undefined {
2110 return this.stationInfo.registrationMaxRetries ?? -1;
2111 }
2112
2113 private getPowerDivider(): number {
2114 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
2115 if (this.stationInfo?.powerSharedByConnectors) {
2116 powerDivider = this.getNumberOfRunningTransactions();
2117 }
2118 return powerDivider;
2119 }
2120
2121 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2122 const maximumPower = this.getMaximumPower(stationInfo);
2123 switch (this.getCurrentOutType(stationInfo)) {
2124 case CurrentType.AC:
2125 return ACElectricUtils.amperagePerPhaseFromPower(
2126 this.getNumberOfPhases(stationInfo),
2127 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2128 this.getVoltageOut(stationInfo),
2129 );
2130 case CurrentType.DC:
2131 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
2132 }
2133 }
2134
2135 private getAmperageLimitation(): number | undefined {
2136 if (
2137 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2138 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
2139 ) {
2140 return (
2141 convertToInt(
2142 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
2143 ) / getAmperageLimitationUnitDivider(this.stationInfo)
2144 );
2145 }
2146 }
2147
2148 private async startMessageSequence(): Promise<void> {
2149 if (this.stationInfo?.autoRegister === true) {
2150 await this.ocppRequestService.requestHandler<
2151 BootNotificationRequest,
2152 BootNotificationResponse
2153 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2154 skipBufferingOnError: true,
2155 });
2156 }
2157 // Start WebSocket ping
2158 this.startWebSocketPing();
2159 // Start heartbeat
2160 this.startHeartbeat();
2161 // Initialize connectors status
2162 if (this.hasEvses) {
2163 for (const [evseId, evseStatus] of this.evses) {
2164 if (evseId > 0) {
2165 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2166 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
2167 await OCPPServiceUtils.sendAndSetConnectorStatus(
2168 this,
2169 connectorId,
2170 connectorBootStatus,
2171 evseId,
2172 );
2173 }
2174 }
2175 }
2176 } else {
2177 for (const connectorId of this.connectors.keys()) {
2178 if (connectorId > 0) {
2179 const connectorBootStatus = getBootConnectorStatus(
2180 this,
2181 connectorId,
2182 this.getConnectorStatus(connectorId)!,
2183 );
2184 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2185 }
2186 }
2187 }
2188 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2189 await this.ocppRequestService.requestHandler<
2190 FirmwareStatusNotificationRequest,
2191 FirmwareStatusNotificationResponse
2192 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2193 status: FirmwareStatus.Installed,
2194 });
2195 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2196 }
2197
2198 // Start the ATG
2199 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2200 this.startAutomaticTransactionGenerator();
2201 }
2202 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2203 }
2204
2205 private async stopMessageSequence(
2206 reason: StopTransactionReason = StopTransactionReason.NONE,
2207 ): Promise<void> {
2208 // Stop WebSocket ping
2209 this.stopWebSocketPing();
2210 // Stop heartbeat
2211 this.stopHeartbeat();
2212 // Stop ongoing transactions
2213 if (this.automaticTransactionGenerator?.started === true) {
2214 this.stopAutomaticTransactionGenerator();
2215 } else {
2216 await this.stopRunningTransactions(reason);
2217 }
2218 if (this.hasEvses) {
2219 for (const [evseId, evseStatus] of this.evses) {
2220 if (evseId > 0) {
2221 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2222 await this.ocppRequestService.requestHandler<
2223 StatusNotificationRequest,
2224 StatusNotificationResponse
2225 >(
2226 this,
2227 RequestCommand.STATUS_NOTIFICATION,
2228 OCPPServiceUtils.buildStatusNotificationRequest(
2229 this,
2230 connectorId,
2231 ConnectorStatusEnum.Unavailable,
2232 evseId,
2233 ),
2234 );
2235 delete connectorStatus?.status;
2236 }
2237 }
2238 }
2239 } else {
2240 for (const connectorId of this.connectors.keys()) {
2241 if (connectorId > 0) {
2242 await this.ocppRequestService.requestHandler<
2243 StatusNotificationRequest,
2244 StatusNotificationResponse
2245 >(
2246 this,
2247 RequestCommand.STATUS_NOTIFICATION,
2248 OCPPServiceUtils.buildStatusNotificationRequest(
2249 this,
2250 connectorId,
2251 ConnectorStatusEnum.Unavailable,
2252 ),
2253 );
2254 delete this.getConnectorStatus(connectorId)?.status;
2255 }
2256 }
2257 }
2258 }
2259
2260 private startWebSocketPing(): void {
2261 const webSocketPingInterval: number = getConfigurationKey(
2262 this,
2263 StandardParametersKey.WebSocketPingInterval,
2264 )
2265 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2266 : 0;
2267 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2268 this.webSocketPingSetInterval = setInterval(() => {
2269 if (this.isWebSocketConnectionOpened() === true) {
2270 this.wsConnection?.ping();
2271 }
2272 }, secondsToMilliseconds(webSocketPingInterval));
2273 logger.info(
2274 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2275 webSocketPingInterval,
2276 )}`,
2277 );
2278 } else if (this.webSocketPingSetInterval) {
2279 logger.info(
2280 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2281 webSocketPingInterval,
2282 )}`,
2283 );
2284 } else {
2285 logger.error(
2286 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
2287 );
2288 }
2289 }
2290
2291 private stopWebSocketPing(): void {
2292 if (this.webSocketPingSetInterval) {
2293 clearInterval(this.webSocketPingSetInterval);
2294 delete this.webSocketPingSetInterval;
2295 }
2296 }
2297
2298 private getConfiguredSupervisionUrl(): URL {
2299 let configuredSupervisionUrl: string;
2300 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2301 if (isNotEmptyArray(supervisionUrls)) {
2302 let configuredSupervisionUrlIndex: number;
2303 switch (Configuration.getSupervisionUrlDistribution()) {
2304 case SupervisionUrlDistribution.RANDOM:
2305 configuredSupervisionUrlIndex = Math.floor(
2306 secureRandom() * (supervisionUrls as string[]).length,
2307 );
2308 break;
2309 case SupervisionUrlDistribution.ROUND_ROBIN:
2310 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2311 default:
2312 Object.values(SupervisionUrlDistribution).includes(
2313 Configuration.getSupervisionUrlDistribution()!,
2314 ) === false &&
2315 logger.error(
2316 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2317 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2318 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2319 }`,
2320 );
2321 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2322 break;
2323 }
2324 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
2325 } else {
2326 configuredSupervisionUrl = supervisionUrls as string;
2327 }
2328 if (isNotEmptyString(configuredSupervisionUrl)) {
2329 return new URL(configuredSupervisionUrl);
2330 }
2331 const errorMsg = 'No supervision url(s) configured';
2332 logger.error(`${this.logPrefix()} ${errorMsg}`);
2333 throw new BaseError(`${errorMsg}`);
2334 }
2335
2336 private stopHeartbeat(): void {
2337 if (this.heartbeatSetInterval) {
2338 clearInterval(this.heartbeatSetInterval);
2339 delete this.heartbeatSetInterval;
2340 }
2341 }
2342
2343 private terminateWSConnection(): void {
2344 if (this.isWebSocketConnectionOpened() === true) {
2345 this.wsConnection?.terminate();
2346 this.wsConnection = null;
2347 }
2348 }
2349
2350 private getReconnectExponentialDelay(): boolean {
2351 return this.stationInfo?.reconnectExponentialDelay ?? false;
2352 }
2353
2354 private async reconnect(): Promise<void> {
2355 // Stop WebSocket ping
2356 this.stopWebSocketPing();
2357 // Stop heartbeat
2358 this.stopHeartbeat();
2359 // Stop the ATG if needed
2360 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
2361 this.stopAutomaticTransactionGenerator();
2362 }
2363 if (
2364 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
2365 this.getAutoReconnectMaxRetries() === -1
2366 ) {
2367 ++this.autoReconnectRetryCount;
2368 const reconnectDelay = this.getReconnectExponentialDelay()
2369 ? exponentialDelay(this.autoReconnectRetryCount)
2370 : secondsToMilliseconds(this.getConnectionTimeout());
2371 const reconnectDelayWithdraw = 1000;
2372 const reconnectTimeout =
2373 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2374 ? reconnectDelay - reconnectDelayWithdraw
2375 : 0;
2376 logger.error(
2377 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2378 reconnectDelay,
2379 2,
2380 )}ms, timeout ${reconnectTimeout}ms`,
2381 );
2382 await sleep(reconnectDelay);
2383 logger.error(
2384 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
2385 );
2386 this.openWSConnection(
2387 {
2388 ...(this.stationInfo?.wsOptions ?? {}),
2389 handshakeTimeout: reconnectTimeout,
2390 },
2391 { closeOpened: true },
2392 );
2393 this.wsConnectionRestarted = true;
2394 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2395 logger.error(
2396 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2397 this.autoReconnectRetryCount
2398 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
2399 );
2400 }
2401 }
2402 }