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