diff --git a/goodwe_controller/DECISIONS/OPT-11-battery-modes.md b/goodwe_controller/DECISIONS/OPT-11-battery-modes.md index b453090..ebaabc7 100644 --- a/goodwe_controller/DECISIONS/OPT-11-battery-modes.md +++ b/goodwe_controller/DECISIONS/OPT-11-battery-modes.md @@ -127,8 +127,11 @@ P1 is its section 6 P1, and the pull request lists what was measured. before its end (two inverters, lagged readings): the hold adds at most +33 W to that quarter against the loop without it, and the most any quarter banks is 3431 W with the charge sized to the aim (3450 W; the cap - is 3500 W). The import during the hold is the rise, below the charge's + is 3500 W), 3456 W with a slow 8 s battery (the lag's own, the same on + e3e4ba6). The import during the hold is the rise, below the charge's own, and a rise past that is the peak floor's, which ends the hold. + A maintenance drain, hold or charge that starts during the hold ends it + on that tick (replayed at edge + 3 s). ## P4: the solar forecast and the day plan (2026-09-29) - Input: a Home Assistant forecast sensor (`pv_forecast_entity`, kWh left diff --git a/goodwe_controller/test_price_run_fixes.py b/goodwe_controller/test_price_run_fixes.py index 6a688e6..ecd6da1 100644 --- a/goodwe_controller/test_price_run_fixes.py +++ b/goodwe_controller/test_price_run_fixes.py @@ -187,6 +187,22 @@ check("...and a sample older than the hold is not credited: past the hold " and peakguard.blindness(_gp, 30.0, False) is not None) check("without `roll` nothing changes (the modes-off path)", PeakTracker.project(_avg, _now, roll=False) == _old) +# Review of #157 (X1): a held export is credited as 0 W, as `add()` banks it, +# never as a negative that would pull the rolled quarter under the meter. +_exp = QuarterAverager(phases=1, max_hold_s=30.0) +for _s in range(-900, 1, 1): + _exp.add(make_sample("ha_signed", 0.0, 1500.0, phases=1, + ingest_ts=_edge + timedelta(seconds=_s - 0.35), + ingest_mono=0.0)) +_ep = PeakTracker.project(_exp, _now, roll=True) +_exp.add(make_sample("ha_signed", 0.0, 1500.0, phases=1, + ingest_ts=_edge + timedelta(seconds=0.6), + ingest_mono=0.0)) +_er = PeakTracker.project(_exp, _now) +check("...and a held export rolls in as 0 W, equal to what the averager " + "banks for those seconds", + _ep.block_start == _er.block_start and _ep.projected_w == 0.0 + and _ep.elapsed_w == 0.0 and _er.projected_w == 0.0) # 1b. The lagged replay: 02:00-03:30 local, a Cheap hours battery charging # under an 800 W house through five quarter edges. @@ -444,6 +460,9 @@ check("the hold: the loop reads the target while the meter still shows the " and _held(3300.0, -2500.0, _hs + 1) == (3300.0, False) and _held(3300.0, -2000.0, _hs + 1) == (-10.0, True) and _held(3300.0, -2000.0, 2 * _hs + 1) == (3300.0, False)) +# Review of #157 (X4): a reading that has passed E is not "still moving". +check("...and a battery reading already past E does not extend the hold", + _held(3300.0, 900.0, _hs + 1, last_b=700.0) == (3300.0, False)) _f = SimpleNamespace(site_batt_w=500.0, grid=90.0, _edge=None, tuning=ctl.Tuning(target_grid_w=-10.0), site_max_w=5000.0, loop_bound=lambda claims: None, target=None, i_w=None)