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