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