SAFETY-118: the peak forecast is read in its own unit.
- `peak_forecast_entity` is read through `pv.watts()` (same table and reader as solar); the `abs(raw) < 50` rule is gone.
- No unit or a non-power unit: forecast not used (peak_fc = None), one WARNING per cause, a status caution row, and a settings note.
- Validator: no warning for W/kW on this entity; other WATT_ENTITY_KEYS unchanged.
- Tests: `test_safety118.py`; DOCS and DECISIONS updated. No CHANGELOG or version change.
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
peak_forecast_entity is read through pv.watts() by its unit_of_measurement;
the abs(raw) < 50 size rule is gone. No unit or a non-power unit means the
forecast is not used, with one warning per cause. The validator no longer
claims a kW sensor is taken as watts for this entity.
Co-Authored-By: Claude <noreply@anthropic.com>
Review of f0b4e68 (SAFETY-118). Not merged: one Medium finding needs a code change.
F1 (Medium) - a missing reading clears the unit fault and logs a false "used again".app/main.py:3475 with app/pv.py:129-144. unit_fault() returns None both for a good unit and for a missing reading (read failed, unavailable/unknown, non-numeric). So a kWh/no-unit forecast that blips unavailable for one cycle does this (probed through the real read_inputs()):
kWh 820 fault='kWh' WARNING not in a power unit
unavailable fault=None INFO "The 15-minute demand forecast is used again." <- false, it is not used
kWh 820 fault='kWh' WARNING again (the cause did not change)
Same for hass.state() = None and for a non-numeric state. Each blip is one false INFO line plus a repeated WARNING, and the status row drops out. A flaky read can do this every other cycle (the SAFETY-107 kind of flood). Fix direction: leave _peak_fc_fault alone when there is no numeric reading (e.g. call _peak_fc_unit_log() only when self.peak_fc is not None or fault is not None). Add a test: kWh -> unavailable -> kWh gives one WARNING and no INFO. Note test_safety118.py:178-186 passes by accident today: rec.rows[-1] is the INFO logged in the unavailable cycle, not on the return to W.
F2 (Low, text) - DECISIONS/SAFETY-118-peak-forecast-unit.md:6-7. "A W sensor reading 17 to 45 W, which a real site did 430 times in one morning, was taken as 17 to 45 kW." The live add-on never read that W sensor (manager correction on the card, 2026-10-16 12:16). Write "would be taken as". Fold into the F1 round.
F3 (Low, test gap). No test that the unit-fault status row is absent while the reading is merely unavailable. Mutation elif getattr(self, "_peak_fc_fault", None) is not None: -> elif self.peak_fc is None: (row on every unavailable reading, with the "no unit" text) survives test_safety118.py.
Deviations, accepted: (a) non-numeric state now silent like the PV path: acceptable, though it drops the only signal for a stuck non-numeric forecast; keep it out of the F1 fault state. (b) NL text not shown: acceptable, rows/notes/log are English-only across this code base.
Checked, fine: every peak_fc reader (main.py:5029 shaping_suspended, :5056 maintenance_charge_floor, status rows :7261) gates on the value, not on the option, except the "not set" row, which is right. Size rule gone in code and comments (pv.py:95-102). W >= 50 and kW < 50 are identical to before; kW >= 50 is now more protective. One GET /api/states/<id> per cycle before and after; hass.state() None and a non-dict body are handled. Validator: no note for W/kW/MW, warns for no unit/kWh; other WATT_ENTITY_KEYS keep their claim (agreed: all read with hass.number(), no conversion). Picker kind power uses the same filter as power_w in haentities.matches(). Strings within UX-08 limits (status row 15 words, option description 30). tools/opt11_identity.py fake state() serves W from the same patched number(), values 2300-2600 W, so it is a true identity (serving kW instead fails test_peak_margin.py). Full suite 79/79 in goodwe-addon-checks:1, --network none, GOODWE_REQUIRE_HWSIM=1 with admin/goodwe at the infra pin. CI 1314/1315 green. Mutations: 6 of the dev's re-run and 4 of mine caught (including the old size-rule code put back verbatim); the F3 one survived.
Review of f0b4e68 (SAFETY-118). **Not merged: one Medium finding needs a code change.**
**F1 (Medium) - a missing reading clears the unit fault and logs a false "used again".** `app/main.py:3475` with `app/pv.py:129-144`. `unit_fault()` returns `None` both for a good unit and for a missing reading (read failed, `unavailable`/`unknown`, non-numeric). So a kWh/no-unit forecast that blips unavailable for one cycle does this (probed through the real `read_inputs()`):
```
kWh 820 fault='kWh' WARNING not in a power unit
unavailable fault=None INFO "The 15-minute demand forecast is used again." <- false, it is not used
kWh 820 fault='kWh' WARNING again (the cause did not change)
```
Same for `hass.state()` = None and for a non-numeric state. Each blip is one false INFO line plus a repeated WARNING, and the status row drops out. A flaky read can do this every other cycle (the SAFETY-107 kind of flood). Fix direction: leave `_peak_fc_fault` alone when there is no numeric reading (e.g. call `_peak_fc_unit_log()` only when `self.peak_fc is not None or fault is not None`). Add a test: kWh -> unavailable -> kWh gives one WARNING and no INFO. Note `test_safety118.py:178-186` passes by accident today: `rec.rows[-1]` is the INFO logged in the *unavailable* cycle, not on the return to W.
**F2 (Low, text) - `DECISIONS/SAFETY-118-peak-forecast-unit.md:6-7`.** "A W sensor reading 17 to 45 W, which a real site did 430 times in one morning, was taken as 17 to 45 kW." The live add-on never read that W sensor (manager correction on the card, 2026-10-16 12:16). Write "would be taken as". Fold into the F1 round.
**F3 (Low, test gap).** No test that the unit-fault status row is absent while the reading is merely unavailable. Mutation `elif getattr(self, "_peak_fc_fault", None) is not None:` -> `elif self.peak_fc is None:` (row on every unavailable reading, with the "no unit" text) survives `test_safety118.py`.
**Deviations, accepted:** (a) non-numeric state now silent like the PV path: acceptable, though it drops the only signal for a stuck non-numeric forecast; keep it out of the F1 fault state. (b) NL text not shown: acceptable, rows/notes/log are English-only across this code base.
**Checked, fine:** every `peak_fc` reader (main.py:5029 `shaping_suspended`, :5056 `maintenance_charge_floor`, status rows :7261) gates on the value, not on the option, except the "not set" row, which is right. Size rule gone in code and comments (pv.py:95-102). W >= 50 and kW < 50 are identical to before; kW >= 50 is now more protective. One `GET /api/states/<id>` per cycle before and after; `hass.state()` None and a non-dict body are handled. Validator: no note for W/kW/MW, warns for no unit/kWh; other `WATT_ENTITY_KEYS` keep their claim (agreed: all read with `hass.number()`, no conversion). Picker kind `power` uses the same filter as `power_w` in `haentities.matches()`. Strings within UX-08 limits (status row 15 words, option description 30). `tools/opt11_identity.py` fake `state()` serves W from the same patched `number()`, values 2300-2600 W, so it is a true identity (serving kW instead fails `test_peak_margin.py`). Full suite 79/79 in `goodwe-addon-checks:1`, `--network none`, `GOODWE_REQUIRE_HWSIM=1` with admin/goodwe at the infra pin. CI 1314/1315 green. Mutations: 6 of the dev's re-run and 4 of mine caught (including the old size-rule code put back verbatim); the F3 one survived.
A missing reading (unavailable, unknown, failed read, non-numeric) no longer
clears the fault: no false 'used again' INFO, no repeated WARNING, the status
row stays. Adds pv.has_reading(). DECISIONS wording fixed; tests added.
Co-Authored-By: Claude <noreply@anthropic.com>
Review round 2 of 7d52dc1 (SAFETY-118). Merging. F1-F3 are fixed; no new findings that block.
F1 fixed.read_inputs() (app/main.py:3476) updates the unit-fault state only when pv.has_reading(st). Round 1 probe on 7d52dc1: kWh 820 warns once, then 820 / unavailable / 820 / None / 820 / "sunny" / 820 all stay fault='kWh' with no new log. A second probe (startup with the sensor missing, W, no unit, kWh, kW) gives one WARNING per cause, one INFO only on a good-unit number, the status row steady during gaps, and peak_fc None on every missing or faulted cycle.
F2: the DECISIONS text is true and short.
F3: the new tests run through the real read_inputs() + checks(). Mutations caught: R4, F1-a, F1-b, F1-f, plus four of mine (gate on st is not None, gate that filters only "unavailable", has_reading accepting non-numeric states, no re-warning when the cause changes).
Round 2 changed only pv.py, main.py (the gate), DECISIONS and test_safety118.py. No string, CHANGELOG or version change.
Offline suite: 79/79 (--network none, GOODWE_REQUIRE_HWSIM=1, admin/goodwe at the pin 3b4ff84). CI runs 1316 and 1317 are green on 7d52dc1.
Info (no change needed): the settings note (app/validate.py:1833) judges the unit from the entity list whatever the state is. If a W sensor went unavailable and HA dropped its unit attribute, the page would show a "no unit" note that the loop does not. Other watt and forecast keys work the same way already.
Review round 2 of 7d52dc1 (SAFETY-118). **Merging.** F1-F3 are fixed; no new findings that block.
- **F1 fixed.** `read_inputs()` (app/main.py:3476) updates the unit-fault state only when `pv.has_reading(st)`. Round 1 probe on 7d52dc1: kWh 820 warns once, then 820 / unavailable / 820 / None / 820 / "sunny" / 820 all stay `fault='kWh'` with no new log. A second probe (startup with the sensor missing, W, no unit, kWh, kW) gives one WARNING per cause, one INFO only on a good-unit number, the status row steady during gaps, and `peak_fc` None on every missing or faulted cycle.
- **F2:** the DECISIONS text is true and short.
- **F3:** the new tests run through the real `read_inputs()` + `checks()`. Mutations caught: R4, F1-a, F1-b, F1-f, plus four of mine (gate on `st is not None`, gate that filters only "unavailable", `has_reading` accepting non-numeric states, no re-warning when the cause changes).
- Round 2 changed only pv.py, main.py (the gate), DECISIONS and test_safety118.py. No string, CHANGELOG or version change.
- Offline suite: 79/79 (`--network none`, `GOODWE_REQUIRE_HWSIM=1`, admin/goodwe at the pin 3b4ff84). CI runs 1316 and 1317 are green on 7d52dc1.
Info (no change needed): the settings note (app/validate.py:1833) judges the unit from the entity list whatever the state is. If a W sensor went unavailable *and* HA dropped its unit attribute, the page would show a "no unit" note that the loop does not. Other watt and forecast keys work the same way already.
admin
merged commit 09a54859f2 into release/1.02026-10-16 19:53:55 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
SAFETY-118: the peak forecast is read in its own unit.
peak_forecast_entityis read throughpv.watts()(same table and reader as solar); theabs(raw) < 50rule is gone.test_safety118.py; DOCS and DECISIONS updated. No CHANGELOG or version change.Co-Authored-By: Claude noreply@anthropic.com
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Review of
f0b4e68(SAFETY-118). Not merged: one Medium finding needs a code change.F1 (Medium) - a missing reading clears the unit fault and logs a false "used again".
app/main.py:3475withapp/pv.py:129-144.unit_fault()returnsNoneboth for a good unit and for a missing reading (read failed,unavailable/unknown, non-numeric). So a kWh/no-unit forecast that blips unavailable for one cycle does this (probed through the realread_inputs()):Same for
hass.state()= None and for a non-numeric state. Each blip is one false INFO line plus a repeated WARNING, and the status row drops out. A flaky read can do this every other cycle (the SAFETY-107 kind of flood). Fix direction: leave_peak_fc_faultalone when there is no numeric reading (e.g. call_peak_fc_unit_log()only whenself.peak_fc is not None or fault is not None). Add a test: kWh -> unavailable -> kWh gives one WARNING and no INFO. Notetest_safety118.py:178-186passes by accident today:rec.rows[-1]is the INFO logged in the unavailable cycle, not on the return to W.F2 (Low, text) -
DECISIONS/SAFETY-118-peak-forecast-unit.md:6-7. "A W sensor reading 17 to 45 W, which a real site did 430 times in one morning, was taken as 17 to 45 kW." The live add-on never read that W sensor (manager correction on the card, 2026-10-16 12:16). Write "would be taken as". Fold into the F1 round.F3 (Low, test gap). No test that the unit-fault status row is absent while the reading is merely unavailable. Mutation
elif getattr(self, "_peak_fc_fault", None) is not None:->elif self.peak_fc is None:(row on every unavailable reading, with the "no unit" text) survivestest_safety118.py.Deviations, accepted: (a) non-numeric state now silent like the PV path: acceptable, though it drops the only signal for a stuck non-numeric forecast; keep it out of the F1 fault state. (b) NL text not shown: acceptable, rows/notes/log are English-only across this code base.
Checked, fine: every
peak_fcreader (main.py:5029shaping_suspended, :5056maintenance_charge_floor, status rows :7261) gates on the value, not on the option, except the "not set" row, which is right. Size rule gone in code and comments (pv.py:95-102). W >= 50 and kW < 50 are identical to before; kW >= 50 is now more protective. OneGET /api/states/<id>per cycle before and after;hass.state()None and a non-dict body are handled. Validator: no note for W/kW/MW, warns for no unit/kWh; otherWATT_ENTITY_KEYSkeep their claim (agreed: all read withhass.number(), no conversion). Picker kindpoweruses the same filter aspower_winhaentities.matches(). Strings within UX-08 limits (status row 15 words, option description 30).tools/opt11_identity.pyfakestate()serves W from the same patchednumber(), values 2300-2600 W, so it is a true identity (serving kW instead failstest_peak_margin.py). Full suite 79/79 ingoodwe-addon-checks:1,--network none,GOODWE_REQUIRE_HWSIM=1with admin/goodwe at the infra pin. CI 1314/1315 green. Mutations: 6 of the dev's re-run and 4 of mine caught (including the old size-rule code put back verbatim); the F3 one survived.Review round 2 of
7d52dc1(SAFETY-118). Merging. F1-F3 are fixed; no new findings that block.read_inputs()(app/main.py:3476) updates the unit-fault state only whenpv.has_reading(st). Round 1 probe on7d52dc1: kWh 820 warns once, then 820 / unavailable / 820 / None / 820 / "sunny" / 820 all stayfault='kWh'with no new log. A second probe (startup with the sensor missing, W, no unit, kWh, kW) gives one WARNING per cause, one INFO only on a good-unit number, the status row steady during gaps, andpeak_fcNone on every missing or faulted cycle.read_inputs()+checks(). Mutations caught: R4, F1-a, F1-b, F1-f, plus four of mine (gate onst is not None, gate that filters only "unavailable",has_readingaccepting non-numeric states, no re-warning when the cause changes).--network none,GOODWE_REQUIRE_HWSIM=1, admin/goodwe at the pin 3b4ff84). CI runs 1316 and 1317 are green on7d52dc1.Info (no change needed): the settings note (app/validate.py:1833) judges the unit from the entity list whatever the state is. If a W sensor went unavailable and HA dropped its unit attribute, the page would show a "no unit" note that the loop does not. Other watt and forecast keys work the same way already.