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