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