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