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