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