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