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