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