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