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