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