@enyo-energy/sunspec-sdk 0.0.32 → 0.0.34
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 +178 -51
- 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 +178 -51
- 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,23 +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";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Returns the discharge power in Watts (positive value), or 0 if no discharge.
|
|
11
|
-
*/
|
|
12
|
-
function extractBatteryDischargePowerFromMPPT(mpptDataList) {
|
|
13
|
-
let dischargePowerW = 0;
|
|
14
|
-
for (const mppt of mpptDataList) {
|
|
15
|
-
if (mppt.stringId === 'StDisCha 4' && mppt.dcPower !== undefined && mppt.dcPower > 0) {
|
|
16
|
-
dischargePowerW += mppt.dcPower;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return dischargePowerW;
|
|
20
|
-
}
|
|
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';
|
|
21
11
|
/**
|
|
22
12
|
* Base abstract class for all Sunspec devices
|
|
23
13
|
*/
|
|
@@ -32,7 +22,10 @@ export class BaseSunspecDevice {
|
|
|
32
22
|
baseAddress;
|
|
33
23
|
applianceId;
|
|
34
24
|
lastUpdateTime = 0;
|
|
35
|
-
|
|
25
|
+
dataBusListenerId;
|
|
26
|
+
dataBus;
|
|
27
|
+
retryManager;
|
|
28
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig) {
|
|
36
29
|
this.energyApp = energyApp;
|
|
37
30
|
this.name = name;
|
|
38
31
|
this.networkDevice = networkDevice;
|
|
@@ -41,6 +34,7 @@ export class BaseSunspecDevice {
|
|
|
41
34
|
this.unitId = unitId;
|
|
42
35
|
this.port = port;
|
|
43
36
|
this.baseAddress = baseAddress;
|
|
37
|
+
this.retryManager = new ConnectionRetryManager(retryConfig);
|
|
44
38
|
}
|
|
45
39
|
/**
|
|
46
40
|
* Check if the device is connected
|
|
@@ -62,11 +56,86 @@ export class BaseSunspecDevice {
|
|
|
62
56
|
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
63
57
|
}
|
|
64
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Attempt a reconnection if the tiered retry schedule allows it.
|
|
61
|
+
* Called from readData() when the device is disconnected.
|
|
62
|
+
* Returns true if reconnection succeeded.
|
|
63
|
+
*/
|
|
64
|
+
async tryReconnect() {
|
|
65
|
+
this.retryManager.markDisconnected();
|
|
66
|
+
if (!this.retryManager.shouldAttemptNow()) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
this.retryManager.recordAttempt();
|
|
70
|
+
const phase = this.retryManager.getCurrentPhase();
|
|
71
|
+
const attempt = this.retryManager.getAttemptCount();
|
|
72
|
+
const elapsed = Math.round(this.retryManager.getElapsedMs() / 1000);
|
|
73
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} ` +
|
|
74
|
+
`(phase: ${phase.intervalMs / 1000}s interval, elapsed: ${elapsed}s)`);
|
|
75
|
+
try {
|
|
76
|
+
const success = await this.sunspecClient.reconnect();
|
|
77
|
+
if (success) {
|
|
78
|
+
// Re-discover models after reconnect
|
|
79
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
80
|
+
this.retryManager.reset();
|
|
81
|
+
// Update appliance state to Connected
|
|
82
|
+
if (this.applianceId) {
|
|
83
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
|
|
84
|
+
}
|
|
85
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s)`);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Mark the device as offline: update appliance state and start tracking disconnection.
|
|
96
|
+
*/
|
|
97
|
+
async markOffline() {
|
|
98
|
+
this.retryManager.markDisconnected();
|
|
99
|
+
if (this.applianceId) {
|
|
100
|
+
try {
|
|
101
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
109
|
+
if (!this.dataBus || !this.applianceId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const ackMessage = {
|
|
113
|
+
id: randomUUID(),
|
|
114
|
+
message: EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
115
|
+
type: 'answer',
|
|
116
|
+
source: EnyoSourceEnum.Device,
|
|
117
|
+
applianceId: this.applianceId,
|
|
118
|
+
timestampIso: new Date().toISOString(),
|
|
119
|
+
data: {
|
|
120
|
+
messageId,
|
|
121
|
+
acknowledgeMessage: acknowledgeMessage,
|
|
122
|
+
answer,
|
|
123
|
+
rejectionReason
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
127
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
128
|
+
}
|
|
65
129
|
}
|
|
66
130
|
/**
|
|
67
131
|
* Sunspec Inverter implementation using dynamic model discovery
|
|
68
132
|
*/
|
|
69
133
|
export class SunspecInverter extends BaseSunspecDevice {
|
|
134
|
+
capabilities;
|
|
135
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
136
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
137
|
+
this.capabilities = capabilities;
|
|
138
|
+
}
|
|
70
139
|
async connect() {
|
|
71
140
|
// Ensure Sunspec client is connected
|
|
72
141
|
if (!this.sunspecClient.isConnected()) {
|
|
@@ -107,8 +176,10 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
107
176
|
if (mpptModel) {
|
|
108
177
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
109
178
|
}
|
|
179
|
+
this.startDataBusListening();
|
|
110
180
|
}
|
|
111
181
|
async disconnect() {
|
|
182
|
+
this.stopDataBusListening();
|
|
112
183
|
if (this.applianceId) {
|
|
113
184
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
114
185
|
}
|
|
@@ -119,7 +190,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
119
190
|
}
|
|
120
191
|
async readData(clockId, resolution) {
|
|
121
192
|
if (!this.isConnected()) {
|
|
122
|
-
|
|
193
|
+
await this.tryReconnect();
|
|
194
|
+
if (!this.isConnected())
|
|
195
|
+
return [];
|
|
123
196
|
}
|
|
124
197
|
const messages = [];
|
|
125
198
|
const timestamp = new Date();
|
|
@@ -129,12 +202,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
129
202
|
const mpptDataList = await this.sunspecClient.readAllMPPTData();
|
|
130
203
|
const inverterSettings = await this.sunspecClient.readInverterSettings();
|
|
131
204
|
const dcStrings = this.mapMPPTToStrings(mpptDataList);
|
|
132
|
-
// Calculate battery discharge power to subtract from AC power
|
|
133
|
-
const batteryDischargePowerW = extractBatteryDischargePowerFromMPPT(mpptDataList);
|
|
134
205
|
if (inverterData) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
console.log(`Got Battery Discharge power ${batteryDischargePowerW} and Inverter Power W ${totalAcPowerW} with pure PV Power ${purePvPowerW}`);
|
|
206
|
+
const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
|
|
207
|
+
console.log(`Got PV Power from DC strings: ${pvPowerW}W`);
|
|
138
208
|
const inverterMessage = {
|
|
139
209
|
id: randomUUID(),
|
|
140
210
|
message: EnyoDataBusMessageEnum.InverterValuesUpdateV1,
|
|
@@ -145,7 +215,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
145
215
|
timestampIso: timestamp.toISOString(),
|
|
146
216
|
resolution,
|
|
147
217
|
data: {
|
|
148
|
-
pvPowerW
|
|
218
|
+
pvPowerW,
|
|
149
219
|
activePowerLimitationW: inverterSettings?.WMax,
|
|
150
220
|
state: this.mapOperatingState(inverterData.operatingState),
|
|
151
221
|
voltageL1: inverterData.voltageAN || 0,
|
|
@@ -169,6 +239,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
169
239
|
}
|
|
170
240
|
catch (error) {
|
|
171
241
|
console.error(`Error updating inverter data: ${error}`);
|
|
242
|
+
await this.markOffline();
|
|
172
243
|
}
|
|
173
244
|
return messages;
|
|
174
245
|
}
|
|
@@ -239,13 +310,72 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
239
310
|
mapDcStringToApplianceMetadata(mpptDataList) {
|
|
240
311
|
return mpptDataList.map(s => ({ index: s.index, name: s.name }));
|
|
241
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Start listening for inverter commands on the data bus.
|
|
315
|
+
* Idempotent — does nothing if already listening.
|
|
316
|
+
*/
|
|
317
|
+
startDataBusListening() {
|
|
318
|
+
if (this.dataBusListenerId) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
322
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1], (entry) => this.handleInverterCommand(entry));
|
|
323
|
+
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Stop listening for inverter commands on the data bus.
|
|
327
|
+
*/
|
|
328
|
+
stopDataBusListening() {
|
|
329
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
330
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
331
|
+
console.log(`Inverter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
332
|
+
}
|
|
333
|
+
this.dataBusListenerId = undefined;
|
|
334
|
+
this.dataBus = undefined;
|
|
335
|
+
}
|
|
336
|
+
handleInverterCommand(entry) {
|
|
337
|
+
if (entry.applianceId !== this.applianceId) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
void (async () => {
|
|
341
|
+
try {
|
|
342
|
+
if (entry.message === ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1) {
|
|
343
|
+
await this.handleSetFeedInLimit(entry);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error(`Inverter ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
348
|
+
}
|
|
349
|
+
})();
|
|
350
|
+
}
|
|
351
|
+
async handleSetFeedInLimit(msg) {
|
|
352
|
+
// Check capability
|
|
353
|
+
if (!this.capabilities.includes(SunspecInverterCapability.FeedInLimit)) {
|
|
354
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'FeedInLimit capability not enabled');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
358
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
console.log(`Inverter ${this.applianceId}: handling SetInverterFeedInLimitV1 (feedInLimitW=${msg.data.feedInLimitW})`);
|
|
362
|
+
const success = await this.sunspecClient.setFeedInLimit(msg.data.feedInLimitW);
|
|
363
|
+
if (!success) {
|
|
364
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
368
|
+
}
|
|
242
369
|
}
|
|
243
370
|
/**
|
|
244
371
|
* Sunspec Battery implementation
|
|
245
372
|
*/
|
|
246
373
|
export class SunspecBattery extends BaseSunspecDevice {
|
|
247
|
-
|
|
248
|
-
|
|
374
|
+
capabilities;
|
|
375
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
376
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
377
|
+
this.capabilities = capabilities;
|
|
378
|
+
}
|
|
249
379
|
/**
|
|
250
380
|
* Connect to the battery and create/update the appliance
|
|
251
381
|
*/
|
|
@@ -332,7 +462,9 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
332
462
|
*/
|
|
333
463
|
async readData(clockId, resolution) {
|
|
334
464
|
if (!this.isConnected()) {
|
|
335
|
-
|
|
465
|
+
await this.tryReconnect();
|
|
466
|
+
if (!this.isConnected())
|
|
467
|
+
return [];
|
|
336
468
|
}
|
|
337
469
|
const messages = [];
|
|
338
470
|
const timestamp = new Date();
|
|
@@ -390,6 +522,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
390
522
|
}
|
|
391
523
|
catch (error) {
|
|
392
524
|
console.error(`Error updating battery data: ${error}`);
|
|
525
|
+
await this.markOffline();
|
|
393
526
|
}
|
|
394
527
|
return messages;
|
|
395
528
|
}
|
|
@@ -706,44 +839,35 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
706
839
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
707
840
|
return;
|
|
708
841
|
}
|
|
709
|
-
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (
|
|
710
|
-
// Read current state for
|
|
842
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitW=${msg.data.dischargeLimitW})`);
|
|
843
|
+
// Read current state to get wChaMax for percentage conversion
|
|
711
844
|
const controls = await this.getBatteryControls();
|
|
712
|
-
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
845
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}, wChaMax=${controls?.wChaMax}`);
|
|
846
|
+
if (!controls?.wChaMax) {
|
|
847
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// Convert watts to percentage of WDisChaMax (using wChaMax), clamped to 0-100%
|
|
851
|
+
const dischargeLimitPercent = Math.min(100, Math.max(0, (msg.data.dischargeLimitW / controls.wChaMax) * 100));
|
|
852
|
+
console.log(`Battery ${this.applianceId}: calculated discharge limit: ${dischargeLimitPercent.toFixed(1)}% (${msg.data.dischargeLimitW}W / ${controls.wChaMax}W)`);
|
|
713
853
|
// Set discharge limit (Register 12: outWRte)
|
|
714
|
-
const success = await this.writeBatteryControls({ outWRte:
|
|
854
|
+
const success = await this.writeBatteryControls({ outWRte: dischargeLimitPercent });
|
|
715
855
|
if (!success) {
|
|
716
856
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
717
857
|
return;
|
|
718
858
|
}
|
|
719
859
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
720
860
|
}
|
|
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
861
|
}
|
|
743
862
|
/**
|
|
744
863
|
* Sunspec Meter implementation
|
|
745
864
|
*/
|
|
746
865
|
export class SunspecMeter extends BaseSunspecDevice {
|
|
866
|
+
capabilities;
|
|
867
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
868
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
869
|
+
this.capabilities = capabilities;
|
|
870
|
+
}
|
|
747
871
|
/**
|
|
748
872
|
* Connect to the meter and create/update the appliance
|
|
749
873
|
*/
|
|
@@ -800,7 +924,9 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
800
924
|
*/
|
|
801
925
|
async readData(clockId, resolution) {
|
|
802
926
|
if (!this.isConnected()) {
|
|
803
|
-
|
|
927
|
+
await this.tryReconnect();
|
|
928
|
+
if (!this.isConnected())
|
|
929
|
+
return [];
|
|
804
930
|
}
|
|
805
931
|
const messages = [];
|
|
806
932
|
const timestamp = new Date();
|
|
@@ -831,6 +957,7 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
831
957
|
}
|
|
832
958
|
catch (error) {
|
|
833
959
|
console.error(`Error updating meter data: ${error}`);
|
|
960
|
+
await this.markOffline();
|
|
834
961
|
}
|
|
835
962
|
return messages;
|
|
836
963
|
}
|
|
@@ -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
|
}
|