RW2: real physical & economic limits — grid cap, EV/DHW losses, enforce-or-reject, ignored-window honesty (specs/003 Phase 2) - #27
Merged
Conversation
…205) _determine_reason in both backends labeled every negative-power slot pv_surplus, so on the real home the 19:45-21:15 peak discharge showed a PV reason at night. Mirror cheap_threshold with a top-quartile expensive_threshold: discharge at/above it is expensive_grid; below it the pv_surplus fallback remains. Goldens carry no reason field - no parity impact. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
…201) grid_import_limit_kw / grid_export_limit_kw (None = legacy unbounded) bound each settlement leg per slot; a cap the fixed loads cannot satisfy returns INFEASIBLE instead of silently over-drawing the fuse. Solving with load_solutions=False makes INFEASIBLE actually reachable - the appsi wrapper used to raise into the ERROR path before status mapping. SC-RW2a property test over randomized instances; goldens bit-identical. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
EVChargerManifest gains charge_efficiency (default 0.9, onboard AC charger + cable losses) wired into its StorageSpec; WaterHeaterManifest gains heating_efficiency (default 0.98) wired into BOTH energy paths - the converter factor (tank q_in) and the storage charge leg - so the two states stay consistent. No discharge fields where no discharge path exists (would be accepted-but-ignored, FR-205). Backend B picks both up through the shared StorageSpec/ConverterSpec. Goldens recaptured; diff is the predicted direction only: DHW slots scale by 1/0.98, EV SoC-target charging draws deficit/0.9 extra input, objectives rise (full_house 11.488->11.538, onboarding 4.619->5.286). ev_departure is unchanged - its window is an energy-input requirement. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
The floor itself landed with RW1 (both backends anchor terminal neutrality to the measured start); this adds the missing FR-204 tests: a 20 % start on flat prices stays idle instead of being force-charged toward 50 %, and arbitrage from an 8.1 kWh start ends at or above it. Backend B asserts with 0.5 kWh slack for its DP level-grid drift. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
…06, T205) Shared partition (solvers/windows.py) used by BOTH backends: windows with a past deadline, a demand deadline beyond the horizon, or an unknown device are rejected with a warning and surfaced in SolverResult.diagnostics["ignored_windows"] - never clamped into the horizon (the old behavior forced e.g. a 30h SoC target to complete by end-of-horizon and applied past-deadline windows at slot 0). Restrictive requirements (forbidden window, max runtime, comfort band) still truncate safely at the horizon edge. Reason annotation now derives from applied windows only. Fallout fixed honestly rather than papered over: pool_pump.yaml's deliberate year-2030 deadline relied on the clamp - now expressed as deadline_offset_hours: 4; test_hold_temp_band_flat_price_cold_day's deadline predated its own price series by 4 months and passed only via the slot-0 clamp artifact. Goldens bit-identical. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
…204 part 1) Enforced (semi-continuous minimums, both backends): - HP min_modulation_pct -> ConverterSpec.min_input_kw / SinkSpec min_power_kw floor while on; controllable sinks switch from a hard lower bound (always-on) to off-or->=min semantics. - EV min_charge_kw -> StorageSpec.min_charge_kw: charge slots run at >= the wallbox minimum or not at all. Rejected at the pydantic layer (fail loud, no silent no-ops): - HeatPumpManifest.defrost_lockout_minutes != 0 (no solver model yet) - RoomManifest.south_facing_windows=True (solar gains not modeled; window_area_m2 still binds via envelope losses) - EVChargerManifest phase consistency (max_charge_kw <= phases x 32 A x 230 V) and min_charge_kw <= max_charge_kw. Testdata cleaned of the rejected no-ops (heat_pump.json defrost, room_living.json south_facing + tiered-config hints). Goldens recaptured: tie-break/FP noise only (objectives move <= 1e-5) - the modulation floor binds without material cost in those scenarios, and the new SC-RW2c tests prove each field can fail when ignored. test_solve_ev_with_energy_constraint summed slots[:32] against an inclusive-deadline constraint over 33 slots - off-by-one previously hidden by tie-breaking. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
The field was accepted on every ConstraintWindow and priced nothing. Backend A adds a linear objective term: flex x hours-before-deadline x delivered fraction of the required energy (charge leg for storage, so discharge is never rewarded). Backend B shades its slot-price sort with the same per-kWh earliness cost (storage charge-to-target, sinks, and converter input-energy targets; thermal-output targets have no per-kWh input basis and stay unshaded). flex=0 (the default) is a no-op - goldens bit-identical. Test proves a priced window charges near the deadline instead of the cheap early slots, in both backends. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
… T206) NodeSpec gains a per-slot 'gains' series (kW), overlaid per zone via apply_internal_gains (mirrors apply_generation_forecast) and consumed by the Backend A node balance: positive = occupant/appliance heat gain, negative = extraction - a hot-water draw on the tank node, completing the FR-207 'energy in - draw - standby loss' tank model (temperature state, standby loss, and measured start landed earlier). Gains are keyed by device_id so they attribute to exactly one zone, never duplicated across rooms (FR-208). solve() grows an optional internal_gains param (default None, behaviour-preserving; goldens bit-identical). Backend B warns loudly that it ignores the series until T404 makes it consume-or-refuse. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
The EV charge-efficiency change made full_house's night-slot allocation degenerate: macOS and Linux HiGHS return different equal-cost vertices (energy swaps between slots 0 and 6). Same mechanism as the existing full_house/dhw entry - per-device totals still must match the golden. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
kipp-ing
added a commit
to hemm-ems/ha-hemm
that referenced
this pull request
Jul 16, 2026
Pin points at the RW2 core release (hemm-ems/hemm#27); container CI on this branch's PR is the docker gate once that release is on PyPI. Claude-Session: https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RW2 — the plan respects real physical & economic limits (specs/003 Phase 2)
One golden-capture cycle covering bug#2 + T201–T206, per
handover-rw2-fixes.md. Every change is default-preserving unless noted; golden diffs are recaptured and eyeballed per commit.What's in here
expensive_grid(newPlanReason), notpv_surplus, in both backends. This is the mislabel observed live on the real home (evening −4 kW slots tagged as PV at night).grid_import_limit_kw/grid_export_limit_kwsolver params bound each settlement leg per slot; an unsatisfiable cap returns INFEASIBLE (now actually reachable — the appsi wrapper used to raise into ERROR before status mapping). SC-RW2a Hypothesis property test.charge_efficiency(0.9) and DHWheating_efficiency(0.98, wired to both the converter factor and the storage charge leg). Golden changes are exactly the predicted physics (DHW ×1/0.98, EV deficit/0.9, objectives rise).min_modulation_pctand EVmin_charge_kwnow bind (semi-continuous, both backends);defrost_lockout_minutes,south_facing_windows=True, and implausible EV phase/power combos are rejected at the pydantic layer;flex_cost_per_hour_earlyis priced (linear earliness term in A, price shading in B); forbidden-window discharge gating and EV-no-window→zero-demand get SC-RW2c guards.solvers/windows.py) in both backends: past/beyond-horizon demand deadlines and unknown devices are rejected with a warning and surfaced indiagnostics["ignored_windows"], never clamped. Two artifacts of the old clamp were fixed honestly:pool_pump.yaml's year-2030 deadline →deadline_offset_hours, and a thermal test whose deadline predated its own price series.NodeSpec.gainsseries overlaid per zone (internal_gainssolve param): occupant gains offset heating in their zone only; hot-water draws (negative gains) force reheat on the tank state. Backend B warns loudly that it ignores the series until T404.Contract tightenings (need the ha-hemm follow-up before Pi rollout)
south_facing_windows=Trueanddefrost_lockout_minutes≠0now fail manifest construction — the ha-hemm device flow exposes both fields, so the follow-up ha-hemm release must drop/guard them before this core version reaches the real home (pin gates this).min_charge_kwandmin_modulation_pctnow change plans where set.Gate
make cigreen (ruff + format + mypy + check-clock + 420 unit tests, incl. newtests/test_rw2_limits.pywith 24 RW2 tests).gsuntil green and released, core-first.https://claude.ai/code/session_01DuXc9AjYvJVrexaa5SwixT