Repository navigation
Conversation
limit_timeout never influenced any control decision: it flowed only into get_effective_timeout(), which feeds diagnostic sensors and the CSV column but gates nothing (min_interval is the actual cooldown). Learning even auto-computed and auto-applied a limit_timeout value that then went unused. It was a third configuration knob for the user to reason about with no functional effect. Remove the parameter end to end: - const: CONF_LIMIT_TIMEOUT, DEFAULT_LIMIT_TIMEOUT, MIN_LIMIT_TIMEOUT - config_flow: the option in both the setup and options schemas - MPCController: the limit_timeout constructor arg and property; get_effective_timeout now falls back to DEFAULT_DEAD_TIME * safety factor internally - ThermalLearning.compute_optimal_parameters: stop computing/returning limit_timeout - __init__: stop applying it; seed last_change_time from min_interval instead - sensor: drop the Learned Limit Timeout sensor and the limit_timeout attributes - translations (en/fr), README The Effective Timeout sensor and the CSV effective_timeout column are kept as diagnostics (they show the learned thermal lag), now clearly documented as advisory rather than control-gating. Config now exposes deadband and min_interval only. Behaviour verified unchanged on the production replay: 130 changes, 21 reversals, identical to the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Near the setpoint the controller previously collapsed to the lowest fan speed, driven by the fan-rank (energy) penalty. On a running heat pump the compressor is the dominant load and keeps producing regardless of fan speed, so minimising fan rank there optimises the wrong term and produces a drift-then-blast cycle instead of a steady hold. Within one deadband of the setpoint (the "hold zone") we now: - tolerate a small, bounded wrong-side undershoot (HOLD_UNDERSHOOT_TOLERANCE) so a discrete speed ladder can actually hold instead of falling back to the weakest mode to avoid a sub-threshold overshoot; - shrink the fan-rank penalty to a tie-breaker (HOLD_RANK_SCALE). The feature is dormant whenever the error exceeds the deadband, so far-from-setpoint escalation is untouched. Enabled by default: on the 899 h production trace it shifted ~14% of time from `low` to a steady `med` hold and cut fan changes 357->313 with no change in predicted comfort (MAE T+10) or average cost. replay_bench exposes the three constants as tunables for A/B comparison. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The min-change interval was a fixed 10 min while the live learned dead time ranges 6-31 min (median 12) on the production trace. Whenever the dead time exceeds the fixed interval the controller could change the fan again before it had observed the effect of the previous change — acting blind, which invites oscillation. The configured min_interval is now a floor: once learning is ready the effective dwell is raised toward the learned dead time, capped at MAX_ADAPTIVE_INTERVAL_FACTOR (3x) the floor so a spuriously large dead time cannot stall the controller. Urgent overrides (setpoint drop, window, defrost, idle) are evaluated before this gate and are never blocked. Direction validated on the 899 h trace by sweeping the effective interval: 10->14 min cut fan changes 313->279 with flat cost/MAE; the adaptive coupling applies this per-system instead of a blanket value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ning The min-change interval was recently coupled to the learned dead time (~19-20 min on production traces vs the old fixed 10 min floor), so a misjudged fan pick near the deadband can now leave the room drifting away from target for up to 20 minutes with no way to react: the lock forced the current fan regardless of how badly comfort was degrading. Track the comfort error at the moment the fan last changed and allow an escalation-only (never a step-down) bypass of the lock once the error has worsened by more than 0.15C since then. This is trend-based rather than a static deadband-multiple threshold, so it reacts to the room's actual trajectory and fires consistently regardless of the configured deadband. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ogress Root cause of the low-fan incident: the cost forecast used to justify a step-down is unreliable for multi-rank jumps. During the dead-time-blind window (change_delay = learned dead time, ~20 min on this trace) every candidate's near-term trajectory is simulated using the *current* mode's momentum, not its own. A weak candidate several ranks below the active fan can therefore look deceptively good right up until the switch is committed — which is exactly what happened: the controller jumped superhigh -> low near the deadband because low's forecast still showed the room coasting down on superhigh's momentum, even though low's own learned profile (-0.1C/h, i.e. net warming even 1C from target) shows it can't cool this room at all. Add a direct capability check: a candidate more than one rank below the current fan is only eligible if its own learned effective slope (the same number surfaced in the per-mode profile sensors) is positive. Profiles that aren't learned yet are unrestricted (existing fallback behaviour). Adjacent-rank switching is untouched, so the hysteresis/cost tuning validated on the production trace still applies to the common case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gamso
force-pushed
the
feat/hold-equilibrium-and-adaptive-interval
branch
2 times, most recently
from
July 11, 2026 11:05
758fc18 to
8cda3ea
Compare
… grey-box investigation Adds an optional outdoor-temperature sensor (CONF_OUTDOOR_ENTITY), read every cycle and logged to the data-collection CSV as pure telemetry -- no control decision reads it, but it lets a trace be correlated with outdoor conditions offline. Also adds two documents and a reproducible script. Why: this branch's history up to here squashes a grey-box thermal model (dT/dt = k_env*(T_ext-T) + u_fan) that was investigated, built, wired into a feasibility gate, validated offline, and then removed -- none of that code is in this diff, only what survived it. Kept for the record because the investigation and its outcome are worth having: - docs/effective_slope_analysis.md: the original root-cause analysis (weak fans are a closed-loop selection-bias problem, not a data-volume one) -- still valid, marked superseded where it proposes the now-removed model. - docs/audit_apprentissage.md: the k_env instability measurements that led to removal (rolling 72h fits gave tau=20h, then 214h, then negative, because k_env absorbs compressor modulation instead of measuring the envelope), and what remains actionable (the gap model's demand term explains ~8x more of dT/dt's variance than the entire envelope machinery did; active exploration and observing compressor modulation, if exposed, are the levers actually worth pursuing next). - scripts/audit_variance_decomposition.py: the script behind that finding. Also carries two cosmetic no-op refactors in mpc_controller.py's step-down guard (upward/adjacent-rank branches split out, a `ranks` variable extracted), left over from the round trip and harmless. 112 tests pass; verified end-to-end by replaying a real 10695-row production trace through the controller.
…mpled profile build_monotone_slopes did a forward max() pass, which always resolves a violation upward. That silently propagates a noisy estimate from a rarely-used speed into every stronger speed above it -- corrupting exactly the profiles with the most data behind them. Reproduced on the real deployment's sample distribution: a 10-sample `med` reading +1.6 dragged 145-sample `high` (+0.5) and 1656-sample `superhigh` (+0.9) both up to +1.6. Now each adjacent violation moves the side backed by fewer samples onto the other, and the better-sampled value is never altered. On an equal sample count the weaker mode is clamped down rather than the stronger one raised: over-estimating a weak fan invites a step-down it cannot sustain, while under-estimating it is merely conservative. Also fixes _warn_if_unordered, which only ever checked "heat" (so a cooling-only install was never validated) and gave up at the first not-yet-learned profile instead of skipping it. Two existing tests encoded the old upward-propagation and are updated to the new behaviour; two tests are added for the sample-count trust rule in both directions.
A fan mode's index in the list is treated as its strength throughout the controller: the energy term (MODE_POWER_RATIO ** index), the multi-rank step-down guard, the monotone slope enforcement and the rank-scaling fallback for unlearned profiles all read it. Until now that order came from the climate entity's fan_modes attribute and was trusted blindly, with no way to inspect or correct it -- a wrong order would quietly mis-price every speed. Adds an optional "Fan Speed Order" multi-select to the options flow, listing the detected speeds and pre-filled with the entity's own order (so the default is a no-op). A partial selection is rejected rather than silently completed: half a ladder is worse than none, since the user would believe the strength order was set explicitly. Modes the entity adds later are appended and removed ones dropped, so drift degrades gracefully instead of losing a speed. The order in use, and the slopes actually fed to the MPC after ordering violations are resolved, are now exposed as attributes on the MPC profiles sensor -- previously neither was observable without reading debug logs. Verified end-to-end against the real options schema: the field renders pre-filled, a reordered submission survives validation with its order intact, and an unknown mode is rejected by the selector.
…its neighbour's Clamping a rejected estimate onto the value that rejected it left two speeds thermally identical, so the MPC could no longer tell them apart and the energy term alone arbitrated -- always picking the weaker rank. That also made the failure self-reinforcing in the other direction: a thin `superhigh` pinned onto `high` would never be selected again, so it could never collect the samples it needed to stop being thin. A rejected estimate is now re-synthesised one geometric step from the neighbour that constrained it (LADDER_CAPACITY_RATIO = 1.8, calibrated on the production ladder whose adjacent ratios measure 2.08 and 1.76). Around zero, where multiplicative spacing collapses, an absolute fallback keeps the speeds apart. For the reported case this puts a 10-sample med at 0.28 instead of tying it with high at 0.5, while high and superhigh keep their measured values. Also replaces the pairwise repair loop from the previous commit, which a fuzz run showed does not converge: a thin profile sitting between two better-sampled ones that are themselves inverted oscillates forever between their two bounds. Profiles are now placed best-sampled first, each clipped into the window left by those already placed -- a single pass, monotone by construction, nothing to converge. The ratio only ever synthesises a replacement and is never imposed as a minimum gap, so a real ladder spaced more tightly than 1.8 is returned untouched. Fuzzed over 3000 randomised ladders (missing profiles, sign changes, lopsided sample counts) for the ordering invariant; a trimmed version of that fuzz is kept as a regression test.
Gamso
force-pushed
the
feat/hold-equilibrium-and-adaptive-interval
branch
from
August 17, 2026 09:45
01f304f to
66edb39
Compare
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.
No description provided.