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