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