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