@enyo-energy/sunspec-sdk 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/connection-retry-manager.cjs +125 -0
- package/dist/cjs/connection-retry-manager.d.cts +53 -0
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/sunspec-devices.cjs +166 -5
- package/dist/cjs/sunspec-devices.d.cts +19 -1
- package/dist/cjs/sunspec-interfaces.cjs +7 -1
- package/dist/cjs/sunspec-interfaces.d.cts +10 -0
- package/dist/cjs/sunspec-modbus-client.cjs +194 -9
- package/dist/cjs/sunspec-modbus-client.d.cts +41 -6
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/connection-retry-manager.d.ts +53 -0
- package/dist/connection-retry-manager.js +121 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sunspec-devices.d.ts +19 -1
- package/dist/sunspec-devices.js +167 -6
- package/dist/sunspec-interfaces.d.ts +10 -0
- package/dist/sunspec-interfaces.js +6 -0
- package/dist/sunspec-modbus-client.d.ts +41 -6
- package/dist/sunspec-modbus-client.js +194 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Connection Retry Manager with Exponential Backoff
|
|
4
|
+
*
|
|
5
|
+
* Manages automatic connection retries with configurable exponential backoff.
|
|
6
|
+
* Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ConnectionRetryManager = void 0;
|
|
10
|
+
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
11
|
+
class ConnectionRetryManager {
|
|
12
|
+
config;
|
|
13
|
+
currentAttempt = 0;
|
|
14
|
+
pendingRetryTimeout = null;
|
|
15
|
+
isRetrying = false;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = { ...sunspec_interfaces_js_1.DEFAULT_RETRY_CONFIG, ...config };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Calculate the next delay using exponential backoff
|
|
21
|
+
* Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
|
|
22
|
+
*/
|
|
23
|
+
getNextDelay() {
|
|
24
|
+
const delay = this.config.initialDelayMs * Math.pow(this.config.backoffFactor, this.currentAttempt);
|
|
25
|
+
return Math.min(delay, this.config.maxDelayMs);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reset the retry state on successful connection
|
|
29
|
+
*/
|
|
30
|
+
reset() {
|
|
31
|
+
this.currentAttempt = 0;
|
|
32
|
+
this.isRetrying = false;
|
|
33
|
+
this.cancelPendingRetry();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if we should retry based on max attempts
|
|
37
|
+
* Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
|
|
38
|
+
*/
|
|
39
|
+
shouldRetry() {
|
|
40
|
+
if (this.config.maxAttempts === -1) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return this.currentAttempt < this.config.maxAttempts;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the current attempt number (0-indexed)
|
|
47
|
+
*/
|
|
48
|
+
getCurrentAttempt() {
|
|
49
|
+
return this.currentAttempt;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the max attempts configuration
|
|
53
|
+
*/
|
|
54
|
+
getMaxAttempts() {
|
|
55
|
+
return this.config.maxAttempts;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a retry is currently in progress
|
|
59
|
+
*/
|
|
60
|
+
isRetryInProgress() {
|
|
61
|
+
return this.isRetrying;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Schedule a reconnection attempt with exponential backoff delay
|
|
65
|
+
* Returns a promise that resolves when the callback completes
|
|
66
|
+
*/
|
|
67
|
+
async scheduleRetry(callback) {
|
|
68
|
+
if (!this.shouldRetry()) {
|
|
69
|
+
console.log(`Max retry attempts (${this.config.maxAttempts}) reached. Giving up.`);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (this.isRetrying) {
|
|
73
|
+
console.log('Retry already in progress, skipping duplicate request.');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
this.isRetrying = true;
|
|
77
|
+
const delay = this.getNextDelay();
|
|
78
|
+
const attemptNumber = this.currentAttempt + 1;
|
|
79
|
+
const maxAttemptsStr = this.config.maxAttempts === -1 ? '∞' : this.config.maxAttempts.toString();
|
|
80
|
+
console.log(`Scheduling reconnection attempt ${attemptNumber}/${maxAttemptsStr} in ${delay}ms`);
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
this.pendingRetryTimeout = setTimeout(async () => {
|
|
83
|
+
this.pendingRetryTimeout = null;
|
|
84
|
+
this.currentAttempt++;
|
|
85
|
+
try {
|
|
86
|
+
console.log(`Attempting reconnection (attempt ${attemptNumber}/${maxAttemptsStr})...`);
|
|
87
|
+
const success = await callback();
|
|
88
|
+
if (success) {
|
|
89
|
+
console.log('Reconnection successful!');
|
|
90
|
+
this.reset();
|
|
91
|
+
resolve(true);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log('Reconnection attempt failed.');
|
|
95
|
+
this.isRetrying = false;
|
|
96
|
+
resolve(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(`Reconnection attempt failed with error: ${error}`);
|
|
101
|
+
this.isRetrying = false;
|
|
102
|
+
resolve(false);
|
|
103
|
+
}
|
|
104
|
+
}, delay);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Cancel any pending retry attempt
|
|
109
|
+
*/
|
|
110
|
+
cancelPendingRetry() {
|
|
111
|
+
if (this.pendingRetryTimeout) {
|
|
112
|
+
clearTimeout(this.pendingRetryTimeout);
|
|
113
|
+
this.pendingRetryTimeout = null;
|
|
114
|
+
this.isRetrying = false;
|
|
115
|
+
console.log('Pending retry cancelled.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the current configuration
|
|
120
|
+
*/
|
|
121
|
+
getConfig() {
|
|
122
|
+
return { ...this.config };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.ConnectionRetryManager = ConnectionRetryManager;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Retry Manager with Exponential Backoff
|
|
3
|
+
*
|
|
4
|
+
* Manages automatic connection retries with configurable exponential backoff.
|
|
5
|
+
* Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
|
|
6
|
+
*/
|
|
7
|
+
import { IRetryConfig } from './sunspec-interfaces.cjs';
|
|
8
|
+
export declare class ConnectionRetryManager {
|
|
9
|
+
private config;
|
|
10
|
+
private currentAttempt;
|
|
11
|
+
private pendingRetryTimeout;
|
|
12
|
+
private isRetrying;
|
|
13
|
+
constructor(config?: Partial<IRetryConfig>);
|
|
14
|
+
/**
|
|
15
|
+
* Calculate the next delay using exponential backoff
|
|
16
|
+
* Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
|
|
17
|
+
*/
|
|
18
|
+
getNextDelay(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Reset the retry state on successful connection
|
|
21
|
+
*/
|
|
22
|
+
reset(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Check if we should retry based on max attempts
|
|
25
|
+
* Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
|
|
26
|
+
*/
|
|
27
|
+
shouldRetry(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Get the current attempt number (0-indexed)
|
|
30
|
+
*/
|
|
31
|
+
getCurrentAttempt(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Get the max attempts configuration
|
|
34
|
+
*/
|
|
35
|
+
getMaxAttempts(): number;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a retry is currently in progress
|
|
38
|
+
*/
|
|
39
|
+
isRetryInProgress(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Schedule a reconnection attempt with exponential backoff delay
|
|
42
|
+
* Returns a promise that resolves when the callback completes
|
|
43
|
+
*/
|
|
44
|
+
scheduleRetry(callback: () => Promise<boolean>): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Cancel any pending retry attempt
|
|
47
|
+
*/
|
|
48
|
+
cancelPendingRetry(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Get the current configuration
|
|
51
|
+
*/
|
|
52
|
+
getConfig(): IRetryConfig;
|
|
53
|
+
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -14,10 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getSdkVersion = exports.SDK_VERSION = void 0;
|
|
17
|
+
exports.getSdkVersion = exports.SDK_VERSION = exports.ConnectionRetryManager = void 0;
|
|
18
18
|
__exportStar(require("./sunspec-interfaces.cjs"), exports);
|
|
19
19
|
__exportStar(require("./sunspec-devices.cjs"), exports);
|
|
20
20
|
__exportStar(require("./sunspec-modbus-client.cjs"), exports);
|
|
21
|
+
var connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
|
|
22
|
+
Object.defineProperty(exports, "ConnectionRetryManager", { enumerable: true, get: function () { return connection_retry_manager_js_1.ConnectionRetryManager; } });
|
|
21
23
|
var version_js_1 = require("./version.cjs");
|
|
22
24
|
Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_js_1.SDK_VERSION; } });
|
|
23
25
|
Object.defineProperty(exports, "getSdkVersion", { enumerable: true, get: function () { return version_js_1.getSdkVersion; } });
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -31,15 +31,19 @@ class BaseSunspecDevice {
|
|
|
31
31
|
sunspecClient;
|
|
32
32
|
applianceManager;
|
|
33
33
|
unitId;
|
|
34
|
+
port;
|
|
35
|
+
baseAddress;
|
|
34
36
|
applianceId;
|
|
35
37
|
lastUpdateTime = 0;
|
|
36
|
-
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
|
|
38
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000) {
|
|
37
39
|
this.energyApp = energyApp;
|
|
38
40
|
this.name = name;
|
|
39
41
|
this.networkDevice = networkDevice;
|
|
40
42
|
this.sunspecClient = sunspecClient;
|
|
41
43
|
this.applianceManager = applianceManager;
|
|
42
44
|
this.unitId = unitId;
|
|
45
|
+
this.port = port;
|
|
46
|
+
this.baseAddress = baseAddress;
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
49
|
* Check if the device is connected
|
|
@@ -55,8 +59,10 @@ class BaseSunspecDevice {
|
|
|
55
59
|
*/
|
|
56
60
|
async ensureConnected() {
|
|
57
61
|
if (!this.sunspecClient.isConnected()) {
|
|
58
|
-
await this.sunspecClient.connect(this.networkDevice.
|
|
59
|
-
|
|
62
|
+
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
63
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
64
|
+
);
|
|
65
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -68,8 +74,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
68
74
|
async connect() {
|
|
69
75
|
// Ensure Sunspec client is connected
|
|
70
76
|
if (!this.sunspecClient.isConnected()) {
|
|
71
|
-
await this.sunspecClient.connect(this.networkDevice.
|
|
72
|
-
|
|
77
|
+
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
78
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
79
|
+
);
|
|
80
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
73
81
|
}
|
|
74
82
|
// Get device info from common block
|
|
75
83
|
const commonData = await this.sunspecClient.readCommonBlock();
|
|
@@ -241,6 +249,8 @@ exports.SunspecInverter = SunspecInverter;
|
|
|
241
249
|
* Sunspec Battery implementation
|
|
242
250
|
*/
|
|
243
251
|
class SunspecBattery extends BaseSunspecDevice {
|
|
252
|
+
dataBusListenerId;
|
|
253
|
+
dataBus;
|
|
244
254
|
/**
|
|
245
255
|
* Connect to the battery and create/update the appliance
|
|
246
256
|
*/
|
|
@@ -284,12 +294,14 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
284
294
|
}
|
|
285
295
|
});
|
|
286
296
|
console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
297
|
+
this.startDataBusListening();
|
|
287
298
|
}
|
|
288
299
|
catch (error) {
|
|
289
300
|
console.error(`Failed to create battery appliance: ${error}`);
|
|
290
301
|
}
|
|
291
302
|
}
|
|
292
303
|
async disconnect() {
|
|
304
|
+
this.stopDataBusListening();
|
|
293
305
|
if (this.applianceId) {
|
|
294
306
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
295
307
|
}
|
|
@@ -558,6 +570,155 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
558
570
|
}
|
|
559
571
|
return controls.chaGriSet === 1; // 1 = GRID, 0 = PV
|
|
560
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Start listening for storage commands on the data bus.
|
|
575
|
+
* Idempotent — does nothing if already listening.
|
|
576
|
+
*/
|
|
577
|
+
startDataBusListening() {
|
|
578
|
+
if (this.dataBusListenerId) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
582
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
583
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartStorageGridChargeV1,
|
|
584
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1,
|
|
585
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1
|
|
586
|
+
], (entry) => this.handleStorageCommand(entry));
|
|
587
|
+
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Stop listening for storage commands on the data bus.
|
|
591
|
+
*/
|
|
592
|
+
stopDataBusListening() {
|
|
593
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
594
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
595
|
+
console.log(`Battery ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
596
|
+
}
|
|
597
|
+
this.dataBusListenerId = undefined;
|
|
598
|
+
this.dataBus = undefined;
|
|
599
|
+
}
|
|
600
|
+
handleStorageCommand(entry) {
|
|
601
|
+
if (entry.applianceId !== this.applianceId) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
void (async () => {
|
|
605
|
+
try {
|
|
606
|
+
switch (entry.message) {
|
|
607
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartStorageGridChargeV1:
|
|
608
|
+
await this.handleStartGridCharge(entry);
|
|
609
|
+
break;
|
|
610
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
611
|
+
await this.handleStopGridCharge(entry);
|
|
612
|
+
break;
|
|
613
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1:
|
|
614
|
+
await this.handleSetDischargeLimit(entry);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
console.error(`Battery ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
620
|
+
}
|
|
621
|
+
})();
|
|
622
|
+
}
|
|
623
|
+
async handleStartGridCharge(msg) {
|
|
624
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
625
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
console.log(`Battery ${this.applianceId}: handling StartStorageGridChargeV1 (powerLimitW=${msg.data.powerLimitW})`);
|
|
629
|
+
// Read current state for logging and rollback
|
|
630
|
+
const controls = await this.getBatteryControls();
|
|
631
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, wChaMax=${controls?.wChaMax}, storCtlMod=${controls?.storCtlMod}`);
|
|
632
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
633
|
+
const originalWChaMax = controls?.wChaMax;
|
|
634
|
+
// Step 1: Enable grid charging (Register 17: chaGriSet=GRID)
|
|
635
|
+
const step1 = await this.enableGridCharging(true);
|
|
636
|
+
if (!step1) {
|
|
637
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Step 2: Set charging power (Register 2: wChaMax)
|
|
641
|
+
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
642
|
+
if (!step2) {
|
|
643
|
+
// Rollback step 1
|
|
644
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
645
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Step 3: Set storage mode to CHARGE (Register 5: storCtlMod=0x0001)
|
|
649
|
+
const step3 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.CHARGE);
|
|
650
|
+
if (!step3) {
|
|
651
|
+
// Rollback steps 1+2
|
|
652
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
653
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
657
|
+
}
|
|
658
|
+
async handleStopGridCharge(msg) {
|
|
659
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
660
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
console.log(`Battery ${this.applianceId}: handling StopStorageGridChargeV1`);
|
|
664
|
+
// Read current state for logging and rollback
|
|
665
|
+
const controls = await this.getBatteryControls();
|
|
666
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
667
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
668
|
+
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
669
|
+
const step1 = await this.enableGridCharging(false);
|
|
670
|
+
if (!step1) {
|
|
671
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Step 2: Set storage mode to AUTO (Register 5: storCtlMod=0x0003)
|
|
675
|
+
const step2 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.AUTO);
|
|
676
|
+
if (!step2) {
|
|
677
|
+
// Rollback step 1
|
|
678
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
679
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
683
|
+
}
|
|
684
|
+
async handleSetDischargeLimit(msg) {
|
|
685
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
686
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitPercent=${msg.data.dischargeLimitPercent})`);
|
|
690
|
+
// Read current state for logging
|
|
691
|
+
const controls = await this.getBatteryControls();
|
|
692
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
693
|
+
// Set discharge limit (Register 12: outWRte)
|
|
694
|
+
const success = await this.writeBatteryControls({ outWRte: msg.data.dischargeLimitPercent });
|
|
695
|
+
if (!success) {
|
|
696
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
700
|
+
}
|
|
701
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
702
|
+
if (!this.dataBus || !this.applianceId) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const ackMessage = {
|
|
706
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
707
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
708
|
+
type: 'answer',
|
|
709
|
+
source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
|
|
710
|
+
applianceId: this.applianceId,
|
|
711
|
+
timestampIso: new Date().toISOString(),
|
|
712
|
+
data: {
|
|
713
|
+
messageId,
|
|
714
|
+
acknowledgeMessage,
|
|
715
|
+
answer,
|
|
716
|
+
rejectionReason
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
720
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
721
|
+
}
|
|
561
722
|
}
|
|
562
723
|
exports.SunspecBattery = SunspecBattery;
|
|
563
724
|
/**
|
|
@@ -14,9 +14,11 @@ export declare abstract class BaseSunspecDevice {
|
|
|
14
14
|
protected readonly sunspecClient: SunspecModbusClient;
|
|
15
15
|
protected readonly applianceManager: ApplianceManager;
|
|
16
16
|
protected readonly unitId: number;
|
|
17
|
+
protected readonly port: number;
|
|
18
|
+
protected readonly baseAddress: number;
|
|
17
19
|
protected applianceId?: string;
|
|
18
20
|
protected lastUpdateTime: number;
|
|
19
|
-
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number);
|
|
21
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number);
|
|
20
22
|
/**
|
|
21
23
|
* Connect to the device and create/update the appliance
|
|
22
24
|
*/
|
|
@@ -62,6 +64,8 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
62
64
|
* Sunspec Battery implementation
|
|
63
65
|
*/
|
|
64
66
|
export declare class SunspecBattery extends BaseSunspecDevice {
|
|
67
|
+
private dataBusListenerId?;
|
|
68
|
+
private dataBus?;
|
|
65
69
|
/**
|
|
66
70
|
* Connect to the battery and create/update the appliance
|
|
67
71
|
*/
|
|
@@ -150,6 +154,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
150
154
|
* @returns Promise<boolean | null> - true if enabled, false if disabled, null if error
|
|
151
155
|
*/
|
|
152
156
|
isGridChargingEnabled(): Promise<boolean | null>;
|
|
157
|
+
/**
|
|
158
|
+
* Start listening for storage commands on the data bus.
|
|
159
|
+
* Idempotent — does nothing if already listening.
|
|
160
|
+
*/
|
|
161
|
+
startDataBusListening(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Stop listening for storage commands on the data bus.
|
|
164
|
+
*/
|
|
165
|
+
stopDataBusListening(): void;
|
|
166
|
+
private handleStorageCommand;
|
|
167
|
+
private handleStartGridCharge;
|
|
168
|
+
private handleStopGridCharge;
|
|
169
|
+
private handleSetDischargeLimit;
|
|
170
|
+
private sendCommandAcknowledge;
|
|
153
171
|
}
|
|
154
172
|
/**
|
|
155
173
|
* Sunspec Meter implementation
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* SunSpec block interfaces with block numbers
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = void 0;
|
|
6
|
+
exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
|
|
7
|
+
exports.DEFAULT_RETRY_CONFIG = {
|
|
8
|
+
initialDelayMs: 1000,
|
|
9
|
+
maxDelayMs: 30000,
|
|
10
|
+
backoffFactor: 1.5,
|
|
11
|
+
maxAttempts: 10
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* Common Sunspec Model IDs
|
|
9
15
|
*/
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SunSpec block interfaces with block numbers
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for connection retry with exponential backoff
|
|
6
|
+
*/
|
|
7
|
+
export interface IRetryConfig {
|
|
8
|
+
initialDelayMs: number;
|
|
9
|
+
maxDelayMs: number;
|
|
10
|
+
backoffFactor: number;
|
|
11
|
+
maxAttempts: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_RETRY_CONFIG: IRetryConfig;
|
|
4
14
|
/**
|
|
5
15
|
* Base interface for all SunSpec blocks
|
|
6
16
|
*/
|