Releases: albinati/home-energy-manager
v12.0.0 — LP residual-class Infeasibility surface closed
Headline
Residual-class LP-Infeasibility surface closed. The 60-day audit of optimizer_log (run during this cycle) found that 8 of 9 above-reserve Infeasible events clustered at the 21:00–21:25 BST tier_boundary MPC fire — slot 0 of the new horizon falling inside the 21:30 BST evening shower window from a cold tank, physically un-liftable to 45 °C in a single 30-min slot. v12 ships the soft-floor fix plus a defensive layer for the remaining classes, snapshot-based diagnostics for any future Infeasibles, and a 200+ binary MILP cleanup.
Honest-mode regression: −£1.09 over 14 d of prod-snapshot replay (new LP code strictly cheaper than v11.x when applied to past snapshots with their own configs). Forward mode: +£0.88/14 d (≪ £0.06/day, within solver noise).
The eight PRs in the v12 fix stack
| PR | Role | What |
|---|---|---|
| #341 | Diagnostic | Snapshot LP inputs on Infeasible runs + new lp_status column |
| #344 | The fix | Shower-window tank floor → soft constraint, 50 p/K-slot penalty. Closes 8/9 residual-class events. |
| #342 | Defense | LP-Infeasible-with-appliance → drop appliance, re-solve once |
| #343 | Modeling | Drop DHW/space mode mutex; matches Altherma firmware; removes 192 binaries/solve |
| #345 | Perf | LP_HP_MIN_ON_SLOTS default 2 → 1; firmware handles anti-short-cycling; another 96 binaries/solve |
| #346 | Test | Real-pipeline appliance dispatch integration test |
| #347 | Diagnostic | replay_run handles Infeasible snapshots; closes the replay loop |
| #348 | Bookkeeping | Refresh lp_regression_baseline.json post-stack |
Migration notes (v11.0.0 → v12.0.0)
- Default config changes:
LP_HP_MIN_ON_SLOTS2 → 1. Set explicitly in.envto keep v11 behaviour. - New env knob:
LP_SHOWER_LO_PENALTY_PENCE_PER_DEGC_SLOT(default 50.0). - DB schema:
lp_inputs_snapshot.lp_statuscolumn added (nullable, NULL = "Optimal" for legacy rows). Auto-applied via the migration block insrc/db.py. - No prod credential changes. No MCP tool surface changes. No Daikin / Fox API surface changes.
Other changes since v11.0.0 (full list)
54 commits since v11.0.0. Highlights beyond the headline stack:
- PV-sufficiency guard rail + daily PV calibration refresh (#331).
- Tariff-window-aware base-load forecast (#311).
- Weekly legionella cycle as a tank-floor constraint (#317).
- Per-installation Daikin LWT→kW calibration (#316, #318, #319, #320).
- Phase A data quality: brief Fox-vs-meter audit, Daikin-quota-friendly heartbeat (#308).
- Real-money PnL from measured grid import (#307).
- Daikin write-budget guard + Sunday legionella skip (#289).
- Climate fields stripped on every Daikin write — closes the 2026-05-11 climate-strip incident (#321).
See CHANGELOG.md for the full categorised list.
Pull image
docker pull ghcr.io/albinati/home-energy-manager:v12.0.0
# Or follow the tag:
docker pull ghcr.io/albinati/home-energy-manager:mainProd is on revision: 6332b3c4... (the tip of v12). Health endpoint: curl http://127.0.0.1:8000/api/v1/health.
Verification
# Reproduce the regression-gate result on your prod DB snapshot:
DB_PATH=/path/to/prod-snapshot.db PYTHONPATH=. .venv/bin/python \
scripts/check_lp_regression.py --vs-ref=v11.0.0 --days 14 --mode=bothExpected: PASS on both honest and forward modes. Aggregate honest delta ≤ 0 vs v11.0.0 baseline.
🤖 Generated with Claude Code
v11.0.0 — V11 stack: Quartz nowcast · per-hour microclimate · MPC triggers · Telegram-direct
V11 — Accuracy via past-data integration · Quartz nowcast · MPC trigger set · Telegram-direct.
23 commits since v10.3.0. The headline is the V11 epic stack landing — Quartz Solar PV nowcasting, per-hour microclimate calibration, the full event-driven MPC trigger family, and a regression baseline rebased on post-V11 prod data. On top of that, HEM now POSTs notifications straight to the Telegram Bot API, removing OpenClaw's LLM-shaping tax from the messaging path.
🌤️ Quartz forecast source (#260, #265, #266, #277, #279, #281, #282)
- New PV nowcast provider: Quartz Solar. Switch via
FORECAST_SOURCE=quartzand the newQUARTZ_*env block. Open-Meteo remains the weather source + automatic fallback when Quartz auth/network is unavailable. - Token-lifecycle hardening: bearer refresh, auth-failure circuit breaker.
- Refresh cadence dropped from hourly to 30 min (parity with Octopus tariff cadence).
- Open-Meteo temp/cloud now interpolated to half-hour slots (#277).
- Per-hour today-aware adjuster fixes asymmetric morning/afternoon bias on partly-cloudy days (#266).
- Direct-PV path skips radiation-trained calibration (irrelevant when Quartz already returns kW directly) (#279).
- Cloud-bucket calibration table rebuild auto-skipped when
FORECAST_SOURCE=quartz(#281). today_factoris now scoped to today only and warm-starts with a per-hour median for unobserved hours (#271, #282) — fixes morning / overcast-day starvation that was costing peak-export commitments.
📅 Per-hour microclimate calibration (#257)
- Forecast-vs-actual rebuild gained per-hour microclimate offsets — the LP now sees
outdoor_temp_calibrated[hour] = forecast + microclimate_offset[hour_of_day]. - Sign-error in #255 fixed up-front; calibration is bi-directional and learns from any consistent provider bias.
- Microclimate offset propagates into Daikin COP curve evaluation, so heat-pump kWh forecasts track measured kWh more closely on sites with persistent local bias.
🏃 MPC trigger set (#262, #264, #270, #283)
- Live PV / load deviation triggers (#264) — gross-AC compare, conservative defaults. Re-solves on real-time deviation rather than waiting for the next clock tick.
- Peak-export economic margin guard (#262) — observation-only by default; logs when a planned
peak_exportslot's marginal margin shrinks belowLP_PEAK_EXPORT_MIN_MARGIN_PENCE_PER_KWH=3.0p. - Daikin 2 h-aligned heartbeat refresh window (#270) — first slice of [Epic #267]: Daikin observations now align to even-numbered hours where Onecta returns 2-hourly consumption data, doubling per-call info density.
- Import-overshoot trigger (#283) — triggers an MPC re-solve when measured grid import overshoots the LP plan by more than the configured threshold (hardware drifting from plan).
📲 Direct Telegram Bot API transport (#284)
- New
src/telegram_transport.pyPOSTs straight toapi.telegram.orgwhenTELEGRAM_BOT_TOKEN+TELEGRAM_CHAT_IDare set in.env. OpenClaw/hooks/agentis fully skipped — no LLM reshaping per notification. push_alertevents (cheap/peak/negative window, daily PnL) now have bespoke per-event rendering inside the notifier — no JSON dump.- Plan-proposed messages ship the schedule inside
<pre>for monospace alignment + a "reject viareject_plan(...)" line. Interactive buttons remain a deferred follow-up; defaultPLAN_AUTO_APPROVE=true+ auto-accept timeout means the workflow is unaffected. OPENCLAW_NOTIFY_ENABLED=falsestill mutes both transports. Per-AlertType routes in thenotification_routesSQLite table still gate enable/severity/silent. Roll back: unsetTELEGRAM_BOT_TOKEN.
🧺 Appliance lifecycle hooks (#280)
- Two new notifications:
appliance_armed(LP picked a window for a registered appliance + armed the cron) andappliance_cancelled(cron dropped because Smart Control was disabled, or the deadline passed, or LP re-planned to a different window). replan=trueflag on the armed hook so OpenClaw / Telegram can phrase the message as a revision rather than a fresh arm.
🔧 Fixes & infrastructure
- Fox V3 fdPwr merging (#278) — when the LP collapses two adjacent
force_chargewindows into one V3 group, the resulting fdPwr is now the duration-weighted average rather than max-of, fixing inverter overdraw on mixed-tariff windows. dispatch_decisionsmigration (#259) — adds the V11 audit columns to existing DBs without manual intervention.- LP regression baseline rebased (#276) —
scripts/check_lp_regression.pybaseline now reflects post-V11 prod data; the gate is operational again on everysrc/scheduler/PR. - Replay canonical schema (#258) — replay-by-fetch path with history exogenous, removes a class of replay flakes when forecast revisions land mid-window.
docker-publishworkflow serialised per ref (#269) — fixes the:maintag race that occasionally pushed a stale digest when two merges arrived inside the build window.- PV telemetry default 5 min (#272) — was 30, dropped for live-MPC-deviation triggers to fire on relevant signal.
- Microclimate offset sign + dead HiGHS branch removed (#255).
📈 Tests
- 936 passed (was 866 on v10.3.0; +70 net).
- New:
tests/test_telegram_transport.py(16 cases — escaping, transport behavior, dispatch routing, push_alert rendering, plan-proposed HTML);tests/test_mpc_import_overshoot.py; numerous Quartz / microclimate / replay coverage additions.
⬆️ Operator upgrade notes
- Pull image:
docker compose pull && systemctl restart hem. - (Optional) Enable Telegram-direct: add
TELEGRAM_BOT_TOKEN=<from @BotFather>andTELEGRAM_CHAT_ID=<your chat id>to/srv/hem/.env, restarthem. To roll back, unset and restart — OpenClaw hook config remains untouched. - (Optional) Switch to Quartz nowcast:
FORECAST_SOURCE=quartz+ theQUARTZ_*block. Open-Meteo continues to serve as the weather source; Quartz only replaces the PV forecast. - DB migrations for
dispatch_decisionsare auto-applied at service startup (#259). No manual schema work. PV_TELEMETRY_INTERVAL_MINUTESdefault is now 5 (was 30). If you previously relied on the 30 min default for rate-limiting purposes, set it explicitly.
The V11 epic itself (#193) remains open — V11-B (quantile-based scenario perturbations), V11-C (DHW draw learning), V11-D (occupancy & variable-load inference) are pending. The accuracy foundation they build on is in this release.
v10.3.0 — Legionella-aware LP + OpenClaw-friendly Daikin/Fox MCP semantics
Highlights from PR #244 — three coupled changes that fell out of the 2026-05-03 OpenClaw "Daikin off" misread audit:
Legionella-aware LP (passive mode)
predict_passive_daikin_loadnow injects a one-shot DHW thermal pulse on the firmware-fired legionella cycle so the LP can allocate PV/battery/grid around it.- New runtime_settings keys (mutable via
PUT /api/v1/settings/{key}+ MCPset_setting, no restart needed):DHW_LEGIONELLA_DAY(-1 = disabled, 0=Mon..6=Sun),DHW_LEGIONELLA_HOUR_LOCAL,DHW_LEGIONELLA_DURATION_MIN,DHW_LEGIONELLA_TANK_TARGET_C. - Defaults to disabled; pre-fix behaviour byte-identical until the day/hour are set to match what's configured in the Daikin Onecta app.
OpenClaw-friendly Daikin status semantics
DaikinStatusnow carries explicitclimate_onanddhw_onper-zone flags, plus a deterministicstate_summaryone-liner like"DHW maintaining (tank 41/40°C); climate weather-regulated (LWT 22°C); hem-control=passive".- Surfaced through both MCP (
get_daikin_status) and REST (/api/v1/daikin/status). - Tool description warns LLM consumers not to read the legacy
is_onas "the unit is on" — it's only the climate zone, which is null/idle in summer when DHW is the active zone (root cause of the OpenClaw misread).
Cockpit _legend block + set_inverter_mode confirmation gate
/api/v1/cockpit/nowand/api/v1/cockpit/atnow carry a_legendblock explaining sign conventions inline (grid_kwpositive=IMPORTING / negative=EXPORTING;battery_kwpositive=CHARGING / negative=DISCHARGING) plus units for every other field. Fixes the gap where PydanticFielddescriptions never reach JSON consumers.set_inverter_modenow hasconfirmed: bool = Falsematching the Daikin write tools, with the same plan-conflict gate. Closes the long-standing[OpenClaw boundary] hardware-write tool 'set_inverter_mode' lacks 'confirmed'startup banner.
Tool description tightening
Several MCP tools had terse one-liners that LLM agents could only guess at. Tightened: get_battery_forecast, get_weather_context, get_action_log, get_optimizer_log, acknowledge_warning, override_schedule, get_optimization_plan, set_daikin_power / mode / lwt_offset, get_soc (now also exposes soc_percent as the canonical name; soc kept as alias).
Removals / cleanup
- Deprecated
DHW_LEGIONELLA_*env block in.env.example(replaced by new commented examples for the runtime-settings shape). - Two legacy unused config keys:
FOX_LP_BRIDGE_GAP_SLOTS,FOX_LP_BRIDGE_MAX_PRICE_PENCE. - Orphan
occupancy_settingstable (superseded byruntime_settings+presence_periods); now dropped on service init via_migrate_schema.
Tests
- 866 passed, 1 skipped (was 848 on v10.2.0; +18 net).
- New:
tests/test_legionella_uplift.py(6 cases pinning the uplift truth table);tests/test_daikin_status_summary.py(9 cases pinningstate_summary+ the enriched_device_status_dictshape). - Strictened:
tests/test_mcp_boundary_selfcheck.pynow assertsaudit_mcp_tool_surface(mcp) == [](was: carve-out forset_inverter_mode).
Operator notes for upgrade
- After pull + restart, set the legionella day/hour to match what's configured in your Daikin Onecta app:
curl -X PUT -H 'Content-Type: application/json' -d '{"value": 6}' http://localhost:8000/api/v1/settings/DHW_LEGIONELLA_DAY # 6 = Sunday curl -X PUT -H 'Content-Type: application/json' -d '{"value": 13}' http://localhost:8000/api/v1/settings/DHW_LEGIONELLA_HOUR_LOCAL # 13 = 1 PM local - If your prod has
REQUIRE_SIMULATION_ID=true, prepend aPUT /api/v1/settings/{key}/simulatestep and pass the returnedsimulation_idasX-Simulation-Id. - OpenClaw / any agent reading
get_daikin_statusshould switch fromis_ontostate_summary(orclimate_on+dhw_on).
🤖 Generated with Claude Code
v10.2.0 — V11-A foundation + post-v10 hardening
Full Changelog: v10.0.1...v10.2.0
v10.0.1 — hotfix: PnL completeness + warning-table TTL
Hotfix on top of v10.0.0. Two bugs surfaced by the V10 audit while planning V11.
Fixes
- #199 — Night-brief PnL was under-reporting realised cost on days the heartbeat missed half-hour slots (service restart spanning a slot, etc.).
update_execution_log_meterednow INSERTs a synthetic row taggedsource='metered_synthetic'when no heartbeat row exists but an Octopus Agile rate is published — looks up the rate, computes all 4 cost columns, preserves the lineage label across idempotent re-runs. - #200 —
acknowledged_warningstable was growing unbounded (per-day-keyed acks likefox_scheduler_disabled_2026-04-29are useless once the date passes but never got cleaned). Added to the existing nightly retention sweep with a 30-day default (new env varACKNOWLEDGED_WARNINGS_RETENTION_DAYS=30).
Tests
637 passed, 1 skipped (pre-existing test_mcp_singleton.py collection error from PR #163's stdio→HTTP migration). New coverage for the synthetic-INSERT path, missing-rate fallback, idempotent lineage preservation, and the new prune policy.
Operational notes
- Pure data-correctness fixes; no LP / scheduler / dispatch behaviour changed.
- Migration-free (
acknowledged_warningstable already exists). - New env var
ACKNOWLEDGED_WARNINGS_RETENTION_DAYSis optional; default 30 d. source='metered_synthetic'is a new sentinel value alongside the existingestimatedandmetered. Filters that match on exact source values may want to add it.
🤖 Generated with Claude Code
v10.0.0 — Event-driven scheduler · scenario robustness · twice-daily digest
v10.0.0 — Event-driven scheduler · scenario robustness · twice-daily digest
96 commits since v9.1.0 (2026-04-19 → 2026-04-29). The headline themes are:
- Immutable Docker deploy with isolation from co-tenant OpenClaw (#163)
- Cockpit/Workbench mobile-first UX with simulate→approve flow (PRs #116–#142)
- Event-driven MPC — fixed-hour cron retired in favour of cooldown + drift + forecast-revision + tariff-tier-boundary triggers (PRs #151, #152, #189)
- Forecast-robust dispatch — 3-pass scenario LP for peak-export commitments; the unsafe live-SoC global gate is gone (#186, #187)
- 48h horizon + per-hour PV calibration + Fox-telemetry load profile — the LP now sees a full Day-ahead window with realistic priors (#153, #154, #155, #156, #169, #175, #176)
- Twice-daily Telegram digest + ad-hoc pings for negative-price 🔵 windows + plan revisions only when material (#188, #189)
- Real metered PnL via Octopus smart-meter consumption backfill (#190)
- Pre-merge LP regression gate — every LP-touching PR must replay the last 14 days and prove "no worse than baseline" (#191, #192)
This is the V10 / V10.1 / V10.2 / V11 / V12 / V13 work merged together as the new release line. Honest improvement vs V9 baselines on the bootstrap window (2026-04-25 → 2026-04-29) is roughly £0.20/day in solver-isolated savings (snapshot-config replay, cloud cover lost in snapshot). Headline figures from forward-mode replay in earlier conversations were inflated by an Infeasible-state day; the regression gate now pins the honest baseline.
Breaking changes
LP_MPC_HOURSremoved. The fixed-hour MPC cron is gone — the system is now fully event-driven. Triggers: cooldown gate (MPC_COOLDOWN_SECONDS), SoC drift hysteresis (MPC_DRIFT_*), forecast revision (MPC_FORECAST_*), tariff tier boundaries (TIER_BOUNDARY_LEAD_MINUTES), Octopus rates land, plan-push rollover.EXPORT_DISCHARGE_MIN_SOC_PERCENTremoved. The live-SoC global gate that dropped tomorrow's profitable peak-export when today's battery was below 95 % is gone. Replaced by the scenario LP filter (LP_PEAK_EXPORT_PESSIMISTIC_FLOOR_KWH, default 0.30 kWh) that commits peak-export only when the pessimistic forecast scenario also exports profitably.OPERATION_MODE=simulation|operationalremoved (#130). The plan lifecycle is nowsimulate → approve → livefor every run;OPENCLAW_READ_ONLYis the only kill switch.- Daily-brief env vars renamed.
DAILY_BRIEF_HOUR/MINUTE→BRIEF_MORNING_HOUR/MINUTE(default 08:00 local). NewBRIEF_NIGHT_HOUR/MINUTEfor the actuals digest (default 22:00 local). - Heartbeat tariff-transition pings muted by default.
NOTIFY_TARIFF_TRANSITIONS=false; the morning brief covers the day's windows. Negative-price 🔵 entries always ping regardless. - Container is the singleton. The MCP server stdio-flock guard was removed when the production launcher moved to long-lived HTTP under
/mcp(#163). Local dev (single user) is unaffected.
Highlights — what's actually new
Deployment & isolation (#163)
- Docker image published from CI to GHCR on every push to
main(ghcr.io/albinati/home-energy-manager:<sha>, ARM64). - Read-only rootfs, tmpfs
/tmp,cap_drop: ALL, mem limits, uid 1001 inside the container. - MCP transport switched from per-call stdio subprocess to long-lived HTTP under
/mcp, guarded byBearerAuthMiddleware(token atdata/.openclaw-token, generated by lifespan on first boot). - OpenClaw runs as user
openclaw(uid 2000), not in the docker group, no write access to/srv/hem/data/. This puts the application code out of OpenClaw's reach — fixes the security regression of the prior native-on-host migration. - Daikin OAuth re-enrollment via one-shot container (
deploy/compose.daikin-auth.yaml); rollback procedure documented indeploy/README.md.
Forecast-robust dispatch (#186, #187, #188)
- New
src/scheduler/scenarios.pyruns three parallel LP solves per planning trigger (optimistic / nominal / pessimistic) with perturbed temperature and load. UsesThreadPoolExecutor(max_workers=3)— HiGHS releases the GIL, so it's near-linear scaling. filter_robust_peak_exportdropspeak_exportslots whose pessimistic export is belowLP_PEAK_EXPORT_PESSIMISTIC_FLOOR_KWH; survivors keep their full original kWh. Decisions persisted to a newdispatch_decisionstable for audit.- New MCP tool
simulate_peak_export_robustnessand API endpointsGET /api/v1/optimization/decisions/{run_id|latest}andGET /api/v1/foxess/schedule_difffor full dispatch-layer observability. - New
validate_scenario_filter.pyscript — replays past peak-export decisions against actually-published rates and scores them; flags filter regressions before merge. - New blue 🔵 PAID-to-use tier on the family Google Calendar for negative-price windows (rare, ~1–2/week, immediately actionable).
Event-driven MPC + tier-boundary triggers (#151, #152, #189)
- The fixed-hour MPC cron (
LP_MPC_HOURS=[5,9,12,15,20]) caused a real loss on 2026-04-28 23:00 — heating ramp depleted the battery in a 9 h overnight gap with no LP re-solve to react. Now retired entirely. - New
_register_tier_boundary_triggers()schedules a one-shot DateTriggerTIER_BOUNDARY_LEAD_MINUTES(default 5 min) before every tariff tier boundary in today + tomorrow, using the sameclassify_day()the family calendar already uses. Re-registers on Octopus fetch (when tomorrow's rates land). - SoC-drift hysteresis tightened from 2 ticks to 1 (
MPC_DRIFT_HYSTERESIS_TICKS=1) to catch heating ramps mid-window. - Cooldown (
MPC_COOLDOWN_SECONDS=300) is the single dedup gate; back-to-back transitions silently coalesce as designed.
Notification redesign (#189)
- Twice-daily digest is now the canonical channel:
bulletproof_morning_brief_job(08:00 local, today's forecast + planned peak-export commitments + expected vs SVT) andbulletproof_night_brief_job(22:00 local, realised cost + slot-by-slot + peak-export verdicts). - Mute by default: cheap- and peak-window-start pings (
NOTIFY_TARIFF_TRANSITIONS=false). - Always ping: 🔵 PAID-to-use entry (rare, immediately actionable).
- Per-day debounce on
notify_risk("Fox ESS scheduler flag disabled")— was firing every 2 min while the flag was off. - Per-action_id dedup on
notify_user_override— fires once at episode start + once at recovery, not every heartbeat. - New
PLAN_REVISIONAlertType — fires only when an in-day MPC re-solve materially changed the next-4 h plan (PLAN_REVISION_MIN_SOC_DELTA_PERCENT≥10orPLAN_REVISION_MIN_GRID_DELTA_KWH≥1.0); cron and routine triggers are suppressed.
LP horizon, calibration, profile (#153, #154, #155, #156, #169, #175, #176)
- 48 h horizon with a historical-prior pre-plan for D+1 (#169) — solver sees a full day-ahead view rather than truncating at midnight.
- Per-hour-of-day PV calibration replaces the flat scale factor (#156) — the LP no longer over-promises midday solar in winter.
- Calibration window shortened 250 → 30 days (#155) — seasonally responsive.
- Per-slot Octopus Outgoing export prices in the LP objective (#154) — was previously using a flat export price.
- Half-hour granularity for priors and load profile (#175); load profile now read from real Fox telemetry rather than the estimator default (#176).
- Daikin physics subtracted from base-load to kill the double-count (#179).
PnL & observability (#190, plus all the cockpit work)
- New
bulletproof_consumption_backfill_job— pulls metered consumption from the Octopus smart-meter API at 04:00 local, rewritesexecution_log.consumption_kwhper slot. The night brief's "actuals" line now uses real meter data, not heartbeat-sample × 0.5h estimates. - Cockpit dashboard rebuilt mobile-first with simulate→approve UI, merged 24-hour timeline, click-for-slot-detail, batch settings apply, Workbench LP-knob tuner, Insights browser with period nav (#119, #120, #122, #123, #125, #126, #127, #128, #129, #133, #135, #137, #139).
- 10 new read-only MCP tools for cockpit-parity LLM clients (#138).
dispatch_decisionsandscenario_solve_logtables provide a per-slot audit trail of every LP decision and the scenarios that informed it.
Hardware-accurate caps (#164)
- Battery / inverter / G98 1φ export caps now match the actual Fox H1-5.0-E-G2 + EP11 + 4.5 kW PV install (3.68 kW export limit).
Pre-merge regression gates (#191, #192)
scripts/check_lp_regression.py— replays the last 14 days of historical inputs through the LP, scores against actually-published rates, fails if total replayed cost exceeds the pinned baseline by more than 50 p over the window.tests/fixtures/lp_regression_baseline.jsonpinned atacf0979(2026-04-29). Bumping it requires--refresh-baselineand committing the JSON in the same PR.- Same workflow for the dispatch-layer scenario filter via
scripts/validate_scenario_filter.py(PR template enforces both gates forsrc/scheduler/changes).
Operational notes
- Honest replay-mode improvement vs the V9.1 baseline on the bootstrap window (2026-04-25 → 2026-04-29) is ~£0.20/day of solver-isolated savings; the regression gate is calibrated against this, with a 50 p tolerance over 14 days for solver float-precision drift.
- Earlier conversations cited a £3.41/4-day forward-mode figure — that was inflated by an Infeasible-state day re-replayed under current code. The honest baseline is the one pinned in
tests/fixtures/lp_regression_baseline.json. - Replay does not simulate cloud cover (snapshot loses it); the comparison is solver-on-snapshot-inputs, not full closed-loop.
- Mobile readers: the night brief is now the default channel for "did we save money today?" — the morning brief is forecast-only.
- Rollback to v9.1.2: pull the
9.1.2image tag andsystemctl restart hem; the data volume is forward-compatible (additive migrations only).
Closed issues / PRs
v9.1.0 — Hardening release
Patch release after v9.0.0.
Changes
- Scheduler: Shared peak window helpers (
scheduler_peak_contains_wall_time,utc_instant_in_scheduler_peak);compute_lwt_adjustmentuses the same local wall clock as Agile slots (fixes BST skew on the legacy Daikin LWT path when enabled). - Config: Removed unused
ALERT_OPENCLAW_URL/ALERT_CHANNEL(useOPENCLAW_*only). - API: British Gas listed as not configured until implemented; clearer 503 copy; energy stub routes in
src/api/routers/energy_providers.py. - FoxESS: Removed
get_device_settings()(unsupported Open API). - Quality: Ruff +
requirements-dev.txt; narrower exception handling in notifier/DB; peak sync tests.
Version: pyproject.toml → 9.1.0. Full list in CHANGELOG.md.
v9.0.0 — V9 feature release
V9 feature release (tagged at 34650d7, pre-hardening).
Highlights (see CHANGELOG.md — V9: solar_charge, MPC cadence, BST fix, preset DHW):
- solar_charge LP slots (PV-heavy windows without blind ForceCharge)
- MPC cadence (
LP_MPC_HOURS,LP_MPC_WRITE_DEVICES) - BST fix for Agile peak windows; notification cleanup; preset-aware DHW
- Strategy string includes
solar=N
Use v9.1.0 for the follow-up hardening patch (peak/LWT sync, config cleanup, tooling).