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