@enyo-energy/sunspec-sdk 0.0.72 → 0.0.74
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/README.md +5 -1
- package/dist/cjs/sunspec-battery-schedule-handler.cjs +72 -5
- package/dist/cjs/sunspec-battery-schedule-handler.d.cts +27 -0
- package/dist/cjs/sunspec-devices.cjs +55 -20
- package/dist/cjs/sunspec-devices.d.cts +32 -2
- package/dist/cjs/sunspec-modbus-client.cjs +12 -5
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-battery-schedule-handler.d.ts +27 -0
- package/dist/sunspec-battery-schedule-handler.js +73 -6
- package/dist/sunspec-devices.d.ts +32 -2
- package/dist/sunspec-devices.js +53 -20
- package/dist/sunspec-modbus-client.js +12 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,7 +332,11 @@ See [`@enyo-energy/appliance-calibration`'s README](https://www.npmjs.com/packag
|
|
|
332
332
|
3. Activates the first entry immediately, then advances to subsequent entries as their `seconds` offsets elapse on a 1-second tick (driven by `EnergyApp.useInterval()`).
|
|
333
333
|
4. Each `Charge` entry writes `chaGriSet=GRID`, `inWRte = powerW / installedWChaMax × 100`, `storCtlMod=CHARGE`.
|
|
334
334
|
5. Each `Discharge` entry writes `chaGriSet=PV`, `outWRte = powerW / installedWChaMax × 100`, `storCtlMod=DISCHARGE`.
|
|
335
|
-
6.
|
|
335
|
+
6. Restores the snapshotted pre-schedule registers (`storCtlMod`, `chaGriSet`, `wChaMax`, `inWRte`, `outWRte`) **only on `mode: auto`, `disconnect()`, or process-restart recovery** — not when one `mode: schedule` is replaced by another. Schedule-to-schedule replacement keeps the device on the active register set and lets the new schedule's first entry take over directly, so consecutive schedules don't leak a `storCtlMod=AUTO` (`0x3`) write between them. The pre-schedule snapshot stays sticky across every replacement until one of the three terminal events fires.
|
|
336
|
+
|
|
337
|
+
### Register write order
|
|
338
|
+
|
|
339
|
+
`writeBatteryControls` issues each register write sequentially in this order so the device never sees a stale-parameter window when the control mode changes: `chaGriSet → wChaMax → inWRte → outWRte → minRsvPct → storCtlMod`. Source pin and the limit/rate parameters land first; the control mode is written last so the device only "starts acting" once every governing value is fresh.
|
|
336
340
|
|
|
337
341
|
### Power cap
|
|
338
342
|
|
|
@@ -18,6 +18,23 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
18
18
|
getSnapshotService;
|
|
19
19
|
onErrorCallback;
|
|
20
20
|
installedWChaMax;
|
|
21
|
+
/**
|
|
22
|
+
* Sticky pre-schedule snapshot, captured once on the first `onInit` and
|
|
23
|
+
* held across every subsequent schedule-to-schedule replacement. Cleared
|
|
24
|
+
* only when a real rollback fires (`mode: auto` from the data bus or
|
|
25
|
+
* `dispose`). Lets the eventual restore write the *true* baseline rather
|
|
26
|
+
* than the last-active-entry register set that the device happens to be
|
|
27
|
+
* in at the moment of replacement.
|
|
28
|
+
*/
|
|
29
|
+
originalBaseline;
|
|
30
|
+
/**
|
|
31
|
+
* Set inside the overridden `applyMessage` when an incoming
|
|
32
|
+
* `mode: schedule` message would replace an already-running schedule.
|
|
33
|
+
* Consumed (and reset) inside `onRollback` so the base library's
|
|
34
|
+
* automatic `doReleaseSchedule → onRollback` step does not actually
|
|
35
|
+
* write the snapshot back during a replacement.
|
|
36
|
+
*/
|
|
37
|
+
suppressNextRollbackWrite = false;
|
|
21
38
|
constructor(opts) {
|
|
22
39
|
super(opts);
|
|
23
40
|
this.sunspecClient = opts.sunspecClient;
|
|
@@ -26,19 +43,53 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
26
43
|
this.getSnapshotService = opts.getSnapshotService;
|
|
27
44
|
this.onErrorCallback = opts.onErrorCallback;
|
|
28
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Override the base-class data-bus router so a schedule-to-schedule
|
|
48
|
+
* replacement marks the next `onRollback` as "skip the write". The base
|
|
49
|
+
* library still owns the actual schedule lifecycle — we just steer one
|
|
50
|
+
* decision inside `onRollback`. `mode: auto` and any path that does not
|
|
51
|
+
* have an active schedule fall through unchanged, so the rollback fires
|
|
52
|
+
* normally there.
|
|
53
|
+
*/
|
|
54
|
+
async applyMessage(msg) {
|
|
55
|
+
if (msg.applianceId === this.applianceIdForLog
|
|
56
|
+
&& msg.data.mode === enyo_data_bus_value_js_1.EnyoStorageScheduleModeEnum.Schedule
|
|
57
|
+
&& this.getActiveEntry() !== undefined) {
|
|
58
|
+
this.suppressNextRollbackWrite = true;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await super.applyMessage(msg);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
// Defence in depth: if the base class skipped the rollback for
|
|
65
|
+
// any reason (validation error, disposed, etc.), the flag would
|
|
66
|
+
// otherwise stay set and silently swallow the next real rollback.
|
|
67
|
+
this.suppressNextRollbackWrite = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
29
70
|
async onInit(_active) {
|
|
71
|
+
if (this.originalBaseline !== undefined) {
|
|
72
|
+
// Replacement install: keep handing the library the sticky
|
|
73
|
+
// baseline so the persisted snapshot continues to point at the
|
|
74
|
+
// original pre-schedule state. The library will overwrite its
|
|
75
|
+
// storage row with the same value — harmless.
|
|
76
|
+
this.installedWChaMax = this.originalBaseline.wChaMax;
|
|
77
|
+
return { ...this.originalBaseline };
|
|
78
|
+
}
|
|
30
79
|
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
31
80
|
if (!controls) {
|
|
32
81
|
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
|
|
33
82
|
}
|
|
34
|
-
|
|
35
|
-
return {
|
|
83
|
+
const baseline = {
|
|
36
84
|
storCtlMod: controls.storCtlMod,
|
|
37
85
|
chaGriSet: controls.chaGriSet,
|
|
38
86
|
wChaMax: controls.wChaMax,
|
|
39
87
|
inWRte: controls.inWRte,
|
|
40
88
|
outWRte: controls.outWRte,
|
|
41
89
|
};
|
|
90
|
+
this.originalBaseline = baseline;
|
|
91
|
+
this.installedWChaMax = baseline.wChaMax;
|
|
92
|
+
return { ...baseline };
|
|
42
93
|
}
|
|
43
94
|
onChange(active, _previous) {
|
|
44
95
|
void this.applyEntry(active).catch(err => {
|
|
@@ -46,6 +97,25 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
46
97
|
});
|
|
47
98
|
}
|
|
48
99
|
onRollback(registers) {
|
|
100
|
+
if (this.suppressNextRollbackWrite) {
|
|
101
|
+
// Schedule-to-schedule replacement. Do NOT write the snapshot
|
|
102
|
+
// back; the new schedule's first `onChange` will own the
|
|
103
|
+
// register state. `installedWChaMax` stays valid (set in
|
|
104
|
+
// onInit) so the very next applyEntry has a baseline to divide
|
|
105
|
+
// against — this is the fix for the
|
|
106
|
+
// "no usable wChaMax baseline (installedWChaMax=undefined)"
|
|
107
|
+
// race seen in production logs.
|
|
108
|
+
this.suppressNextRollbackWrite = false;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Real rollback (mode: auto, dispose, restart recovery). Clear the
|
|
112
|
+
// sticky baseline so the next `onInit` re-reads the device state.
|
|
113
|
+
// Important: clear synchronously here — NOT in the async finally
|
|
114
|
+
// that follows — so a concurrent `onInit` queued behind this one
|
|
115
|
+
// (impossible today, but defensive) cannot observe a half-cleared
|
|
116
|
+
// state.
|
|
117
|
+
this.originalBaseline = undefined;
|
|
118
|
+
this.installedWChaMax = undefined;
|
|
49
119
|
void (async () => {
|
|
50
120
|
try {
|
|
51
121
|
await this.sunspecClient.writeBatteryControls(this.unitId, registers);
|
|
@@ -56,9 +126,6 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
56
126
|
catch (err) {
|
|
57
127
|
console.error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: onRollback failed: ${err}`);
|
|
58
128
|
}
|
|
59
|
-
finally {
|
|
60
|
-
this.installedWChaMax = undefined;
|
|
61
|
-
}
|
|
62
129
|
})();
|
|
63
130
|
}
|
|
64
131
|
onError(err) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StorageScheduleHandler, type ActiveStorageScheduleEntry, type StorageScheduleHandlerOptions } from "@enyo-energy/energy-app-sdk/dist/implementations/storage/storage-schedule-handler.js";
|
|
2
|
+
import { type EnyoDataBusSetStorageScheduleV1 } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
2
3
|
import { type SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
3
4
|
import { type SunspecBatteryControls } from "./sunspec-interfaces.cjs";
|
|
4
5
|
/**
|
|
@@ -58,7 +59,33 @@ export declare class SunspecBatteryScheduleHandler extends StorageScheduleHandle
|
|
|
58
59
|
private readonly getSnapshotService;
|
|
59
60
|
private readonly onErrorCallback?;
|
|
60
61
|
private installedWChaMax;
|
|
62
|
+
/**
|
|
63
|
+
* Sticky pre-schedule snapshot, captured once on the first `onInit` and
|
|
64
|
+
* held across every subsequent schedule-to-schedule replacement. Cleared
|
|
65
|
+
* only when a real rollback fires (`mode: auto` from the data bus or
|
|
66
|
+
* `dispose`). Lets the eventual restore write the *true* baseline rather
|
|
67
|
+
* than the last-active-entry register set that the device happens to be
|
|
68
|
+
* in at the moment of replacement.
|
|
69
|
+
*/
|
|
70
|
+
private originalBaseline?;
|
|
71
|
+
/**
|
|
72
|
+
* Set inside the overridden `applyMessage` when an incoming
|
|
73
|
+
* `mode: schedule` message would replace an already-running schedule.
|
|
74
|
+
* Consumed (and reset) inside `onRollback` so the base library's
|
|
75
|
+
* automatic `doReleaseSchedule → onRollback` step does not actually
|
|
76
|
+
* write the snapshot back during a replacement.
|
|
77
|
+
*/
|
|
78
|
+
private suppressNextRollbackWrite;
|
|
61
79
|
constructor(opts: SunspecBatteryScheduleHandlerOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Override the base-class data-bus router so a schedule-to-schedule
|
|
82
|
+
* replacement marks the next `onRollback` as "skip the write". The base
|
|
83
|
+
* library still owns the actual schedule lifecycle — we just steer one
|
|
84
|
+
* decision inside `onRollback`. `mode: auto` and any path that does not
|
|
85
|
+
* have an active schedule fall through unchanged, so the rollback fires
|
|
86
|
+
* normally there.
|
|
87
|
+
*/
|
|
88
|
+
applyMessage(msg: EnyoDataBusSetStorageScheduleV1): Promise<void>;
|
|
62
89
|
protected onInit(_active: ActiveStorageScheduleEntry): Promise<SunspecScheduleRegisters>;
|
|
63
90
|
protected onChange(active: ActiveStorageScheduleEntry, _previous: ActiveStorageScheduleEntry | undefined): void;
|
|
64
91
|
protected onRollback(registers: SunspecScheduleRegisters): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
|
|
4
|
+
exports.detectFeaturesFromRegisters = detectFeaturesFromRegisters;
|
|
5
|
+
exports.filterFeaturesByCalibrationResult = filterFeaturesByCalibrationResult;
|
|
4
6
|
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
5
7
|
const node_crypto_1 = require("node:crypto");
|
|
6
8
|
const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
|
|
@@ -864,6 +866,54 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
864
866
|
}
|
|
865
867
|
}
|
|
866
868
|
exports.SunspecInverter = SunspecInverter;
|
|
869
|
+
/**
|
|
870
|
+
* Pure register-presence detection — exported so it can be unit-tested
|
|
871
|
+
* without the full `SunspecBattery` scaffold. Maps each Model 124 writable
|
|
872
|
+
* register to the `EnyoBatteryFeature` it represents:
|
|
873
|
+
*
|
|
874
|
+
* - `chaGriSet` → `GridCharging` (charge source PV/GRID)
|
|
875
|
+
* - `wChaMax` → `ChargeLimitation` (max charge power)
|
|
876
|
+
* - `outWRte` → `DischargeLimitation` (discharge rate %)
|
|
877
|
+
* - `storCtlMod` → `GridDischarging` (closest signal that external discharge
|
|
878
|
+
* control is available; the inverter decides where it goes)
|
|
879
|
+
*/
|
|
880
|
+
function detectFeaturesFromRegisters(batteryData) {
|
|
881
|
+
const features = [];
|
|
882
|
+
if (batteryData?.chaGriSet !== undefined)
|
|
883
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
884
|
+
if (batteryData?.wChaMax !== undefined)
|
|
885
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
886
|
+
if (batteryData?.outWRte !== undefined)
|
|
887
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.DischargeLimitation);
|
|
888
|
+
if (batteryData?.storCtlMod !== undefined)
|
|
889
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridDischarging);
|
|
890
|
+
return features;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Apply the per-feature calibration filter to a register-detected set.
|
|
894
|
+
* Exported for unit-test access to the legacy-result fallback branch.
|
|
895
|
+
*
|
|
896
|
+
* The contract:
|
|
897
|
+
* - No `result` or `result.state !== 'calibrated'` → strip every controllable
|
|
898
|
+
* feature (safe fallback while waiting for calibration).
|
|
899
|
+
* - `result.state === 'calibrated'` with a decodable per-feature payload →
|
|
900
|
+
* publish only the controllable features whose probes passed.
|
|
901
|
+
* - `result.state === 'calibrated'` with **no** decodable payload → legacy
|
|
902
|
+
* data (pre-feature-calibrator SDK). The new calibrator only marks
|
|
903
|
+
* `state=calibrated` when at least one probe passes, so an empty decoded
|
|
904
|
+
* set with `state=calibrated` unambiguously signals legacy results. Publish
|
|
905
|
+
* the full detected set to preserve the old all-or-nothing semantics on
|
|
906
|
+
* upgrade.
|
|
907
|
+
*/
|
|
908
|
+
function filterFeaturesByCalibrationResult(detected, result, controllable) {
|
|
909
|
+
if (!result || result.state !== 'calibrated') {
|
|
910
|
+
return detected.filter(f => !controllable.includes(f));
|
|
911
|
+
}
|
|
912
|
+
const passed = (0, sunspec_battery_feature_calibrator_js_1.decodeFeatureResults)(result.notes);
|
|
913
|
+
if (passed.size === 0)
|
|
914
|
+
return detected;
|
|
915
|
+
return detected.filter(f => !controllable.includes(f) || passed.has(f));
|
|
916
|
+
}
|
|
867
917
|
/**
|
|
868
918
|
* Sunspec Battery implementation
|
|
869
919
|
*/
|
|
@@ -939,14 +989,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
939
989
|
* {@link resolveAdvertisedFeatures}.
|
|
940
990
|
*/
|
|
941
991
|
detectFromRegisters(batteryData) {
|
|
942
|
-
|
|
943
|
-
if (batteryData?.chaGriSet !== undefined) {
|
|
944
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
945
|
-
}
|
|
946
|
-
if (batteryData?.wChaMax !== undefined) {
|
|
947
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
948
|
-
}
|
|
949
|
-
return features;
|
|
992
|
+
return detectFeaturesFromRegisters(batteryData);
|
|
950
993
|
}
|
|
951
994
|
/**
|
|
952
995
|
* Resolve `appliance.battery.features` for the configured mode. Called on
|
|
@@ -964,18 +1007,10 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
964
1007
|
return intersect(this.detectFromRegisters(batteryData));
|
|
965
1008
|
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased: {
|
|
966
1009
|
const detected = intersect(this.detectFromRegisters(batteryData));
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (!result || result.state !== 'calibrated') {
|
|
972
|
-
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
973
|
-
}
|
|
974
|
-
// Per-feature gating: only publish controllable features whose
|
|
975
|
-
// probe passed during the last calibration. See
|
|
976
|
-
// `decodeFeatureResults` and `SunspecBatteryFeatureCalibrator`.
|
|
977
|
-
const passed = (0, sunspec_battery_feature_calibrator_js_1.decodeFeatureResults)(result.notes);
|
|
978
|
-
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f) || passed.has(f));
|
|
1010
|
+
const result = this.applianceId
|
|
1011
|
+
? this.calibrationResultStore?.getResult(this.applianceId)
|
|
1012
|
+
: undefined;
|
|
1013
|
+
return filterFeaturesByCalibrationResult(detected, result, SunspecBattery.CONTROLLABLE_FEATURES);
|
|
979
1014
|
}
|
|
980
1015
|
}
|
|
981
1016
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecBatteryFeatureMode, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
|
|
1
|
+
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecBatteryData, type SunspecBatteryFeatureMode, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
|
|
2
2
|
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
3
3
|
import { type EnyoAppliance, type EnyoApplianceErrorCode, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
5
5
|
import { SunspecModbusClient } from "./sunspec-modbus-client.cjs";
|
|
6
6
|
import { ConnectionRetryManager } from "./connection-retry-manager.cjs";
|
|
7
7
|
import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
8
|
-
import { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
8
|
+
import { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResult, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
9
|
+
import { EnyoBatteryFeature } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
9
10
|
import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
|
|
10
11
|
/**
|
|
11
12
|
* Base abstract class for all Sunspec devices
|
|
@@ -188,6 +189,35 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
188
189
|
*/
|
|
189
190
|
private restoreInverterSnapshot;
|
|
190
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Pure register-presence detection — exported so it can be unit-tested
|
|
194
|
+
* without the full `SunspecBattery` scaffold. Maps each Model 124 writable
|
|
195
|
+
* register to the `EnyoBatteryFeature` it represents:
|
|
196
|
+
*
|
|
197
|
+
* - `chaGriSet` → `GridCharging` (charge source PV/GRID)
|
|
198
|
+
* - `wChaMax` → `ChargeLimitation` (max charge power)
|
|
199
|
+
* - `outWRte` → `DischargeLimitation` (discharge rate %)
|
|
200
|
+
* - `storCtlMod` → `GridDischarging` (closest signal that external discharge
|
|
201
|
+
* control is available; the inverter decides where it goes)
|
|
202
|
+
*/
|
|
203
|
+
export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryData | null): EnyoBatteryFeature[];
|
|
204
|
+
/**
|
|
205
|
+
* Apply the per-feature calibration filter to a register-detected set.
|
|
206
|
+
* Exported for unit-test access to the legacy-result fallback branch.
|
|
207
|
+
*
|
|
208
|
+
* The contract:
|
|
209
|
+
* - No `result` or `result.state !== 'calibrated'` → strip every controllable
|
|
210
|
+
* feature (safe fallback while waiting for calibration).
|
|
211
|
+
* - `result.state === 'calibrated'` with a decodable per-feature payload →
|
|
212
|
+
* publish only the controllable features whose probes passed.
|
|
213
|
+
* - `result.state === 'calibrated'` with **no** decodable payload → legacy
|
|
214
|
+
* data (pre-feature-calibrator SDK). The new calibrator only marks
|
|
215
|
+
* `state=calibrated` when at least one probe passes, so an empty decoded
|
|
216
|
+
* set with `state=calibrated` unambiguously signals legacy results. Publish
|
|
217
|
+
* the full detected set to preserve the old all-or-nothing semantics on
|
|
218
|
+
* upgrade.
|
|
219
|
+
*/
|
|
220
|
+
export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
|
|
191
221
|
/**
|
|
192
222
|
* Sunspec Battery implementation
|
|
193
223
|
*/
|
|
@@ -1666,11 +1666,11 @@ class SunspecModbusClient {
|
|
|
1666
1666
|
const baseAddr = model.address;
|
|
1667
1667
|
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1668
1668
|
try {
|
|
1669
|
-
// Write
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1669
|
+
// Write order: source pin and parameter writes land BEFORE the
|
|
1670
|
+
// control mode so the device only "starts acting" once every
|
|
1671
|
+
// value that governs the action is fresh. The reverse order
|
|
1672
|
+
// leaks a Modbus RTT-sized window where storCtlMod sees stale
|
|
1673
|
+
// chaGriSet / wChaMax / inWRte / outWRte.
|
|
1674
1674
|
// Write charge source setting (Register 17)
|
|
1675
1675
|
if (controls.chaGriSet !== undefined) {
|
|
1676
1676
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
@@ -1708,6 +1708,13 @@ class SunspecModbusClient {
|
|
|
1708
1708
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1709
1709
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1710
1710
|
}
|
|
1711
|
+
// Storage control mode (Register 5) — written LAST so all
|
|
1712
|
+
// governing parameters are already in place when the device
|
|
1713
|
+
// transitions into the new mode.
|
|
1714
|
+
if (controls.storCtlMod !== undefined) {
|
|
1715
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1716
|
+
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1717
|
+
}
|
|
1711
1718
|
console.log('Battery controls written successfully');
|
|
1712
1719
|
return true;
|
|
1713
1720
|
}
|
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.74';
|
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StorageScheduleHandler, type ActiveStorageScheduleEntry, type StorageScheduleHandlerOptions } from "@enyo-energy/energy-app-sdk/dist/implementations/storage/storage-schedule-handler.js";
|
|
2
|
+
import { type EnyoDataBusSetStorageScheduleV1 } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
2
3
|
import { type SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
3
4
|
import { type SunspecBatteryControls } from "./sunspec-interfaces.js";
|
|
4
5
|
/**
|
|
@@ -58,7 +59,33 @@ export declare class SunspecBatteryScheduleHandler extends StorageScheduleHandle
|
|
|
58
59
|
private readonly getSnapshotService;
|
|
59
60
|
private readonly onErrorCallback?;
|
|
60
61
|
private installedWChaMax;
|
|
62
|
+
/**
|
|
63
|
+
* Sticky pre-schedule snapshot, captured once on the first `onInit` and
|
|
64
|
+
* held across every subsequent schedule-to-schedule replacement. Cleared
|
|
65
|
+
* only when a real rollback fires (`mode: auto` from the data bus or
|
|
66
|
+
* `dispose`). Lets the eventual restore write the *true* baseline rather
|
|
67
|
+
* than the last-active-entry register set that the device happens to be
|
|
68
|
+
* in at the moment of replacement.
|
|
69
|
+
*/
|
|
70
|
+
private originalBaseline?;
|
|
71
|
+
/**
|
|
72
|
+
* Set inside the overridden `applyMessage` when an incoming
|
|
73
|
+
* `mode: schedule` message would replace an already-running schedule.
|
|
74
|
+
* Consumed (and reset) inside `onRollback` so the base library's
|
|
75
|
+
* automatic `doReleaseSchedule → onRollback` step does not actually
|
|
76
|
+
* write the snapshot back during a replacement.
|
|
77
|
+
*/
|
|
78
|
+
private suppressNextRollbackWrite;
|
|
61
79
|
constructor(opts: SunspecBatteryScheduleHandlerOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Override the base-class data-bus router so a schedule-to-schedule
|
|
82
|
+
* replacement marks the next `onRollback` as "skip the write". The base
|
|
83
|
+
* library still owns the actual schedule lifecycle — we just steer one
|
|
84
|
+
* decision inside `onRollback`. `mode: auto` and any path that does not
|
|
85
|
+
* have an active schedule fall through unchanged, so the rollback fires
|
|
86
|
+
* normally there.
|
|
87
|
+
*/
|
|
88
|
+
applyMessage(msg: EnyoDataBusSetStorageScheduleV1): Promise<void>;
|
|
62
89
|
protected onInit(_active: ActiveStorageScheduleEntry): Promise<SunspecScheduleRegisters>;
|
|
63
90
|
protected onChange(active: ActiveStorageScheduleEntry, _previous: ActiveStorageScheduleEntry | undefined): void;
|
|
64
91
|
protected onRollback(registers: SunspecScheduleRegisters): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StorageScheduleHandler, } from "@enyo-energy/energy-app-sdk/dist/implementations/storage/storage-schedule-handler.js";
|
|
2
|
-
import { EnyoStorageScheduleDirectionEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
2
|
+
import { EnyoStorageScheduleDirectionEnum, EnyoStorageScheduleModeEnum, } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
3
3
|
import { SunspecChargeSource, SunspecStorageControlMode, } from "./sunspec-interfaces.js";
|
|
4
4
|
/**
|
|
5
5
|
* Concrete `StorageScheduleHandler` for SunSpec Model 124 batteries. The base
|
|
@@ -15,6 +15,23 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
|
|
|
15
15
|
getSnapshotService;
|
|
16
16
|
onErrorCallback;
|
|
17
17
|
installedWChaMax;
|
|
18
|
+
/**
|
|
19
|
+
* Sticky pre-schedule snapshot, captured once on the first `onInit` and
|
|
20
|
+
* held across every subsequent schedule-to-schedule replacement. Cleared
|
|
21
|
+
* only when a real rollback fires (`mode: auto` from the data bus or
|
|
22
|
+
* `dispose`). Lets the eventual restore write the *true* baseline rather
|
|
23
|
+
* than the last-active-entry register set that the device happens to be
|
|
24
|
+
* in at the moment of replacement.
|
|
25
|
+
*/
|
|
26
|
+
originalBaseline;
|
|
27
|
+
/**
|
|
28
|
+
* Set inside the overridden `applyMessage` when an incoming
|
|
29
|
+
* `mode: schedule` message would replace an already-running schedule.
|
|
30
|
+
* Consumed (and reset) inside `onRollback` so the base library's
|
|
31
|
+
* automatic `doReleaseSchedule → onRollback` step does not actually
|
|
32
|
+
* write the snapshot back during a replacement.
|
|
33
|
+
*/
|
|
34
|
+
suppressNextRollbackWrite = false;
|
|
18
35
|
constructor(opts) {
|
|
19
36
|
super(opts);
|
|
20
37
|
this.sunspecClient = opts.sunspecClient;
|
|
@@ -23,19 +40,53 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
|
|
|
23
40
|
this.getSnapshotService = opts.getSnapshotService;
|
|
24
41
|
this.onErrorCallback = opts.onErrorCallback;
|
|
25
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Override the base-class data-bus router so a schedule-to-schedule
|
|
45
|
+
* replacement marks the next `onRollback` as "skip the write". The base
|
|
46
|
+
* library still owns the actual schedule lifecycle — we just steer one
|
|
47
|
+
* decision inside `onRollback`. `mode: auto` and any path that does not
|
|
48
|
+
* have an active schedule fall through unchanged, so the rollback fires
|
|
49
|
+
* normally there.
|
|
50
|
+
*/
|
|
51
|
+
async applyMessage(msg) {
|
|
52
|
+
if (msg.applianceId === this.applianceIdForLog
|
|
53
|
+
&& msg.data.mode === EnyoStorageScheduleModeEnum.Schedule
|
|
54
|
+
&& this.getActiveEntry() !== undefined) {
|
|
55
|
+
this.suppressNextRollbackWrite = true;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await super.applyMessage(msg);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
// Defence in depth: if the base class skipped the rollback for
|
|
62
|
+
// any reason (validation error, disposed, etc.), the flag would
|
|
63
|
+
// otherwise stay set and silently swallow the next real rollback.
|
|
64
|
+
this.suppressNextRollbackWrite = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
26
67
|
async onInit(_active) {
|
|
68
|
+
if (this.originalBaseline !== undefined) {
|
|
69
|
+
// Replacement install: keep handing the library the sticky
|
|
70
|
+
// baseline so the persisted snapshot continues to point at the
|
|
71
|
+
// original pre-schedule state. The library will overwrite its
|
|
72
|
+
// storage row with the same value — harmless.
|
|
73
|
+
this.installedWChaMax = this.originalBaseline.wChaMax;
|
|
74
|
+
return { ...this.originalBaseline };
|
|
75
|
+
}
|
|
27
76
|
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
28
77
|
if (!controls) {
|
|
29
78
|
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
|
|
30
79
|
}
|
|
31
|
-
|
|
32
|
-
return {
|
|
80
|
+
const baseline = {
|
|
33
81
|
storCtlMod: controls.storCtlMod,
|
|
34
82
|
chaGriSet: controls.chaGriSet,
|
|
35
83
|
wChaMax: controls.wChaMax,
|
|
36
84
|
inWRte: controls.inWRte,
|
|
37
85
|
outWRte: controls.outWRte,
|
|
38
86
|
};
|
|
87
|
+
this.originalBaseline = baseline;
|
|
88
|
+
this.installedWChaMax = baseline.wChaMax;
|
|
89
|
+
return { ...baseline };
|
|
39
90
|
}
|
|
40
91
|
onChange(active, _previous) {
|
|
41
92
|
void this.applyEntry(active).catch(err => {
|
|
@@ -43,6 +94,25 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
|
|
|
43
94
|
});
|
|
44
95
|
}
|
|
45
96
|
onRollback(registers) {
|
|
97
|
+
if (this.suppressNextRollbackWrite) {
|
|
98
|
+
// Schedule-to-schedule replacement. Do NOT write the snapshot
|
|
99
|
+
// back; the new schedule's first `onChange` will own the
|
|
100
|
+
// register state. `installedWChaMax` stays valid (set in
|
|
101
|
+
// onInit) so the very next applyEntry has a baseline to divide
|
|
102
|
+
// against — this is the fix for the
|
|
103
|
+
// "no usable wChaMax baseline (installedWChaMax=undefined)"
|
|
104
|
+
// race seen in production logs.
|
|
105
|
+
this.suppressNextRollbackWrite = false;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Real rollback (mode: auto, dispose, restart recovery). Clear the
|
|
109
|
+
// sticky baseline so the next `onInit` re-reads the device state.
|
|
110
|
+
// Important: clear synchronously here — NOT in the async finally
|
|
111
|
+
// that follows — so a concurrent `onInit` queued behind this one
|
|
112
|
+
// (impossible today, but defensive) cannot observe a half-cleared
|
|
113
|
+
// state.
|
|
114
|
+
this.originalBaseline = undefined;
|
|
115
|
+
this.installedWChaMax = undefined;
|
|
46
116
|
void (async () => {
|
|
47
117
|
try {
|
|
48
118
|
await this.sunspecClient.writeBatteryControls(this.unitId, registers);
|
|
@@ -53,9 +123,6 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
|
|
|
53
123
|
catch (err) {
|
|
54
124
|
console.error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: onRollback failed: ${err}`);
|
|
55
125
|
}
|
|
56
|
-
finally {
|
|
57
|
-
this.installedWChaMax = undefined;
|
|
58
|
-
}
|
|
59
126
|
})();
|
|
60
127
|
}
|
|
61
128
|
onError(err) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecBatteryFeatureMode, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
1
|
+
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecBatteryData, type SunspecBatteryFeatureMode, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
2
2
|
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
3
3
|
import { type EnyoAppliance, type EnyoApplianceErrorCode, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
5
5
|
import { SunspecModbusClient } from "./sunspec-modbus-client.js";
|
|
6
6
|
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
7
7
|
import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
8
|
-
import { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
8
|
+
import { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResult, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
|
|
9
|
+
import { EnyoBatteryFeature } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
9
10
|
import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
|
|
10
11
|
/**
|
|
11
12
|
* Base abstract class for all Sunspec devices
|
|
@@ -188,6 +189,35 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
188
189
|
*/
|
|
189
190
|
private restoreInverterSnapshot;
|
|
190
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Pure register-presence detection — exported so it can be unit-tested
|
|
194
|
+
* without the full `SunspecBattery` scaffold. Maps each Model 124 writable
|
|
195
|
+
* register to the `EnyoBatteryFeature` it represents:
|
|
196
|
+
*
|
|
197
|
+
* - `chaGriSet` → `GridCharging` (charge source PV/GRID)
|
|
198
|
+
* - `wChaMax` → `ChargeLimitation` (max charge power)
|
|
199
|
+
* - `outWRte` → `DischargeLimitation` (discharge rate %)
|
|
200
|
+
* - `storCtlMod` → `GridDischarging` (closest signal that external discharge
|
|
201
|
+
* control is available; the inverter decides where it goes)
|
|
202
|
+
*/
|
|
203
|
+
export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryData | null): EnyoBatteryFeature[];
|
|
204
|
+
/**
|
|
205
|
+
* Apply the per-feature calibration filter to a register-detected set.
|
|
206
|
+
* Exported for unit-test access to the legacy-result fallback branch.
|
|
207
|
+
*
|
|
208
|
+
* The contract:
|
|
209
|
+
* - No `result` or `result.state !== 'calibrated'` → strip every controllable
|
|
210
|
+
* feature (safe fallback while waiting for calibration).
|
|
211
|
+
* - `result.state === 'calibrated'` with a decodable per-feature payload →
|
|
212
|
+
* publish only the controllable features whose probes passed.
|
|
213
|
+
* - `result.state === 'calibrated'` with **no** decodable payload → legacy
|
|
214
|
+
* data (pre-feature-calibrator SDK). The new calibrator only marks
|
|
215
|
+
* `state=calibrated` when at least one probe passes, so an empty decoded
|
|
216
|
+
* set with `state=calibrated` unambiguously signals legacy results. Publish
|
|
217
|
+
* the full detected set to preserve the old all-or-nothing semantics on
|
|
218
|
+
* upgrade.
|
|
219
|
+
*/
|
|
220
|
+
export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
|
|
191
221
|
/**
|
|
192
222
|
* Sunspec Battery implementation
|
|
193
223
|
*/
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -859,6 +859,54 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
859
859
|
}
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* Pure register-presence detection — exported so it can be unit-tested
|
|
864
|
+
* without the full `SunspecBattery` scaffold. Maps each Model 124 writable
|
|
865
|
+
* register to the `EnyoBatteryFeature` it represents:
|
|
866
|
+
*
|
|
867
|
+
* - `chaGriSet` → `GridCharging` (charge source PV/GRID)
|
|
868
|
+
* - `wChaMax` → `ChargeLimitation` (max charge power)
|
|
869
|
+
* - `outWRte` → `DischargeLimitation` (discharge rate %)
|
|
870
|
+
* - `storCtlMod` → `GridDischarging` (closest signal that external discharge
|
|
871
|
+
* control is available; the inverter decides where it goes)
|
|
872
|
+
*/
|
|
873
|
+
export function detectFeaturesFromRegisters(batteryData) {
|
|
874
|
+
const features = [];
|
|
875
|
+
if (batteryData?.chaGriSet !== undefined)
|
|
876
|
+
features.push(EnyoBatteryFeature.GridCharging);
|
|
877
|
+
if (batteryData?.wChaMax !== undefined)
|
|
878
|
+
features.push(EnyoBatteryFeature.ChargeLimitation);
|
|
879
|
+
if (batteryData?.outWRte !== undefined)
|
|
880
|
+
features.push(EnyoBatteryFeature.DischargeLimitation);
|
|
881
|
+
if (batteryData?.storCtlMod !== undefined)
|
|
882
|
+
features.push(EnyoBatteryFeature.GridDischarging);
|
|
883
|
+
return features;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Apply the per-feature calibration filter to a register-detected set.
|
|
887
|
+
* Exported for unit-test access to the legacy-result fallback branch.
|
|
888
|
+
*
|
|
889
|
+
* The contract:
|
|
890
|
+
* - No `result` or `result.state !== 'calibrated'` → strip every controllable
|
|
891
|
+
* feature (safe fallback while waiting for calibration).
|
|
892
|
+
* - `result.state === 'calibrated'` with a decodable per-feature payload →
|
|
893
|
+
* publish only the controllable features whose probes passed.
|
|
894
|
+
* - `result.state === 'calibrated'` with **no** decodable payload → legacy
|
|
895
|
+
* data (pre-feature-calibrator SDK). The new calibrator only marks
|
|
896
|
+
* `state=calibrated` when at least one probe passes, so an empty decoded
|
|
897
|
+
* set with `state=calibrated` unambiguously signals legacy results. Publish
|
|
898
|
+
* the full detected set to preserve the old all-or-nothing semantics on
|
|
899
|
+
* upgrade.
|
|
900
|
+
*/
|
|
901
|
+
export function filterFeaturesByCalibrationResult(detected, result, controllable) {
|
|
902
|
+
if (!result || result.state !== 'calibrated') {
|
|
903
|
+
return detected.filter(f => !controllable.includes(f));
|
|
904
|
+
}
|
|
905
|
+
const passed = decodeFeatureResults(result.notes);
|
|
906
|
+
if (passed.size === 0)
|
|
907
|
+
return detected;
|
|
908
|
+
return detected.filter(f => !controllable.includes(f) || passed.has(f));
|
|
909
|
+
}
|
|
862
910
|
/**
|
|
863
911
|
* Sunspec Battery implementation
|
|
864
912
|
*/
|
|
@@ -934,14 +982,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
934
982
|
* {@link resolveAdvertisedFeatures}.
|
|
935
983
|
*/
|
|
936
984
|
detectFromRegisters(batteryData) {
|
|
937
|
-
|
|
938
|
-
if (batteryData?.chaGriSet !== undefined) {
|
|
939
|
-
features.push(EnyoBatteryFeature.GridCharging);
|
|
940
|
-
}
|
|
941
|
-
if (batteryData?.wChaMax !== undefined) {
|
|
942
|
-
features.push(EnyoBatteryFeature.ChargeLimitation);
|
|
943
|
-
}
|
|
944
|
-
return features;
|
|
985
|
+
return detectFeaturesFromRegisters(batteryData);
|
|
945
986
|
}
|
|
946
987
|
/**
|
|
947
988
|
* Resolve `appliance.battery.features` for the configured mode. Called on
|
|
@@ -959,18 +1000,10 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
959
1000
|
return intersect(this.detectFromRegisters(batteryData));
|
|
960
1001
|
case SunspecBatteryFeatureModeKind.CalibrationBased: {
|
|
961
1002
|
const detected = intersect(this.detectFromRegisters(batteryData));
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (!result || result.state !== 'calibrated') {
|
|
967
|
-
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
968
|
-
}
|
|
969
|
-
// Per-feature gating: only publish controllable features whose
|
|
970
|
-
// probe passed during the last calibration. See
|
|
971
|
-
// `decodeFeatureResults` and `SunspecBatteryFeatureCalibrator`.
|
|
972
|
-
const passed = decodeFeatureResults(result.notes);
|
|
973
|
-
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f) || passed.has(f));
|
|
1003
|
+
const result = this.applianceId
|
|
1004
|
+
? this.calibrationResultStore?.getResult(this.applianceId)
|
|
1005
|
+
: undefined;
|
|
1006
|
+
return filterFeaturesByCalibrationResult(detected, result, SunspecBattery.CONTROLLABLE_FEATURES);
|
|
974
1007
|
}
|
|
975
1008
|
}
|
|
976
1009
|
}
|
|
@@ -1661,11 +1661,11 @@ export class SunspecModbusClient {
|
|
|
1661
1661
|
const baseAddr = model.address;
|
|
1662
1662
|
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1663
1663
|
try {
|
|
1664
|
-
// Write
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1664
|
+
// Write order: source pin and parameter writes land BEFORE the
|
|
1665
|
+
// control mode so the device only "starts acting" once every
|
|
1666
|
+
// value that governs the action is fresh. The reverse order
|
|
1667
|
+
// leaks a Modbus RTT-sized window where storCtlMod sees stale
|
|
1668
|
+
// chaGriSet / wChaMax / inWRte / outWRte.
|
|
1669
1669
|
// Write charge source setting (Register 17)
|
|
1670
1670
|
if (controls.chaGriSet !== undefined) {
|
|
1671
1671
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
@@ -1703,6 +1703,13 @@ export class SunspecModbusClient {
|
|
|
1703
1703
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1704
1704
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1705
1705
|
}
|
|
1706
|
+
// Storage control mode (Register 5) — written LAST so all
|
|
1707
|
+
// governing parameters are already in place when the device
|
|
1708
|
+
// transitions into the new mode.
|
|
1709
|
+
if (controls.storCtlMod !== undefined) {
|
|
1710
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1711
|
+
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1712
|
+
}
|
|
1706
1713
|
console.log('Battery controls written successfully');
|
|
1707
1714
|
return true;
|
|
1708
1715
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED