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