@enyo-energy/sunspec-sdk 0.0.32 → 0.0.33
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 +63 -80
- package/dist/cjs/connection-retry-manager.d.cts +32 -27
- package/dist/cjs/sunspec-devices.cjs +175 -32
- package/dist/cjs/sunspec-devices.d.cts +47 -6
- package/dist/cjs/sunspec-interfaces.cjs +28 -5
- package/dist/cjs/sunspec-interfaces.d.cts +18 -5
- package/dist/cjs/sunspec-modbus-client.cjs +61 -91
- package/dist/cjs/sunspec-modbus-client.d.cts +27 -28
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/connection-retry-manager.d.ts +32 -27
- package/dist/connection-retry-manager.js +63 -80
- package/dist/sunspec-devices.d.ts +47 -6
- package/dist/sunspec-devices.js +175 -32
- package/dist/sunspec-interfaces.d.ts +18 -5
- package/dist/sunspec-interfaces.js +27 -4
- package/dist/sunspec-modbus-client.d.ts +27 -28
- package/dist/sunspec-modbus-client.js +62 -92
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { SunspecBatteryChargeState, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
1
|
+
import { SunspecBatteryChargeState, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SunspecInverterCapability } from "./sunspec-interfaces.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
|
+
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
4
5
|
import { EnyoBatteryStateEnum, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum, EnyoStringStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
5
6
|
import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
|
|
6
7
|
import { EnyoMeterApplianceAvailableFeaturesEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js";
|
|
7
8
|
import { EnyoBatteryFeature, EnyoBatteryStorageMode } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
9
|
+
// TODO: Remove once added to @enyo-energy/energy-app-sdk EnyoDataBusMessageEnum
|
|
10
|
+
export const ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = 'SetInverterFeedInLimitV1';
|
|
8
11
|
/**
|
|
9
12
|
* Extract battery discharge power from MPPT data.
|
|
10
13
|
* Returns the discharge power in Watts (positive value), or 0 if no discharge.
|
|
@@ -32,7 +35,10 @@ export class BaseSunspecDevice {
|
|
|
32
35
|
baseAddress;
|
|
33
36
|
applianceId;
|
|
34
37
|
lastUpdateTime = 0;
|
|
35
|
-
|
|
38
|
+
dataBusListenerId;
|
|
39
|
+
dataBus;
|
|
40
|
+
retryManager;
|
|
41
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig) {
|
|
36
42
|
this.energyApp = energyApp;
|
|
37
43
|
this.name = name;
|
|
38
44
|
this.networkDevice = networkDevice;
|
|
@@ -41,6 +47,7 @@ export class BaseSunspecDevice {
|
|
|
41
47
|
this.unitId = unitId;
|
|
42
48
|
this.port = port;
|
|
43
49
|
this.baseAddress = baseAddress;
|
|
50
|
+
this.retryManager = new ConnectionRetryManager(retryConfig);
|
|
44
51
|
}
|
|
45
52
|
/**
|
|
46
53
|
* Check if the device is connected
|
|
@@ -62,11 +69,86 @@ export class BaseSunspecDevice {
|
|
|
62
69
|
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
63
70
|
}
|
|
64
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Attempt a reconnection if the tiered retry schedule allows it.
|
|
74
|
+
* Called from readData() when the device is disconnected.
|
|
75
|
+
* Returns true if reconnection succeeded.
|
|
76
|
+
*/
|
|
77
|
+
async tryReconnect() {
|
|
78
|
+
this.retryManager.markDisconnected();
|
|
79
|
+
if (!this.retryManager.shouldAttemptNow()) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
this.retryManager.recordAttempt();
|
|
83
|
+
const phase = this.retryManager.getCurrentPhase();
|
|
84
|
+
const attempt = this.retryManager.getAttemptCount();
|
|
85
|
+
const elapsed = Math.round(this.retryManager.getElapsedMs() / 1000);
|
|
86
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} ` +
|
|
87
|
+
`(phase: ${phase.intervalMs / 1000}s interval, elapsed: ${elapsed}s)`);
|
|
88
|
+
try {
|
|
89
|
+
const success = await this.sunspecClient.reconnect();
|
|
90
|
+
if (success) {
|
|
91
|
+
// Re-discover models after reconnect
|
|
92
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
93
|
+
this.retryManager.reset();
|
|
94
|
+
// Update appliance state to Connected
|
|
95
|
+
if (this.applianceId) {
|
|
96
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
|
|
97
|
+
}
|
|
98
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s)`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Mark the device as offline: update appliance state and start tracking disconnection.
|
|
109
|
+
*/
|
|
110
|
+
async markOffline() {
|
|
111
|
+
this.retryManager.markDisconnected();
|
|
112
|
+
if (this.applianceId) {
|
|
113
|
+
try {
|
|
114
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
122
|
+
if (!this.dataBus || !this.applianceId) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const ackMessage = {
|
|
126
|
+
id: randomUUID(),
|
|
127
|
+
message: EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
128
|
+
type: 'answer',
|
|
129
|
+
source: EnyoSourceEnum.Device,
|
|
130
|
+
applianceId: this.applianceId,
|
|
131
|
+
timestampIso: new Date().toISOString(),
|
|
132
|
+
data: {
|
|
133
|
+
messageId,
|
|
134
|
+
acknowledgeMessage: acknowledgeMessage,
|
|
135
|
+
answer,
|
|
136
|
+
rejectionReason
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
140
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
141
|
+
}
|
|
65
142
|
}
|
|
66
143
|
/**
|
|
67
144
|
* Sunspec Inverter implementation using dynamic model discovery
|
|
68
145
|
*/
|
|
69
146
|
export class SunspecInverter extends BaseSunspecDevice {
|
|
147
|
+
capabilities;
|
|
148
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
149
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
150
|
+
this.capabilities = capabilities;
|
|
151
|
+
}
|
|
70
152
|
async connect() {
|
|
71
153
|
// Ensure Sunspec client is connected
|
|
72
154
|
if (!this.sunspecClient.isConnected()) {
|
|
@@ -107,8 +189,10 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
107
189
|
if (mpptModel) {
|
|
108
190
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
109
191
|
}
|
|
192
|
+
this.startDataBusListening();
|
|
110
193
|
}
|
|
111
194
|
async disconnect() {
|
|
195
|
+
this.stopDataBusListening();
|
|
112
196
|
if (this.applianceId) {
|
|
113
197
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
114
198
|
}
|
|
@@ -119,7 +203,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
119
203
|
}
|
|
120
204
|
async readData(clockId, resolution) {
|
|
121
205
|
if (!this.isConnected()) {
|
|
122
|
-
|
|
206
|
+
await this.tryReconnect();
|
|
207
|
+
if (!this.isConnected())
|
|
208
|
+
return [];
|
|
123
209
|
}
|
|
124
210
|
const messages = [];
|
|
125
211
|
const timestamp = new Date();
|
|
@@ -169,6 +255,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
169
255
|
}
|
|
170
256
|
catch (error) {
|
|
171
257
|
console.error(`Error updating inverter data: ${error}`);
|
|
258
|
+
await this.markOffline();
|
|
172
259
|
}
|
|
173
260
|
return messages;
|
|
174
261
|
}
|
|
@@ -239,13 +326,72 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
239
326
|
mapDcStringToApplianceMetadata(mpptDataList) {
|
|
240
327
|
return mpptDataList.map(s => ({ index: s.index, name: s.name }));
|
|
241
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Start listening for inverter commands on the data bus.
|
|
331
|
+
* Idempotent — does nothing if already listening.
|
|
332
|
+
*/
|
|
333
|
+
startDataBusListening() {
|
|
334
|
+
if (this.dataBusListenerId) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
338
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1], (entry) => this.handleInverterCommand(entry));
|
|
339
|
+
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Stop listening for inverter commands on the data bus.
|
|
343
|
+
*/
|
|
344
|
+
stopDataBusListening() {
|
|
345
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
346
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
347
|
+
console.log(`Inverter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
348
|
+
}
|
|
349
|
+
this.dataBusListenerId = undefined;
|
|
350
|
+
this.dataBus = undefined;
|
|
351
|
+
}
|
|
352
|
+
handleInverterCommand(entry) {
|
|
353
|
+
if (entry.applianceId !== this.applianceId) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
void (async () => {
|
|
357
|
+
try {
|
|
358
|
+
if (entry.message === ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1) {
|
|
359
|
+
await this.handleSetFeedInLimit(entry);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
console.error(`Inverter ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
364
|
+
}
|
|
365
|
+
})();
|
|
366
|
+
}
|
|
367
|
+
async handleSetFeedInLimit(msg) {
|
|
368
|
+
// Check capability
|
|
369
|
+
if (!this.capabilities.includes(SunspecInverterCapability.FeedInLimit)) {
|
|
370
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'FeedInLimit capability not enabled');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
374
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(`Inverter ${this.applianceId}: handling SetInverterFeedInLimitV1 (feedInLimitW=${msg.data.feedInLimitW})`);
|
|
378
|
+
const success = await this.sunspecClient.setFeedInLimit(msg.data.feedInLimitW);
|
|
379
|
+
if (!success) {
|
|
380
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
384
|
+
}
|
|
242
385
|
}
|
|
243
386
|
/**
|
|
244
387
|
* Sunspec Battery implementation
|
|
245
388
|
*/
|
|
246
389
|
export class SunspecBattery extends BaseSunspecDevice {
|
|
247
|
-
|
|
248
|
-
|
|
390
|
+
capabilities;
|
|
391
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
392
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
393
|
+
this.capabilities = capabilities;
|
|
394
|
+
}
|
|
249
395
|
/**
|
|
250
396
|
* Connect to the battery and create/update the appliance
|
|
251
397
|
*/
|
|
@@ -332,7 +478,9 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
332
478
|
*/
|
|
333
479
|
async readData(clockId, resolution) {
|
|
334
480
|
if (!this.isConnected()) {
|
|
335
|
-
|
|
481
|
+
await this.tryReconnect();
|
|
482
|
+
if (!this.isConnected())
|
|
483
|
+
return [];
|
|
336
484
|
}
|
|
337
485
|
const messages = [];
|
|
338
486
|
const timestamp = new Date();
|
|
@@ -390,6 +538,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
390
538
|
}
|
|
391
539
|
catch (error) {
|
|
392
540
|
console.error(`Error updating battery data: ${error}`);
|
|
541
|
+
await this.markOffline();
|
|
393
542
|
}
|
|
394
543
|
return messages;
|
|
395
544
|
}
|
|
@@ -706,44 +855,35 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
706
855
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
707
856
|
return;
|
|
708
857
|
}
|
|
709
|
-
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (
|
|
710
|
-
// Read current state for
|
|
858
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitW=${msg.data.dischargeLimitW})`);
|
|
859
|
+
// Read current state to get wChaMax for percentage conversion
|
|
711
860
|
const controls = await this.getBatteryControls();
|
|
712
|
-
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
861
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}, wChaMax=${controls?.wChaMax}`);
|
|
862
|
+
if (!controls?.wChaMax) {
|
|
863
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
// Convert watts to percentage of WDisChaMax (using wChaMax), clamped to 0-100%
|
|
867
|
+
const dischargeLimitPercent = Math.min(100, Math.max(0, (msg.data.dischargeLimitW / controls.wChaMax) * 100));
|
|
868
|
+
console.log(`Battery ${this.applianceId}: calculated discharge limit: ${dischargeLimitPercent.toFixed(1)}% (${msg.data.dischargeLimitW}W / ${controls.wChaMax}W)`);
|
|
713
869
|
// Set discharge limit (Register 12: outWRte)
|
|
714
|
-
const success = await this.writeBatteryControls({ outWRte:
|
|
870
|
+
const success = await this.writeBatteryControls({ outWRte: dischargeLimitPercent });
|
|
715
871
|
if (!success) {
|
|
716
872
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
717
873
|
return;
|
|
718
874
|
}
|
|
719
875
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
720
876
|
}
|
|
721
|
-
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
722
|
-
if (!this.dataBus || !this.applianceId) {
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
const ackMessage = {
|
|
726
|
-
id: randomUUID(),
|
|
727
|
-
message: EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
728
|
-
type: 'answer',
|
|
729
|
-
source: EnyoSourceEnum.Device,
|
|
730
|
-
applianceId: this.applianceId,
|
|
731
|
-
timestampIso: new Date().toISOString(),
|
|
732
|
-
data: {
|
|
733
|
-
messageId,
|
|
734
|
-
acknowledgeMessage,
|
|
735
|
-
answer,
|
|
736
|
-
rejectionReason
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
740
|
-
this.dataBus.sendMessage([ackMessage]);
|
|
741
|
-
}
|
|
742
877
|
}
|
|
743
878
|
/**
|
|
744
879
|
* Sunspec Meter implementation
|
|
745
880
|
*/
|
|
746
881
|
export class SunspecMeter extends BaseSunspecDevice {
|
|
882
|
+
capabilities;
|
|
883
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
884
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
885
|
+
this.capabilities = capabilities;
|
|
886
|
+
}
|
|
747
887
|
/**
|
|
748
888
|
* Connect to the meter and create/update the appliance
|
|
749
889
|
*/
|
|
@@ -800,7 +940,9 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
800
940
|
*/
|
|
801
941
|
async readData(clockId, resolution) {
|
|
802
942
|
if (!this.isConnected()) {
|
|
803
|
-
|
|
943
|
+
await this.tryReconnect();
|
|
944
|
+
if (!this.isConnected())
|
|
945
|
+
return [];
|
|
804
946
|
}
|
|
805
947
|
const messages = [];
|
|
806
948
|
const timestamp = new Date();
|
|
@@ -831,6 +973,7 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
831
973
|
}
|
|
832
974
|
catch (error) {
|
|
833
975
|
console.error(`Error updating meter data: ${error}`);
|
|
976
|
+
await this.markOffline();
|
|
834
977
|
}
|
|
835
978
|
return messages;
|
|
836
979
|
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* SunSpec block interfaces with block numbers
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* A single phase in the tiered retry schedule
|
|
6
|
+
*/
|
|
7
|
+
export interface IRetryPhase {
|
|
8
|
+
intervalMs: number;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for connection retry with tiered schedule
|
|
6
13
|
*/
|
|
7
14
|
export interface IRetryConfig {
|
|
8
|
-
|
|
9
|
-
maxDelayMs: number;
|
|
10
|
-
backoffFactor: number;
|
|
11
|
-
maxAttempts: number;
|
|
15
|
+
phases: IRetryPhase[];
|
|
12
16
|
}
|
|
13
17
|
export declare const DEFAULT_RETRY_CONFIG: IRetryConfig;
|
|
14
18
|
/**
|
|
@@ -551,6 +555,15 @@ export declare enum SunspecStorageMode {
|
|
|
551
555
|
* 2. Set chaGriSet = 1 to allow grid charging
|
|
552
556
|
* 3. Set wChaMax to the desired charging power in Watts
|
|
553
557
|
*/
|
|
558
|
+
export declare enum SunspecInverterCapability {
|
|
559
|
+
FeedInLimit = "feed-in-limit"
|
|
560
|
+
}
|
|
561
|
+
export declare enum SunspecBatteryCapability {
|
|
562
|
+
GridCharging = "grid-charging",
|
|
563
|
+
DischargeLimit = "discharge-limit"
|
|
564
|
+
}
|
|
565
|
+
export declare enum SunspecMeterCapability {
|
|
566
|
+
}
|
|
554
567
|
export interface SunspecBatteryControls {
|
|
555
568
|
storCtlMod?: number;
|
|
556
569
|
chaGriSet?: number;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* SunSpec block interfaces with block numbers
|
|
3
3
|
*/
|
|
4
4
|
export const DEFAULT_RETRY_CONFIG = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
phases: [
|
|
6
|
+
{ intervalMs: 10_000, durationMs: 60_000 }, // Phase 1: every 10s for 1 minute
|
|
7
|
+
{ intervalMs: 30_000, durationMs: 120_000 }, // Phase 2: every 30s for 2 minutes
|
|
8
|
+
{ intervalMs: 60_000, durationMs: 300_000 }, // Phase 3: every 1m for 5 minutes
|
|
9
|
+
{ intervalMs: 300_000, durationMs: 0 }, // Phase 4: every 5m forever
|
|
10
|
+
]
|
|
9
11
|
};
|
|
10
12
|
/**
|
|
11
13
|
* Common Sunspec Model IDs
|
|
@@ -197,3 +199,24 @@ export var SunspecStorageMode;
|
|
|
197
199
|
SunspecStorageMode["HOLDING"] = "holding";
|
|
198
200
|
SunspecStorageMode["AUTO"] = "auto"; // Both charge and discharge allowed
|
|
199
201
|
})(SunspecStorageMode || (SunspecStorageMode = {}));
|
|
202
|
+
/**
|
|
203
|
+
* Battery control structure for writing to Model 124
|
|
204
|
+
* Used for controlling battery charge/discharge behavior
|
|
205
|
+
*
|
|
206
|
+
* IMPORTANT: To enable grid charging with specific power:
|
|
207
|
+
* 1. Set storCtlMod with appropriate bits to enable external control
|
|
208
|
+
* 2. Set chaGriSet = 1 to allow grid charging
|
|
209
|
+
* 3. Set wChaMax to the desired charging power in Watts
|
|
210
|
+
*/
|
|
211
|
+
export var SunspecInverterCapability;
|
|
212
|
+
(function (SunspecInverterCapability) {
|
|
213
|
+
SunspecInverterCapability["FeedInLimit"] = "feed-in-limit";
|
|
214
|
+
})(SunspecInverterCapability || (SunspecInverterCapability = {}));
|
|
215
|
+
export var SunspecBatteryCapability;
|
|
216
|
+
(function (SunspecBatteryCapability) {
|
|
217
|
+
SunspecBatteryCapability["GridCharging"] = "grid-charging";
|
|
218
|
+
SunspecBatteryCapability["DischargeLimit"] = "discharge-limit";
|
|
219
|
+
})(SunspecBatteryCapability || (SunspecBatteryCapability = {}));
|
|
220
|
+
export var SunspecMeterCapability;
|
|
221
|
+
(function (SunspecMeterCapability) {
|
|
222
|
+
})(SunspecMeterCapability || (SunspecMeterCapability = {}));
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
* - pad: 0x8000 (always returns this value)
|
|
17
17
|
* - string: all registers 0x0000 (NULL)
|
|
18
18
|
*/
|
|
19
|
-
import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode
|
|
20
|
-
import {
|
|
21
|
-
import { IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
|
|
19
|
+
import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
20
|
+
import { EnergyAppModbusDataType, IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
|
|
22
21
|
import { EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
23
22
|
export declare class SunspecModbusClient {
|
|
24
23
|
private energyApp;
|
|
@@ -29,9 +28,8 @@ export declare class SunspecModbusClient {
|
|
|
29
28
|
private faultTolerantReader;
|
|
30
29
|
private modbusDataTypeConverter;
|
|
31
30
|
private connectionParams;
|
|
32
|
-
private retryManager;
|
|
33
31
|
private autoReconnectEnabled;
|
|
34
|
-
constructor(energyApp: EnergyApp
|
|
32
|
+
constructor(energyApp: EnergyApp);
|
|
35
33
|
/**
|
|
36
34
|
* Connect to Modbus device
|
|
37
35
|
* @param host Primary host (hostname) to connect to
|
|
@@ -55,11 +53,6 @@ export declare class SunspecModbusClient {
|
|
|
55
53
|
* Returns true if successful, false otherwise
|
|
56
54
|
*/
|
|
57
55
|
private attemptConnection;
|
|
58
|
-
/**
|
|
59
|
-
* Check connection health and trigger automatic reconnection if unhealthy
|
|
60
|
-
* Returns true if connection is healthy or was successfully restored
|
|
61
|
-
*/
|
|
62
|
-
ensureHealthyConnection(): Promise<boolean>;
|
|
63
56
|
/**
|
|
64
57
|
* Enable or disable automatic reconnection
|
|
65
58
|
*/
|
|
@@ -68,10 +61,6 @@ export declare class SunspecModbusClient {
|
|
|
68
61
|
* Check if auto-reconnect is enabled
|
|
69
62
|
*/
|
|
70
63
|
isAutoReconnectEnabled(): boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Get the retry manager for advanced configuration
|
|
73
|
-
*/
|
|
74
|
-
getRetryManager(): ConnectionRetryManager;
|
|
75
64
|
/**
|
|
76
65
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
77
66
|
*/
|
|
@@ -119,13 +108,13 @@ export declare class SunspecModbusClient {
|
|
|
119
108
|
/**
|
|
120
109
|
* Helper to read register value(s) using the fault-tolerant reader with data type conversion
|
|
121
110
|
*/
|
|
122
|
-
|
|
111
|
+
readRegisterValue(address: number, quantity: number | undefined, dataType: EnergyAppModbusDataType): Promise<number | string | number[]>;
|
|
123
112
|
/**
|
|
124
113
|
* Helper to write register value(s)
|
|
125
114
|
*/
|
|
126
|
-
|
|
115
|
+
writeRegisterValue(address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
|
|
127
116
|
/**
|
|
128
|
-
* Read inverter data from Model 103 (Three Phase)
|
|
117
|
+
* Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
|
|
129
118
|
*/
|
|
130
119
|
readInverterData(): Promise<SunspecInverterData | null>;
|
|
131
120
|
/**
|
|
@@ -140,11 +129,10 @@ export declare class SunspecModbusClient {
|
|
|
140
129
|
* Apply scale factor to a value
|
|
141
130
|
* Returns undefined if the value is unimplemented or scale factor is out of range
|
|
142
131
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
applyScaleFactor(value: number, scaleFactor: number, dataType?: 'uint16' | 'int16' | 'acc32', fieldName?: string, offset?: number, modelId?: number): number | undefined;
|
|
133
|
+
logRegisterRead(modelId: number, offset: number, fieldName: string, rawValue: number | string | undefined, dataType?: string): void;
|
|
145
134
|
/**
|
|
146
|
-
* Read
|
|
147
|
-
* Returns the scale factors for DC Current, DC Voltage, DC Power, and DC Energy
|
|
135
|
+
* Read scale factors from Model 160 (MPPT)
|
|
148
136
|
*
|
|
149
137
|
* MPPT Model 160 Scale Factor Register Offsets (relative to module start):
|
|
150
138
|
* - DCA_SF (Current Scale Factor): Offset 2
|
|
@@ -165,7 +153,7 @@ export declare class SunspecModbusClient {
|
|
|
165
153
|
*/
|
|
166
154
|
readMPPTData(moduleId?: number): Promise<SunspecMPPTData | null>;
|
|
167
155
|
/**
|
|
168
|
-
* Read all
|
|
156
|
+
* Read all MPPT strings from Model 160 (Multiple MPPT)
|
|
169
157
|
*/
|
|
170
158
|
readAllMPPTData(): Promise<SunspecMPPTData[]>;
|
|
171
159
|
/**
|
|
@@ -191,7 +179,7 @@ export declare class SunspecModbusClient {
|
|
|
191
179
|
*/
|
|
192
180
|
readBatteryBaseData(): Promise<SunspecBatteryBaseData | null>;
|
|
193
181
|
/**
|
|
194
|
-
* Read battery data from Model 124 (Basic Storage
|
|
182
|
+
* Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
|
|
195
183
|
*/
|
|
196
184
|
readBatteryData(): Promise<SunspecBatteryData | null>;
|
|
197
185
|
/**
|
|
@@ -207,11 +195,11 @@ export declare class SunspecModbusClient {
|
|
|
207
195
|
*/
|
|
208
196
|
enableGridCharging(enable: boolean): Promise<boolean>;
|
|
209
197
|
/**
|
|
210
|
-
* Read
|
|
198
|
+
* Read battery control settings from Model 124 (Basic Storage Controls)
|
|
211
199
|
*/
|
|
212
200
|
readBatteryControls(): Promise<SunspecBatteryControls | null>;
|
|
213
201
|
/**
|
|
214
|
-
* Read meter data (Model 203
|
|
202
|
+
* Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
|
|
215
203
|
*/
|
|
216
204
|
readMeterData(): Promise<SunspecMeterData | null>;
|
|
217
205
|
/**
|
|
@@ -235,11 +223,11 @@ export declare class SunspecModbusClient {
|
|
|
235
223
|
*/
|
|
236
224
|
getConnectionHealth(): IConnectionHealth;
|
|
237
225
|
/**
|
|
238
|
-
* Read
|
|
226
|
+
* Read inverter settings from Model 121 (Inverter Settings)
|
|
239
227
|
*/
|
|
240
228
|
readInverterSettings(): Promise<SunspecInverterSettings | null>;
|
|
241
229
|
/**
|
|
242
|
-
* Read
|
|
230
|
+
* Read inverter controls from Model 123 (Immediate Inverter Controls)
|
|
243
231
|
*/
|
|
244
232
|
readInverterControls(): Promise<SunspecInverterControls | null>;
|
|
245
233
|
/**
|
|
@@ -247,7 +235,18 @@ export declare class SunspecModbusClient {
|
|
|
247
235
|
*/
|
|
248
236
|
writeInverterSettings(settings: Partial<SunspecInverterSettings>): Promise<boolean>;
|
|
249
237
|
/**
|
|
250
|
-
* Write
|
|
238
|
+
* Write inverter controls to Model 123 (Immediate Inverter Controls)
|
|
251
239
|
*/
|
|
252
240
|
writeInverterControls(controls: Partial<SunspecInverterControls>): Promise<boolean>;
|
|
241
|
+
/**
|
|
242
|
+
* Set the inverter feed-in power limit using Model 123 (Immediate Inverter Controls)
|
|
243
|
+
*
|
|
244
|
+
* When limitW is a number, reads WMax from Model 121, computes percentage,
|
|
245
|
+
* writes WMaxLimPct and enables WMaxLim_Ena.
|
|
246
|
+
* When limitW is null, disables the limit by setting WMaxLim_Ena = DISABLED.
|
|
247
|
+
*
|
|
248
|
+
* @param limitW - Power limit in Watts, or null to remove the limit
|
|
249
|
+
* @returns true if successful, false otherwise
|
|
250
|
+
*/
|
|
251
|
+
setFeedInLimit(limitW: number | null): Promise<boolean>;
|
|
253
252
|
}
|