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