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