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