@enyo-energy/sunspec-sdk 0.0.74 → 0.0.76
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-devices.cjs +36 -149
- package/dist/cjs/sunspec-devices.d.cts +18 -11
- package/dist/cjs/sunspec-modbus-client.cjs +64 -44
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.d.ts +18 -11
- package/dist/sunspec-devices.js +35 -149
- package/dist/sunspec-modbus-client.js +64 -44
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
|
|
4
4
|
exports.detectFeaturesFromRegisters = detectFeaturesFromRegisters;
|
|
5
|
+
exports.selectLiveBatteryPowerW = selectLiveBatteryPowerW;
|
|
5
6
|
exports.filterFeaturesByCalibrationResult = filterFeaturesByCalibrationResult;
|
|
6
7
|
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -711,8 +712,6 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
711
712
|
this.dataBus = this.energyApp.useDataBus();
|
|
712
713
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
713
714
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
714
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
715
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
716
715
|
], (entry) => this.handleInverterCommand(entry));
|
|
717
716
|
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
718
717
|
}
|
|
@@ -737,12 +736,6 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
737
736
|
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1:
|
|
738
737
|
await this.handleSetFeedInLimit(entry);
|
|
739
738
|
break;
|
|
740
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
741
|
-
await this.handleStartCalibration(entry);
|
|
742
|
-
break;
|
|
743
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
744
|
-
await this.handleStopCalibration(entry);
|
|
745
|
-
break;
|
|
746
739
|
}
|
|
747
740
|
}
|
|
748
741
|
catch (error) {
|
|
@@ -788,51 +781,6 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
788
781
|
await this.snapshotService.initialize();
|
|
789
782
|
console.log(`Inverter ${this.applianceId}: snapshot service initialized`);
|
|
790
783
|
}
|
|
791
|
-
async handleStartCalibration(msg) {
|
|
792
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
793
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
if (!this.snapshotService) {
|
|
797
|
-
await this.initSnapshotService();
|
|
798
|
-
}
|
|
799
|
-
if (!this.snapshotService) {
|
|
800
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (this.snapshotService.isCalibrating()) {
|
|
804
|
-
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
805
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
console.log(`Inverter ${this.applianceId}: handling StartCalibrationV1`);
|
|
809
|
-
const controls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
810
|
-
if (!controls) {
|
|
811
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read inverter controls for snapshot');
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
try {
|
|
815
|
-
await this.snapshotService.startCalibration(controls);
|
|
816
|
-
}
|
|
817
|
-
catch (error) {
|
|
818
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
822
|
-
}
|
|
823
|
-
async handleStopCalibration(msg) {
|
|
824
|
-
if (!this.applianceId) {
|
|
825
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
|
|
829
|
-
// SnapshotService.stopCalibration fires `restoreInverterSnapshot` internally with
|
|
830
|
-
// reason="stop" before returning. Any restore failure is logged by the callback
|
|
831
|
-
// (the persisted snapshot is already gone by then — see library notes) so we
|
|
832
|
-
// always ack Accepted from here.
|
|
833
|
-
await this.snapshotService?.stopCalibration();
|
|
834
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
835
|
-
}
|
|
836
784
|
/**
|
|
837
785
|
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
838
786
|
* subset of writable inverter fields that other commands actually touched during the
|
|
@@ -905,6 +853,35 @@ function detectFeaturesFromRegisters(batteryData) {
|
|
|
905
853
|
* the full detected set to preserve the old all-or-nothing semantics on
|
|
906
854
|
* upgrade.
|
|
907
855
|
*/
|
|
856
|
+
/**
|
|
857
|
+
* Pick the live battery power, in preference order:
|
|
858
|
+
*
|
|
859
|
+
* 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
|
|
860
|
+
* passed in as the already-summed `mpptBatteryPowerW`.
|
|
861
|
+
* 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
|
|
862
|
+
* by the Model 802 path of `SunspecModbusClient.readBatteryData`.
|
|
863
|
+
* 3. `undefined` — no reliable live source on this device.
|
|
864
|
+
*
|
|
865
|
+
* The Model 124 path of `readBatteryData` no longer manufactures
|
|
866
|
+
* `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
|
|
867
|
+
* cap, not a live measurement). So whenever this function sees
|
|
868
|
+
* `chargePower` / `dischargePower` defined, they came from Model 802.
|
|
869
|
+
*
|
|
870
|
+
* Exported as a free function so the preference logic can be unit-tested
|
|
871
|
+
* without the full `SunspecBattery` scaffold.
|
|
872
|
+
*/
|
|
873
|
+
function selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData) {
|
|
874
|
+
if (mpptBatteryPowerW !== undefined) {
|
|
875
|
+
return mpptBatteryPowerW;
|
|
876
|
+
}
|
|
877
|
+
if (!batteryData) {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
if (batteryData.chargePower === undefined && batteryData.dischargePower === undefined) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
return (batteryData.chargePower ?? 0) - (batteryData.dischargePower ?? 0);
|
|
884
|
+
}
|
|
908
885
|
function filterFeaturesByCalibrationResult(detected, result, controllable) {
|
|
909
886
|
if (!result || result.state !== 'calibrated') {
|
|
910
887
|
return detected.filter(f => !controllable.includes(f));
|
|
@@ -1168,17 +1145,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1168
1145
|
return messages;
|
|
1169
1146
|
}
|
|
1170
1147
|
if (batteryData) {
|
|
1171
|
-
|
|
1172
|
-
const
|
|
1173
|
-
// Determine battery power: prefer model 802 w field, then MPPT extraction, then undefined
|
|
1174
|
-
let batteryPowerW;
|
|
1175
|
-
if (batteryBaseModel && (batteryData.chargePower !== undefined || batteryData.dischargePower !== undefined)) {
|
|
1176
|
-
// Model 802 provides power directly: positive = charge, negative = discharge
|
|
1177
|
-
batteryPowerW = (batteryData.chargePower || 0) - (batteryData.dischargePower || 0);
|
|
1178
|
-
}
|
|
1179
|
-
else if (!advancedBatteryModel) {
|
|
1180
|
-
batteryPowerW = mpptBatteryPowerW;
|
|
1181
|
-
}
|
|
1148
|
+
// See `selectLiveBatteryPowerW` for the source-preference rationale.
|
|
1149
|
+
const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
|
|
1182
1150
|
// Feed the calibration driver's synchronous power cache. No-op when calibration
|
|
1183
1151
|
// was never configured.
|
|
1184
1152
|
if (batteryPowerW !== undefined && this.calibrationDriver) {
|
|
@@ -1371,7 +1339,10 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1371
1339
|
console.error('Battery not connected');
|
|
1372
1340
|
return false;
|
|
1373
1341
|
}
|
|
1374
|
-
|
|
1342
|
+
// Redundant with the modbus client's own per-write debug trace. Keep at debug
|
|
1343
|
+
// so the high-level info logs (setStorageMode / enableGridCharging / etc.) stay
|
|
1344
|
+
// the only info-level signal per battery-control action.
|
|
1345
|
+
console.debug('Writing battery controls:', controls);
|
|
1375
1346
|
return this.sunspecClient.writeBatteryControls(this.unitId, controls);
|
|
1376
1347
|
}
|
|
1377
1348
|
mapToEnyoStorageMode(storageMode) {
|
|
@@ -1469,8 +1440,6 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1469
1440
|
this.dataBus = this.energyApp.useDataBus();
|
|
1470
1441
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1471
1442
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1472
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1473
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1474
1443
|
], (entry) => this.handleStorageCommand(entry));
|
|
1475
1444
|
if (!this.applianceId) {
|
|
1476
1445
|
throw new Error("SunspecBattery.startDataBusListening: applianceId required — call connect() first.");
|
|
@@ -1509,12 +1478,6 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1509
1478
|
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1510
1479
|
this.handleSetStorageScheduleAck(entry);
|
|
1511
1480
|
break;
|
|
1512
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1513
|
-
await this.handleStartCalibration(entry);
|
|
1514
|
-
break;
|
|
1515
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
1516
|
-
await this.handleStopCalibration(entry);
|
|
1517
|
-
break;
|
|
1518
1481
|
}
|
|
1519
1482
|
}
|
|
1520
1483
|
catch (error) {
|
|
@@ -1554,51 +1517,6 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1554
1517
|
await this.snapshotService.initialize();
|
|
1555
1518
|
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1556
1519
|
}
|
|
1557
|
-
async handleStartCalibration(msg) {
|
|
1558
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1559
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
if (!this.snapshotService) {
|
|
1563
|
-
await this.initSnapshotService();
|
|
1564
|
-
}
|
|
1565
|
-
if (!this.snapshotService) {
|
|
1566
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
if (this.snapshotService.isCalibrating()) {
|
|
1570
|
-
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1571
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
console.log(`Battery ${this.applianceId}: handling StartCalibrationV1`);
|
|
1575
|
-
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
1576
|
-
if (!controls) {
|
|
1577
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read battery controls for snapshot');
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
try {
|
|
1581
|
-
await this.snapshotService.startCalibration(controls);
|
|
1582
|
-
}
|
|
1583
|
-
catch (error) {
|
|
1584
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1588
|
-
}
|
|
1589
|
-
async handleStopCalibration(msg) {
|
|
1590
|
-
if (!this.applianceId) {
|
|
1591
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
|
|
1595
|
-
// SnapshotService.stopCalibration fires `restoreBatterySnapshot` internally
|
|
1596
|
-
// with reason="stop" before returning. Failures from the restore callback are
|
|
1597
|
-
// logged inside the callback (the persisted snapshot is already gone — see
|
|
1598
|
-
// library notes) so we always ack Accepted here.
|
|
1599
|
-
await this.snapshotService?.stopCalibration();
|
|
1600
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1601
|
-
}
|
|
1602
1520
|
/**
|
|
1603
1521
|
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
1604
1522
|
* subset of writable battery fields touched during the calibration. Errors are
|
|
@@ -1704,13 +1622,11 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1704
1622
|
console.error(`Failed to update meter appliance: ${error}`);
|
|
1705
1623
|
}
|
|
1706
1624
|
}
|
|
1707
|
-
this.startDataBusListening();
|
|
1708
1625
|
}
|
|
1709
1626
|
/**
|
|
1710
1627
|
* Disconnect from the meter and update appliance state
|
|
1711
1628
|
*/
|
|
1712
1629
|
async disconnect() {
|
|
1713
|
-
this.stopDataBusListening();
|
|
1714
1630
|
if (this.applianceId) {
|
|
1715
1631
|
try {
|
|
1716
1632
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
@@ -1722,35 +1638,6 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1722
1638
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1723
1639
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
|
1724
1640
|
}
|
|
1725
|
-
/**
|
|
1726
|
-
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
1727
|
-
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
1728
|
-
*/
|
|
1729
|
-
startDataBusListening() {
|
|
1730
|
-
if (this.dataBusListenerId) {
|
|
1731
|
-
return;
|
|
1732
|
-
}
|
|
1733
|
-
this.dataBus = this.energyApp.useDataBus();
|
|
1734
|
-
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1735
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1736
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1737
|
-
], (entry) => this.handleMeterCommand(entry));
|
|
1738
|
-
console.log(`Meter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1739
|
-
}
|
|
1740
|
-
stopDataBusListening() {
|
|
1741
|
-
if (this.dataBusListenerId && this.dataBus) {
|
|
1742
|
-
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
1743
|
-
console.log(`Meter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
1744
|
-
}
|
|
1745
|
-
this.dataBusListenerId = undefined;
|
|
1746
|
-
this.dataBus = undefined;
|
|
1747
|
-
}
|
|
1748
|
-
handleMeterCommand(entry) {
|
|
1749
|
-
if (entry.applianceId !== this.applianceId) {
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
this.sendCommandAcknowledge(entry.id, entry.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'Meter does not support calibration');
|
|
1753
|
-
}
|
|
1754
1641
|
/**
|
|
1755
1642
|
* Update meter data and return data bus messages
|
|
1756
1643
|
*/
|
|
@@ -178,8 +178,6 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
178
178
|
* process restarts. Idempotent.
|
|
179
179
|
*/
|
|
180
180
|
private initSnapshotService;
|
|
181
|
-
private handleStartCalibration;
|
|
182
|
-
private handleStopCalibration;
|
|
183
181
|
/**
|
|
184
182
|
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
185
183
|
* subset of writable inverter fields that other commands actually touched during the
|
|
@@ -217,6 +215,24 @@ export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryD
|
|
|
217
215
|
* the full detected set to preserve the old all-or-nothing semantics on
|
|
218
216
|
* upgrade.
|
|
219
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* Pick the live battery power, in preference order:
|
|
220
|
+
*
|
|
221
|
+
* 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
|
|
222
|
+
* passed in as the already-summed `mpptBatteryPowerW`.
|
|
223
|
+
* 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
|
|
224
|
+
* by the Model 802 path of `SunspecModbusClient.readBatteryData`.
|
|
225
|
+
* 3. `undefined` — no reliable live source on this device.
|
|
226
|
+
*
|
|
227
|
+
* The Model 124 path of `readBatteryData` no longer manufactures
|
|
228
|
+
* `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
|
|
229
|
+
* cap, not a live measurement). So whenever this function sees
|
|
230
|
+
* `chargePower` / `dischargePower` defined, they came from Model 802.
|
|
231
|
+
*
|
|
232
|
+
* Exported as a free function so the preference logic can be unit-tested
|
|
233
|
+
* without the full `SunspecBattery` scaffold.
|
|
234
|
+
*/
|
|
235
|
+
export declare function selectLiveBatteryPowerW(mpptBatteryPowerW: number | undefined, batteryData: SunspecBatteryData | null): number | undefined;
|
|
220
236
|
export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
|
|
221
237
|
/**
|
|
222
238
|
* Sunspec Battery implementation
|
|
@@ -408,8 +424,6 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
408
424
|
* process restarts. Idempotent.
|
|
409
425
|
*/
|
|
410
426
|
private initSnapshotService;
|
|
411
|
-
private handleStartCalibration;
|
|
412
|
-
private handleStopCalibration;
|
|
413
427
|
/**
|
|
414
428
|
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
415
429
|
* subset of writable battery fields touched during the calibration. Errors are
|
|
@@ -432,13 +446,6 @@ export declare class SunspecMeter extends BaseSunspecDevice {
|
|
|
432
446
|
* Disconnect from the meter and update appliance state
|
|
433
447
|
*/
|
|
434
448
|
disconnect(): Promise<void>;
|
|
435
|
-
/**
|
|
436
|
-
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
437
|
-
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
438
|
-
*/
|
|
439
|
-
startDataBusListening(): void;
|
|
440
|
-
stopDataBusListening(): void;
|
|
441
|
-
private handleMeterCommand;
|
|
442
449
|
/**
|
|
443
450
|
* Update meter data and return data bus messages
|
|
444
451
|
*/
|
|
@@ -486,7 +486,10 @@ class SunspecModbusClient {
|
|
|
486
486
|
}
|
|
487
487
|
catch (error) {
|
|
488
488
|
console.debug(`No SunSpec device at unit ${unitId}: ${error}`);
|
|
489
|
-
|
|
489
|
+
// Discovery summary at line 594 below is the info-level outcome.
|
|
490
|
+
// This 0-models branch is the same outcome as a normal "no device found",
|
|
491
|
+
// covered already by the debug above. Keep at debug.
|
|
492
|
+
console.debug(`Discovery complete for unit ${unitId}. Found 0 models: []`);
|
|
490
493
|
return models;
|
|
491
494
|
}
|
|
492
495
|
currentAddress = addressInfo.nextAddress;
|
|
@@ -495,14 +498,14 @@ class SunspecModbusClient {
|
|
|
495
498
|
const buffer = await instance.readHoldingRegisters(currentAddress, 2);
|
|
496
499
|
const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
|
|
497
500
|
if (!modelData || modelData.length < 2) {
|
|
498
|
-
console.
|
|
501
|
+
console.debug(`No data at address ${currentAddress}, ending discovery`);
|
|
499
502
|
break;
|
|
500
503
|
}
|
|
501
504
|
const modelId = modelData[0];
|
|
502
505
|
const modelLength = modelData[1];
|
|
503
506
|
// Check for end marker
|
|
504
507
|
if (modelId === 0xFFFF || modelId === 65535) {
|
|
505
|
-
console.
|
|
508
|
+
console.debug(`Found end marker at address ${currentAddress}`);
|
|
506
509
|
break;
|
|
507
510
|
}
|
|
508
511
|
// Store discovered model
|
|
@@ -512,7 +515,9 @@ class SunspecModbusClient {
|
|
|
512
515
|
length: modelLength
|
|
513
516
|
};
|
|
514
517
|
models.set(modelId, model);
|
|
515
|
-
|
|
518
|
+
// Per-model discovery step. The end-of-discovery summary below is the
|
|
519
|
+
// info-level outcome; the per-model walk is debug-only.
|
|
520
|
+
console.debug(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
|
|
516
521
|
// Jump to next model: current address + 2 (header) + model length
|
|
517
522
|
currentAddress = currentAddress + 2 + modelLength;
|
|
518
523
|
}
|
|
@@ -701,7 +706,9 @@ class SunspecModbusClient {
|
|
|
701
706
|
// Write to holding registers
|
|
702
707
|
await instance.writeMultipleRegisters(address, registerValues);
|
|
703
708
|
this.connectionHealth.recordSuccess();
|
|
704
|
-
|
|
709
|
+
// Per-register write success — fires for every parameter inside higher-level
|
|
710
|
+
// writers (writeBatteryControls / writeInverterControls). Demoted to debug.
|
|
711
|
+
console.debug(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
|
|
705
712
|
return true;
|
|
706
713
|
}
|
|
707
714
|
catch (error) {
|
|
@@ -1589,17 +1596,21 @@ class SunspecModbusClient {
|
|
|
1589
1596
|
voltage: this.applyScaleFactor(inBatVRaw, scaleFactors.InBatV_SF, 'uint16'),
|
|
1590
1597
|
status: !this.isUnimplementedValue(chaStRaw, 'enum16') ? chaStRaw : undefined
|
|
1591
1598
|
};
|
|
1592
|
-
//
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1599
|
+
// chargePower / dischargePower used to be derived from
|
|
1600
|
+
// `(inWRte / 100) * wChaMax` and `(outWRte / 100) * wChaMax`
|
|
1601
|
+
// here. That formula uses Model 124's COMMANDED RATE CAPS — not
|
|
1602
|
+
// a live measurement — and produces e.g. 5000 W of "charging"
|
|
1603
|
+
// for an idle battery with inWRte = 100 % and a 5 kW nameplate.
|
|
1604
|
+
// Live battery power must come from a real source: Model 160
|
|
1605
|
+
// StCha 3 / StDisCha 4 (see SunspecBattery.extractBatteryPower
|
|
1606
|
+
// FromMPPT) or Model 802 `w` at offset 47 (the Model 802 branch
|
|
1607
|
+
// of this method, just below). Model-124-only devices should
|
|
1608
|
+
// publish "no signal" rather than the commanded cap.
|
|
1609
|
+
// See plans/please-review-the-implementation-groovy-thacker.md.
|
|
1610
|
+
// Single-line JSON debug dump — Node's default formatter would split
|
|
1611
|
+
// the object across many lines; stringify keeps the whole snapshot
|
|
1612
|
+
// on one log entry so it stays grep-able and round-trippable.
|
|
1613
|
+
console.debug(`[Battery] unit=${unitId} model=124 data=${JSON.stringify(data)}`);
|
|
1603
1614
|
return data;
|
|
1604
1615
|
}
|
|
1605
1616
|
else if (model.id === 802) {
|
|
@@ -1636,17 +1647,18 @@ class SunspecModbusClient {
|
|
|
1636
1647
|
chargePower,
|
|
1637
1648
|
dischargePower,
|
|
1638
1649
|
};
|
|
1639
|
-
console.debug(
|
|
1650
|
+
console.debug(`[Battery] unit=${unitId} model=802 data=${JSON.stringify(result)}`);
|
|
1640
1651
|
return result;
|
|
1641
1652
|
}
|
|
1642
1653
|
else {
|
|
1643
|
-
// Handle other battery models (803) if needed
|
|
1644
|
-
|
|
1645
|
-
return {
|
|
1654
|
+
// Handle other battery models (803) if needed.
|
|
1655
|
+
const stub = {
|
|
1646
1656
|
blockNumber: model.id,
|
|
1647
1657
|
blockAddress: model.address,
|
|
1648
|
-
blockLength: model.length
|
|
1658
|
+
blockLength: model.length,
|
|
1649
1659
|
};
|
|
1660
|
+
console.debug(`[Battery] unit=${unitId} model=${model.id} (not yet implemented) data=${JSON.stringify(stub)}`);
|
|
1661
|
+
return stub;
|
|
1650
1662
|
}
|
|
1651
1663
|
}
|
|
1652
1664
|
catch (error) {
|
|
@@ -1664,7 +1676,11 @@ class SunspecModbusClient {
|
|
|
1664
1676
|
return false;
|
|
1665
1677
|
}
|
|
1666
1678
|
const baseAddr = model.address;
|
|
1667
|
-
|
|
1679
|
+
// Per-register write trace is high-volume (every schedule entry transition fires
|
|
1680
|
+
// a writeBatteryControls). Demoted to debug; consumers that need the trail can
|
|
1681
|
+
// raise their log level. High-level callers (setStorageMode, enableGridCharging,
|
|
1682
|
+
// setFeedInLimit, etc.) still emit one info-level line per action.
|
|
1683
|
+
console.debug(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1668
1684
|
try {
|
|
1669
1685
|
// Write order: source pin and parameter writes land BEFORE the
|
|
1670
1686
|
// control mode so the device only "starts acting" once every
|
|
@@ -1674,7 +1690,7 @@ class SunspecModbusClient {
|
|
|
1674
1690
|
// Write charge source setting (Register 17)
|
|
1675
1691
|
if (controls.chaGriSet !== undefined) {
|
|
1676
1692
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1677
|
-
console.
|
|
1693
|
+
console.debug(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
|
|
1678
1694
|
}
|
|
1679
1695
|
// Write maximum charge power (Register 2) - needs scale factor
|
|
1680
1696
|
if (controls.wChaMax !== undefined) {
|
|
@@ -1682,7 +1698,7 @@ class SunspecModbusClient {
|
|
|
1682
1698
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1683
1699
|
const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
|
|
1684
1700
|
await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
|
|
1685
|
-
console.
|
|
1701
|
+
console.debug(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
|
|
1686
1702
|
}
|
|
1687
1703
|
// Write charge rate (Register 13) - needs scale factor
|
|
1688
1704
|
if (controls.inWRte !== undefined) {
|
|
@@ -1690,7 +1706,7 @@ class SunspecModbusClient {
|
|
|
1690
1706
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1691
1707
|
const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
|
|
1692
1708
|
await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
|
|
1693
|
-
console.
|
|
1709
|
+
console.debug(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
|
|
1694
1710
|
}
|
|
1695
1711
|
// Write discharge rate (Register 12) - needs scale factor
|
|
1696
1712
|
if (controls.outWRte !== undefined) {
|
|
@@ -1698,7 +1714,7 @@ class SunspecModbusClient {
|
|
|
1698
1714
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1699
1715
|
const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
|
|
1700
1716
|
await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
|
|
1701
|
-
console.
|
|
1717
|
+
console.debug(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
|
|
1702
1718
|
}
|
|
1703
1719
|
// Write minimum reserve percentage (Register 7) - needs scale factor
|
|
1704
1720
|
if (controls.minRsvPct !== undefined) {
|
|
@@ -1706,16 +1722,16 @@ class SunspecModbusClient {
|
|
|
1706
1722
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1707
1723
|
const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
|
|
1708
1724
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1709
|
-
console.
|
|
1725
|
+
console.debug(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1710
1726
|
}
|
|
1711
1727
|
// Storage control mode (Register 5) — written LAST so all
|
|
1712
1728
|
// governing parameters are already in place when the device
|
|
1713
1729
|
// transitions into the new mode.
|
|
1714
1730
|
if (controls.storCtlMod !== undefined) {
|
|
1715
1731
|
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1716
|
-
console.
|
|
1732
|
+
console.debug(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1717
1733
|
}
|
|
1718
|
-
console.
|
|
1734
|
+
console.debug('Battery controls written successfully');
|
|
1719
1735
|
return true;
|
|
1720
1736
|
}
|
|
1721
1737
|
catch (error) {
|
|
@@ -1766,10 +1782,12 @@ class SunspecModbusClient {
|
|
|
1766
1782
|
async readBatteryControls(unitId) {
|
|
1767
1783
|
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1768
1784
|
if (!model) {
|
|
1769
|
-
|
|
1785
|
+
// Fires per readBatteryControls attempt — noisy on non-battery units
|
|
1786
|
+
// that go through the same code path. Demoted to debug.
|
|
1787
|
+
console.debug(`Battery model 124 not found on unit ${unitId}`);
|
|
1770
1788
|
return null;
|
|
1771
1789
|
}
|
|
1772
|
-
console.
|
|
1790
|
+
console.debug(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
|
|
1773
1791
|
try {
|
|
1774
1792
|
// Read entire model block in a single Modbus call
|
|
1775
1793
|
const buffer = await this.readModelBlock(unitId, model);
|
|
@@ -1984,7 +2002,7 @@ class SunspecModbusClient {
|
|
|
1984
2002
|
console.debug(`Common block model not found on unit ${unitId}`);
|
|
1985
2003
|
return null;
|
|
1986
2004
|
}
|
|
1987
|
-
console.
|
|
2005
|
+
console.debug(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
|
|
1988
2006
|
try {
|
|
1989
2007
|
// Read entire model block in a single Modbus call
|
|
1990
2008
|
const buffer = await this.readModelBlock(unitId, model);
|
|
@@ -2144,7 +2162,8 @@ class SunspecModbusClient {
|
|
|
2144
2162
|
async readInverterControls(unitId) {
|
|
2145
2163
|
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
2146
2164
|
if (!model) {
|
|
2147
|
-
|
|
2165
|
+
// Same trace-only rationale as the Model 124 not-found message above.
|
|
2166
|
+
console.debug(`Controls model 123 not found on unit ${unitId}`);
|
|
2148
2167
|
return null;
|
|
2149
2168
|
}
|
|
2150
2169
|
try {
|
|
@@ -2253,17 +2272,17 @@ class SunspecModbusClient {
|
|
|
2253
2272
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
2254
2273
|
const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
|
|
2255
2274
|
// Writing registers needs to be implemented in EnergyAppModbusInstance
|
|
2256
|
-
// For now, log the write operation
|
|
2257
|
-
console.
|
|
2275
|
+
// For now, log the (would-be) write operation. Dry-run trace only — debug.
|
|
2276
|
+
console.debug(`Would write value ${scaledValue} to register ${baseAddr}`);
|
|
2258
2277
|
}
|
|
2259
2278
|
if (settings.VRef !== undefined) {
|
|
2260
2279
|
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
|
|
2261
2280
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
2262
2281
|
const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
|
|
2263
|
-
console.
|
|
2282
|
+
console.debug(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
|
|
2264
2283
|
}
|
|
2265
2284
|
// Add more write operations for other settings as needed
|
|
2266
|
-
console.
|
|
2285
|
+
console.debug('Inverter settings written successfully');
|
|
2267
2286
|
return true;
|
|
2268
2287
|
}
|
|
2269
2288
|
catch (error) {
|
|
@@ -2282,36 +2301,37 @@ class SunspecModbusClient {
|
|
|
2282
2301
|
}
|
|
2283
2302
|
const baseAddr = model.address;
|
|
2284
2303
|
try {
|
|
2285
|
-
//
|
|
2304
|
+
// Per-field inverter-control writes are debug-only; the high-level callers
|
|
2305
|
+
// (`setFeedInLimit`, etc.) still log the action and outcome at info level.
|
|
2286
2306
|
if (controls.Conn !== undefined) {
|
|
2287
2307
|
await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
|
|
2288
|
-
console.
|
|
2308
|
+
console.debug(`Set connection control to ${controls.Conn}`);
|
|
2289
2309
|
}
|
|
2290
2310
|
// Power limit control (Register 3) - needs scale factor
|
|
2291
2311
|
if (controls.WMaxLimPct !== undefined) {
|
|
2292
2312
|
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
|
|
2293
2313
|
const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
|
|
2294
2314
|
await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
|
|
2295
|
-
console.
|
|
2315
|
+
console.debug(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
|
|
2296
2316
|
}
|
|
2297
2317
|
// Throttle enable/disable (Register 7)
|
|
2298
2318
|
if (controls.WMaxLim_Ena !== undefined) {
|
|
2299
2319
|
await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
2300
|
-
console.
|
|
2320
|
+
console.debug(`Set throttle enable to ${controls.WMaxLim_Ena}`);
|
|
2301
2321
|
}
|
|
2302
2322
|
// Power factor control (Register 8) - needs scale factor
|
|
2303
2323
|
if (controls.OutPFSet !== undefined) {
|
|
2304
2324
|
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
|
|
2305
2325
|
const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
|
|
2306
2326
|
await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
|
|
2307
|
-
console.
|
|
2327
|
+
console.debug(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
|
|
2308
2328
|
}
|
|
2309
2329
|
// Power factor enable/disable (Register 12)
|
|
2310
2330
|
if (controls.OutPFSet_Ena !== undefined) {
|
|
2311
2331
|
await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
2312
|
-
console.
|
|
2332
|
+
console.debug(`Set PF enable to ${controls.OutPFSet_Ena}`);
|
|
2313
2333
|
}
|
|
2314
|
-
console.
|
|
2334
|
+
console.debug('Inverter controls written successfully');
|
|
2315
2335
|
return true;
|
|
2316
2336
|
}
|
|
2317
2337
|
catch (error) {
|
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.76';
|
|
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
|
@@ -178,8 +178,6 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
178
178
|
* process restarts. Idempotent.
|
|
179
179
|
*/
|
|
180
180
|
private initSnapshotService;
|
|
181
|
-
private handleStartCalibration;
|
|
182
|
-
private handleStopCalibration;
|
|
183
181
|
/**
|
|
184
182
|
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
185
183
|
* subset of writable inverter fields that other commands actually touched during the
|
|
@@ -217,6 +215,24 @@ export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryD
|
|
|
217
215
|
* the full detected set to preserve the old all-or-nothing semantics on
|
|
218
216
|
* upgrade.
|
|
219
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* Pick the live battery power, in preference order:
|
|
220
|
+
*
|
|
221
|
+
* 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
|
|
222
|
+
* passed in as the already-summed `mpptBatteryPowerW`.
|
|
223
|
+
* 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
|
|
224
|
+
* by the Model 802 path of `SunspecModbusClient.readBatteryData`.
|
|
225
|
+
* 3. `undefined` — no reliable live source on this device.
|
|
226
|
+
*
|
|
227
|
+
* The Model 124 path of `readBatteryData` no longer manufactures
|
|
228
|
+
* `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
|
|
229
|
+
* cap, not a live measurement). So whenever this function sees
|
|
230
|
+
* `chargePower` / `dischargePower` defined, they came from Model 802.
|
|
231
|
+
*
|
|
232
|
+
* Exported as a free function so the preference logic can be unit-tested
|
|
233
|
+
* without the full `SunspecBattery` scaffold.
|
|
234
|
+
*/
|
|
235
|
+
export declare function selectLiveBatteryPowerW(mpptBatteryPowerW: number | undefined, batteryData: SunspecBatteryData | null): number | undefined;
|
|
220
236
|
export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
|
|
221
237
|
/**
|
|
222
238
|
* Sunspec Battery implementation
|
|
@@ -408,8 +424,6 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
408
424
|
* process restarts. Idempotent.
|
|
409
425
|
*/
|
|
410
426
|
private initSnapshotService;
|
|
411
|
-
private handleStartCalibration;
|
|
412
|
-
private handleStopCalibration;
|
|
413
427
|
/**
|
|
414
428
|
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
415
429
|
* subset of writable battery fields touched during the calibration. Errors are
|
|
@@ -432,13 +446,6 @@ export declare class SunspecMeter extends BaseSunspecDevice {
|
|
|
432
446
|
* Disconnect from the meter and update appliance state
|
|
433
447
|
*/
|
|
434
448
|
disconnect(): Promise<void>;
|
|
435
|
-
/**
|
|
436
|
-
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
437
|
-
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
438
|
-
*/
|
|
439
|
-
startDataBusListening(): void;
|
|
440
|
-
stopDataBusListening(): void;
|
|
441
|
-
private handleMeterCommand;
|
|
442
449
|
/**
|
|
443
450
|
* Update meter data and return data bus messages
|
|
444
451
|
*/
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -705,8 +705,6 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
705
705
|
this.dataBus = this.energyApp.useDataBus();
|
|
706
706
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
707
707
|
EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
708
|
-
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
709
|
-
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
710
708
|
], (entry) => this.handleInverterCommand(entry));
|
|
711
709
|
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
712
710
|
}
|
|
@@ -731,12 +729,6 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
731
729
|
case EnyoDataBusMessageEnum.SetInverterFeedInLimitV1:
|
|
732
730
|
await this.handleSetFeedInLimit(entry);
|
|
733
731
|
break;
|
|
734
|
-
case EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
735
|
-
await this.handleStartCalibration(entry);
|
|
736
|
-
break;
|
|
737
|
-
case EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
738
|
-
await this.handleStopCalibration(entry);
|
|
739
|
-
break;
|
|
740
732
|
}
|
|
741
733
|
}
|
|
742
734
|
catch (error) {
|
|
@@ -782,51 +774,6 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
782
774
|
await this.snapshotService.initialize();
|
|
783
775
|
console.log(`Inverter ${this.applianceId}: snapshot service initialized`);
|
|
784
776
|
}
|
|
785
|
-
async handleStartCalibration(msg) {
|
|
786
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
787
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (!this.snapshotService) {
|
|
791
|
-
await this.initSnapshotService();
|
|
792
|
-
}
|
|
793
|
-
if (!this.snapshotService) {
|
|
794
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (this.snapshotService.isCalibrating()) {
|
|
798
|
-
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
799
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
console.log(`Inverter ${this.applianceId}: handling StartCalibrationV1`);
|
|
803
|
-
const controls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
804
|
-
if (!controls) {
|
|
805
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read inverter controls for snapshot');
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
try {
|
|
809
|
-
await this.snapshotService.startCalibration(controls);
|
|
810
|
-
}
|
|
811
|
-
catch (error) {
|
|
812
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
816
|
-
}
|
|
817
|
-
async handleStopCalibration(msg) {
|
|
818
|
-
if (!this.applianceId) {
|
|
819
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
|
|
823
|
-
// SnapshotService.stopCalibration fires `restoreInverterSnapshot` internally with
|
|
824
|
-
// reason="stop" before returning. Any restore failure is logged by the callback
|
|
825
|
-
// (the persisted snapshot is already gone by then — see library notes) so we
|
|
826
|
-
// always ack Accepted from here.
|
|
827
|
-
await this.snapshotService?.stopCalibration();
|
|
828
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
829
|
-
}
|
|
830
777
|
/**
|
|
831
778
|
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
832
779
|
* subset of writable inverter fields that other commands actually touched during the
|
|
@@ -898,6 +845,35 @@ export function detectFeaturesFromRegisters(batteryData) {
|
|
|
898
845
|
* the full detected set to preserve the old all-or-nothing semantics on
|
|
899
846
|
* upgrade.
|
|
900
847
|
*/
|
|
848
|
+
/**
|
|
849
|
+
* Pick the live battery power, in preference order:
|
|
850
|
+
*
|
|
851
|
+
* 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
|
|
852
|
+
* passed in as the already-summed `mpptBatteryPowerW`.
|
|
853
|
+
* 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
|
|
854
|
+
* by the Model 802 path of `SunspecModbusClient.readBatteryData`.
|
|
855
|
+
* 3. `undefined` — no reliable live source on this device.
|
|
856
|
+
*
|
|
857
|
+
* The Model 124 path of `readBatteryData` no longer manufactures
|
|
858
|
+
* `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
|
|
859
|
+
* cap, not a live measurement). So whenever this function sees
|
|
860
|
+
* `chargePower` / `dischargePower` defined, they came from Model 802.
|
|
861
|
+
*
|
|
862
|
+
* Exported as a free function so the preference logic can be unit-tested
|
|
863
|
+
* without the full `SunspecBattery` scaffold.
|
|
864
|
+
*/
|
|
865
|
+
export function selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData) {
|
|
866
|
+
if (mpptBatteryPowerW !== undefined) {
|
|
867
|
+
return mpptBatteryPowerW;
|
|
868
|
+
}
|
|
869
|
+
if (!batteryData) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
if (batteryData.chargePower === undefined && batteryData.dischargePower === undefined) {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
return (batteryData.chargePower ?? 0) - (batteryData.dischargePower ?? 0);
|
|
876
|
+
}
|
|
901
877
|
export function filterFeaturesByCalibrationResult(detected, result, controllable) {
|
|
902
878
|
if (!result || result.state !== 'calibrated') {
|
|
903
879
|
return detected.filter(f => !controllable.includes(f));
|
|
@@ -1161,17 +1137,8 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1161
1137
|
return messages;
|
|
1162
1138
|
}
|
|
1163
1139
|
if (batteryData) {
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
// Determine battery power: prefer model 802 w field, then MPPT extraction, then undefined
|
|
1167
|
-
let batteryPowerW;
|
|
1168
|
-
if (batteryBaseModel && (batteryData.chargePower !== undefined || batteryData.dischargePower !== undefined)) {
|
|
1169
|
-
// Model 802 provides power directly: positive = charge, negative = discharge
|
|
1170
|
-
batteryPowerW = (batteryData.chargePower || 0) - (batteryData.dischargePower || 0);
|
|
1171
|
-
}
|
|
1172
|
-
else if (!advancedBatteryModel) {
|
|
1173
|
-
batteryPowerW = mpptBatteryPowerW;
|
|
1174
|
-
}
|
|
1140
|
+
// See `selectLiveBatteryPowerW` for the source-preference rationale.
|
|
1141
|
+
const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
|
|
1175
1142
|
// Feed the calibration driver's synchronous power cache. No-op when calibration
|
|
1176
1143
|
// was never configured.
|
|
1177
1144
|
if (batteryPowerW !== undefined && this.calibrationDriver) {
|
|
@@ -1364,7 +1331,10 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1364
1331
|
console.error('Battery not connected');
|
|
1365
1332
|
return false;
|
|
1366
1333
|
}
|
|
1367
|
-
|
|
1334
|
+
// Redundant with the modbus client's own per-write debug trace. Keep at debug
|
|
1335
|
+
// so the high-level info logs (setStorageMode / enableGridCharging / etc.) stay
|
|
1336
|
+
// the only info-level signal per battery-control action.
|
|
1337
|
+
console.debug('Writing battery controls:', controls);
|
|
1368
1338
|
return this.sunspecClient.writeBatteryControls(this.unitId, controls);
|
|
1369
1339
|
}
|
|
1370
1340
|
mapToEnyoStorageMode(storageMode) {
|
|
@@ -1462,8 +1432,6 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1462
1432
|
this.dataBus = this.energyApp.useDataBus();
|
|
1463
1433
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1464
1434
|
EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1465
|
-
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1466
|
-
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1467
1435
|
], (entry) => this.handleStorageCommand(entry));
|
|
1468
1436
|
if (!this.applianceId) {
|
|
1469
1437
|
throw new Error("SunspecBattery.startDataBusListening: applianceId required — call connect() first.");
|
|
@@ -1502,12 +1470,6 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1502
1470
|
case EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1503
1471
|
this.handleSetStorageScheduleAck(entry);
|
|
1504
1472
|
break;
|
|
1505
|
-
case EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1506
|
-
await this.handleStartCalibration(entry);
|
|
1507
|
-
break;
|
|
1508
|
-
case EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
1509
|
-
await this.handleStopCalibration(entry);
|
|
1510
|
-
break;
|
|
1511
1473
|
}
|
|
1512
1474
|
}
|
|
1513
1475
|
catch (error) {
|
|
@@ -1547,51 +1509,6 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1547
1509
|
await this.snapshotService.initialize();
|
|
1548
1510
|
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1549
1511
|
}
|
|
1550
|
-
async handleStartCalibration(msg) {
|
|
1551
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1552
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
if (!this.snapshotService) {
|
|
1556
|
-
await this.initSnapshotService();
|
|
1557
|
-
}
|
|
1558
|
-
if (!this.snapshotService) {
|
|
1559
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
if (this.snapshotService.isCalibrating()) {
|
|
1563
|
-
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1564
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
console.log(`Battery ${this.applianceId}: handling StartCalibrationV1`);
|
|
1568
|
-
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
1569
|
-
if (!controls) {
|
|
1570
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read battery controls for snapshot');
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
try {
|
|
1574
|
-
await this.snapshotService.startCalibration(controls);
|
|
1575
|
-
}
|
|
1576
|
-
catch (error) {
|
|
1577
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1581
|
-
}
|
|
1582
|
-
async handleStopCalibration(msg) {
|
|
1583
|
-
if (!this.applianceId) {
|
|
1584
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
|
|
1588
|
-
// SnapshotService.stopCalibration fires `restoreBatterySnapshot` internally
|
|
1589
|
-
// with reason="stop" before returning. Failures from the restore callback are
|
|
1590
|
-
// logged inside the callback (the persisted snapshot is already gone — see
|
|
1591
|
-
// library notes) so we always ack Accepted here.
|
|
1592
|
-
await this.snapshotService?.stopCalibration();
|
|
1593
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1594
|
-
}
|
|
1595
1512
|
/**
|
|
1596
1513
|
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
1597
1514
|
* subset of writable battery fields touched during the calibration. Errors are
|
|
@@ -1696,13 +1613,11 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1696
1613
|
console.error(`Failed to update meter appliance: ${error}`);
|
|
1697
1614
|
}
|
|
1698
1615
|
}
|
|
1699
|
-
this.startDataBusListening();
|
|
1700
1616
|
}
|
|
1701
1617
|
/**
|
|
1702
1618
|
* Disconnect from the meter and update appliance state
|
|
1703
1619
|
*/
|
|
1704
1620
|
async disconnect() {
|
|
1705
|
-
this.stopDataBusListening();
|
|
1706
1621
|
if (this.applianceId) {
|
|
1707
1622
|
try {
|
|
1708
1623
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
@@ -1714,35 +1629,6 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1714
1629
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1715
1630
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
|
1716
1631
|
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
1719
|
-
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
1720
|
-
*/
|
|
1721
|
-
startDataBusListening() {
|
|
1722
|
-
if (this.dataBusListenerId) {
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
this.dataBus = this.energyApp.useDataBus();
|
|
1726
|
-
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1727
|
-
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1728
|
-
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1729
|
-
], (entry) => this.handleMeterCommand(entry));
|
|
1730
|
-
console.log(`Meter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1731
|
-
}
|
|
1732
|
-
stopDataBusListening() {
|
|
1733
|
-
if (this.dataBusListenerId && this.dataBus) {
|
|
1734
|
-
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
1735
|
-
console.log(`Meter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
1736
|
-
}
|
|
1737
|
-
this.dataBusListenerId = undefined;
|
|
1738
|
-
this.dataBus = undefined;
|
|
1739
|
-
}
|
|
1740
|
-
handleMeterCommand(entry) {
|
|
1741
|
-
if (entry.applianceId !== this.applianceId) {
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
this.sendCommandAcknowledge(entry.id, entry.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'Meter does not support calibration');
|
|
1745
|
-
}
|
|
1746
1632
|
/**
|
|
1747
1633
|
* Update meter data and return data bus messages
|
|
1748
1634
|
*/
|
|
@@ -481,7 +481,10 @@ export class SunspecModbusClient {
|
|
|
481
481
|
}
|
|
482
482
|
catch (error) {
|
|
483
483
|
console.debug(`No SunSpec device at unit ${unitId}: ${error}`);
|
|
484
|
-
|
|
484
|
+
// Discovery summary at line 594 below is the info-level outcome.
|
|
485
|
+
// This 0-models branch is the same outcome as a normal "no device found",
|
|
486
|
+
// covered already by the debug above. Keep at debug.
|
|
487
|
+
console.debug(`Discovery complete for unit ${unitId}. Found 0 models: []`);
|
|
485
488
|
return models;
|
|
486
489
|
}
|
|
487
490
|
currentAddress = addressInfo.nextAddress;
|
|
@@ -490,14 +493,14 @@ export class SunspecModbusClient {
|
|
|
490
493
|
const buffer = await instance.readHoldingRegisters(currentAddress, 2);
|
|
491
494
|
const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
|
|
492
495
|
if (!modelData || modelData.length < 2) {
|
|
493
|
-
console.
|
|
496
|
+
console.debug(`No data at address ${currentAddress}, ending discovery`);
|
|
494
497
|
break;
|
|
495
498
|
}
|
|
496
499
|
const modelId = modelData[0];
|
|
497
500
|
const modelLength = modelData[1];
|
|
498
501
|
// Check for end marker
|
|
499
502
|
if (modelId === 0xFFFF || modelId === 65535) {
|
|
500
|
-
console.
|
|
503
|
+
console.debug(`Found end marker at address ${currentAddress}`);
|
|
501
504
|
break;
|
|
502
505
|
}
|
|
503
506
|
// Store discovered model
|
|
@@ -507,7 +510,9 @@ export class SunspecModbusClient {
|
|
|
507
510
|
length: modelLength
|
|
508
511
|
};
|
|
509
512
|
models.set(modelId, model);
|
|
510
|
-
|
|
513
|
+
// Per-model discovery step. The end-of-discovery summary below is the
|
|
514
|
+
// info-level outcome; the per-model walk is debug-only.
|
|
515
|
+
console.debug(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
|
|
511
516
|
// Jump to next model: current address + 2 (header) + model length
|
|
512
517
|
currentAddress = currentAddress + 2 + modelLength;
|
|
513
518
|
}
|
|
@@ -696,7 +701,9 @@ export class SunspecModbusClient {
|
|
|
696
701
|
// Write to holding registers
|
|
697
702
|
await instance.writeMultipleRegisters(address, registerValues);
|
|
698
703
|
this.connectionHealth.recordSuccess();
|
|
699
|
-
|
|
704
|
+
// Per-register write success — fires for every parameter inside higher-level
|
|
705
|
+
// writers (writeBatteryControls / writeInverterControls). Demoted to debug.
|
|
706
|
+
console.debug(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
|
|
700
707
|
return true;
|
|
701
708
|
}
|
|
702
709
|
catch (error) {
|
|
@@ -1584,17 +1591,21 @@ export class SunspecModbusClient {
|
|
|
1584
1591
|
voltage: this.applyScaleFactor(inBatVRaw, scaleFactors.InBatV_SF, 'uint16'),
|
|
1585
1592
|
status: !this.isUnimplementedValue(chaStRaw, 'enum16') ? chaStRaw : undefined
|
|
1586
1593
|
};
|
|
1587
|
-
//
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1594
|
+
// chargePower / dischargePower used to be derived from
|
|
1595
|
+
// `(inWRte / 100) * wChaMax` and `(outWRte / 100) * wChaMax`
|
|
1596
|
+
// here. That formula uses Model 124's COMMANDED RATE CAPS — not
|
|
1597
|
+
// a live measurement — and produces e.g. 5000 W of "charging"
|
|
1598
|
+
// for an idle battery with inWRte = 100 % and a 5 kW nameplate.
|
|
1599
|
+
// Live battery power must come from a real source: Model 160
|
|
1600
|
+
// StCha 3 / StDisCha 4 (see SunspecBattery.extractBatteryPower
|
|
1601
|
+
// FromMPPT) or Model 802 `w` at offset 47 (the Model 802 branch
|
|
1602
|
+
// of this method, just below). Model-124-only devices should
|
|
1603
|
+
// publish "no signal" rather than the commanded cap.
|
|
1604
|
+
// See plans/please-review-the-implementation-groovy-thacker.md.
|
|
1605
|
+
// Single-line JSON debug dump — Node's default formatter would split
|
|
1606
|
+
// the object across many lines; stringify keeps the whole snapshot
|
|
1607
|
+
// on one log entry so it stays grep-able and round-trippable.
|
|
1608
|
+
console.debug(`[Battery] unit=${unitId} model=124 data=${JSON.stringify(data)}`);
|
|
1598
1609
|
return data;
|
|
1599
1610
|
}
|
|
1600
1611
|
else if (model.id === 802) {
|
|
@@ -1631,17 +1642,18 @@ export class SunspecModbusClient {
|
|
|
1631
1642
|
chargePower,
|
|
1632
1643
|
dischargePower,
|
|
1633
1644
|
};
|
|
1634
|
-
console.debug(
|
|
1645
|
+
console.debug(`[Battery] unit=${unitId} model=802 data=${JSON.stringify(result)}`);
|
|
1635
1646
|
return result;
|
|
1636
1647
|
}
|
|
1637
1648
|
else {
|
|
1638
|
-
// Handle other battery models (803) if needed
|
|
1639
|
-
|
|
1640
|
-
return {
|
|
1649
|
+
// Handle other battery models (803) if needed.
|
|
1650
|
+
const stub = {
|
|
1641
1651
|
blockNumber: model.id,
|
|
1642
1652
|
blockAddress: model.address,
|
|
1643
|
-
blockLength: model.length
|
|
1653
|
+
blockLength: model.length,
|
|
1644
1654
|
};
|
|
1655
|
+
console.debug(`[Battery] unit=${unitId} model=${model.id} (not yet implemented) data=${JSON.stringify(stub)}`);
|
|
1656
|
+
return stub;
|
|
1645
1657
|
}
|
|
1646
1658
|
}
|
|
1647
1659
|
catch (error) {
|
|
@@ -1659,7 +1671,11 @@ export class SunspecModbusClient {
|
|
|
1659
1671
|
return false;
|
|
1660
1672
|
}
|
|
1661
1673
|
const baseAddr = model.address;
|
|
1662
|
-
|
|
1674
|
+
// Per-register write trace is high-volume (every schedule entry transition fires
|
|
1675
|
+
// a writeBatteryControls). Demoted to debug; consumers that need the trail can
|
|
1676
|
+
// raise their log level. High-level callers (setStorageMode, enableGridCharging,
|
|
1677
|
+
// setFeedInLimit, etc.) still emit one info-level line per action.
|
|
1678
|
+
console.debug(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1663
1679
|
try {
|
|
1664
1680
|
// Write order: source pin and parameter writes land BEFORE the
|
|
1665
1681
|
// control mode so the device only "starts acting" once every
|
|
@@ -1669,7 +1685,7 @@ export class SunspecModbusClient {
|
|
|
1669
1685
|
// Write charge source setting (Register 17)
|
|
1670
1686
|
if (controls.chaGriSet !== undefined) {
|
|
1671
1687
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1672
|
-
console.
|
|
1688
|
+
console.debug(`Set charge source to ${controls.chaGriSet === SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
|
|
1673
1689
|
}
|
|
1674
1690
|
// Write maximum charge power (Register 2) - needs scale factor
|
|
1675
1691
|
if (controls.wChaMax !== undefined) {
|
|
@@ -1677,7 +1693,7 @@ export class SunspecModbusClient {
|
|
|
1677
1693
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1678
1694
|
const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
|
|
1679
1695
|
await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
|
|
1680
|
-
console.
|
|
1696
|
+
console.debug(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
|
|
1681
1697
|
}
|
|
1682
1698
|
// Write charge rate (Register 13) - needs scale factor
|
|
1683
1699
|
if (controls.inWRte !== undefined) {
|
|
@@ -1685,7 +1701,7 @@ export class SunspecModbusClient {
|
|
|
1685
1701
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1686
1702
|
const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
|
|
1687
1703
|
await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
|
|
1688
|
-
console.
|
|
1704
|
+
console.debug(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
|
|
1689
1705
|
}
|
|
1690
1706
|
// Write discharge rate (Register 12) - needs scale factor
|
|
1691
1707
|
if (controls.outWRte !== undefined) {
|
|
@@ -1693,7 +1709,7 @@ export class SunspecModbusClient {
|
|
|
1693
1709
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1694
1710
|
const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
|
|
1695
1711
|
await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
|
|
1696
|
-
console.
|
|
1712
|
+
console.debug(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
|
|
1697
1713
|
}
|
|
1698
1714
|
// Write minimum reserve percentage (Register 7) - needs scale factor
|
|
1699
1715
|
if (controls.minRsvPct !== undefined) {
|
|
@@ -1701,16 +1717,16 @@ export class SunspecModbusClient {
|
|
|
1701
1717
|
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1702
1718
|
const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
|
|
1703
1719
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1704
|
-
console.
|
|
1720
|
+
console.debug(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1705
1721
|
}
|
|
1706
1722
|
// Storage control mode (Register 5) — written LAST so all
|
|
1707
1723
|
// governing parameters are already in place when the device
|
|
1708
1724
|
// transitions into the new mode.
|
|
1709
1725
|
if (controls.storCtlMod !== undefined) {
|
|
1710
1726
|
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1711
|
-
console.
|
|
1727
|
+
console.debug(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1712
1728
|
}
|
|
1713
|
-
console.
|
|
1729
|
+
console.debug('Battery controls written successfully');
|
|
1714
1730
|
return true;
|
|
1715
1731
|
}
|
|
1716
1732
|
catch (error) {
|
|
@@ -1761,10 +1777,12 @@ export class SunspecModbusClient {
|
|
|
1761
1777
|
async readBatteryControls(unitId) {
|
|
1762
1778
|
const model = this.findModel(unitId, SunspecModelId.Battery);
|
|
1763
1779
|
if (!model) {
|
|
1764
|
-
|
|
1780
|
+
// Fires per readBatteryControls attempt — noisy on non-battery units
|
|
1781
|
+
// that go through the same code path. Demoted to debug.
|
|
1782
|
+
console.debug(`Battery model 124 not found on unit ${unitId}`);
|
|
1765
1783
|
return null;
|
|
1766
1784
|
}
|
|
1767
|
-
console.
|
|
1785
|
+
console.debug(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
|
|
1768
1786
|
try {
|
|
1769
1787
|
// Read entire model block in a single Modbus call
|
|
1770
1788
|
const buffer = await this.readModelBlock(unitId, model);
|
|
@@ -1979,7 +1997,7 @@ export class SunspecModbusClient {
|
|
|
1979
1997
|
console.debug(`Common block model not found on unit ${unitId}`);
|
|
1980
1998
|
return null;
|
|
1981
1999
|
}
|
|
1982
|
-
console.
|
|
2000
|
+
console.debug(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
|
|
1983
2001
|
try {
|
|
1984
2002
|
// Read entire model block in a single Modbus call
|
|
1985
2003
|
const buffer = await this.readModelBlock(unitId, model);
|
|
@@ -2139,7 +2157,8 @@ export class SunspecModbusClient {
|
|
|
2139
2157
|
async readInverterControls(unitId) {
|
|
2140
2158
|
const model = this.findModel(unitId, SunspecModelId.Controls);
|
|
2141
2159
|
if (!model) {
|
|
2142
|
-
|
|
2160
|
+
// Same trace-only rationale as the Model 124 not-found message above.
|
|
2161
|
+
console.debug(`Controls model 123 not found on unit ${unitId}`);
|
|
2143
2162
|
return null;
|
|
2144
2163
|
}
|
|
2145
2164
|
try {
|
|
@@ -2248,17 +2267,17 @@ export class SunspecModbusClient {
|
|
|
2248
2267
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
2249
2268
|
const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
|
|
2250
2269
|
// Writing registers needs to be implemented in EnergyAppModbusInstance
|
|
2251
|
-
// For now, log the write operation
|
|
2252
|
-
console.
|
|
2270
|
+
// For now, log the (would-be) write operation. Dry-run trace only — debug.
|
|
2271
|
+
console.debug(`Would write value ${scaledValue} to register ${baseAddr}`);
|
|
2253
2272
|
}
|
|
2254
2273
|
if (settings.VRef !== undefined) {
|
|
2255
2274
|
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
|
|
2256
2275
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
2257
2276
|
const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
|
|
2258
|
-
console.
|
|
2277
|
+
console.debug(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
|
|
2259
2278
|
}
|
|
2260
2279
|
// Add more write operations for other settings as needed
|
|
2261
|
-
console.
|
|
2280
|
+
console.debug('Inverter settings written successfully');
|
|
2262
2281
|
return true;
|
|
2263
2282
|
}
|
|
2264
2283
|
catch (error) {
|
|
@@ -2277,36 +2296,37 @@ export class SunspecModbusClient {
|
|
|
2277
2296
|
}
|
|
2278
2297
|
const baseAddr = model.address;
|
|
2279
2298
|
try {
|
|
2280
|
-
//
|
|
2299
|
+
// Per-field inverter-control writes are debug-only; the high-level callers
|
|
2300
|
+
// (`setFeedInLimit`, etc.) still log the action and outcome at info level.
|
|
2281
2301
|
if (controls.Conn !== undefined) {
|
|
2282
2302
|
await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
|
|
2283
|
-
console.
|
|
2303
|
+
console.debug(`Set connection control to ${controls.Conn}`);
|
|
2284
2304
|
}
|
|
2285
2305
|
// Power limit control (Register 3) - needs scale factor
|
|
2286
2306
|
if (controls.WMaxLimPct !== undefined) {
|
|
2287
2307
|
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
|
|
2288
2308
|
const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
|
|
2289
2309
|
await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
|
|
2290
|
-
console.
|
|
2310
|
+
console.debug(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
|
|
2291
2311
|
}
|
|
2292
2312
|
// Throttle enable/disable (Register 7)
|
|
2293
2313
|
if (controls.WMaxLim_Ena !== undefined) {
|
|
2294
2314
|
await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
2295
|
-
console.
|
|
2315
|
+
console.debug(`Set throttle enable to ${controls.WMaxLim_Ena}`);
|
|
2296
2316
|
}
|
|
2297
2317
|
// Power factor control (Register 8) - needs scale factor
|
|
2298
2318
|
if (controls.OutPFSet !== undefined) {
|
|
2299
2319
|
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
|
|
2300
2320
|
const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
|
|
2301
2321
|
await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
|
|
2302
|
-
console.
|
|
2322
|
+
console.debug(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
|
|
2303
2323
|
}
|
|
2304
2324
|
// Power factor enable/disable (Register 12)
|
|
2305
2325
|
if (controls.OutPFSet_Ena !== undefined) {
|
|
2306
2326
|
await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
2307
|
-
console.
|
|
2327
|
+
console.debug(`Set PF enable to ${controls.OutPFSet_Ena}`);
|
|
2308
2328
|
}
|
|
2309
|
-
console.
|
|
2329
|
+
console.debug('Inverter controls written successfully');
|
|
2310
2330
|
return true;
|
|
2311
2331
|
}
|
|
2312
2332
|
catch (error) {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enyo-energy/sunspec-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.76",
|
|
4
4
|
"description": "enyo Energy Sunspec SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@enyo-energy/appliance-calibration": "0.0.2",
|
|
44
|
-
"@enyo-energy/energy-app-sdk": "0.0.
|
|
44
|
+
"@enyo-energy/energy-app-sdk": "0.0.144"
|
|
45
45
|
},
|
|
46
46
|
"volta": {
|
|
47
47
|
"node": "22.17.0"
|