Predictive fan speed control for HVAC systems, designed to work with Versatile Thermostat.
- Smart Fan Controller — Home Assistant Custom Integration
- Open HACS in Home Assistant
- Go to Integrations
- Click the three-dot menu → Custom repositories
- Add
https://github.com/Gamso/smart_fan_controllerwith category Integration - Search for Smart Fan Controller and install it
- Restart Home Assistant
Alternatively, click the button below to open this repository directly in HACS:
- Copy the
custom_components/smart_fan_controllerdirectory to your Home Assistantcustom_componentsfolder - Restart Home Assistant
- Add the integration via the UI (Settings → Devices & Services → Add Integration)
Smart Fan Controller is a custom Home Assistant integration that adjusts HVAC fan speed using a predictive MPC (Model Predictive Control) engine. It learns the thermal behavior of your room and selects the optimal fan mode to reach and maintain the target temperature.
- Every 2 minutes, reads the current temperature, target, and temperature slope from Versatile Thermostat
- Simulates every available fan mode over a 30-minute prediction horizon using learned thermal profiles
- Selects the fan mode that minimizes a cost function balancing comfort, overshoot, and energy use
- Applies safety guards (hysteresis, min interval) before changing the fan
- Continuously learns the thermal response of each fan mode to improve predictions over time
The MPC controller supports both
heatandcoolmodes. It includes a hysteresis guard so tiny cost differences do not create fan oscillations near the setpoint. When all fan-mode profiles are learned, a monotone constraint guarantees higher fan modes have steeper slopes than lower ones.
- A climate entity with multiple fan speeds (e.g.,
low,medium,high) - Versatile Thermostat (or compatible integration) that exposes
temperature_slopein itsspecific_statesattribute
- Go to Settings → Devices & Services → Add Integration → Smart Fan Controller
- Select your climate entity
- Configure parameters (or use defaults)
- Save — the controller starts working immediately
All parameters can be changed at any time via Settings → Devices & Services → Smart Fan Controller → Configure.
| Parameter | Default | Range | Description |
|---|---|---|---|
| Deadband | 0.2°C |
0.0 – 5.0°C |
Comfort zone around target — no action taken within this range. Increase to reduce fan changes. |
| Min Interval | 10 min |
1 – 60 min |
Minimum time between non-emergency fan changes. Prevents rapid oscillations. |
| Limit Timeout | 15 min |
10 – 120 min |
Fallback timeout used before learning calibrates the dead time. |
| Data Collection | true |
— | Records one CSV row every 2 minutes in the HA config folder (smart_fan_controller_data_XXXXXXXX.csv, max 10 MB, auto-rotated). Useful for offline analysis. |
| Defrost Entity | (none) | — | Optional entity (binary_sensor, sensor, or input_boolean) that reports when the heat pump is in defrost cycle. See Defrost Detection. |
| Operating Entity | (none) | — | Optional entity (binary_sensor, sensor, or input_boolean) that reports whether the heat pump compressor is actively running. See HVAC Idle Detection. |
The MPC (Model Predictive Control) engine is the sole decision-maker for fan speed. Each cycle, it evaluates every available fan mode by simulating temperature evolution over an adaptive horizon and selecting the mode with the lowest cost.
To eliminate "dead-time blindness" during startup or large setpoint changes when the physical system has lag, the simulation horizon resolves adaptively to:
See docs/mpc_mode.md for the full technical design.
Each candidate fan mode is scored with:
| Component | Purpose |
|---|---|
| Comfort error × urgency | Penalizes being outside the deadband, with dynamic step-by-step urgency |
| Overshoot² | Strongly penalizes going past the target temperature |
| Floor violation | Penalizes predicted temperature dropping below setpoint (linear + quadratic) |
| Mode-change cost | Penalizes unnecessary fan jumps (proportional to step distance) |
| Mode-rank cost | Slight preference for lower fan speeds using physical non-linear power curve |
| Min-interval penalty | Blocks changes before the minimum interval has elapsed |
- Hysteresis: a recommendation that changes the fan must beat the current mode by a minimum cost margin. The margin is larger when near the target (0.30) and smaller when far away (0.10).
- Step-down hold: blocks downward moves when still under target and the temperature slope hasn't established yet.
- Min interval: non-emergency changes respect the effective timeout (learned dead time × 1.5 for the specified HVAC mode, or the configured limit timeout).
After each fan speed change, the controller classifies elapsed time into three phases:
| Phase | Condition | Meaning |
|---|---|---|
| DEAD_TIME | elapsed < dead_time |
Sensor hasn't reacted yet |
| TRANSIENT | dead_time ≤ elapsed < dead_time×1.5 |
Sensor starting to respond |
| ESTABLISHED | elapsed ≥ dead_time × 1.5 |
Slope reflects current fan regime |
The default dead time is 10 minutes. When the learning system is ready, it is replaced by the learned median response time.
The MPC tracks a disturbance bias — an EMA estimate of unmodeled thermal effects (solar gains, occupancy). This correction is added to learned slopes during simulation. The bias only updates during ESTABLISHED phase with a known profile and decays during disturbed periods.
When a disturbance is detected, the MPC pauses and returns "Disturbed" status — the current fan mode is held.
When a heat pump defrosts its outdoor coil, the heat output drops sharply. Without defrost awareness, the controller would misinterpret the falling slope.
External entity (optional): Configure a binary_sensor, sensor, or input_boolean from your PAC integration that reports defrost state. When this entity is on/true/1, defrost protection is activated with a 20-minute cooldown.
During defrost protection:
- The MPC pauses and returns "Disturbed" status
- Learning samples are excluded (slope data during defrost corrupts profiles)
When the heat pump compressor is off (setpoint reached, system coasting), the HVAC is not actively heating or cooling.
Operating entity (optional): Configure a binary_sensor, sensor, or input_boolean that reports compressor state. When off/false/0, the compressor is considered idle.
When no operating entity is configured, HVAC idle detection is disabled.
During HVAC idle:
- The MPC pauses and returns "Disturbed" status
- Learning samples are excluded
The integration includes an automatic learning system that collects data during normal operation and computes optimal parameters after approximately 48–72 hours (≥240 samples, collected every 2 minutes).
Data collected:
- Temperature slope and active fan mode (every 2 minutes)
- Time from fan speed change to next significant slope change (thermal response time)
- HVAC mode (heat/cool) for per-mode profiling
Parameters computed from data:
| Parameter | Formula |
|---|---|
deadband |
0.15 + (volatility_factor × 0.2) |
limit_timeout |
rounded median of measured thermal response times |
Where volatility_factor = min(slope_stdev / slope_mean, 3.0).
Important: the learned
limit_timeoutis the stored base response estimate. At runtime, non-emergency decisions useeffective_timeout = max(min_interval, dead_time × 1.5)once learning is ready.
Once learning is ready, parameters are automatically applied and the integration reloads. To apply manually, use the apply_learned_settings service. To start over, use reset_learning.
The learning system tracks the effective slope per fan mode and HVAC mode (e.g., "medium in heat" vs "high in cool"). This data provides visibility into which fan speeds are most effective for each mode.
Profiles require at least 10 samples per mode to be considered reliable. Samples are automatically filtered out when:
- Window is open (external disturbance)
- Large setpoint drop occurred (night mode) — including a 30-minute cooldown after the drop to avoid EMA inertia
- HVAC is idle or defrost is active
- The fan mode hasn't been active long enough (2× dead time) for the VTherm EMA to fully reflect the current mode
- The phase is not yet ESTABLISHED
The effective slope is computed as the median (not mean) of collected samples, providing robustness against occasional outlier readings caused by thermal inertia from previous high-speed modes.
The system measures the thermal response time — the delay between a fan speed change and the first observable slope change at the sensor. This median value replaces the default 10-minute dead time, allowing the controller to be patient during the actual thermal lag period and reactive once the effect materializes.
Response events are only recorded when the delay is between 2 and 60 minutes (filtering sensor noise and system-off periods).
When Versatile Thermostat reports a window as open (via the window_manager.window_state attribute):
- The MPC pauses and returns "Disturbed" status — the current fan mode is held
- Learning data collection stops, including both per-mode slope samples and response-time events used to learn
dead_time
This prevents window-open periods from corrupting the learned profiles.
Slope samples and response-time events collected during an active defrost period (via external entity, including the 20-minute cooldown) are not added to learned profiles. Defrost distorts the effective slope per fan mode and would bias the learning system toward lower heating capacity estimates.
Slope samples and response-time events collected while the compressor is detected as idle (via operating_entity or power_entity) are not added to learned profiles. When the compressor is off the measured slope reflects ambient drift rather than active heating or cooling capacity, and recording it would corrupt per-mode profiles and dead-time calibration.
Entity IDs are scoped by the configured climate entity. For example, a controller attached to climate.living_room exposes sensor.smart_fan_controller_living_room_mpc_status.
| Entity | Unit | Description |
|---|---|---|
sensor.smart_fan_controller_living_room_mpc_status |
— | MPC state (Not ready, Ready, Disturbed, Idle, etc.) |
sensor.smart_fan_controller_living_room_mpc_reason |
— | Explanation of the current MPC recommendation |
sensor.smart_fan_controller_living_room_mpc_fan_mode |
— | Fan mode chosen by the MPC |
sensor.smart_fan_controller_living_room_mpc_would_change_now |
— | Whether the MPC would actively change the fan right now |
sensor.smart_fan_controller_living_room_mpc_cost |
— | Lowest simulation cost returned by the MPC optimizer |
sensor.smart_fan_controller_living_room_mpc_confidence |
% | Confidence derived from learned profile coverage |
sensor.smart_fan_controller_living_room_mpc_predicted_temperature_10_min |
°C | Predicted temperature after 10 minutes with the recommended mode |
sensor.smart_fan_controller_living_room_mpc_predicted_temperature_30_min |
°C | Predicted temperature after 30 minutes with the recommended mode |
sensor.smart_fan_controller_living_room_mpc_dead_time |
min | Dead time currently used by the MPC simulator |
sensor.smart_fan_controller_living_room_mpc_known_profiles |
count | Number of reliable learned fan-mode profiles |
sensor.smart_fan_controller_living_room_mpc_disturbance_bias |
°C/h | Learned disturbance correction currently applied by the MPC model |
| Entity | Unit | Description |
|---|---|---|
sensor.smart_fan_controller_living_room_learning_progress |
% | Learning completion (100% = ≥240 samples) |
sensor.smart_fan_controller_living_room_learning_status |
— | "Learning (45%)" or "Ready" |
sensor.smart_fan_controller_living_room_learning_samples |
count | Number of slope samples collected |
sensor.smart_fan_controller_living_room_learning_response_events |
count | Number of thermal response time measurements |
sensor.smart_fan_controller_living_room_learned_dead_time |
min | Median learned thermal response delay (dead_time) |
sensor.smart_fan_controller_living_room_effective_timeout |
min | Actual non-emergency timeout currently used |
sensor.smart_fan_controller_living_room_learned_deadband |
°C | Learned optimal deadband |
sensor.smart_fan_controller_living_room_learned_limit_timeout |
min | Learned base timeout stored in config |
Once fan modes are detected, the integration creates per-HVAC-mode profile summary sensors and one effective slope sensor per fan mode:
Entity (example with low/medium/high fan modes) |
Unit | Description |
|---|---|---|
sensor.smart_fan_controller_living_room_mpc_heat_profiles |
— | JSON summary of learned heat profiles per fan mode |
sensor.smart_fan_controller_living_room_mpc_cool_profiles |
— | JSON summary of learned cool profiles per fan mode |
sensor.smart_fan_controller_living_room_heat_low_effective_slope |
°C/h | Effective slope learned for low in heat mode |
sensor.smart_fan_controller_living_room_heat_medium_effective_slope |
°C/h | Effective slope learned for medium in heat mode |
sensor.smart_fan_controller_living_room_heat_high_effective_slope |
°C/h | Effective slope learned for high in heat mode |
sensor.smart_fan_controller_living_room_cool_low_effective_slope |
°C/h | Effective slope learned for low in cool mode |
| … (one per fan mode × HVAC mode combination) | … | … |
These sensors appear automatically when the climate entity's fan modes become known and require at least 10 samples per mode to show reliable data. Existing installations are migrated automatically from the legacy non-scoped entity IDs to the new climate-scoped names.
Manually apply the parameters computed by the learning system. Useful when auto-apply is disabled or to re-apply after a manual change.
When several Smart Fan Controller entries are configured, pass climate_entity to target the correct controller.
Requirement: sensor.smart_fan_controller_living_room_learning_status must be "Ready".
Clear all learning data and start fresh. Use after HVAC maintenance or a significant system change.
When several Smart Fan Controller entries are configured, pass climate_entity to target the correct controller.
Manually set the effective slope for a specific fan mode / HVAC mode profile without resetting all learning data. Replaces existing samples for that profile with synthetic ones matching the provided slope.
Parameters:
| Parameter | Required | Example | Description |
|---|---|---|---|
climate_entity |
No* | climate.living_room |
Required when several Smart Fan Controller entries are configured |
hvac_mode |
Yes | heat |
The HVAC mode (heat or cool) |
fan_mode |
Yes | silent |
The fan mode name |
effective_slope |
Yes | 0.15 |
Target effective slope in °C/h (positive = towards target) |
Example (Developer Tools → Services):
service: smart_fan_controller.set_effective_slope
data:
climate_entity: climate.living_room
hvac_mode: heat
fan_mode: silent
effective_slope: 0.15Force a specific fan mode for a fixed duration, overriding the MPC. The override is applied
immediately and automatically expires after the duration, handing control back to the MPC. Set
duration_minutes to 0 to cancel an active override early.
Parameters:
| Parameter | Required | Example | Description |
|---|---|---|---|
climate_entity |
No* | climate.living_room |
Required when several Smart Fan Controller entries are configured |
fan_mode |
Yes | high |
The fan mode to force |
duration_minutes |
Yes | 30 |
How long to hold the forced mode; 0 cancels an active override |
Example (Developer Tools → Services):
service: smart_fan_controller.force_fan
data:
climate_entity: climate.living_room
fan_mode: high
duration_minutes: 30While a force is active, mpc_status reports Forced and mpc_reason shows the remaining time.
| Symptom | What to check / do |
|---|---|
| Fan not changing | Check sensor.smart_fan_controller_living_room_mpc_status and mpc_reason. The MPC may be paused (disturbed) or the min interval hasn't elapsed. |
| MPC status: Not ready | Learning hasn't collected enough profiles. Check sensor.smart_fan_controller_living_room_mpc_known_profiles and learning_progress. |
| MPC status: Disturbed | Defrost, HVAC idle, or window open detected. Normal — MPC holds current fan until the disturbance clears. |
| Too many fan changes | Increase deadband or min_interval. Enable learning to auto-optimize. |
| Temperature overshoots | Decrease deadband. Verify Versatile Thermostat is providing an accurate slope. |
| Learning not progressing | Verify HVAC is running and windows are closed. |
| Auto-apply not working | Verify sensor.smart_fan_controller_living_room_learning_status is "Ready". Auto-apply fires once — use apply_learned_settings to re-apply. |
This project is licensed under the MIT License.