Skip to content

Releases: albinati/home-energy-manager

v12.0.0 — LP residual-class Infeasibility surface closed

20 May 00:39
30e3355

Choose a tag to compare

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_SLOTS 2 → 1. Set explicitly in .env to keep v11 behaviour.
  • New env knob: LP_SHOWER_LO_PENALTY_PENCE_PER_DEGC_SLOT (default 50.0).
  • DB schema: lp_inputs_snapshot.lp_status column added (nullable, NULL = "Optimal" for legacy rows). Auto-applied via the migration block in src/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:main

Prod 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=both

Expected: 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

09 May 00:42

Choose a tag to compare

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=quartz and the new QUARTZ_* 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_factor is 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_export slot's marginal margin shrinks below LP_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.py POSTs straight to api.telegram.org when TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID are set in .env. OpenClaw /hooks/agent is fully skipped — no LLM reshaping per notification.
  • push_alert events (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 via reject_plan(...)" line. Interactive buttons remain a deferred follow-up; default PLAN_AUTO_APPROVE=true + auto-accept timeout means the workflow is unaffected.
  • OPENCLAW_NOTIFY_ENABLED=false still mutes both transports. Per-AlertType routes in the notification_routes SQLite table still gate enable/severity/silent. Roll back: unset TELEGRAM_BOT_TOKEN.

🧺 Appliance lifecycle hooks (#280)

  • Two new notifications: appliance_armed (LP picked a window for a registered appliance + armed the cron) and appliance_cancelled (cron dropped because Smart Control was disabled, or the deadline passed, or LP re-planned to a different window).
  • replan=true flag 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_charge windows 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_decisions migration (#259) — adds the V11 audit columns to existing DBs without manual intervention.
  • LP regression baseline rebased (#276)scripts/check_lp_regression.py baseline now reflects post-V11 prod data; the gate is operational again on every src/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-publish workflow serialised per ref (#269) — fixes the :main tag 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

  1. Pull image: docker compose pull && systemctl restart hem.
  2. (Optional) Enable Telegram-direct: add TELEGRAM_BOT_TOKEN=<from @BotFather> and TELEGRAM_CHAT_ID=<your chat id> to /srv/hem/.env, restart hem. To roll back, unset and restart — OpenClaw hook config remains untouched.
  3. (Optional) Switch to Quartz nowcast: FORECAST_SOURCE=quartz + the QUARTZ_* block. Open-Meteo continues to serve as the weather source; Quartz only replaces the PV forecast.
  4. DB migrations for dispatch_decisions are auto-applied at service startup (#259). No manual schema work.
  5. PV_TELEMETRY_INTERVAL_MINUTES default 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

03 May 14:52

Choose a tag to compare

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_load now 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} + MCP set_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

  • DaikinStatus now carries explicit climate_on and dhw_on per-zone flags, plus a deterministic state_summary one-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_on as "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/now and /api/v1/cockpit/at now carry a _legend block explaining sign conventions inline (grid_kw positive=IMPORTING / negative=EXPORTING; battery_kw positive=CHARGING / negative=DISCHARGING) plus units for every other field. Fixes the gap where Pydantic Field descriptions never reach JSON consumers.
  • set_inverter_mode now has confirmed: bool = False matching 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_settings table (superseded by runtime_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 pinning state_summary + the enriched _device_status_dict shape).
  • Strictened: tests/test_mcp_boundary_selfcheck.py now asserts audit_mcp_tool_surface(mcp) == [] (was: carve-out for set_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 a PUT /api/v1/settings/{key}/simulate step and pass the returned simulation_id as X-Simulation-Id.
  • OpenClaw / any agent reading get_daikin_status should switch from is_on to state_summary (or climate_on + dhw_on).

🤖 Generated with Claude Code

v10.2.0 — V11-A foundation + post-v10 hardening

02 May 22:26

Choose a tag to compare

v10.0.1 — hotfix: PnL completeness + warning-table TTL

29 Apr 11:44
caf1894

Choose a tag to compare

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_metered now INSERTs a synthetic row tagged source='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.
  • #200acknowledged_warnings table was growing unbounded (per-day-keyed acks like fox_scheduler_disabled_2026-04-29 are useless once the date passes but never got cleaned). Added to the existing nightly retention sweep with a 30-day default (new env var ACKNOWLEDGED_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_warnings table already exists).
  • New env var ACKNOWLEDGED_WARNINGS_RETENTION_DAYS is optional; default 30 d.
  • source='metered_synthetic' is a new sentinel value alongside the existing estimated and metered. 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

29 Apr 09:24

Choose a tag to compare

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:

  1. Immutable Docker deploy with isolation from co-tenant OpenClaw (#163)
  2. Cockpit/Workbench mobile-first UX with simulate→approve flow (PRs #116#142)
  3. Event-driven MPC — fixed-hour cron retired in favour of cooldown + drift + forecast-revision + tariff-tier-boundary triggers (PRs #151, #152, #189)
  4. Forecast-robust dispatch — 3-pass scenario LP for peak-export commitments; the unsafe live-SoC global gate is gone (#186, #187)
  5. 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)
  6. Twice-daily Telegram digest + ad-hoc pings for negative-price 🔵 windows + plan revisions only when material (#188, #189)
  7. Real metered PnL via Octopus smart-meter consumption backfill (#190)
  8. 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_HOURS removed. 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_PERCENT removed. 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|operational removed (#130). The plan lifecycle is now simulate → approve → live for every run; OPENCLAW_READ_ONLY is the only kill switch.
  • Daily-brief env vars renamed. DAILY_BRIEF_HOUR/MINUTEBRIEF_MORNING_HOUR/MINUTE (default 08:00 local). New BRIEF_NIGHT_HOUR/MINUTE for 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 by BearerAuthMiddleware (token at data/.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 in deploy/README.md.

Forecast-robust dispatch (#186, #187, #188)

  • New src/scheduler/scenarios.py runs three parallel LP solves per planning trigger (optimistic / nominal / pessimistic) with perturbed temperature and load. Uses ThreadPoolExecutor(max_workers=3) — HiGHS releases the GIL, so it's near-linear scaling.
  • filter_robust_peak_export drops peak_export slots whose pessimistic export is below LP_PEAK_EXPORT_PESSIMISTIC_FLOOR_KWH; survivors keep their full original kWh. Decisions persisted to a new dispatch_decisions table for audit.
  • New MCP tool simulate_peak_export_robustness and API endpoints GET /api/v1/optimization/decisions/{run_id|latest} and GET /api/v1/foxess/schedule_diff for full dispatch-layer observability.
  • New validate_scenario_filter.py script — 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 DateTrigger TIER_BOUNDARY_LEAD_MINUTES (default 5 min) before every tariff tier boundary in today + tomorrow, using the same classify_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) and bulletproof_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_REVISION AlertType — fires only when an in-day MPC re-solve materially changed the next-4 h plan (PLAN_REVISION_MIN_SOC_DELTA_PERCENT≥10 or PLAN_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, rewrites execution_log.consumption_kwh per 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_decisions and scenario_solve_log tables 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.json pinned at acf0979 (2026-04-29). Bumping it requires --refresh-baseline and 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 for src/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.2 image tag and systemctl restart hem; the data volume is forward-compatible (additive migrations only).

Closed issues / PRs

#116, #1...

Read more

v9.1.0 — Hardening release

19 Apr 18:56

Choose a tag to compare

Patch release after v9.0.0.

Changes

  • Scheduler: Shared peak window helpers (scheduler_peak_contains_wall_time, utc_instant_in_scheduler_peak); compute_lwt_adjustment uses 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 (use OPENCLAW_* 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.toml9.1.0. Full list in CHANGELOG.md.

v9.0.0 — V9 feature release

19 Apr 18:56

Choose a tag to compare

V9 feature release (tagged at 34650d7, pre-hardening).

Highlights (see CHANGELOG.mdV9: 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).