Temperature Curve Dual
Purpose
Section titled “Purpose”The Temperature Curve Dual module holds two complete 5-point temperature-to-speed curves, A and B, and lets you switch between them from Home Assistant with a single switch. Curve A is meant to be an aggressive profile (cools hard, early) and curve B a defensive one (stays quiet longer, spins up later), but both are fully configurable.
When to Use
Section titled “When to Use”Choose Temperature Curve Dual when you want two distinct fan profiles you can flip between, instead of constantly re-tuning a single curve. The classic case is pairing it with an air-quality automation: when the air is bad, a Home Assistant automation flips to the defensive curve so the fans stay quiet and ramp up later.
The module is sensor-agnostic. It does not read any air-quality sensor itself. Curve selection is just a switch; the automation logic lives in Home Assistant. For a single fixed curve, see Temperature Curve. The modules overview has a comparison table.
Configuration
Section titled “Configuration”| Variable | Default | Description |
|---|---|---|
friendly_name | "Fancontroller" | Device name prefix for the output sensor |
curveA_temp1 | "20.0" | Curve A temperature point 1 (C) |
curveA_speed1 | "0.0" | Curve A fan speed at point 1 (%) |
curveA_temp2 | "22.0" | Curve A temperature point 2 (C) |
curveA_speed2 | "30.0" | Curve A fan speed at point 2 (%) |
curveA_temp3 | "28.0" | Curve A temperature point 3 (C) |
curveA_speed3 | "60.0" | Curve A fan speed at point 3 (%) |
curveA_temp4 | "35.0" | Curve A temperature point 4 (C) |
curveA_speed4 | "85.0" | Curve A fan speed at point 4 (%) |
curveA_temp5 | "40.0" | Curve A temperature point 5 (C) |
curveA_speed5 | "100.0" | Curve A fan speed at point 5 (%) |
curveB_temp1 | "25.0" | Curve B temperature point 1 (C) |
curveB_speed1 | "0.0" | Curve B fan speed at point 1 (%) |
curveB_temp2 | "35.0" | Curve B temperature point 2 (C) |
curveB_speed2 | "25.0" | Curve B fan speed at point 2 (%) |
curveB_temp3 | "45.0" | Curve B temperature point 3 (C) |
curveB_speed3 | "50.0" | Curve B fan speed at point 3 (%) |
curveB_temp4 | "50.0" | Curve B temperature point 4 (C) |
curveB_speed4 | "65.0" | Curve B fan speed at point 4 (%) |
curveB_temp5 | "55.0" | Curve B temperature point 5 (C) |
curveB_speed5 | "80.0" | Curve B fan speed at point 5 (%) |
temperature_sensor_id | "fancontroller_temperature" | Id of the sensor supplying the control temperature |
How It Works
Section titled “How It Works”Every 10 seconds the module reads the board temperature, picks the active curve based on the Curve Selection switch (off = A, on = B), sorts that curve’s five points by temperature, and linearly interpolates the fan speed. Below the lowest point it holds the first speed; above the highest point it holds the last speed.
The result drives all four fans, gated per fan by the Auto Control Fan 1—Fan 4 switches and by any active user override, and clamped by each fan’s safety floor (the floor only applies to non-zero speeds; a commanded 0% always turns the fan off).
Entities
Section titled “Entities”| Entity | Type | Purpose |
|---|---|---|
| Curve Selection | switch | Selects curve A (off) or B (on). Remembers its last state across reboots. |
| Curve A/B Temperature 1—5, Speed 1—5 | number (20) | The two curves’ points, editable live |
| Auto Control Fan 1—4 | switch (4) | Per-fan enable for automatic control |
$friendly_name Curve Output | sensor | The active curve’s computed fan speed (%) |
| Curve Configuration Warning | binary_sensor | Flags a non-monotonic curve at boot |
Web UI grouping
Section titled “Web UI grouping”This block goes in your own configuration, not in the module. See why the assignment lives in the consuming config for the reason. version: 3 is what makes entity grouping work at all; without it, entity grouping has no effect.
web_server: version: 3 sorting_groups: - id: grp_temp_curve_dual name: "Temperature Curve Dual" sorting_weight: 120
number: - id: !extend curveA_temp1 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 10 - id: !extend curveA_speed1 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 20 - id: !extend curveA_temp2 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 30 - id: !extend curveA_speed2 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 40 - id: !extend curveA_temp3 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 50 - id: !extend curveA_speed3 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 60 - id: !extend curveA_temp4 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 70 - id: !extend curveA_speed4 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 80 - id: !extend curveA_temp5 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 90 - id: !extend curveA_speed5 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 100 - id: !extend curveB_temp1 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 110 - id: !extend curveB_speed1 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 120 - id: !extend curveB_temp2 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 130 - id: !extend curveB_speed2 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 140 - id: !extend curveB_temp3 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 150 - id: !extend curveB_speed3 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 160 - id: !extend curveB_temp4 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 170 - id: !extend curveB_speed4 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 180 - id: !extend curveB_temp5 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 190 - id: !extend curveB_speed5 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 200
switch: - id: !extend curve_select web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 210 - id: !extend auto_control_fan1 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 220 - id: !extend auto_control_fan2 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 230 - id: !extend auto_control_fan3 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 240 - id: !extend auto_control_fan4 web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 250
sensor: - id: !extend curve_output_value web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 260
binary_sensor: - id: !extend curve_config_warning_sensor web_server: sorting_group_id: grp_temp_curve_dual sorting_weight: 900Switching Automatically
Section titled “Switching Automatically”Pair the Curve Selection switch with a Home Assistant automation. For example, switch to the defensive curve when an air-quality sensor reports poor air:
automation: - alias: "Defensive fan curve on poor air quality" trigger: - platform: numeric_state entity_id: sensor.living_room_pm2_5 above: 35 action: - service: switch.turn_on target: { entity_id: switch.my_fancontroller_curve_selection }See examples/with-temperature-curve-dual-rev-3.1.yaml for a complete configuration.
Controlling From a Different Sensor
Section titled “Controlling From a Different Sensor”By default the module reads fancontroller_temperature, the onboard HDC1080 that every hardware package declares. Set temperature_sensor_id to the id of any other temperature sensor in your configuration to control from that instead:
packages: # modules/bme680.yaml is not standalone - it needs the shared I2C bus (bus_a) # that your hardware package declares. Use your own board revision here. hardware: url: https://github.com/zeroflow/wifi-fancontroller ref: main files: [hardware-rev-3.1.yaml] bme680: url: https://github.com/zeroflow/wifi-fancontroller ref: main files: [modules/bme680.yaml] temperature_curve_dual: url: https://github.com/zeroflow/wifi-fancontroller ref: main files: - path: modules/temperature_curve_dual.yaml vars: temperature_sensor_id: "bme680_temperature"Both curves read the same sensor; the A/B switch selects which curve is applied, not which sensor is used.
Omit the variable and it falls back to fancontroller_temperature, so existing configurations keep working unchanged. A misspelled id fails the build rather than quietly falling back to the onboard sensor.
The Qwiic modules build their sensor ids from an id-prefix substitution, so BME680 gives you bme680_temperature, SCD41 gives scd41_temperature, and DS2484 gives whatever ids you assign your own dallas_temp sensors.
If you also run the SSD1306 OLED module, note that it displays the onboard sensor regardless of this setting, so the screen and the fan control can show different temperatures.
The module names the sensor it is reading in its boot log, so you can confirm from a log dump that the override took effect.