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