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