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