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