diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index 6fce22e..bd2141a 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -49,7 +49,10 @@ reports a different value. At two or more inverters it also removes two false alarms that had no gate at all: [[SAFETY-30]]'s per-unit sign check no longer trips on a frozen reading -(measured: it trips on tick 7 without the gate, never with it), and +(measured: it trips without the gate and NEVER with it - the tick index is +deliberately not quoted, because it is counted on the wall clock inside +`cycle()` and is therefore load-dependent; the checkable statement is the +presence or absence of the trip, which part 3 grades), and [[SAFETY-31]]'s circulating-power sensor reports `None` instead of a loop that is not happening. diff --git a/goodwe_controller/DECISIONS/SAFETY-37-freshness-policy.md b/goodwe_controller/DECISIONS/SAFETY-37-freshness-policy.md index 7479f99..ec8e2d3 100644 --- a/goodwe_controller/DECISIONS/SAFETY-37-freshness-policy.md +++ b/goodwe_controller/DECISIONS/SAFETY-37-freshness-policy.md @@ -218,7 +218,7 @@ The counterfactual is run, not argued. `test_freshness.py` part 3 drives real ``` standing on A: [0, 445, 890, 1335, 1335, ...] (the shipped control law) gated : sign trip tick=None circulating_w=[1800, 1800, 1800, None, None, ...] -counterfact : sign trip tick=7 circulating_w=[1800, 1800, 1800, 1800, 1800, ...] +counterfact : sign trip tick=11 circulating_w=[1800, 1800, 1800, 1800, 1800, ...] ``` --- @@ -257,7 +257,7 @@ surprises. | # | kind | what survives | size | |---|---|---|---| | 1 | **false positive** | an inverter physically unable to follow — saturating, thermally derating, or a BMS refusing a charge — is declared stale while telling the truth | one spurious **site stop**, behind `stale_input_s`, self-clearing the moment the register moves. It cannot drive the battery anywhere | -| 2 | **miss** | a register frozen under a command that never travels 250 W is never detected, for 400 s or for ever | scoped, not overlooked: such a unit is one no per-unit sign check can speak about (`unit_sign_disagree_w` = 250 W) and one contributing at most 250 W to a loop whose reporting floor is 700 W | +| 2 | **miss** | a register frozen under a command that never travels 250 W is never detected, for 400 s or for ever | ⚠️ scoped for ONE consumer, NOT both, and the earlier claim here was wrong. TRUE for the per-unit sign check: no such check can speak about that unit (`unit_sign_disagree_w` = 250 W). **FALSE for circulation.** "Contributing at most 250 W" bounds the COMMAND; once the register is frozen its REPORTED value is unrelated to the command, which is exactly HW-01's shape - commanded ~0 W, register stuck on the last large number. Measured: a unit frozen at −1200 W beside a healthy +900 W sibling yields `circulating_w` = **1800 W against the 700 W floor, 2.6× over**, with no gate armed. Found in review; part 9's S2 check asserts only `not stale` and part 4 pairs the frozen unit with a RESTING sibling, so neither graded the sentence | | 3 | **miss — the largest** | a dead publisher whose last values **alternate**, or a cache replaying two old samples, never starts the unchanged clock at all | **this gate detects a value that STOPPED; it cannot detect a value that is WRONG.** Nothing in this add-on can. TEL-06's arrival stamp inside the firmware is the only layer that could | Recorded because it looks like a survivor and is not: a publisher failure that diff --git a/goodwe_controller/app/freshness.py b/goodwe_controller/app/freshness.py index 4ff600b..f4116bc 100644 --- a/goodwe_controller/app/freshness.py +++ b/goodwe_controller/app/freshness.py @@ -136,10 +136,19 @@ only one of them is WIRED - see `BOUNDS` below and the note on SoC. number here would be a guess wearing a measurement's clothes. WHAT IT COSTS, stated rather than absorbed: a frozen 0x0518 on a unit that is - never asked to move 250 W is not detected by this module at all. That unit is - also one no per-unit sign check can speak about (`unit_sign_disagree_w` is - 250 W) and one contributing at most 250 W to a circulating loop whose floor is - 700 W. The gate is scoped to where the consumers can be misled, on purpose. + never asked to move 250 W is not detected by this module at all. + + ⚠️ THAT COST IS BOUNDED FOR ONE CONSUMER AND NOT FOR THE OTHER, and an earlier + version of this note claimed both. TRUE for the per-unit sign check: no such + check can speak about that unit either, since `unit_sign_disagree_w` is 250 W. + FALSE for circulation. "Contributing at most 250 W" bounds the COMMAND - but a + frozen register's REPORTED value is unrelated to its command, and that is + precisely HW-01's shape: commanded ~0 W, register stuck on the last large + number. Measured in review: a unit frozen at -1200 W beside a healthy +900 W + sibling gives `circulating_w` = 1800 W against a 700 W floor, 2.6x over, with + no gate armed. So this gate is scoped to where the SIGN consumer can be misled; + the circulation consumer keeps an unbounded exposure below MOVE_W and that is + a known, stated gap rather than a covered one. `soc` / 0x050E, U16, 1 %, published every 10 s - STATED, NOT WIRED -----------------------------------------------------------------