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