Merge pull request #634 from SAP/fix-limitation-from-profiles
[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 // Restart the ATG
693 this.stopAutomaticTransactionGenerator();
694 delete this.automaticTransactionGeneratorConfiguration;
695 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
696 this.startAutomaticTransactionGenerator();
697 }
698 if (this.getEnableStatistics() === true) {
699 this.performanceStatistics?.restart();
700 } else {
701 this.performanceStatistics?.stop();
702 }
703 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
704 } catch (error) {
705 logger.error(
706 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
707 error,
708 );
709 }
710 }
711 },
712 );
713 this.started = true;
714 parentPort?.postMessage(buildStartedMessage(this));
715 this.starting = false;
716 } else {
717 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
718 }
719 } else {
720 logger.warn(`${this.logPrefix()} Charging station is already started...`);
721 }
722 }
723
724 public async stop(reason?: StopTransactionReason): Promise<void> {
725 if (this.started === true) {
726 if (this.stopping === false) {
727 this.stopping = true;
728 await this.stopMessageSequence(reason);
729 this.closeWSConnection();
730 if (this.getEnableStatistics() === true) {
731 this.performanceStatistics?.stop();
732 }
733 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 currentDate = new Date();
1035 if (this.hasEvses) {
1036 for (const evseStatus of this.evses.values()) {
1037 for (const connectorStatus of evseStatus.connectors.values()) {
1038 if (
1039 connectorStatus.reservation &&
1040 connectorStatus.reservation.expiryDate < currentDate
1041 ) {
1042 this.removeReservation(
1043 connectorStatus.reservation,
1044 ReservationTerminationReason.EXPIRED,
1045 ).catch(Constants.EMPTY_FUNCTION);
1046 }
1047 }
1048 }
1049 } else {
1050 for (const connectorStatus of this.connectors.values()) {
1051 if (
1052 connectorStatus.reservation &&
1053 connectorStatus.reservation.expiryDate < currentDate
1054 ) {
1055 this.removeReservation(
1056 connectorStatus.reservation,
1057 ReservationTerminationReason.EXPIRED,
1058 ).catch(Constants.EMPTY_FUNCTION);
1059 }
1060 }
1061 }
1062 }, interval);
1063 }
1064 }
1065
1066 public restartReservationExpiryDateSetInterval(): void {
1067 this.stopReservationExpirationSetInterval();
1068 this.startReservationExpirationSetInterval();
1069 }
1070
1071 public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
1072 return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag;
1073 }
1074
1075 public isConnectorReservable(
1076 reservationId: number,
1077 idTag?: string,
1078 connectorId?: number,
1079 ): boolean {
1080 const [alreadyExists] = this.doesReservationExists({ id: reservationId });
1081 if (alreadyExists) {
1082 return alreadyExists;
1083 }
1084 const userReservedAlready = isUndefined(
1085 this.getReservationBy(ReservationFilterKey.ID_TAG, idTag!),
1086 )
1087 ? false
1088 : true;
1089 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
1090 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1091 return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
1092 }
1093
1094 private getNumberOfReservableConnectors(): number {
1095 let reservableConnectors = 0;
1096 if (this.hasEvses) {
1097 for (const evseStatus of this.evses.values()) {
1098 reservableConnectors += countReservableConnectors(evseStatus.connectors);
1099 }
1100 } else {
1101 reservableConnectors = countReservableConnectors(this.connectors);
1102 }
1103 return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
1104 }
1105
1106 private getNumberOfReservationsOnConnectorZero(): number {
1107 let numberOfReservations = 0;
1108 if (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) {
1109 ++numberOfReservations;
1110 } else if (this.connectors.get(0)?.reservation) {
1111 ++numberOfReservations;
1112 }
1113 return numberOfReservations;
1114 }
1115
1116 private flushMessageBuffer(): void {
1117 if (this.messageBuffer.size > 0) {
1118 for (const message of this.messageBuffer.values()) {
1119 let beginId: string | undefined;
1120 let commandName: RequestCommand | undefined;
1121 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1122 const isRequest = messageType === MessageType.CALL_MESSAGE;
1123 if (isRequest) {
1124 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1125 beginId = PerformanceStatistics.beginMeasure(commandName);
1126 }
1127 this.wsConnection?.send(message);
1128 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!);
1129 logger.debug(
1130 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1131 messageType,
1132 )} payload sent: ${message}`,
1133 );
1134 this.messageBuffer.delete(message);
1135 }
1136 }
1137 }
1138
1139 private getSupervisionUrlOcppConfiguration(): boolean {
1140 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
1141 }
1142
1143 private stopReservationExpirationSetInterval(): void {
1144 if (this.reservationExpirationSetInterval) {
1145 clearInterval(this.reservationExpirationSetInterval);
1146 }
1147 }
1148
1149 private getSupervisionUrlOcppKey(): string {
1150 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
1151 }
1152
1153 private getTemplateFromFile(): ChargingStationTemplate | undefined {
1154 let template: ChargingStationTemplate | undefined;
1155 try {
1156 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1157 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
1158 } else {
1159 const measureId = `${FileType.ChargingStationTemplate} read`;
1160 const beginId = PerformanceStatistics.beginMeasure(measureId);
1161 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
1162 PerformanceStatistics.endMeasure(measureId, beginId);
1163 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1164 .update(JSON.stringify(template))
1165 .digest('hex');
1166 this.sharedLRUCache.setChargingStationTemplate(template);
1167 this.templateFileHash = template.templateHash;
1168 }
1169 } catch (error) {
1170 handleFileException(
1171 this.templateFile,
1172 FileType.ChargingStationTemplate,
1173 error as NodeJS.ErrnoException,
1174 this.logPrefix(),
1175 );
1176 }
1177 return template;
1178 }
1179
1180 private getStationInfoFromTemplate(): ChargingStationInfo {
1181 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
1182 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
1183 warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
1184 if (stationTemplate?.Connectors) {
1185 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
1186 }
1187 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
1188 stationInfo.hashId = getHashId(this.index, stationTemplate);
1189 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
1190 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
1191 createSerialNumber(stationTemplate, stationInfo);
1192 if (isNotEmptyArray(stationTemplate?.power)) {
1193 stationTemplate.power = stationTemplate.power as number[];
1194 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
1195 stationInfo.maximumPower =
1196 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
1197 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1198 : stationTemplate.power[powerArrayRandomIndex];
1199 } else {
1200 stationTemplate.power = stationTemplate?.power as number;
1201 stationInfo.maximumPower =
1202 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
1203 ? stationTemplate.power * 1000
1204 : stationTemplate.power;
1205 }
1206 stationInfo.firmwareVersionPattern =
1207 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
1208 if (
1209 isNotEmptyString(stationInfo.firmwareVersion) &&
1210 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) === false
1211 ) {
1212 logger.warn(
1213 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1214 this.templateFile
1215 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
1216 );
1217 }
1218 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
1219 {
1220 versionUpgrade: {
1221 step: 1,
1222 },
1223 reset: true,
1224 },
1225 stationTemplate?.firmwareUpgrade ?? {},
1226 );
1227 stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
1228 ? stationTemplate.resetTime! * 1000
1229 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
1230 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
1231 return stationInfo;
1232 }
1233
1234 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1235 let stationInfo: ChargingStationInfo | undefined;
1236 if (this.getStationInfoPersistentConfiguration()) {
1237 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1238 if (stationInfo) {
1239 delete stationInfo?.infoHash;
1240 }
1241 }
1242 return stationInfo;
1243 }
1244
1245 private getStationInfo(): ChargingStationInfo {
1246 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
1247 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
1248 // Priority:
1249 // 1. charging station info from template
1250 // 2. charging station info from configuration file
1251 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
1252 return stationInfoFromFile!;
1253 }
1254 stationInfoFromFile &&
1255 propagateSerialNumber(
1256 this.getTemplateFromFile()!,
1257 stationInfoFromFile,
1258 stationInfoFromTemplate,
1259 );
1260 return stationInfoFromTemplate;
1261 }
1262
1263 private saveStationInfo(): void {
1264 if (this.getStationInfoPersistentConfiguration()) {
1265 this.saveConfiguration();
1266 }
1267 }
1268
1269 private getOcppPersistentConfiguration(): boolean {
1270 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1271 }
1272
1273 private getStationInfoPersistentConfiguration(): boolean {
1274 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
1275 }
1276
1277 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1278 return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
1279 }
1280
1281 private handleUnsupportedVersion(version: OCPPVersion) {
1282 const errorMsg = `Unsupported protocol version '${version}' configured
1283 in template file ${this.templateFile}`;
1284 logger.error(`${this.logPrefix()} ${errorMsg}`);
1285 throw new BaseError(errorMsg);
1286 }
1287
1288 private initialize(): void {
1289 const stationTemplate = this.getTemplateFromFile()!;
1290 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
1291 this.configurationFile = join(
1292 dirname(this.templateFile.replace('station-templates', 'configurations')),
1293 `${getHashId(this.index, stationTemplate)}.json`,
1294 );
1295 const chargingStationConfiguration = this.getConfigurationFromFile();
1296 if (
1297 chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
1298 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1299 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1300 ) {
1301 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1302 } else {
1303 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1304 }
1305 this.stationInfo = this.getStationInfo();
1306 if (
1307 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1308 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1309 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1310 ) {
1311 const patternGroup: number | undefined =
1312 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1313 this.stationInfo.firmwareVersion?.split('.').length;
1314 const match = this.stationInfo
1315 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1316 .slice(1, patternGroup! + 1);
1317 const patchLevelIndex = match.length - 1;
1318 match[patchLevelIndex] = (
1319 convertToInt(match[patchLevelIndex]) +
1320 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1321 ).toString();
1322 this.stationInfo.firmwareVersion = match?.join('.');
1323 }
1324 this.saveStationInfo();
1325 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1326 if (this.getEnableStatistics() === true) {
1327 this.performanceStatistics = PerformanceStatistics.getInstance(
1328 this.stationInfo.hashId,
1329 this.stationInfo.chargingStationId!,
1330 this.configuredSupervisionUrl,
1331 );
1332 }
1333 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
1334 this.powerDivider = this.getPowerDivider();
1335 // OCPP configuration
1336 this.ocppConfiguration = this.getOcppConfiguration();
1337 this.initializeOcppConfiguration();
1338 this.initializeOcppServices();
1339 if (this.stationInfo?.autoRegister === true) {
1340 this.bootNotificationResponse = {
1341 currentTime: new Date(),
1342 interval: this.getHeartbeatInterval() / 1000,
1343 status: RegistrationStatusEnumType.ACCEPTED,
1344 };
1345 }
1346 }
1347
1348 private initializeOcppServices(): void {
1349 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1350 switch (ocppVersion) {
1351 case OCPPVersion.VERSION_16:
1352 this.ocppIncomingRequestService =
1353 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1354 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1355 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
1356 );
1357 break;
1358 case OCPPVersion.VERSION_20:
1359 case OCPPVersion.VERSION_201:
1360 this.ocppIncomingRequestService =
1361 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1362 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1363 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
1364 );
1365 break;
1366 default:
1367 this.handleUnsupportedVersion(ocppVersion);
1368 break;
1369 }
1370 }
1371
1372 private initializeOcppConfiguration(): void {
1373 if (!getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)) {
1374 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
1375 }
1376 if (!getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)) {
1377 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
1378 }
1379 if (
1380 this.getSupervisionUrlOcppConfiguration() &&
1381 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1382 !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1383 ) {
1384 addConfigurationKey(
1385 this,
1386 this.getSupervisionUrlOcppKey(),
1387 this.configuredSupervisionUrl.href,
1388 { reboot: true },
1389 );
1390 } else if (
1391 !this.getSupervisionUrlOcppConfiguration() &&
1392 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1393 getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1394 ) {
1395 deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save: false });
1396 }
1397 if (
1398 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1399 !getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
1400 ) {
1401 addConfigurationKey(
1402 this,
1403 this.stationInfo.amperageLimitationOcppKey!,
1404 (
1405 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
1406 ).toString(),
1407 );
1408 }
1409 if (!getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)) {
1410 addConfigurationKey(
1411 this,
1412 StandardParametersKey.SupportedFeatureProfiles,
1413 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
1414 );
1415 }
1416 addConfigurationKey(
1417 this,
1418 StandardParametersKey.NumberOfConnectors,
1419 this.getNumberOfConnectors().toString(),
1420 { readonly: true },
1421 { overwrite: true },
1422 );
1423 if (!getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData)) {
1424 addConfigurationKey(
1425 this,
1426 StandardParametersKey.MeterValuesSampledData,
1427 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
1428 );
1429 }
1430 if (!getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation)) {
1431 const connectorsPhaseRotation: string[] = [];
1432 if (this.hasEvses) {
1433 for (const evseStatus of this.evses.values()) {
1434 for (const connectorId of evseStatus.connectors.keys()) {
1435 connectorsPhaseRotation.push(
1436 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1437 );
1438 }
1439 }
1440 } else {
1441 for (const connectorId of this.connectors.keys()) {
1442 connectorsPhaseRotation.push(
1443 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
1444 );
1445 }
1446 }
1447 addConfigurationKey(
1448 this,
1449 StandardParametersKey.ConnectorPhaseRotation,
1450 connectorsPhaseRotation.toString(),
1451 );
1452 }
1453 if (!getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests)) {
1454 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
1455 }
1456 if (
1457 !getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) &&
1458 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1459 SupportedFeatureProfiles.LocalAuthListManagement,
1460 )
1461 ) {
1462 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1463 }
1464 if (!getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
1465 addConfigurationKey(
1466 this,
1467 StandardParametersKey.ConnectionTimeOut,
1468 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
1469 );
1470 }
1471 this.saveOcppConfiguration();
1472 }
1473
1474 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1475 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
1476 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1477 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1478 }
1479 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1480 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1481 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
1482 delete evseStatus.connectorsStatus;
1483 this.evses.set(evseId, {
1484 ...(evseStatus as EvseStatus),
1485 connectors: new Map<number, ConnectorStatus>(
1486 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1487 connectorId,
1488 connectorStatus,
1489 ]),
1490 ),
1491 });
1492 }
1493 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1494 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1495 logger.error(`${this.logPrefix()} ${errorMsg}`);
1496 throw new BaseError(errorMsg);
1497 } else {
1498 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1499 logger.error(`${this.logPrefix()} ${errorMsg}`);
1500 throw new BaseError(errorMsg);
1501 }
1502 }
1503
1504 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
1505 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
1506 this.initializeConnectorsFromTemplate(stationTemplate);
1507 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
1508 this.initializeEvsesFromTemplate(stationTemplate);
1509 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
1510 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1511 logger.error(`${this.logPrefix()} ${errorMsg}`);
1512 throw new BaseError(errorMsg);
1513 } else {
1514 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1515 logger.error(`${this.logPrefix()} ${errorMsg}`);
1516 throw new BaseError(errorMsg);
1517 }
1518 }
1519
1520 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
1521 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
1522 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1523 logger.error(`${this.logPrefix()} ${errorMsg}`);
1524 throw new BaseError(errorMsg);
1525 }
1526 if (!stationTemplate?.Connectors?.[0]) {
1527 logger.warn(
1528 `${this.logPrefix()} Charging station information from template ${
1529 this.templateFile
1530 } with no connector id 0 configuration`,
1531 );
1532 }
1533 if (stationTemplate?.Connectors) {
1534 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1535 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
1536 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1537 .update(
1538 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
1539 )
1540 .digest('hex');
1541 const connectorsConfigChanged =
1542 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1543 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1544 connectorsConfigChanged && this.connectors.clear();
1545 this.connectorsConfigurationHash = connectorsConfigHash;
1546 if (templateMaxConnectors > 0) {
1547 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1548 if (
1549 connectorId === 0 &&
1550 (!stationTemplate?.Connectors[connectorId] ||
1551 this.getUseConnectorId0(stationTemplate) === false)
1552 ) {
1553 continue;
1554 }
1555 const templateConnectorId =
1556 connectorId > 0 && stationTemplate?.randomConnectors
1557 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1558 : connectorId;
1559 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
1560 checkStationInfoConnectorStatus(
1561 templateConnectorId,
1562 connectorStatus,
1563 this.logPrefix(),
1564 this.templateFile,
1565 );
1566 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
1567 }
1568 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1569 this.saveConnectorsStatus();
1570 } else {
1571 logger.warn(
1572 `${this.logPrefix()} Charging station information from template ${
1573 this.templateFile
1574 } with no connectors configuration defined, cannot create connectors`,
1575 );
1576 }
1577 }
1578 } else {
1579 logger.warn(
1580 `${this.logPrefix()} Charging station information from template ${
1581 this.templateFile
1582 } with no connectors configuration defined, using already defined connectors`,
1583 );
1584 }
1585 }
1586
1587 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
1588 if (!stationTemplate?.Evses && this.evses.size === 0) {
1589 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1590 logger.error(`${this.logPrefix()} ${errorMsg}`);
1591 throw new BaseError(errorMsg);
1592 }
1593 if (!stationTemplate?.Evses?.[0]) {
1594 logger.warn(
1595 `${this.logPrefix()} Charging station information from template ${
1596 this.templateFile
1597 } with no evse id 0 configuration`,
1598 );
1599 }
1600 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
1601 logger.warn(
1602 `${this.logPrefix()} Charging station information from template ${
1603 this.templateFile
1604 } with evse id 0 with no connector id 0 configuration`,
1605 );
1606 }
1607 if (stationTemplate?.Evses) {
1608 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1609 .update(JSON.stringify(stationTemplate?.Evses))
1610 .digest('hex');
1611 const evsesConfigChanged =
1612 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1613 if (this.evses?.size === 0 || evsesConfigChanged) {
1614 evsesConfigChanged && this.evses.clear();
1615 this.evsesConfigurationHash = evsesConfigHash;
1616 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
1617 if (templateMaxEvses > 0) {
1618 for (const evse in stationTemplate.Evses) {
1619 const evseId = convertToInt(evse);
1620 this.evses.set(evseId, {
1621 connectors: buildConnectorsMap(
1622 stationTemplate?.Evses[evse]?.Connectors,
1623 this.logPrefix(),
1624 this.templateFile,
1625 ),
1626 availability: AvailabilityType.Operative,
1627 });
1628 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
1629 }
1630 this.saveEvsesStatus();
1631 } else {
1632 logger.warn(
1633 `${this.logPrefix()} Charging station information from template ${
1634 this.templateFile
1635 } with no evses configuration defined, cannot create evses`,
1636 );
1637 }
1638 }
1639 } else {
1640 logger.warn(
1641 `${this.logPrefix()} Charging station information from template ${
1642 this.templateFile
1643 } with no evses configuration defined, using already defined evses`,
1644 );
1645 }
1646 }
1647
1648 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1649 let configuration: ChargingStationConfiguration | undefined;
1650 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1651 try {
1652 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1653 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1654 this.configurationFileHash,
1655 );
1656 } else {
1657 const measureId = `${FileType.ChargingStationConfiguration} read`;
1658 const beginId = PerformanceStatistics.beginMeasure(measureId);
1659 configuration = JSON.parse(
1660 readFileSync(this.configurationFile, 'utf8'),
1661 ) as ChargingStationConfiguration;
1662 PerformanceStatistics.endMeasure(measureId, beginId);
1663 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1664 this.configurationFileHash = configuration.configurationHash!;
1665 }
1666 } catch (error) {
1667 handleFileException(
1668 this.configurationFile,
1669 FileType.ChargingStationConfiguration,
1670 error as NodeJS.ErrnoException,
1671 this.logPrefix(),
1672 );
1673 }
1674 }
1675 return configuration;
1676 }
1677
1678 private saveAutomaticTransactionGeneratorConfiguration(): void {
1679 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1680 this.saveConfiguration();
1681 }
1682 }
1683
1684 private saveConnectorsStatus() {
1685 this.saveConfiguration();
1686 }
1687
1688 private saveEvsesStatus() {
1689 this.saveConfiguration();
1690 }
1691
1692 private saveConfiguration(): void {
1693 if (isNotEmptyString(this.configurationFile)) {
1694 try {
1695 if (!existsSync(dirname(this.configurationFile))) {
1696 mkdirSync(dirname(this.configurationFile), { recursive: true });
1697 }
1698 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1699 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1700 : {};
1701 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
1702 configurationData.stationInfo = this.stationInfo;
1703 } else {
1704 delete configurationData.stationInfo;
1705 }
1706 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
1707 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1708 } else {
1709 delete configurationData.configurationKey;
1710 }
1711 configurationData = merge<ChargingStationConfiguration>(
1712 configurationData,
1713 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
1714 );
1715 if (
1716 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1717 !this.getAutomaticTransactionGeneratorConfiguration()
1718 ) {
1719 delete configurationData.automaticTransactionGenerator;
1720 }
1721 if (this.connectors.size > 0) {
1722 configurationData.connectorsStatus = buildConnectorsStatus(this);
1723 } else {
1724 delete configurationData.connectorsStatus;
1725 }
1726 if (this.evses.size > 0) {
1727 configurationData.evsesStatus = buildEvsesStatus(this);
1728 } else {
1729 delete configurationData.evsesStatus;
1730 }
1731 delete configurationData.configurationHash;
1732 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1733 .update(
1734 JSON.stringify({
1735 stationInfo: configurationData.stationInfo,
1736 configurationKey: configurationData.configurationKey,
1737 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1738 } as ChargingStationConfiguration),
1739 )
1740 .digest('hex');
1741 if (this.configurationFileHash !== configurationHash) {
1742 AsyncLock.acquire(AsyncLockType.configuration)
1743 .then(() => {
1744 configurationData.configurationHash = configurationHash;
1745 const measureId = `${FileType.ChargingStationConfiguration} write`;
1746 const beginId = PerformanceStatistics.beginMeasure(measureId);
1747 const fileDescriptor = openSync(this.configurationFile, 'w');
1748 writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1749 closeSync(fileDescriptor);
1750 PerformanceStatistics.endMeasure(measureId, beginId);
1751 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1752 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1753 this.configurationFileHash = configurationHash;
1754 })
1755 .catch((error) => {
1756 handleFileException(
1757 this.configurationFile,
1758 FileType.ChargingStationConfiguration,
1759 error as NodeJS.ErrnoException,
1760 this.logPrefix(),
1761 );
1762 })
1763 .finally(() => {
1764 AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
1765 });
1766 } else {
1767 logger.debug(
1768 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1769 this.configurationFile
1770 }`,
1771 );
1772 }
1773 } catch (error) {
1774 handleFileException(
1775 this.configurationFile,
1776 FileType.ChargingStationConfiguration,
1777 error as NodeJS.ErrnoException,
1778 this.logPrefix(),
1779 );
1780 }
1781 } else {
1782 logger.error(
1783 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
1784 );
1785 }
1786 }
1787
1788 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1789 return this.getTemplateFromFile()?.Configuration;
1790 }
1791
1792 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1793 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1794 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1795 return { configurationKey };
1796 }
1797 return undefined;
1798 }
1799
1800 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1801 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1802 this.getOcppConfigurationFromFile();
1803 if (!ocppConfiguration) {
1804 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1805 }
1806 return ocppConfiguration;
1807 }
1808
1809 private async onOpen(): Promise<void> {
1810 if (this.isWebSocketConnectionOpened() === true) {
1811 logger.info(
1812 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
1813 );
1814 if (this.isRegistered() === false) {
1815 // Send BootNotification
1816 let registrationRetryCount = 0;
1817 do {
1818 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1819 BootNotificationRequest,
1820 BootNotificationResponse
1821 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1822 skipBufferingOnError: true,
1823 });
1824 if (this.isRegistered() === false) {
1825 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
1826 await sleep(
1827 this?.bootNotificationResponse?.interval
1828 ? this.bootNotificationResponse.interval * 1000
1829 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
1830 );
1831 }
1832 } while (
1833 this.isRegistered() === false &&
1834 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
1835 this.getRegistrationMaxRetries() === -1)
1836 );
1837 }
1838 if (this.isRegistered() === true) {
1839 if (this.inAcceptedState() === true) {
1840 await this.startMessageSequence();
1841 }
1842 } else {
1843 logger.error(
1844 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
1845 );
1846 }
1847 this.wsConnectionRestarted = false;
1848 this.autoReconnectRetryCount = 0;
1849 parentPort?.postMessage(buildUpdatedMessage(this));
1850 } else {
1851 logger.warn(
1852 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
1853 );
1854 }
1855 }
1856
1857 private async onClose(code: number, reason: Buffer): Promise<void> {
1858 switch (code) {
1859 // Normal close
1860 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1861 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1862 logger.info(
1863 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1864 code,
1865 )}' and reason '${reason.toString()}'`,
1866 );
1867 this.autoReconnectRetryCount = 0;
1868 break;
1869 // Abnormal close
1870 default:
1871 logger.error(
1872 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1873 code,
1874 )}' and reason '${reason.toString()}'`,
1875 );
1876 this.started === true && (await this.reconnect());
1877 break;
1878 }
1879 parentPort?.postMessage(buildUpdatedMessage(this));
1880 }
1881
1882 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1883 const cachedRequest = this.requests.get(messageId);
1884 if (Array.isArray(cachedRequest) === true) {
1885 return cachedRequest;
1886 }
1887 throw new OCPPError(
1888 ErrorType.PROTOCOL_ERROR,
1889 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1890 messageType,
1891 )} is not an array`,
1892 undefined,
1893 cachedRequest as JsonType,
1894 );
1895 }
1896
1897 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1898 const [messageType, messageId, commandName, commandPayload] = request;
1899 if (this.getEnableStatistics() === true) {
1900 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1901 }
1902 logger.debug(
1903 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1904 request,
1905 )}`,
1906 );
1907 // Process the message
1908 await this.ocppIncomingRequestService.incomingRequestHandler(
1909 this,
1910 messageId,
1911 commandName,
1912 commandPayload,
1913 );
1914 }
1915
1916 private handleResponseMessage(response: Response): void {
1917 const [messageType, messageId, commandPayload] = response;
1918 if (this.requests.has(messageId) === false) {
1919 // Error
1920 throw new OCPPError(
1921 ErrorType.INTERNAL_ERROR,
1922 `Response for unknown message id ${messageId}`,
1923 undefined,
1924 commandPayload,
1925 );
1926 }
1927 // Respond
1928 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1929 messageType,
1930 messageId,
1931 )!;
1932 logger.debug(
1933 `${this.logPrefix()} << Command '${
1934 requestCommandName ?? Constants.UNKNOWN_COMMAND
1935 }' received response payload: ${JSON.stringify(response)}`,
1936 );
1937 responseCallback(commandPayload, requestPayload);
1938 }
1939
1940 private handleErrorMessage(errorResponse: ErrorResponse): void {
1941 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1942 if (this.requests.has(messageId) === false) {
1943 // Error
1944 throw new OCPPError(
1945 ErrorType.INTERNAL_ERROR,
1946 `Error response for unknown message id ${messageId}`,
1947 undefined,
1948 { errorType, errorMessage, errorDetails },
1949 );
1950 }
1951 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1952 logger.debug(
1953 `${this.logPrefix()} << Command '${
1954 requestCommandName ?? Constants.UNKNOWN_COMMAND
1955 }' received error response payload: ${JSON.stringify(errorResponse)}`,
1956 );
1957 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1958 }
1959
1960 private async onMessage(data: RawData): Promise<void> {
1961 let request: IncomingRequest | Response | ErrorResponse | undefined;
1962 let messageType: number | undefined;
1963 let errorMsg: string;
1964 try {
1965 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1966 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1967 if (Array.isArray(request) === true) {
1968 [messageType] = request;
1969 // Check the type of message
1970 switch (messageType) {
1971 // Incoming Message
1972 case MessageType.CALL_MESSAGE:
1973 await this.handleIncomingMessage(request as IncomingRequest);
1974 break;
1975 // Response Message
1976 case MessageType.CALL_RESULT_MESSAGE:
1977 this.handleResponseMessage(request as Response);
1978 break;
1979 // Error Message
1980 case MessageType.CALL_ERROR_MESSAGE:
1981 this.handleErrorMessage(request as ErrorResponse);
1982 break;
1983 // Unknown Message
1984 default:
1985 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1986 errorMsg = `Wrong message type ${messageType}`;
1987 logger.error(`${this.logPrefix()} ${errorMsg}`);
1988 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1989 }
1990 parentPort?.postMessage(buildUpdatedMessage(this));
1991 } else {
1992 throw new OCPPError(
1993 ErrorType.PROTOCOL_ERROR,
1994 'Incoming message is not an array',
1995 undefined,
1996 {
1997 request,
1998 },
1999 );
2000 }
2001 } catch (error) {
2002 let commandName: IncomingRequestCommand | undefined;
2003 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
2004 let errorCallback: ErrorCallback;
2005 const [, messageId] = request!;
2006 switch (messageType) {
2007 case MessageType.CALL_MESSAGE:
2008 [, , commandName] = request as IncomingRequest;
2009 // Send error
2010 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
2011 break;
2012 case MessageType.CALL_RESULT_MESSAGE:
2013 case MessageType.CALL_ERROR_MESSAGE:
2014 if (this.requests.has(messageId) === true) {
2015 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
2016 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2017 errorCallback(error as OCPPError, false);
2018 } else {
2019 // Remove the request from the cache in case of error at response handling
2020 this.requests.delete(messageId);
2021 }
2022 break;
2023 }
2024 if (error instanceof OCPPError === false) {
2025 logger.warn(
2026 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2027 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2028 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2029 }' message '${data.toString()}' handling is not an OCPPError:`,
2030 error,
2031 );
2032 }
2033 logger.error(
2034 `${this.logPrefix()} Incoming OCPP command '${
2035 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2036 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2037 }' message '${data.toString()}'${
2038 messageType !== MessageType.CALL_MESSAGE
2039 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2040 : ''
2041 } processing error:`,
2042 error,
2043 );
2044 }
2045 }
2046
2047 private onPing(): void {
2048 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2049 }
2050
2051 private onPong(): void {
2052 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2053 }
2054
2055 private onError(error: WSError): void {
2056 this.closeWSConnection();
2057 logger.error(`${this.logPrefix()} WebSocket error:`, error);
2058 }
2059
2060 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
2061 if (this.getMeteringPerTransaction() === true) {
2062 return (
2063 (rounded === true
2064 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
2065 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2066 );
2067 }
2068 return (
2069 (rounded === true
2070 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
2071 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2072 );
2073 }
2074
2075 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2076 return stationTemplate?.useConnectorId0 ?? true;
2077 }
2078
2079 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
2080 if (this.hasEvses) {
2081 for (const [evseId, evseStatus] of this.evses) {
2082 if (evseId === 0) {
2083 continue;
2084 }
2085 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2086 if (connectorStatus.transactionStarted === true) {
2087 await this.stopTransactionOnConnector(connectorId, reason);
2088 }
2089 }
2090 }
2091 } else {
2092 for (const connectorId of this.connectors.keys()) {
2093 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2094 await this.stopTransactionOnConnector(connectorId, reason);
2095 }
2096 }
2097 }
2098 }
2099
2100 // 0 for disabling
2101 private getConnectionTimeout(): number {
2102 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
2103 return (
2104 parseInt(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value!) ??
2105 Constants.DEFAULT_CONNECTION_TIMEOUT
2106 );
2107 }
2108 return Constants.DEFAULT_CONNECTION_TIMEOUT;
2109 }
2110
2111 // -1 for unlimited, 0 for disabling
2112 private getAutoReconnectMaxRetries(): number | undefined {
2113 return (
2114 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
2115 );
2116 }
2117
2118 // 0 for disabling
2119 private getRegistrationMaxRetries(): number | undefined {
2120 return this.stationInfo.registrationMaxRetries ?? -1;
2121 }
2122
2123 private getPowerDivider(): number {
2124 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
2125 if (this.stationInfo?.powerSharedByConnectors) {
2126 powerDivider = this.getNumberOfRunningTransactions();
2127 }
2128 return powerDivider;
2129 }
2130
2131 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2132 const maximumPower = this.getMaximumPower(stationInfo);
2133 switch (this.getCurrentOutType(stationInfo)) {
2134 case CurrentType.AC:
2135 return ACElectricUtils.amperagePerPhaseFromPower(
2136 this.getNumberOfPhases(stationInfo),
2137 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2138 this.getVoltageOut(stationInfo),
2139 );
2140 case CurrentType.DC:
2141 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
2142 }
2143 }
2144
2145 private getAmperageLimitation(): number | undefined {
2146 if (
2147 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2148 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
2149 ) {
2150 return (
2151 convertToInt(
2152 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
2153 ) / getAmperageLimitationUnitDivider(this.stationInfo)
2154 );
2155 }
2156 }
2157
2158 private async startMessageSequence(): Promise<void> {
2159 if (this.stationInfo?.autoRegister === true) {
2160 await this.ocppRequestService.requestHandler<
2161 BootNotificationRequest,
2162 BootNotificationResponse
2163 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2164 skipBufferingOnError: true,
2165 });
2166 }
2167 // Start WebSocket ping
2168 this.startWebSocketPing();
2169 // Start heartbeat
2170 this.startHeartbeat();
2171 // Initialize connectors status
2172 if (this.hasEvses) {
2173 for (const [evseId, evseStatus] of this.evses) {
2174 if (evseId > 0) {
2175 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2176 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
2177 await OCPPServiceUtils.sendAndSetConnectorStatus(
2178 this,
2179 connectorId,
2180 connectorBootStatus,
2181 evseId,
2182 );
2183 }
2184 }
2185 }
2186 } else {
2187 for (const connectorId of this.connectors.keys()) {
2188 if (connectorId > 0) {
2189 const connectorBootStatus = getBootConnectorStatus(
2190 this,
2191 connectorId,
2192 this.getConnectorStatus(connectorId)!,
2193 );
2194 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2195 }
2196 }
2197 }
2198 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2199 await this.ocppRequestService.requestHandler<
2200 FirmwareStatusNotificationRequest,
2201 FirmwareStatusNotificationResponse
2202 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2203 status: FirmwareStatus.Installed,
2204 });
2205 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2206 }
2207
2208 // Start the ATG
2209 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2210 this.startAutomaticTransactionGenerator();
2211 }
2212 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2213 }
2214
2215 private async stopMessageSequence(
2216 reason: StopTransactionReason = StopTransactionReason.NONE,
2217 ): Promise<void> {
2218 // Stop WebSocket ping
2219 this.stopWebSocketPing();
2220 // Stop heartbeat
2221 this.stopHeartbeat();
2222 // Stop ongoing transactions
2223 if (this.automaticTransactionGenerator?.started === true) {
2224 this.stopAutomaticTransactionGenerator();
2225 } else {
2226 await this.stopRunningTransactions(reason);
2227 }
2228 if (this.hasEvses) {
2229 for (const [evseId, evseStatus] of this.evses) {
2230 if (evseId > 0) {
2231 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2232 await this.ocppRequestService.requestHandler<
2233 StatusNotificationRequest,
2234 StatusNotificationResponse
2235 >(
2236 this,
2237 RequestCommand.STATUS_NOTIFICATION,
2238 OCPPServiceUtils.buildStatusNotificationRequest(
2239 this,
2240 connectorId,
2241 ConnectorStatusEnum.Unavailable,
2242 evseId,
2243 ),
2244 );
2245 delete connectorStatus?.status;
2246 }
2247 }
2248 }
2249 } else {
2250 for (const connectorId of this.connectors.keys()) {
2251 if (connectorId > 0) {
2252 await this.ocppRequestService.requestHandler<
2253 StatusNotificationRequest,
2254 StatusNotificationResponse
2255 >(
2256 this,
2257 RequestCommand.STATUS_NOTIFICATION,
2258 OCPPServiceUtils.buildStatusNotificationRequest(
2259 this,
2260 connectorId,
2261 ConnectorStatusEnum.Unavailable,
2262 ),
2263 );
2264 delete this.getConnectorStatus(connectorId)?.status;
2265 }
2266 }
2267 }
2268 }
2269
2270 private startWebSocketPing(): void {
2271 const webSocketPingInterval: number = getConfigurationKey(
2272 this,
2273 StandardParametersKey.WebSocketPingInterval,
2274 )
2275 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2276 : 0;
2277 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2278 this.webSocketPingSetInterval = setInterval(() => {
2279 if (this.isWebSocketConnectionOpened() === true) {
2280 this.wsConnection?.ping();
2281 }
2282 }, webSocketPingInterval * 1000);
2283 logger.info(
2284 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2285 webSocketPingInterval,
2286 )}`,
2287 );
2288 } else if (this.webSocketPingSetInterval) {
2289 logger.info(
2290 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2291 webSocketPingInterval,
2292 )}`,
2293 );
2294 } else {
2295 logger.error(
2296 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
2297 );
2298 }
2299 }
2300
2301 private stopWebSocketPing(): void {
2302 if (this.webSocketPingSetInterval) {
2303 clearInterval(this.webSocketPingSetInterval);
2304 delete this.webSocketPingSetInterval;
2305 }
2306 }
2307
2308 private getConfiguredSupervisionUrl(): URL {
2309 let configuredSupervisionUrl: string;
2310 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2311 if (isNotEmptyArray(supervisionUrls)) {
2312 let configuredSupervisionUrlIndex: number;
2313 switch (Configuration.getSupervisionUrlDistribution()) {
2314 case SupervisionUrlDistribution.RANDOM:
2315 configuredSupervisionUrlIndex = Math.floor(
2316 secureRandom() * (supervisionUrls as string[]).length,
2317 );
2318 break;
2319 case SupervisionUrlDistribution.ROUND_ROBIN:
2320 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2321 default:
2322 Object.values(SupervisionUrlDistribution).includes(
2323 Configuration.getSupervisionUrlDistribution()!,
2324 ) === false &&
2325 logger.error(
2326 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2327 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2328 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2329 }`,
2330 );
2331 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2332 break;
2333 }
2334 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
2335 } else {
2336 configuredSupervisionUrl = supervisionUrls as string;
2337 }
2338 if (isNotEmptyString(configuredSupervisionUrl)) {
2339 return new URL(configuredSupervisionUrl);
2340 }
2341 const errorMsg = 'No supervision url(s) configured';
2342 logger.error(`${this.logPrefix()} ${errorMsg}`);
2343 throw new BaseError(`${errorMsg}`);
2344 }
2345
2346 private stopHeartbeat(): void {
2347 if (this.heartbeatSetInterval) {
2348 clearInterval(this.heartbeatSetInterval);
2349 delete this.heartbeatSetInterval;
2350 }
2351 }
2352
2353 private terminateWSConnection(): void {
2354 if (this.isWebSocketConnectionOpened() === true) {
2355 this.wsConnection?.terminate();
2356 this.wsConnection = null;
2357 }
2358 }
2359
2360 private getReconnectExponentialDelay(): boolean {
2361 return this.stationInfo?.reconnectExponentialDelay ?? false;
2362 }
2363
2364 private async reconnect(): Promise<void> {
2365 // Stop WebSocket ping
2366 this.stopWebSocketPing();
2367 // Stop heartbeat
2368 this.stopHeartbeat();
2369 // Stop the ATG if needed
2370 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
2371 this.stopAutomaticTransactionGenerator();
2372 }
2373 if (
2374 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
2375 this.getAutoReconnectMaxRetries() === -1
2376 ) {
2377 ++this.autoReconnectRetryCount;
2378 const reconnectDelay = this.getReconnectExponentialDelay()
2379 ? exponentialDelay(this.autoReconnectRetryCount)
2380 : this.getConnectionTimeout() * 1000;
2381 const reconnectDelayWithdraw = 1000;
2382 const reconnectTimeout =
2383 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2384 ? reconnectDelay - reconnectDelayWithdraw
2385 : 0;
2386 logger.error(
2387 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2388 reconnectDelay,
2389 2,
2390 )}ms, timeout ${reconnectTimeout}ms`,
2391 );
2392 await sleep(reconnectDelay);
2393 logger.error(
2394 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
2395 );
2396 this.openWSConnection(
2397 {
2398 ...(this.stationInfo?.wsOptions ?? {}),
2399 handshakeTimeout: reconnectTimeout,
2400 },
2401 { closeOpened: true },
2402 );
2403 this.wsConnectionRestarted = true;
2404 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2405 logger.error(
2406 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2407 this.autoReconnectRetryCount
2408 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
2409 );
2410 }
2411 }
2412 }