OPT-17: the solar forecast can come from several entities, summed #187

Merged
admin merged 2 commits from OPT-17-forecast-entities into release/1.0 2026-10-11 05:49:38 +02:00
Owner

OPT-17: the solar forecast can come from several HA entities, summed per day. Not merged by the developer.

Options

  • New: pv_forecast_entities, pv_forecast_tomorrow_entities. Both are lists of sensors with default [], the same shape as pv_entities.
  • pv_forecast_entity and pv_forecast_tomorrow_entity are unchanged. Existing installs need no edit.
  • Why there are new keys and no "string or list" on the old ones: this was measured on Supervisor 2026.08.0 with tools/supervisor_shape_probe.py §J (all cases pass).
    • If an old key is declared as a list, Supervisor refuses the string an install has stored: Invalid list for option 'pv_forecast_entity'. A [] default does not rescue it.
    • match(.*)? accepts a string and a list, but it also accepts the integer 417, and the tab shows it as a plain text box.
  • A 0.17.0 blob validates unchanged. The new lists render as repeatable fields. The [] defaults are load-bearing. A downgrade drops the lists and keeps the single keys.

Rules (DECISIONS/OPT-17-several-forecast-entities.md)

  1. The single key and the list set for the same day are refused at start, with one message: "Keep the list and clear the single one."
  2. Each member is converted by its own unit (Wh/kWh/MWh, as solarfc._day() already does), then the day is summed. A repeated id counts once.
  3. If one member is missing, unavailable, unknown, stale, not in an energy unit or bad, that day is not usable, exactly as when the single sensor reads that way. If today fails, there is no forecast (P3 option A). If tomorrow fails, only today is used. There is never a partial sum.
  4. If every member has an hourly split, the splits are merged. If any member has none, the total goes on the bell.
  5. The forecast is as old as its oldest member.
  6. All members of both days are read in one gather, inside the existing input gather.
  7. With 2 or more members, a caution row names each failing member by its position in the list (STYLE.md does not allow entity ids on the page): "Solar forecast for today: not used, source 2 of 2 has no reading". The action log names the entity id once each time the set of failing members changes.
  8. A one-member list reads, plans, shows and logs exactly like the single key.

