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