refactor: use RegExp.exec()
[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 const patchLevelIndex = match.length - 1;
1206 match[patchLevelIndex] = (
1207 convertToInt(match[patchLevelIndex]) +
1208 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1209 ).toString();
1210 this.stationInfo.firmwareVersion = match?.join('.');
1211 }
1212 this.saveStationInfo();
1213 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1214 if (this.stationInfo?.enableStatistics === true) {
1215 this.performanceStatistics = PerformanceStatistics.getInstance(
1216 this.stationInfo.hashId,
1217 this.stationInfo.chargingStationId!,
1218 this.configuredSupervisionUrl,
1219 );
1220 }
1221 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
1222 this.powerDivider = this.getPowerDivider();
1223 // OCPP configuration
1224 this.ocppConfiguration = this.getOcppConfiguration();
1225 this.initializeOcppConfiguration();
1226 this.initializeOcppServices();
1227 this.once(ChargingStationEvents.accepted, () => {
1228 this.startMessageSequence().catch((error) => {
1229 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error);
1230 });
1231 });
1232 if (this.stationInfo?.autoRegister === true) {
1233 this.bootNotificationResponse = {
1234 currentTime: new Date(),
1235 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1236 status: RegistrationStatusEnumType.ACCEPTED,
1237 };
1238 }
1239 }
1240
1241 private initializeOcppServices(): void {
1242 const ocppVersion = this.stationInfo?.ocppVersion;
1243 switch (ocppVersion) {
1244 case OCPPVersion.VERSION_16:
1245 this.ocppIncomingRequestService =
1246 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1247 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1248 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
1249 );
1250 break;
1251 case OCPPVersion.VERSION_20:
1252 case OCPPVersion.VERSION_201:
1253 this.ocppIncomingRequestService =
1254 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1255 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1256 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
1257 );
1258 break;
1259 default:
1260 this.handleUnsupportedVersion(ocppVersion);
1261 break;
1262 }
1263 }
1264
1265 private initializeOcppConfiguration(): void {
1266 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.HeartbeatInterval))) {
1267 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
1268 }
1269 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.HeartBeatInterval))) {
1270 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
1271 }
1272 if (
1273 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1274 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
1275 isNullOrUndefined(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!))
1276 ) {
1277 addConfigurationKey(
1278 this,
1279 this.stationInfo.supervisionUrlOcppKey!,
1280 this.configuredSupervisionUrl.href,
1281 { reboot: true },
1282 );
1283 } else if (
1284 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1285 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
1286 !isNullOrUndefined(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!))
1287 ) {
1288 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false });
1289 }
1290 if (
1291 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1292 isNullOrUndefined(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!))
1293 ) {
1294 addConfigurationKey(
1295 this,
1296 this.stationInfo.amperageLimitationOcppKey!,
1297 (
1298 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
1299 ).toString(),
1300 );
1301 }
1302 if (
1303 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles))
1304 ) {
1305 addConfigurationKey(
1306 this,
1307 StandardParametersKey.SupportedFeatureProfiles,
1308 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
1309 );
1310 }
1311 addConfigurationKey(
1312 this,
1313 StandardParametersKey.NumberOfConnectors,
1314 this.getNumberOfConnectors().toString(),
1315 { readonly: true },
1316 { overwrite: true },
1317 );
1318 if (
1319 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData))
1320 ) {
1321 addConfigurationKey(
1322 this,
1323 StandardParametersKey.MeterValuesSampledData,
1324 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
1325 );
1326 }
1327 if (
1328 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation))
1329 ) {
1330 const connectorsPhaseRotation: string[] = [];
1331 if (this.hasEvses) {
1332 for (const evseStatus of this.evses.values()) {
1333 for (const connectorId of evseStatus.connectors.keys()) {
1334 connectorsPhaseRotation.push(
1335 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1336 );
1337 }
1338 }
1339 } else {
1340 for (const connectorId of this.connectors.keys()) {
1341 connectorsPhaseRotation.push(
1342 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1343 );
1344 }
1345 }
1346 addConfigurationKey(
1347 this,
1348 StandardParametersKey.ConnectorPhaseRotation,
1349 connectorsPhaseRotation.toString(),
1350 );
1351 }
1352 if (
1353 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests))
1354 ) {
1355 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
1356 }
1357 if (
1358 isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled)) &&
1359 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1360 SupportedFeatureProfiles.LocalAuthListManagement,
1361 )
1362 ) {
1363 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1364 }
1365 if (isNullOrUndefined(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut))) {
1366 addConfigurationKey(
1367 this,
1368 StandardParametersKey.ConnectionTimeOut,
1369 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
1370 );
1371 }
1372 this.saveOcppConfiguration();
1373 }
1374
1375 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1376 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
1377 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1378 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1379 }
1380 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1381 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1382 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
1383 delete evseStatus.connectorsStatus;
1384 this.evses.set(evseId, {
1385 ...(evseStatus as EvseStatus),
1386 connectors: new Map<number, ConnectorStatus>(
1387 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1388 connectorId,
1389 connectorStatus,
1390 ]),
1391 ),
1392 });
1393 }
1394 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1395 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1396 logger.error(`${this.logPrefix()} ${errorMsg}`);
1397 throw new BaseError(errorMsg);
1398 } else {
1399 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1400 logger.error(`${this.logPrefix()} ${errorMsg}`);
1401 throw new BaseError(errorMsg);
1402 }
1403 }
1404
1405 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
1406 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
1407 this.initializeConnectorsFromTemplate(stationTemplate);
1408 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
1409 this.initializeEvsesFromTemplate(stationTemplate);
1410 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
1411 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1412 logger.error(`${this.logPrefix()} ${errorMsg}`);
1413 throw new BaseError(errorMsg);
1414 } else {
1415 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1416 logger.error(`${this.logPrefix()} ${errorMsg}`);
1417 throw new BaseError(errorMsg);
1418 }
1419 }
1420
1421 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
1422 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
1423 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1424 logger.error(`${this.logPrefix()} ${errorMsg}`);
1425 throw new BaseError(errorMsg);
1426 }
1427 if (!stationTemplate?.Connectors?.[0]) {
1428 logger.warn(
1429 `${this.logPrefix()} Charging station information from template ${
1430 this.templateFile
1431 } with no connector id 0 configuration`,
1432 );
1433 }
1434 if (stationTemplate?.Connectors) {
1435 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1436 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
1437 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1438 .update(
1439 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
1440 )
1441 .digest('hex');
1442 const connectorsConfigChanged =
1443 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1444 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1445 connectorsConfigChanged && this.connectors.clear();
1446 this.connectorsConfigurationHash = connectorsConfigHash;
1447 if (templateMaxConnectors > 0) {
1448 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1449 if (
1450 connectorId === 0 &&
1451 (!stationTemplate?.Connectors?.[connectorId] ||
1452 this.getUseConnectorId0(stationTemplate) === false)
1453 ) {
1454 continue;
1455 }
1456 const templateConnectorId =
1457 connectorId > 0 && stationTemplate?.randomConnectors
1458 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1459 : connectorId;
1460 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
1461 checkStationInfoConnectorStatus(
1462 templateConnectorId,
1463 connectorStatus,
1464 this.logPrefix(),
1465 this.templateFile,
1466 );
1467 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1468 }
1469 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1470 this.saveConnectorsStatus();
1471 } else {
1472 logger.warn(
1473 `${this.logPrefix()} Charging station information from template ${
1474 this.templateFile
1475 } with no connectors configuration defined, cannot create connectors`,
1476 );
1477 }
1478 }
1479 } else {
1480 logger.warn(
1481 `${this.logPrefix()} Charging station information from template ${
1482 this.templateFile
1483 } with no connectors configuration defined, using already defined connectors`,
1484 );
1485 }
1486 }
1487
1488 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
1489 if (!stationTemplate?.Evses && this.evses.size === 0) {
1490 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1491 logger.error(`${this.logPrefix()} ${errorMsg}`);
1492 throw new BaseError(errorMsg);
1493 }
1494 if (!stationTemplate?.Evses?.[0]) {
1495 logger.warn(
1496 `${this.logPrefix()} Charging station information from template ${
1497 this.templateFile
1498 } with no evse id 0 configuration`,
1499 );
1500 }
1501 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
1502 logger.warn(
1503 `${this.logPrefix()} Charging station information from template ${
1504 this.templateFile
1505 } with evse id 0 with no connector id 0 configuration`,
1506 );
1507 }
1508 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1509 logger.warn(
1510 `${this.logPrefix()} Charging station information from template ${
1511 this.templateFile
1512 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
1513 );
1514 }
1515 if (stationTemplate?.Evses) {
1516 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1517 .update(JSON.stringify(stationTemplate?.Evses))
1518 .digest('hex');
1519 const evsesConfigChanged =
1520 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1521 if (this.evses?.size === 0 || evsesConfigChanged) {
1522 evsesConfigChanged && this.evses.clear();
1523 this.evsesConfigurationHash = evsesConfigHash;
1524 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
1525 if (templateMaxEvses > 0) {
1526 for (const evseKey in stationTemplate.Evses) {
1527 const evseId = convertToInt(evseKey);
1528 this.evses.set(evseId, {
1529 connectors: buildConnectorsMap(
1530 stationTemplate?.Evses[evseKey]?.Connectors,
1531 this.logPrefix(),
1532 this.templateFile,
1533 ),
1534 availability: AvailabilityType.Operative,
1535 });
1536 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
1537 }
1538 this.saveEvsesStatus();
1539 } else {
1540 logger.warn(
1541 `${this.logPrefix()} Charging station information from template ${
1542 this.templateFile
1543 } with no evses configuration defined, cannot create evses`,
1544 );
1545 }
1546 }
1547 } else {
1548 logger.warn(
1549 `${this.logPrefix()} Charging station information from template ${
1550 this.templateFile
1551 } with no evses configuration defined, using already defined evses`,
1552 );
1553 }
1554 }
1555
1556 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1557 let configuration: ChargingStationConfiguration | undefined;
1558 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1559 try {
1560 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1561 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1562 this.configurationFileHash,
1563 );
1564 } else {
1565 const measureId = `${FileType.ChargingStationConfiguration} read`;
1566 const beginId = PerformanceStatistics.beginMeasure(measureId);
1567 configuration = JSON.parse(
1568 readFileSync(this.configurationFile, 'utf8'),
1569 ) as ChargingStationConfiguration;
1570 PerformanceStatistics.endMeasure(measureId, beginId);
1571 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1572 this.configurationFileHash = configuration.configurationHash!;
1573 }
1574 } catch (error) {
1575 handleFileException(
1576 this.configurationFile,
1577 FileType.ChargingStationConfiguration,
1578 error as NodeJS.ErrnoException,
1579 this.logPrefix(),
1580 );
1581 }
1582 }
1583 return configuration;
1584 }
1585
1586 private saveAutomaticTransactionGeneratorConfiguration(): void {
1587 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1588 this.saveConfiguration();
1589 }
1590 }
1591
1592 private saveConnectorsStatus() {
1593 this.saveConfiguration();
1594 }
1595
1596 private saveEvsesStatus() {
1597 this.saveConfiguration();
1598 }
1599
1600 private saveConfiguration(): void {
1601 if (isNotEmptyString(this.configurationFile)) {
1602 try {
1603 if (!existsSync(dirname(this.configurationFile))) {
1604 mkdirSync(dirname(this.configurationFile), { recursive: true });
1605 }
1606 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1607 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1608 : {};
1609 if (this.stationInfo?.stationInfoPersistentConfiguration === true && this.stationInfo) {
1610 configurationData.stationInfo = this.stationInfo;
1611 } else {
1612 delete configurationData.stationInfo;
1613 }
1614 if (
1615 this.stationInfo?.ocppPersistentConfiguration === true &&
1616 Array.isArray(this.ocppConfiguration?.configurationKey)
1617 ) {
1618 configurationData.configurationKey = this.ocppConfiguration?.configurationKey;
1619 } else {
1620 delete configurationData.configurationKey;
1621 }
1622 configurationData = merge<ChargingStationConfiguration>(
1623 configurationData,
1624 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
1625 );
1626 if (
1627 !this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ||
1628 !this.getAutomaticTransactionGeneratorConfiguration()
1629 ) {
1630 delete configurationData.automaticTransactionGenerator;
1631 }
1632 if (this.connectors.size > 0) {
1633 configurationData.connectorsStatus = buildConnectorsStatus(this);
1634 } else {
1635 delete configurationData.connectorsStatus;
1636 }
1637 if (this.evses.size > 0) {
1638 configurationData.evsesStatus = buildEvsesStatus(this);
1639 } else {
1640 delete configurationData.evsesStatus;
1641 }
1642 delete configurationData.configurationHash;
1643 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1644 .update(
1645 JSON.stringify({
1646 stationInfo: configurationData.stationInfo,
1647 configurationKey: configurationData.configurationKey,
1648 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1649 ...(this.connectors.size > 0 && {
1650 connectorsStatus: configurationData.connectorsStatus,
1651 }),
1652 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }),
1653 } as ChargingStationConfiguration),
1654 )
1655 .digest('hex');
1656 if (this.configurationFileHash !== configurationHash) {
1657 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1658 configurationData.configurationHash = configurationHash;
1659 const measureId = `${FileType.ChargingStationConfiguration} write`;
1660 const beginId = PerformanceStatistics.beginMeasure(measureId);
1661 writeFileSync(
1662 this.configurationFile,
1663 JSON.stringify(configurationData, undefined, 2),
1664 'utf8',
1665 );
1666 PerformanceStatistics.endMeasure(measureId, beginId);
1667 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1668 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1669 this.configurationFileHash = configurationHash;
1670 }).catch((error) => {
1671 handleFileException(
1672 this.configurationFile,
1673 FileType.ChargingStationConfiguration,
1674 error as NodeJS.ErrnoException,
1675 this.logPrefix(),
1676 );
1677 });
1678 } else {
1679 logger.debug(
1680 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1681 this.configurationFile
1682 }`,
1683 );
1684 }
1685 } catch (error) {
1686 handleFileException(
1687 this.configurationFile,
1688 FileType.ChargingStationConfiguration,
1689 error as NodeJS.ErrnoException,
1690 this.logPrefix(),
1691 );
1692 }
1693 } else {
1694 logger.error(
1695 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
1696 );
1697 }
1698 }
1699
1700 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1701 return this.getTemplateFromFile()?.Configuration;
1702 }
1703
1704 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1705 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1706 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1707 return { configurationKey };
1708 }
1709 return undefined;
1710 }
1711
1712 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1713 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1714 this.getOcppConfigurationFromFile();
1715 if (!ocppConfiguration) {
1716 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1717 }
1718 return ocppConfiguration;
1719 }
1720
1721 private async onOpen(): Promise<void> {
1722 if (this.isWebSocketConnectionOpened() === true) {
1723 logger.info(
1724 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
1725 );
1726 let registrationRetryCount = 0;
1727 if (this.isRegistered() === false) {
1728 // Send BootNotification
1729 do {
1730 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1731 BootNotificationRequest,
1732 BootNotificationResponse
1733 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1734 skipBufferingOnError: true,
1735 });
1736 if (this.isRegistered() === false) {
1737 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount;
1738 await sleep(
1739 this?.bootNotificationResponse?.interval
1740 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1741 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
1742 );
1743 }
1744 } while (
1745 this.isRegistered() === false &&
1746 (registrationRetryCount <= this.stationInfo.registrationMaxRetries! ||
1747 this.stationInfo?.registrationMaxRetries === -1)
1748 );
1749 }
1750 if (this.isRegistered() === true) {
1751 this.emit(ChargingStationEvents.registered);
1752 if (this.inAcceptedState() === true) {
1753 this.emit(ChargingStationEvents.accepted);
1754 }
1755 } else {
1756 logger.error(
1757 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${this
1758 .stationInfo?.registrationMaxRetries})`,
1759 );
1760 }
1761 this.autoReconnectRetryCount = 0;
1762 this.emit(ChargingStationEvents.updated);
1763 } else {
1764 logger.warn(
1765 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
1766 );
1767 }
1768 }
1769
1770 private async onClose(code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
1771 switch (code) {
1772 // Normal close
1773 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1774 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1775 logger.info(
1776 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1777 code,
1778 )}' and reason '${reason.toString()}'`,
1779 );
1780 this.autoReconnectRetryCount = 0;
1781 break;
1782 // Abnormal close
1783 default:
1784 logger.error(
1785 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1786 code,
1787 )}' and reason '${reason.toString()}'`,
1788 );
1789 this.started === true && (await this.reconnect());
1790 break;
1791 }
1792 this.emit(ChargingStationEvents.updated);
1793 }
1794
1795 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1796 const cachedRequest = this.requests.get(messageId);
1797 if (Array.isArray(cachedRequest) === true) {
1798 return cachedRequest;
1799 }
1800 throw new OCPPError(
1801 ErrorType.PROTOCOL_ERROR,
1802 `Cached request for message id ${messageId} ${getMessageTypeString(
1803 messageType,
1804 )} is not an array`,
1805 undefined,
1806 cachedRequest,
1807 );
1808 }
1809
1810 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1811 const [messageType, messageId, commandName, commandPayload] = request;
1812 if (this.stationInfo?.enableStatistics === true) {
1813 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1814 }
1815 logger.debug(
1816 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1817 request,
1818 )}`,
1819 );
1820 // Process the message
1821 await this.ocppIncomingRequestService.incomingRequestHandler(
1822 this,
1823 messageId,
1824 commandName,
1825 commandPayload,
1826 );
1827 this.emit(ChargingStationEvents.updated);
1828 }
1829
1830 private handleResponseMessage(response: Response): void {
1831 const [messageType, messageId, commandPayload] = response;
1832 if (this.requests.has(messageId) === false) {
1833 // Error
1834 throw new OCPPError(
1835 ErrorType.INTERNAL_ERROR,
1836 `Response for unknown message id ${messageId}`,
1837 undefined,
1838 commandPayload,
1839 );
1840 }
1841 // Respond
1842 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1843 messageType,
1844 messageId,
1845 )!;
1846 logger.debug(
1847 `${this.logPrefix()} << Command '${
1848 requestCommandName ?? Constants.UNKNOWN_COMMAND
1849 }' received response payload: ${JSON.stringify(response)}`,
1850 );
1851 responseCallback(commandPayload, requestPayload);
1852 }
1853
1854 private handleErrorMessage(errorResponse: ErrorResponse): void {
1855 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1856 if (this.requests.has(messageId) === false) {
1857 // Error
1858 throw new OCPPError(
1859 ErrorType.INTERNAL_ERROR,
1860 `Error response for unknown message id ${messageId}`,
1861 undefined,
1862 { errorType, errorMessage, errorDetails },
1863 );
1864 }
1865 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1866 logger.debug(
1867 `${this.logPrefix()} << Command '${
1868 requestCommandName ?? Constants.UNKNOWN_COMMAND
1869 }' received error response payload: ${JSON.stringify(errorResponse)}`,
1870 );
1871 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1872 }
1873
1874 private async onMessage(data: RawData): Promise<void> {
1875 let request: IncomingRequest | Response | ErrorResponse | undefined;
1876 let messageType: MessageType | undefined;
1877 let errorMsg: string;
1878 try {
1879 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1880 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1881 if (Array.isArray(request) === true) {
1882 [messageType] = request;
1883 // Check the type of message
1884 switch (messageType) {
1885 // Incoming Message
1886 case MessageType.CALL_MESSAGE:
1887 await this.handleIncomingMessage(request as IncomingRequest);
1888 break;
1889 // Response Message
1890 case MessageType.CALL_RESULT_MESSAGE:
1891 this.handleResponseMessage(request as Response);
1892 break;
1893 // Error Message
1894 case MessageType.CALL_ERROR_MESSAGE:
1895 this.handleErrorMessage(request as ErrorResponse);
1896 break;
1897 // Unknown Message
1898 default:
1899 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1900 errorMsg = `Wrong message type ${messageType}`;
1901 logger.error(`${this.logPrefix()} ${errorMsg}`);
1902 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1903 }
1904 } else {
1905 throw new OCPPError(
1906 ErrorType.PROTOCOL_ERROR,
1907 'Incoming message is not an array',
1908 undefined,
1909 {
1910 request,
1911 },
1912 );
1913 }
1914 } catch (error) {
1915 let commandName: IncomingRequestCommand | undefined;
1916 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
1917 let errorCallback: ErrorCallback;
1918 const [, messageId] = request!;
1919 switch (messageType) {
1920 case MessageType.CALL_MESSAGE:
1921 [, , commandName] = request as IncomingRequest;
1922 // Send error
1923 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1924 break;
1925 case MessageType.CALL_RESULT_MESSAGE:
1926 case MessageType.CALL_ERROR_MESSAGE:
1927 if (this.requests.has(messageId) === true) {
1928 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1929 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1930 errorCallback(error as OCPPError, false);
1931 } else {
1932 // Remove the request from the cache in case of error at response handling
1933 this.requests.delete(messageId);
1934 }
1935 break;
1936 }
1937 if (error instanceof OCPPError === false) {
1938 logger.warn(
1939 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1940 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1941 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1942 }' message '${data.toString()}' handling is not an OCPPError:`,
1943 error,
1944 );
1945 }
1946 logger.error(
1947 `${this.logPrefix()} Incoming OCPP command '${
1948 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1949 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1950 }' message '${data.toString()}'${
1951 messageType !== MessageType.CALL_MESSAGE
1952 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1953 : ''
1954 } processing error:`,
1955 error,
1956 );
1957 }
1958 }
1959
1960 private onPing(): void {
1961 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1962 }
1963
1964 private onPong(): void {
1965 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1966 }
1967
1968 private onError(error: WSError): void {
1969 this.closeWSConnection();
1970 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1971 }
1972
1973 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1974 if (this.stationInfo?.meteringPerTransaction === true) {
1975 return (
1976 (rounded === true
1977 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
1978 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1979 );
1980 }
1981 return (
1982 (rounded === true
1983 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
1984 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1985 );
1986 }
1987
1988 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
1989 return stationTemplate?.useConnectorId0 ?? true;
1990 }
1991
1992 private async stopRunningTransactions(reason?: StopTransactionReason): Promise<void> {
1993 if (this.hasEvses) {
1994 for (const [evseId, evseStatus] of this.evses) {
1995 if (evseId === 0) {
1996 continue;
1997 }
1998 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1999 if (connectorStatus.transactionStarted === true) {
2000 await this.stopTransactionOnConnector(connectorId, reason);
2001 }
2002 }
2003 }
2004 } else {
2005 for (const connectorId of this.connectors.keys()) {
2006 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2007 await this.stopTransactionOnConnector(connectorId, reason);
2008 }
2009 }
2010 }
2011 }
2012
2013 // 0 for disabling
2014 private getConnectionTimeout(): number {
2015 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) !== undefined) {
2016 return convertToInt(
2017 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value! ??
2018 Constants.DEFAULT_CONNECTION_TIMEOUT,
2019 );
2020 }
2021 return Constants.DEFAULT_CONNECTION_TIMEOUT;
2022 }
2023
2024 private getPowerDivider(): number {
2025 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
2026 if (this.stationInfo?.powerSharedByConnectors === true) {
2027 powerDivider = this.getNumberOfRunningTransactions();
2028 }
2029 return powerDivider;
2030 }
2031
2032 private getMaximumAmperage(stationInfo?: ChargingStationInfo): number | undefined {
2033 const maximumPower = (stationInfo ?? this.stationInfo).maximumPower!;
2034 switch (this.getCurrentOutType(stationInfo)) {
2035 case CurrentType.AC:
2036 return ACElectricUtils.amperagePerPhaseFromPower(
2037 this.getNumberOfPhases(stationInfo),
2038 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2039 this.getVoltageOut(stationInfo),
2040 );
2041 case CurrentType.DC:
2042 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
2043 }
2044 }
2045
2046 private getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
2047 return (stationInfo ?? this.stationInfo).currentOutType ?? CurrentType.AC;
2048 }
2049
2050 private getVoltageOut(stationInfo?: ChargingStationInfo): Voltage {
2051 return (
2052 (stationInfo ?? this.stationInfo).voltageOut ??
2053 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2054 );
2055 }
2056
2057 private getAmperageLimitation(): number | undefined {
2058 if (
2059 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2060 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) !== undefined
2061 ) {
2062 return (
2063 convertToInt(
2064 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
2065 ) / getAmperageLimitationUnitDivider(this.stationInfo)
2066 );
2067 }
2068 }
2069
2070 private async startMessageSequence(): Promise<void> {
2071 if (this.stationInfo?.autoRegister === true) {
2072 await this.ocppRequestService.requestHandler<
2073 BootNotificationRequest,
2074 BootNotificationResponse
2075 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2076 skipBufferingOnError: true,
2077 });
2078 }
2079 // Start WebSocket ping
2080 this.startWebSocketPing();
2081 // Start heartbeat
2082 this.startHeartbeat();
2083 // Initialize connectors status
2084 if (this.hasEvses) {
2085 for (const [evseId, evseStatus] of this.evses) {
2086 if (evseId > 0) {
2087 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2088 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
2089 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId);
2090 }
2091 }
2092 }
2093 } else {
2094 for (const connectorId of this.connectors.keys()) {
2095 if (connectorId > 0) {
2096 const connectorBootStatus = getBootConnectorStatus(
2097 this,
2098 connectorId,
2099 this.getConnectorStatus(connectorId)!,
2100 );
2101 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2102 }
2103 }
2104 }
2105 if (this.stationInfo.firmwareStatus === FirmwareStatus.Installing) {
2106 await this.ocppRequestService.requestHandler<
2107 FirmwareStatusNotificationRequest,
2108 FirmwareStatusNotificationResponse
2109 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2110 status: FirmwareStatus.Installed,
2111 });
2112 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2113 }
2114
2115 // Start the ATG
2116 if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
2117 this.startAutomaticTransactionGenerator();
2118 }
2119 this.flushMessageBuffer();
2120 }
2121
2122 private async stopMessageSequence(
2123 reason?: StopTransactionReason,
2124 stopTransactions = this.stationInfo?.stopTransactionsOnStopped,
2125 ): Promise<void> {
2126 // Stop WebSocket ping
2127 this.stopWebSocketPing();
2128 // Stop heartbeat
2129 this.stopHeartbeat();
2130 // Stop the ATG
2131 if (this.automaticTransactionGenerator?.started === true) {
2132 this.stopAutomaticTransactionGenerator();
2133 }
2134 // Stop ongoing transactions
2135 stopTransactions && (await this.stopRunningTransactions(reason));
2136 if (this.hasEvses) {
2137 for (const [evseId, evseStatus] of this.evses) {
2138 if (evseId > 0) {
2139 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2140 await this.ocppRequestService.requestHandler<
2141 StatusNotificationRequest,
2142 StatusNotificationResponse
2143 >(
2144 this,
2145 RequestCommand.STATUS_NOTIFICATION,
2146 buildStatusNotificationRequest(
2147 this,
2148 connectorId,
2149 ConnectorStatusEnum.Unavailable,
2150 evseId,
2151 ),
2152 );
2153 delete connectorStatus?.status;
2154 }
2155 }
2156 }
2157 } else {
2158 for (const connectorId of this.connectors.keys()) {
2159 if (connectorId > 0) {
2160 await this.ocppRequestService.requestHandler<
2161 StatusNotificationRequest,
2162 StatusNotificationResponse
2163 >(
2164 this,
2165 RequestCommand.STATUS_NOTIFICATION,
2166 buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable),
2167 );
2168 delete this.getConnectorStatus(connectorId)?.status;
2169 }
2170 }
2171 }
2172 }
2173
2174 private startWebSocketPing(): void {
2175 const webSocketPingInterval: number =
2176 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) !== undefined
2177 ? convertToInt(
2178 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value,
2179 )
2180 : 0;
2181 if (webSocketPingInterval > 0 && this.webSocketPingSetInterval === undefined) {
2182 this.webSocketPingSetInterval = setInterval(() => {
2183 if (this.isWebSocketConnectionOpened() === true) {
2184 this.wsConnection?.ping();
2185 }
2186 }, secondsToMilliseconds(webSocketPingInterval));
2187 logger.info(
2188 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2189 webSocketPingInterval,
2190 )}`,
2191 );
2192 } else if (this.webSocketPingSetInterval !== undefined) {
2193 logger.info(
2194 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2195 webSocketPingInterval,
2196 )}`,
2197 );
2198 } else {
2199 logger.error(
2200 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
2201 );
2202 }
2203 }
2204
2205 private stopWebSocketPing(): void {
2206 if (this.webSocketPingSetInterval !== undefined) {
2207 clearInterval(this.webSocketPingSetInterval);
2208 delete this.webSocketPingSetInterval;
2209 }
2210 }
2211
2212 private getConfiguredSupervisionUrl(): URL {
2213 let configuredSupervisionUrl: string;
2214 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2215 if (isNotEmptyArray(supervisionUrls)) {
2216 let configuredSupervisionUrlIndex: number;
2217 switch (Configuration.getSupervisionUrlDistribution()) {
2218 case SupervisionUrlDistribution.RANDOM:
2219 configuredSupervisionUrlIndex = Math.floor(
2220 secureRandom() * (supervisionUrls as string[]).length,
2221 );
2222 break;
2223 case SupervisionUrlDistribution.ROUND_ROBIN:
2224 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2225 default:
2226 Object.values(SupervisionUrlDistribution).includes(
2227 Configuration.getSupervisionUrlDistribution()!,
2228 ) === false &&
2229 logger.error(
2230 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2231 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2232 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2233 }`,
2234 );
2235 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2236 break;
2237 }
2238 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
2239 } else {
2240 configuredSupervisionUrl = supervisionUrls as string;
2241 }
2242 if (isNotEmptyString(configuredSupervisionUrl)) {
2243 return new URL(configuredSupervisionUrl);
2244 }
2245 const errorMsg = 'No supervision url(s) configured';
2246 logger.error(`${this.logPrefix()} ${errorMsg}`);
2247 throw new BaseError(`${errorMsg}`);
2248 }
2249
2250 private stopHeartbeat(): void {
2251 if (this.heartbeatSetInterval !== undefined) {
2252 clearInterval(this.heartbeatSetInterval);
2253 delete this.heartbeatSetInterval;
2254 }
2255 }
2256
2257 private terminateWSConnection(): void {
2258 if (this.isWebSocketConnectionOpened() === true) {
2259 this.wsConnection?.terminate();
2260 this.wsConnection = null;
2261 }
2262 }
2263
2264 private async reconnect(): Promise<void> {
2265 // Stop WebSocket ping
2266 this.stopWebSocketPing();
2267 // Stop heartbeat
2268 this.stopHeartbeat();
2269 // Stop the ATG if needed
2270 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
2271 this.stopAutomaticTransactionGenerator();
2272 }
2273 if (
2274 this.autoReconnectRetryCount < this.stationInfo.autoReconnectMaxRetries! ||
2275 this.stationInfo?.autoReconnectMaxRetries === -1
2276 ) {
2277 ++this.autoReconnectRetryCount;
2278 const reconnectDelay =
2279 this.stationInfo?.reconnectExponentialDelay === true
2280 ? exponentialDelay(this.autoReconnectRetryCount)
2281 : secondsToMilliseconds(this.getConnectionTimeout());
2282 const reconnectDelayWithdraw = 1000;
2283 const reconnectTimeout =
2284 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2285 ? reconnectDelay - reconnectDelayWithdraw
2286 : 0;
2287 logger.error(
2288 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2289 reconnectDelay,
2290 2,
2291 )}ms, timeout ${reconnectTimeout}ms`,
2292 );
2293 await sleep(reconnectDelay);
2294 logger.error(
2295 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
2296 );
2297 this.openWSConnection(
2298 {
2299 handshakeTimeout: reconnectTimeout,
2300 },
2301 { closeOpened: true },
2302 );
2303 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2304 logger.error(
2305 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2306 this.autoReconnectRetryCount
2307 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`,
2308 );
2309 }
2310 }
2311 }