Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ Release notes for each tagged version are extracted from this file by the

## [Unreleased]

### Added
- **Operating modes (Auto / Manual).** New `Operating Mode` select switches
between the existing temperature-driven curve (**Auto**) and a new **Manual**
mode with a `Manual Fan` on/off switch and a `Manual Fan Speed` (0–100 %)
number. A single resolved-speed template sensor drives the PWM in both modes,
so `Fan Speed` always reflects the actual duty. Safety failsafes (NaN read,
>60 s sensor stall → 100 %) still apply in **all** modes, including Manual.
Mode + manual controls are grouped on the web UI via `web_server`
sorting groups (the stock UI can't hide entities by state, so they are shown
but inert in Auto).

### Changed
- Home Assistant transport switched from the native ESPHome **`api:` to
`mqtt:`** in `fancontrol.yaml`. The encrypted native-API block is now
Expand Down
15 changes: 15 additions & 0 deletions PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ or the device won't join WiFi.
components with `restore_value: true` for user-tunable settings. No manual
NVS wrapping needed.

### F7 — Operating modes
- **Operating Mode** `select` (`Auto` / `Manual`, persisted). Default `Auto`.
- **Auto** — unchanged: temperature-driven 5-point fan curve, clamped to Fan Min %.
- **Manual** — **Manual Fan** `switch` (on/off) + **Manual Fan Speed** `number`
(0–100 %). Off forces 0 %.
- A single template sensor (`fan_speed_pct`) resolves the duty for both modes
and drives the PWM output, so "Fan Speed" always reflects the actual duty.
- **Safety overrides apply in all modes:** NaN sensor read and the >60 s
sensor-stall failsafe still force the fan to 100 % even in Manual. The
temperature alarm remains indicator-only (does not force the fan).
- The stock ESPHome web UI cannot hide entities by state, so manual controls
are always shown (inert in Auto); `web_server` sorting groups keep them
organised. Per-mode conditional UI, if wanted, belongs in a Home Assistant
dashboard keyed on the mode entity.

## 3. Architecture

One file, one responsibility:
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,34 @@ broker — no manual entity setup needed. Exposed entities:
|---|---|---|
| Temperature | sensor | °C, from DHT22 |
| Humidity | sensor | %, from DHT22 |
| Fan Speed | sensor | % duty, derived from the curve |
| Fan Speed | sensor | % duty actually applied (curve in Auto, manual setting in Manual) |
| Fan PWM Frequency | sensor | Hz |
| Restart Counter | sensor | monotonic, persisted in NVS |
| Uptime | sensor | seconds |
| WiFi Signal | sensor | dBm |
| Temperature Alarm | binary_sensor | `safety` class, fires at the configured threshold |
| Sensor Alarm | binary_sensor | `problem` class, fires after 60 s of no successful reads |
| Operating Mode | select | `Auto` (curve) or `Manual`, persisted |
| Manual Fan | switch | on/off — Manual mode only |
| Manual Fan Speed | number | 0–100 % — Manual mode only |
| Alarm Temperature | number | tunable threshold |
| Fan Min % | number | minimum duty under normal operation |
| Fan Min % | number | minimum duty under normal operation (Auto) |
| Fan PWM Frequency Setting | number | 1000–5000 Hz, applied live via `ledc.set_frequency` |
| Restart / Factory Reset | button | standard ESPHome buttons |

## Operating modes

- **Auto** (default) — fan speed follows the temperature curve, clamped to `Fan Min %`.
- **Manual** — `Manual Fan` toggles the fan and `Manual Fan Speed` sets the duty
directly (off forces 0 %).

Safety failsafes apply in **both** modes: a NaN reading or a >60 s sensor stall
forces the fan to 100 %, even in Manual.

> The built-in ESPHome web page can't hide entities by mode, so the manual
> controls are always visible (they're simply inert in Auto). For per-mode UI,
> use a Home Assistant dashboard with conditional cards keyed on `Operating Mode`.

## Safety behaviour

- **Sensor stall** (no successful DHT22 read for 60 s) → fan forced to 100 %.
Expand Down
79 changes: 76 additions & 3 deletions fancontrol.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ mqtt:
web_server:
port: 80
version: 3
# The stock web UI cannot hide entities by state, so manual controls are
# always visible (they are simply inert in Auto mode). Groups keep it tidy.
sorting_groups:
- id: group_operation
name: "Operation"
sorting_weight: 10
- id: group_manual
name: "Manual control"
sorting_weight: 20

# -------- Globals (restore_value = persisted across reboots) --------

Expand Down Expand Up @@ -127,7 +136,8 @@ sensor:
filters:
- filter_out: nan

# Fan speed derived from the curve. Drives the PWM output via on_value.
# Resolved fan speed. Single driver of the PWM output (via on_value) for
# BOTH modes, so this sensor always reflects the actual duty being applied.
- platform: template
name: "Fan Speed"
id: fan_speed_pct
Expand All @@ -136,9 +146,21 @@ sensor:
update_interval: 1s
lambda: |-
const float t = id(dht_temperature).state;
if (std::isnan(t)) return 100.0f; // sensor warmup / NaN → failsafe bias
if (id(alarm_sensor_stall).state) return 100.0f; // SW watchdog stall

// Safety overrides apply in ALL modes (Auto and Manual). Preserve these.
if (std::isnan(t)) return 100.0f; // sensor warmup / NaN → failsafe
if (id(alarm_sensor_stall).state) return 100.0f; // SW watchdog stall → failsafe

// Manual mode: user-controlled on/off + speed. Off forces 0 %.
if (id(operating_mode).state == "Manual") {
if (!id(manual_fan_switch).state) return 0.0f;
float m = id(manual_fan_speed).state;
if (m < 0.0f) m = 0.0f;
if (m > 100.0f) m = 100.0f;
return m;
}

// Auto mode: temperature-driven curve (clamped to Fan Min %).
const float ts[5] = { ${curve_t0}, ${curve_t1}, ${curve_t2}, ${curve_t3}, ${curve_t4} };
const float ps[5] = { ${curve_p0}, ${curve_p1}, ${curve_p2}, ${curve_p3}, ${curve_p4} };

Expand Down Expand Up @@ -206,9 +228,60 @@ binary_sensor:
if (id(last_sensor_read_ms) == 0) return {};
return (millis() - id(last_sensor_read_ms)) > 60000;

# -------- Operating mode + manual controls --------

select:
- platform: template
name: "Operating Mode"
id: operating_mode
icon: mdi:fan-auto
web_server:
sorting_group_id: group_operation
optimistic: true
restore_value: true
initial_option: "Auto"
options:
- "Auto"
- "Manual"
# Recompute the resolved fan speed immediately on mode change.
on_value:
- component.update: fan_speed_pct

switch:
- platform: template
name: "Manual Fan"
id: manual_fan_switch
icon: mdi:fan
web_server:
sorting_group_id: group_manual
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
# Only takes effect in Manual mode; inert in Auto.
on_turn_on:
- component.update: fan_speed_pct
on_turn_off:
- component.update: fan_speed_pct

# -------- Tunable settings (number components, persisted via restore_value) --------

number:
- platform: template
name: "Manual Fan Speed"
id: manual_fan_speed
web_server:
sorting_group_id: group_manual
min_value: 0
max_value: 100
step: 1
unit_of_measurement: "%"
initial_value: 50
restore_value: true
optimistic: true
mode: slider
# Only takes effect in Manual mode (with Manual Fan on); inert in Auto.
on_value:
- component.update: fan_speed_pct

- platform: template
name: "Alarm Temperature"
id: alarm_temp_threshold
Expand Down