@enyo-energy/sunspec-sdk 0.0.84 → 0.0.86
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/sunspec-battery-schedule-handler.cjs +11 -3
- package/dist/cjs/sunspec-devices.cjs +22 -2
- package/dist/cjs/sunspec-devices.d.cts +9 -0
- package/dist/cjs/sunspec-interfaces.d.cts +1 -0
- package/dist/cjs/sunspec-modbus-client.cjs +29 -2
- package/dist/cjs/sunspec-modbus-client.d.cts +13 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-battery-schedule-handler.js +11 -3
- package/dist/sunspec-devices.d.ts +9 -0
- package/dist/sunspec-devices.js +22 -2
- package/dist/sunspec-interfaces.d.ts +1 -0
- package/dist/sunspec-modbus-client.d.ts +13 -0
- package/dist/sunspec-modbus-client.js +29 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -73,22 +73,30 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
73
73
|
// baseline so the persisted snapshot continues to point at the
|
|
74
74
|
// original pre-schedule state. The library will overwrite its
|
|
75
75
|
// storage row with the same value — harmless.
|
|
76
|
-
|
|
76
|
+
const wChaMax = this.originalBaseline.wChaMax;
|
|
77
|
+
if (!wChaMax || wChaMax <= 0) {
|
|
78
|
+
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: cannot start schedule — originalBaseline has no usable wChaMax (got ${wChaMax})`);
|
|
79
|
+
}
|
|
80
|
+
this.installedWChaMax = wChaMax;
|
|
77
81
|
return { ...this.originalBaseline };
|
|
78
82
|
}
|
|
79
83
|
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
80
84
|
if (!controls) {
|
|
81
85
|
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
|
|
82
86
|
}
|
|
87
|
+
const wChaMax = controls.wChaMax;
|
|
88
|
+
if (!wChaMax || wChaMax <= 0) {
|
|
89
|
+
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: cannot start schedule — device did not report a usable wChaMax (got ${wChaMax})`);
|
|
90
|
+
}
|
|
83
91
|
const baseline = {
|
|
84
92
|
storCtlMod: controls.storCtlMod,
|
|
85
93
|
chaGriSet: controls.chaGriSet,
|
|
86
|
-
wChaMax
|
|
94
|
+
wChaMax,
|
|
87
95
|
inWRte: controls.inWRte,
|
|
88
96
|
outWRte: controls.outWRte,
|
|
89
97
|
};
|
|
90
98
|
this.originalBaseline = baseline;
|
|
91
|
-
this.installedWChaMax =
|
|
99
|
+
this.installedWChaMax = wChaMax;
|
|
92
100
|
return { ...baseline };
|
|
93
101
|
}
|
|
94
102
|
onChange(active, _previous) {
|
|
@@ -343,7 +343,16 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
343
343
|
capabilities;
|
|
344
344
|
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
345
345
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
346
|
+
/**
|
|
347
|
+
* Require this many consecutive operatingState=7 (FAULT) reads before
|
|
348
|
+
* reporting Faulted. Debounces transient single-poll FAULT reads (e.g. a
|
|
349
|
+
* DC-coupled battery disturbing a string) that would otherwise oscillate
|
|
350
|
+
* the appliance status. Recovery is immediate (counter resets on the first
|
|
351
|
+
* non-fault read).
|
|
352
|
+
*/
|
|
353
|
+
static OPERATING_FAULT_THRESHOLD = 3;
|
|
346
354
|
storage;
|
|
355
|
+
consecutiveOperatingFaults = 0;
|
|
347
356
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
348
357
|
snapshotService;
|
|
349
358
|
// Whether we've emitted a status update at least once since (re)connecting.
|
|
@@ -656,7 +665,15 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
656
665
|
// fault; MPPT (4), SLEEPING (2) and the other states are normal. This
|
|
657
666
|
// is re-evaluated every poll so a producing inverter (operatingState=4,
|
|
658
667
|
// no error bits) reports Healthy even if it was previously faulted.
|
|
659
|
-
|
|
668
|
+
//
|
|
669
|
+
// A single FAULT read is debounced: we only treat it as a fault after
|
|
670
|
+
// OPERATING_FAULT_THRESHOLD consecutive FAULT polls, so a transient
|
|
671
|
+
// wobble (e.g. a faulted DC-coupled battery on a string) does not
|
|
672
|
+
// oscillate the status. Recovery is immediate — the counter resets on
|
|
673
|
+
// the first non-fault read. Event-register error bits stay immediate.
|
|
674
|
+
const operatingFaultRaw = data.operatingState === sunspec_interfaces_js_1.SunspecInverterOperatingState.FAULT;
|
|
675
|
+
this.consecutiveOperatingFaults = operatingFaultRaw ? this.consecutiveOperatingFaults + 1 : 0;
|
|
676
|
+
const operatingFault = this.consecutiveOperatingFaults >= SunspecInverter.OPERATING_FAULT_THRESHOLD;
|
|
660
677
|
const newStatus = (codeIds.length > 0 || operatingFault)
|
|
661
678
|
? enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted
|
|
662
679
|
: enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy;
|
|
@@ -772,7 +789,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
772
789
|
// Only include strings with valid data
|
|
773
790
|
if (mppt.dcVoltage !== undefined || mppt.dcPower !== undefined) {
|
|
774
791
|
result.push({
|
|
775
|
-
|
|
792
|
+
// Use the stable physical module position so a string keeps
|
|
793
|
+
// its identity when an earlier module drops out of a poll.
|
|
794
|
+
// Fall back to array position if the field is absent.
|
|
795
|
+
index: mppt.moduleNumber ?? (index + 1),
|
|
776
796
|
name: mppt.stringId,
|
|
777
797
|
state: this.mapMPPTOperatingState(mppt.operatingState),
|
|
778
798
|
current: mppt.dcCurrent,
|
|
@@ -129,7 +129,16 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
129
129
|
private readonly capabilities;
|
|
130
130
|
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
131
131
|
private static readonly CONNECTION_FAULT_THRESHOLD;
|
|
132
|
+
/**
|
|
133
|
+
* Require this many consecutive operatingState=7 (FAULT) reads before
|
|
134
|
+
* reporting Faulted. Debounces transient single-poll FAULT reads (e.g. a
|
|
135
|
+
* DC-coupled battery disturbing a string) that would otherwise oscillate
|
|
136
|
+
* the appliance status. Recovery is immediate (counter resets on the first
|
|
137
|
+
* non-fault read).
|
|
138
|
+
*/
|
|
139
|
+
private static readonly OPERATING_FAULT_THRESHOLD;
|
|
132
140
|
private storage?;
|
|
141
|
+
private consecutiveOperatingFaults;
|
|
133
142
|
private errorState;
|
|
134
143
|
private snapshotService?;
|
|
135
144
|
private statusReassertedThisSession;
|
|
@@ -365,6 +365,20 @@ class SunspecModbusClient {
|
|
|
365
365
|
throw new Error(`Modbus instance for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
366
366
|
return inst;
|
|
367
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Expose this client's open Modbus connection for a unit so a co-located driver can
|
|
370
|
+
* share the SAME TCP socket instead of opening a second one. This matters for devices
|
|
371
|
+
* behind single-connection bridges (e.g. the Solax ESP32 Pocket WiFi dongle, which
|
|
372
|
+
* resets the link when a second Modbus-TCP client connects): the driver reads its own
|
|
373
|
+
* function codes (e.g. FC04 input registers) over this socket.
|
|
374
|
+
*
|
|
375
|
+
* Throws if the unit isn't open. The connection's lifecycle stays owned by this client —
|
|
376
|
+
* borrowers MUST NOT call disconnect() on the returned instance; release it via
|
|
377
|
+
* disconnectUnit(unitId) / releaseSunspecClient() as usual.
|
|
378
|
+
*/
|
|
379
|
+
getModbusInstance(unitId) {
|
|
380
|
+
return this.getInstance(unitId);
|
|
381
|
+
}
|
|
368
382
|
/**
|
|
369
383
|
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
370
384
|
*/
|
|
@@ -1254,6 +1268,10 @@ class SunspecModbusClient {
|
|
|
1254
1268
|
(data.dcCurrent !== undefined ||
|
|
1255
1269
|
data.dcVoltage !== undefined ||
|
|
1256
1270
|
data.dcPower !== undefined)) {
|
|
1271
|
+
// Stamp the stable 1-based module position so downstream
|
|
1272
|
+
// string identity does not shift when an earlier module
|
|
1273
|
+
// drops out of a poll (e.g. transient undefined reads).
|
|
1274
|
+
data.moduleNumber = i;
|
|
1257
1275
|
mpptData.push(data);
|
|
1258
1276
|
}
|
|
1259
1277
|
}
|
|
@@ -1769,8 +1787,17 @@ class SunspecModbusClient {
|
|
|
1769
1787
|
// governing parameters are already in place when the device
|
|
1770
1788
|
// transitions into the new mode.
|
|
1771
1789
|
if (controls.storCtlMod !== undefined) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1790
|
+
// storCtlMod = 0 is rejected with Modbus Exception code 3 (Illegal Data Value)
|
|
1791
|
+
// by some devices (e.g. Fronius GEN24). 0 means "no external control" per the
|
|
1792
|
+
// SunSpec spec, but Fronius firmware requires at least one bit to be set.
|
|
1793
|
+
// Mapping 0 -> CHARGE | DISCHARGE returns the battery to autonomous
|
|
1794
|
+
// self-managed operation on all known SunSpec devices (matches the neutral
|
|
1795
|
+
// reset state used in sunspec-battery-feature-calibrator.ts).
|
|
1796
|
+
const safeMode = controls.storCtlMod === 0
|
|
1797
|
+
? sunspec_interfaces_js_1.SunspecStorageControlMode.CHARGE | sunspec_interfaces_js_1.SunspecStorageControlMode.DISCHARGE
|
|
1798
|
+
: controls.storCtlMod;
|
|
1799
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, safeMode, 'uint16');
|
|
1800
|
+
console.debug(`Set storage control mode to 0x${safeMode.toString(16)}`);
|
|
1774
1801
|
}
|
|
1775
1802
|
console.debug('Battery controls written successfully');
|
|
1776
1803
|
return true;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.cjs";
|
|
20
20
|
import { EnergyAppModbusDataType, IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
|
|
21
|
+
import { EnergyAppModbusInstance } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-modbus.js";
|
|
21
22
|
import { EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
22
23
|
/**
|
|
23
24
|
* Get (or create) the singleton SunspecModbusClient for this network device.
|
|
@@ -121,6 +122,18 @@ export declare class SunspecModbusClient {
|
|
|
121
122
|
* Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
|
|
122
123
|
*/
|
|
123
124
|
private getInstance;
|
|
125
|
+
/**
|
|
126
|
+
* Expose this client's open Modbus connection for a unit so a co-located driver can
|
|
127
|
+
* share the SAME TCP socket instead of opening a second one. This matters for devices
|
|
128
|
+
* behind single-connection bridges (e.g. the Solax ESP32 Pocket WiFi dongle, which
|
|
129
|
+
* resets the link when a second Modbus-TCP client connects): the driver reads its own
|
|
130
|
+
* function codes (e.g. FC04 input registers) over this socket.
|
|
131
|
+
*
|
|
132
|
+
* Throws if the unit isn't open. The connection's lifecycle stays owned by this client —
|
|
133
|
+
* borrowers MUST NOT call disconnect() on the returned instance; release it via
|
|
134
|
+
* disconnectUnit(unitId) / releaseSunspecClient() as usual.
|
|
135
|
+
*/
|
|
136
|
+
getModbusInstance(unitId: number): EnergyAppModbusInstance;
|
|
124
137
|
/**
|
|
125
138
|
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
126
139
|
*/
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
|
|
|
9
9
|
/**
|
|
10
10
|
* Current version of the enyo Energy App SDK.
|
|
11
11
|
*/
|
|
12
|
-
exports.SDK_VERSION = '0.0.
|
|
12
|
+
exports.SDK_VERSION = '0.0.86';
|
|
13
13
|
/**
|
|
14
14
|
* Gets the current SDK version.
|
|
15
15
|
* @returns The semantic version string of the SDK
|
package/dist/cjs/version.d.cts
CHANGED
|
@@ -70,22 +70,30 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
|
|
|
70
70
|
// baseline so the persisted snapshot continues to point at the
|
|
71
71
|
// original pre-schedule state. The library will overwrite its
|
|
72
72
|
// storage row with the same value — harmless.
|
|
73
|
-
|
|
73
|
+
const wChaMax = this.originalBaseline.wChaMax;
|
|
74
|
+
if (!wChaMax || wChaMax <= 0) {
|
|
75
|
+
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: cannot start schedule — originalBaseline has no usable wChaMax (got ${wChaMax})`);
|
|
76
|
+
}
|
|
77
|
+
this.installedWChaMax = wChaMax;
|
|
74
78
|
return { ...this.originalBaseline };
|
|
75
79
|
}
|
|
76
80
|
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
77
81
|
if (!controls) {
|
|
78
82
|
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
|
|
79
83
|
}
|
|
84
|
+
const wChaMax = controls.wChaMax;
|
|
85
|
+
if (!wChaMax || wChaMax <= 0) {
|
|
86
|
+
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: cannot start schedule — device did not report a usable wChaMax (got ${wChaMax})`);
|
|
87
|
+
}
|
|
80
88
|
const baseline = {
|
|
81
89
|
storCtlMod: controls.storCtlMod,
|
|
82
90
|
chaGriSet: controls.chaGriSet,
|
|
83
|
-
wChaMax
|
|
91
|
+
wChaMax,
|
|
84
92
|
inWRte: controls.inWRte,
|
|
85
93
|
outWRte: controls.outWRte,
|
|
86
94
|
};
|
|
87
95
|
this.originalBaseline = baseline;
|
|
88
|
-
this.installedWChaMax =
|
|
96
|
+
this.installedWChaMax = wChaMax;
|
|
89
97
|
return { ...baseline };
|
|
90
98
|
}
|
|
91
99
|
onChange(active, _previous) {
|
|
@@ -129,7 +129,16 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
129
129
|
private readonly capabilities;
|
|
130
130
|
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
131
131
|
private static readonly CONNECTION_FAULT_THRESHOLD;
|
|
132
|
+
/**
|
|
133
|
+
* Require this many consecutive operatingState=7 (FAULT) reads before
|
|
134
|
+
* reporting Faulted. Debounces transient single-poll FAULT reads (e.g. a
|
|
135
|
+
* DC-coupled battery disturbing a string) that would otherwise oscillate
|
|
136
|
+
* the appliance status. Recovery is immediate (counter resets on the first
|
|
137
|
+
* non-fault read).
|
|
138
|
+
*/
|
|
139
|
+
private static readonly OPERATING_FAULT_THRESHOLD;
|
|
132
140
|
private storage?;
|
|
141
|
+
private consecutiveOperatingFaults;
|
|
133
142
|
private errorState;
|
|
134
143
|
private snapshotService?;
|
|
135
144
|
private statusReassertedThisSession;
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -336,7 +336,16 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
336
336
|
capabilities;
|
|
337
337
|
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
338
338
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
339
|
+
/**
|
|
340
|
+
* Require this many consecutive operatingState=7 (FAULT) reads before
|
|
341
|
+
* reporting Faulted. Debounces transient single-poll FAULT reads (e.g. a
|
|
342
|
+
* DC-coupled battery disturbing a string) that would otherwise oscillate
|
|
343
|
+
* the appliance status. Recovery is immediate (counter resets on the first
|
|
344
|
+
* non-fault read).
|
|
345
|
+
*/
|
|
346
|
+
static OPERATING_FAULT_THRESHOLD = 3;
|
|
339
347
|
storage;
|
|
348
|
+
consecutiveOperatingFaults = 0;
|
|
340
349
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
341
350
|
snapshotService;
|
|
342
351
|
// Whether we've emitted a status update at least once since (re)connecting.
|
|
@@ -649,7 +658,15 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
649
658
|
// fault; MPPT (4), SLEEPING (2) and the other states are normal. This
|
|
650
659
|
// is re-evaluated every poll so a producing inverter (operatingState=4,
|
|
651
660
|
// no error bits) reports Healthy even if it was previously faulted.
|
|
652
|
-
|
|
661
|
+
//
|
|
662
|
+
// A single FAULT read is debounced: we only treat it as a fault after
|
|
663
|
+
// OPERATING_FAULT_THRESHOLD consecutive FAULT polls, so a transient
|
|
664
|
+
// wobble (e.g. a faulted DC-coupled battery on a string) does not
|
|
665
|
+
// oscillate the status. Recovery is immediate — the counter resets on
|
|
666
|
+
// the first non-fault read. Event-register error bits stay immediate.
|
|
667
|
+
const operatingFaultRaw = data.operatingState === SunspecInverterOperatingState.FAULT;
|
|
668
|
+
this.consecutiveOperatingFaults = operatingFaultRaw ? this.consecutiveOperatingFaults + 1 : 0;
|
|
669
|
+
const operatingFault = this.consecutiveOperatingFaults >= SunspecInverter.OPERATING_FAULT_THRESHOLD;
|
|
653
670
|
const newStatus = (codeIds.length > 0 || operatingFault)
|
|
654
671
|
? EnyoApplianceStatusEnum.Faulted
|
|
655
672
|
: EnyoApplianceStatusEnum.Healthy;
|
|
@@ -765,7 +782,10 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
765
782
|
// Only include strings with valid data
|
|
766
783
|
if (mppt.dcVoltage !== undefined || mppt.dcPower !== undefined) {
|
|
767
784
|
result.push({
|
|
768
|
-
|
|
785
|
+
// Use the stable physical module position so a string keeps
|
|
786
|
+
// its identity when an earlier module drops out of a poll.
|
|
787
|
+
// Fall back to array position if the field is absent.
|
|
788
|
+
index: mppt.moduleNumber ?? (index + 1),
|
|
769
789
|
name: mppt.stringId,
|
|
770
790
|
state: this.mapMPPTOperatingState(mppt.operatingState),
|
|
771
791
|
current: mppt.dcCurrent,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
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
20
|
import { EnergyAppModbusDataType, IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
|
|
21
|
+
import { EnergyAppModbusInstance } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-modbus.js";
|
|
21
22
|
import { EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
22
23
|
/**
|
|
23
24
|
* Get (or create) the singleton SunspecModbusClient for this network device.
|
|
@@ -121,6 +122,18 @@ export declare class SunspecModbusClient {
|
|
|
121
122
|
* Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
|
|
122
123
|
*/
|
|
123
124
|
private getInstance;
|
|
125
|
+
/**
|
|
126
|
+
* Expose this client's open Modbus connection for a unit so a co-located driver can
|
|
127
|
+
* share the SAME TCP socket instead of opening a second one. This matters for devices
|
|
128
|
+
* behind single-connection bridges (e.g. the Solax ESP32 Pocket WiFi dongle, which
|
|
129
|
+
* resets the link when a second Modbus-TCP client connects): the driver reads its own
|
|
130
|
+
* function codes (e.g. FC04 input registers) over this socket.
|
|
131
|
+
*
|
|
132
|
+
* Throws if the unit isn't open. The connection's lifecycle stays owned by this client —
|
|
133
|
+
* borrowers MUST NOT call disconnect() on the returned instance; release it via
|
|
134
|
+
* disconnectUnit(unitId) / releaseSunspecClient() as usual.
|
|
135
|
+
*/
|
|
136
|
+
getModbusInstance(unitId: number): EnergyAppModbusInstance;
|
|
124
137
|
/**
|
|
125
138
|
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
126
139
|
*/
|
|
@@ -360,6 +360,20 @@ export class SunspecModbusClient {
|
|
|
360
360
|
throw new Error(`Modbus instance for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
361
361
|
return inst;
|
|
362
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Expose this client's open Modbus connection for a unit so a co-located driver can
|
|
365
|
+
* share the SAME TCP socket instead of opening a second one. This matters for devices
|
|
366
|
+
* behind single-connection bridges (e.g. the Solax ESP32 Pocket WiFi dongle, which
|
|
367
|
+
* resets the link when a second Modbus-TCP client connects): the driver reads its own
|
|
368
|
+
* function codes (e.g. FC04 input registers) over this socket.
|
|
369
|
+
*
|
|
370
|
+
* Throws if the unit isn't open. The connection's lifecycle stays owned by this client —
|
|
371
|
+
* borrowers MUST NOT call disconnect() on the returned instance; release it via
|
|
372
|
+
* disconnectUnit(unitId) / releaseSunspecClient() as usual.
|
|
373
|
+
*/
|
|
374
|
+
getModbusInstance(unitId) {
|
|
375
|
+
return this.getInstance(unitId);
|
|
376
|
+
}
|
|
363
377
|
/**
|
|
364
378
|
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
365
379
|
*/
|
|
@@ -1249,6 +1263,10 @@ export class SunspecModbusClient {
|
|
|
1249
1263
|
(data.dcCurrent !== undefined ||
|
|
1250
1264
|
data.dcVoltage !== undefined ||
|
|
1251
1265
|
data.dcPower !== undefined)) {
|
|
1266
|
+
// Stamp the stable 1-based module position so downstream
|
|
1267
|
+
// string identity does not shift when an earlier module
|
|
1268
|
+
// drops out of a poll (e.g. transient undefined reads).
|
|
1269
|
+
data.moduleNumber = i;
|
|
1252
1270
|
mpptData.push(data);
|
|
1253
1271
|
}
|
|
1254
1272
|
}
|
|
@@ -1764,8 +1782,17 @@ export class SunspecModbusClient {
|
|
|
1764
1782
|
// governing parameters are already in place when the device
|
|
1765
1783
|
// transitions into the new mode.
|
|
1766
1784
|
if (controls.storCtlMod !== undefined) {
|
|
1767
|
-
|
|
1768
|
-
|
|
1785
|
+
// storCtlMod = 0 is rejected with Modbus Exception code 3 (Illegal Data Value)
|
|
1786
|
+
// by some devices (e.g. Fronius GEN24). 0 means "no external control" per the
|
|
1787
|
+
// SunSpec spec, but Fronius firmware requires at least one bit to be set.
|
|
1788
|
+
// Mapping 0 -> CHARGE | DISCHARGE returns the battery to autonomous
|
|
1789
|
+
// self-managed operation on all known SunSpec devices (matches the neutral
|
|
1790
|
+
// reset state used in sunspec-battery-feature-calibrator.ts).
|
|
1791
|
+
const safeMode = controls.storCtlMod === 0
|
|
1792
|
+
? SunspecStorageControlMode.CHARGE | SunspecStorageControlMode.DISCHARGE
|
|
1793
|
+
: controls.storCtlMod;
|
|
1794
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, safeMode, 'uint16');
|
|
1795
|
+
console.debug(`Set storage control mode to 0x${safeMode.toString(16)}`);
|
|
1769
1796
|
}
|
|
1770
1797
|
console.debug('Battery controls written successfully');
|
|
1771
1798
|
return true;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED