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