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