Tests

  • test_opt17.py (new):
    • sums of 2 and 3 members in mixed kWh/Wh/MWh;
    • members in W, kW, %, or with no unit;
    • members that are unavailable, unknown, missing, bad, stale, or from before midnight;
    • an empty list;
    • list plus single key refused, for both days;
    • list entries that are not sensors;
    • the refusal labels;
    • a one-member list equal to read() over 270 cases;
    • hourly splits merged, and falling back to the bell;
    • today and tomorrow;
    • entities() precedence and de-duplication;
    • settings notes;
    • config.yaml.
  • test_opt17.py also runs the real controller:
    • a one-member list equals the single key cycle by cycle (2880 cycles);
    • two halves equal one whole;
    • a member down or stale equals the forecast down, with the row and a single log line;
    • a tomorrow member down;
    • one gather;
    • modes off reads nothing.
  • test_modes_p4.py: the "harness bites" check now patches solarfc.read_sum (the controller's seam). No other change.
  • The full suite passes locally (goodwe-addon-checks:1).

Single-key path unchanged (differential)

  • The same script ran on e36565f and on this branch: 12 single-key scenarios × Leftover solar/Cheap hours, 4320 cycles each.
  • Scenarios: dark, sunny, state-only, Wh unit, with tomorrow, tomorrow down, down, stale, missing, lost mid-run, back mid-run, modes off.
  • The digests cover every cycle's grid, shares, action, roles, SoC, levels and plan verdict, plus the status rows, the forecast log lines and the page hint.
  • All 24 digests are identical.
  • test_modes_p4.py part 1 (12 pinned N=1/N=2 digests) still passes.

Mutations (purged copies, python3 -B, PYTHONDONTWRITEBYTECODE=1)

# mutation killed by
M1 today: partial sum of the usable members test_opt17 "a member that reads 'unavailable': no forecast at all..." (+11)
M2 tomorrow: partial sum "a tomorrow member unavailable: tomorrow is not used..." (+3)
M3 sum → max "two members, kWh + Wh: 5 + 7.5 = 12.5 kWh" (+9)
M4 keep the split when only some members have one "one member without a split: the sum goes on the bell..." (+2)
M5 merge keeps only the first member's split "every member has a split: the splits are merged..." (+2)
M6 a non-energy member is skipped, not refused "a member in W makes today unusable (unit)..." (+3)
M7 no stale check per member "a stale member (older than 3 h)..." (+6)
M8 validator accepts single key + list "pv_forecast_entity and pv_forecast_entities both set: refused..." (+1)
M9 entities(): union instead of the list "a list with an entry replaces the single key"
M10 no de-duplication "a repeated id counts once"
M11 no status row "...the status page names it by place, in one short line" (+2)
M12 member logged on every read "...and the action log names it once in 2880 cycles" (+2)
M12b member never logged "...and the action log names it once in 2880 cycles"
M13 members read one after another "every member of both days is read in one gather"
M14 a one-member list names its member too "the single key down: no new row and no new log line"
M15 forecast time = newest member "the forecast's time is the oldest member's report"
M16 empty list reads as missing "an empty list is not set"
M17 forecast read with modes off test_opt17 "battery modes off: no forecast member is read", test_modes_p4 "battery modes off: the forecast sensor is never read"
M18 tomorrow-list-without-today note lost "a tomorrow list without today's forecast gets the same warning"
M19 list entries not checked as sensors "...an entry in pv_forecast_entities that is not a sensor is refused" (+1)
M20 pv_forecast_entities: [] dropped from options "config.yaml: pv_forecast_entities defaults to []..." (also probe §J)
M21 EN name dropped test_translations "en covers config.yaml exactly"
M22 NL name dropped test_translations "nl covers config.yaml exactly"
M23 DOCS glance row drifts test_translations "DOCS.md 'Settings at a glance'..."
M24 settings page field dropped test_uilayout "config.yaml's 85 options are each a field..."
M25 setting label dropped test_opt17 crashes with KeyError in the refusal label check (the add-on's refusal would crash the same way)

Not covered

  • No per-member weighting.
  • Each member must be fresh on its own.
  • The Batteries screen line still only says "no solar forecast now". The member is named on the status page.
  • The version bump and CHANGELOG are left for the release. Two options are added, 85 in all.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

OPT-17: the solar forecast can come from several HA entities, summed per day. Not merged by the developer. ## Options - New: `pv_forecast_entities`, `pv_forecast_tomorrow_entities`. Both are lists of sensors with default `[]`, the same shape as `pv_entities`. - `pv_forecast_entity` and `pv_forecast_tomorrow_entity` are unchanged. Existing installs need no edit. - Why there are new keys and no "string or list" on the old ones: this was measured on Supervisor 2026.08.0 with `tools/supervisor_shape_probe.py` §J (all cases pass). - If an old key is declared as a list, Supervisor refuses the string an install has stored: `Invalid list for option 'pv_forecast_entity'`. A `[]` default does not rescue it. - `match(.*)?` accepts a string and a list, but it also accepts the integer 417, and the tab shows it as a plain text box. - A 0.17.0 blob validates unchanged. The new lists render as repeatable fields. The `[]` defaults are load-bearing. A downgrade drops the lists and keeps the single keys. ## Rules (DECISIONS/OPT-17-several-forecast-entities.md) 1. The single key and the list set for the same day are refused at start, with one message: "Keep the list and clear the single one." 2. Each member is converted by its own unit (Wh/kWh/MWh, as `solarfc._day()` already does), then the day is summed. A repeated id counts once. 3. If one member is missing, unavailable, unknown, stale, not in an energy unit or bad, that day is not usable, exactly as when the single sensor reads that way. If today fails, there is no forecast (P3 option A). If tomorrow fails, only today is used. There is never a partial sum. 4. If every member has an hourly split, the splits are merged. If any member has none, the total goes on the bell. 5. The forecast is as old as its oldest member. 6. All members of both days are read in one gather, inside the existing input gather. 7. With 2 or more members, a caution row names each failing member by its position in the list (STYLE.md does not allow entity ids on the page): "Solar forecast for today: not used, source 2 of 2 has no reading". The action log names the entity id once each time the set of failing members changes. 8. A one-member list reads, plans, shows and logs exactly like the single key. ## Tests - `test_opt17.py` (new): - sums of 2 and 3 members in mixed kWh/Wh/MWh; - members in W, kW, %, or with no unit; - members that are unavailable, unknown, missing, bad, stale, or from before midnight; - an empty list; - list plus single key refused, for both days; - list entries that are not sensors; - the refusal labels; - a one-member list equal to `read()` over 270 cases; - hourly splits merged, and falling back to the bell; - today and tomorrow; - `entities()` precedence and de-duplication; - settings notes; - config.yaml. - `test_opt17.py` also runs the real controller: - a one-member list equals the single key cycle by cycle (2880 cycles); - two halves equal one whole; - a member down or stale equals the forecast down, with the row and a single log line; - a tomorrow member down; - one gather; - modes off reads nothing. - `test_modes_p4.py`: the "harness bites" check now patches `solarfc.read_sum` (the controller's seam). No other change. - The full suite passes locally (`goodwe-addon-checks:1`). ## Single-key path unchanged (differential) - The same script ran on e36565f and on this branch: 12 single-key scenarios × Leftover solar/Cheap hours, 4320 cycles each. - Scenarios: dark, sunny, state-only, Wh unit, with tomorrow, tomorrow down, down, stale, missing, lost mid-run, back mid-run, modes off. - The digests cover every cycle's grid, shares, action, roles, SoC, levels and plan verdict, plus the status rows, the forecast log lines and the page hint. - All 24 digests are identical. - `test_modes_p4.py` part 1 (12 pinned N=1/N=2 digests) still passes. ## Mutations (purged copies, `python3 -B`, `PYTHONDONTWRITEBYTECODE=1`) | # | mutation | killed by | |---|---|---| | M1 | today: partial sum of the usable members | test_opt17 "a member that reads 'unavailable': no forecast at all..." (+11) | | M2 | tomorrow: partial sum | "a tomorrow member unavailable: tomorrow is not used..." (+3) | | M3 | sum → max | "two members, kWh + Wh: 5 + 7.5 = 12.5 kWh" (+9) | | M4 | keep the split when only some members have one | "one member without a split: the sum goes on the bell..." (+2) | | M5 | merge keeps only the first member's split | "every member has a split: the splits are merged..." (+2) | | M6 | a non-energy member is skipped, not refused | "a member in W makes today unusable (unit)..." (+3) | | M7 | no stale check per member | "a stale member (older than 3 h)..." (+6) | | M8 | validator accepts single key + list | "pv_forecast_entity and pv_forecast_entities both set: refused..." (+1) | | M9 | entities(): union instead of the list | "a list with an entry replaces the single key" | | M10 | no de-duplication | "a repeated id counts once" | | M11 | no status row | "...the status page names it by place, in one short line" (+2) | | M12 | member logged on every read | "...and the action log names it once in 2880 cycles" (+2) | | M12b | member never logged | "...and the action log names it once in 2880 cycles" | | M13 | members read one after another | "every member of both days is read in one gather" | | M14 | a one-member list names its member too | "the single key down: no new row and no new log line" | | M15 | forecast time = newest member | "the forecast's time is the oldest member's report" | | M16 | empty list reads as missing | "an empty list is not set" | | M17 | forecast read with modes off | test_opt17 "battery modes off: no forecast member is read", test_modes_p4 "battery modes off: the forecast sensor is never read" | | M18 | tomorrow-list-without-today note lost | "a tomorrow list without today's forecast gets the same warning" | | M19 | list entries not checked as sensors | "...an entry in pv_forecast_entities that is not a sensor is refused" (+1) | | M20 | `pv_forecast_entities: []` dropped from options | "config.yaml: pv_forecast_entities defaults to []..." (also probe §J) | | M21 | EN name dropped | test_translations "`en` covers config.yaml exactly" | | M22 | NL name dropped | test_translations "`nl` covers config.yaml exactly" | | M23 | DOCS glance row drifts | test_translations "DOCS.md 'Settings at a glance'..." | | M24 | settings page field dropped | test_uilayout "config.yaml's 85 options are each a field..." | | M25 | setting label dropped | test_opt17 crashes with KeyError in the refusal label check (the add-on's refusal would crash the same way) | ## Not covered - No per-member weighting. - Each member must be fresh on its own. - The Batteries screen line still only says "no solar forecast now". The member is named on the status page. - The version bump and CHANGELOG are left for the release. Two options are added, 85 in all. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-11 03:32:41 +02:00
OPT-17: the solar forecast can come from several entities, summed
checks / unit (pull_request) Successful in 36m13s
infra-pin / fresh (pull_request) Successful in 1s
9b1fa758ad
New list options pv_forecast_entities and pv_forecast_tomorrow_entities
(same shape as pv_entities); the single-entity keys are unchanged.

- Each member is read in its own unit (Wh/kWh/MWh) in one gather and the
  day is summed. Hourly splits are merged when every member has one, else
  the total goes on the bell.
- A member missing, unavailable, stale, non-energy or bad makes that
  day's forecast unusable (no partial sum). A caution row names it by
  position; the action log names the entity once per change.
- Single key and list both set for one day: refused at start.
- A one-member list behaves exactly like the single key (test_opt17.py);
  the single-key path is byte-identical to e36565f over 24 scenarios.
- Supervisor shape measured: tools/supervisor_shape_probe.py section J.

DECISIONS/OPT-17-several-forecast-entities.md has the rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-11 05:12:39 +02:00
OPT-17 review: title the forecast row on Home; test the hint and blank rows
checks / unit (pull_request) Successful in 36m20s
infra-pin / fresh (pull_request) Successful in 2s
92a80c09b2
The new status row's id `solar_forecast` had no Home title, so Home showed
it under the fallback "Inverter connection". Add "Solar forecast" /
"Zonneprognose" to ui_text, and checks for the title, the tomorrow-gap hint
and blank list rows (review mutations R7, R9, R14 now killed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Author
Owner

Review of OPT-17 (head reviewed: 9b1fa75; review fix pushed: 92a80c0)

Verdict: the logic is right. I pushed one small UI-text fix and three test checks. I merge once checks / unit is green on 92a80c0.

Findings

  • Medium, fixed in 92a80c0. The new status row id solar_forecast (app/main.py:7027) had no Home title in ui_text/*.json home_words.attn. Home's attention list showed the row under the fallback "Inverter connection". I checked this with homeview.attention(). I added "Solar forecast" / "Zonneprognose", plus a check in test_opt17.
  • Low, test gaps, fixed in 92a80c0:
    • The forecast_no_tomorrow hint was untested: mutation R7 survived.
    • Blank rows in a list were untested: R9 (blank rows refused) survived.
    • Both are now checked and both mutations are killed.
  • Low, not changed (follow-up if wanted):
    • Position can drift from the row on the tab. "source N of M" counts the list after blanks and repeated ids are dropped (solarfc.entities()). With a blank or repeated row on the tab, "source 2" may not be row 2. This is rare. The validator does not warn about a repeated id.
    • Notes for list-only setups point at the single key. In app/validate.py:1808-1814, the notes "used only with battery modes on" and "Tomorrow's forecast is used only with today's" attach to the single-key field. The second also tells a list user to set "Solar forecast, today". test_opt17 pins the current text.
    • Wording of the row. "has no reading" also covers stale data and a wrong unit. This matches row_pv_partial, and the log gives the reason. Acceptable.
    • Dev differential window. In the dev's single-key differential, tomorrow/tom_down give the same digest as dark: the 00:45–06:45 window does not use tomorrow. I re-ran it with a 20:30 start across midnight (below), and there tomorrow matters.

What I ran

  • Full suite (goodwe-addon-checks:1, --network none): 72/72 on 9b1fa75, and 72/72 on 92a80c0.
  • Probe §J on Supervisor 2026.08.0: all cases pass, exit 0.
  • My own Supervisor check:
    • the real 0.17.0 config.yaml defaults plus the single keys validate under the new schema, and the lists are [];
    • the stored live blob gives the same verdict under the old and new schema, and only the two list keys differ;
    • a downgrade to the real e36565f schema drops the lists and keeps the single keys;
    • the schema diff is exactly the two keys.
  • My differential, 15 new single-key scenarios × 2 modes (30 digests):
    • evening window with tomorrow ok, big, small, down, stale, gone, in W, or without a split; no tomorrow;
    • N=1 sunny and down;
    • MWh, W, a bad number, a future timestamp, and a flapping sensor.
    • e36565f equals the branch in all 30.
    • A one-member list equals the single key in all 30.
    • The scenarios bite: tomorrow big, small and down give different digests.
  • Mutations not in the dev table: 12 in all. 9 were killed on 9b1fa75. R7 and R9 survived, and R14 (the title) had no check yet; all three are killed on 92a80c0. The table is in the review report.

Dev's open points

  1. Position, not entity id, on the page. Usable. The owner fills the list in order on the tab, the action log has the entity id, and it matches TEL-12's row_pv_partial. The page is now also titled correctly.
  2. A tomorrow member down with N≥2 gets a row; the single key stays silent. Agree. A one-member list must equal the single key. A silent single tomorrow sensor is the behaviour before this PR; changing it is a separate card.
  3. A forecast down from the start is not logged (single key). Agree. It is unchanged from e36565f, and the Batteries screen already says there is no forecast.
## Review of OPT-17 (head reviewed: 9b1fa75; review fix pushed: 92a80c0) **Verdict:** the logic is right. I pushed one small UI-text fix and three test checks. I merge once `checks / unit` is green on 92a80c0. ### Findings - **Medium, fixed in 92a80c0.** The new status row id `solar_forecast` (app/main.py:7027) had no Home title in `ui_text/*.json` `home_words.attn`. Home's attention list showed the row under the fallback **"Inverter connection"**. I checked this with `homeview.attention()`. I added "Solar forecast" / "Zonneprognose", plus a check in test_opt17. - **Low, test gaps, fixed in 92a80c0:** - The `forecast_no_tomorrow` hint was untested: mutation R7 survived. - Blank rows in a list were untested: R9 (blank rows refused) survived. - Both are now checked and both mutations are killed. - **Low, not changed (follow-up if wanted):** - **Position can drift from the row on the tab.** "source N of M" counts the list after blanks and repeated ids are dropped (`solarfc.entities()`). With a blank or repeated row on the tab, "source 2" may not be row 2. This is rare. The validator does not warn about a repeated id. - **Notes for list-only setups point at the single key.** In app/validate.py:1808-1814, the notes "used only with battery modes on" and "Tomorrow's forecast is used only with today's" attach to the single-key field. The second also tells a list user to set "Solar forecast, today". test_opt17 pins the current text. - **Wording of the row.** "has no reading" also covers stale data and a wrong unit. This matches `row_pv_partial`, and the log gives the reason. Acceptable. - **Dev differential window.** In the dev's single-key differential, `tomorrow`/`tom_down` give the same digest as `dark`: the 00:45–06:45 window does not use tomorrow. I re-ran it with a 20:30 start across midnight (below), and there tomorrow matters. ### What I ran - **Full suite** (goodwe-addon-checks:1, `--network none`): 72/72 on 9b1fa75, and 72/72 on 92a80c0. - **Probe §J** on Supervisor 2026.08.0: all cases pass, exit 0. - **My own Supervisor check:** - the real 0.17.0 `config.yaml` defaults plus the single keys validate under the new schema, and the lists are `[]`; - the stored live blob gives the same verdict under the old and new schema, and only the two list keys differ; - a downgrade to the real e36565f schema drops the lists and keeps the single keys; - the schema diff is exactly the two keys. - **My differential, 15 new single-key scenarios × 2 modes (30 digests):** - evening window with tomorrow ok, big, small, down, stale, gone, in W, or without a split; no tomorrow; - N=1 sunny and down; - MWh, W, a bad number, a future timestamp, and a flapping sensor. - e36565f equals the branch in all 30. - A one-member list equals the single key in all 30. - The scenarios bite: tomorrow big, small and down give different digests. - **Mutations not in the dev table:** 12 in all. 9 were killed on 9b1fa75. R7 and R9 survived, and R14 (the title) had no check yet; all three are killed on 92a80c0. The table is in the review report. ### Dev's open points 1. **Position, not entity id, on the page.** Usable. The owner fills the list in order on the tab, the action log has the entity id, and it matches TEL-12's `row_pv_partial`. The page is now also titled correctly. 2. **A tomorrow member down with N≥2 gets a row; the single key stays silent.** Agree. A one-member list must equal the single key. A silent single tomorrow sensor is the behaviour before this PR; changing it is a separate card. 3. **A forecast down from the start is not logged (single key).** Agree. It is unchanged from e36565f, and the Batteries screen already says there is no forecast.
admin merged commit 3050fa5208 into release/1.0 2026-10-11 05:49:38 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#187