{"id":50815502,"url":"https://github.com/ilia-ae/rpm-fun_esphome","last_synced_at":"2026-06-13T09:04:45.699Z","repository":{"id":278745634,"uuid":"936640832","full_name":"ilia-ae/rpm-fun_esphome","owner":"ilia-ae","description":"📝📦🔓 ESPHome fan controller for ESP32-S3-DevKitC-1: 4 PWM channels + 6 RPM inputs, Home Assistant native API.","archived":false,"fork":false,"pushed_at":"2026-05-18T06:14:11.000Z","size":53,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-18T07:43:05.520Z","etag":null,"topics":["arduino","esp32","esp32-s3","esphome","fan-control","fan-controller","home-assistant","pc-fan","pulse-counter","pwm","ws2812"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ilia-ae.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-21T12:37:49.000Z","updated_at":"2026-05-18T06:14:14.000Z","dependencies_parsed_at":"2025-02-21T13:34:51.830Z","dependency_job_id":"5c80c31d-e1a5-4430-aae0-dc0323f8584e","html_url":"https://github.com/ilia-ae/rpm-fun_esphome","commit_stats":null,"previous_names":["razqqm/rpm-fun_esphome","ilia-ae/rpm-fun_esphome"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ilia-ae/rpm-fun_esphome","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilia-ae%2Frpm-fun_esphome","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilia-ae%2Frpm-fun_esphome/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilia-ae%2Frpm-fun_esphome/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilia-ae%2Frpm-fun_esphome/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ilia-ae","download_url":"https://codeload.github.com/ilia-ae/rpm-fun_esphome/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilia-ae%2Frpm-fun_esphome/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34278184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arduino","esp32","esp32-s3","esphome","fan-control","fan-controller","home-assistant","pc-fan","pulse-counter","pwm","ws2812"],"created_at":"2026-06-13T09:04:45.100Z","updated_at":"2026-06-13T09:04:45.693Z","avatar_url":"https://github.com/ilia-ae.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# rpm-fun\n\n[![ESPHome](https://img.shields.io/badge/ESPHome-%E2%89%A5%202024.12.4-000000?logo=esphome\u0026logoColor=white)](https://esphome.io/)\n[![Platform](https://img.shields.io/badge/platform-ESP32--S3-E7352C?logo=espressif\u0026logoColor=white)](https://www.espressif.com/en/products/socs/esp32-s3)\n[![Framework](https://img.shields.io/badge/framework-Arduino-00979D?logo=arduino\u0026logoColor=white)](https://github.com/espressif/arduino-esp32)\n[![Home Assistant](https://img.shields.io/badge/Home%20Assistant-compatible-41BDF5?logo=home-assistant\u0026logoColor=white)](https://www.home-assistant.io/)\n[![License](https://img.shields.io/badge/license-MIT-green)](#license)\n[![Made with YAML](https://img.shields.io/badge/made%20with-YAML-CB171E?logo=yaml\u0026logoColor=white)](rpm-fun.yaml)\n\nESPHome firmware for the **ESP32-S3-DevKitC-1** that turns the board into a fan controller with **Home Assistant** integration:\n\n- **4 fans with full PWM speed control + RPM readback** (variable 0 – 100 %, 25 kHz, Intel 4-Wire Fan Spec)\n- **+ 2 fans, RPM readback only** (no PWM channel — these fans run at whatever fixed speed they are wired to: usually 100 % if connected straight to +12 V)\n- → **6 tachometer inputs total, 4 PWM outputs total**\n- **WS2812 status LED ring** showing Wi-Fi state and fan activity\n- **Native API with encryption** for Home Assistant\n- **OTA updates** over the network\n- **Fallback Wi-Fi access point** + captive portal when the main SSID is unreachable\n- persistent speed/brightness state (`restore_value: true`)\n- SNTP time sync and a ready-to-uncomment OLED display block\n\n---\n\n## Table of Contents\n\n- [Hardware Requirements](#hardware-requirements)\n- [Pinout](#pinout)\n- [Why this layout? (hardware constraints)](#why-this-layout-hardware-constraints)\n- [Wiring a 4-pin PC fan](#wiring-a-4-pin-pc-fan)\n- [Power budget](#power-budget)\n- [Home Assistant entities](#home-assistant-entities)\n- [LED indication](#led-indication)\n- [Installation](#installation)\n- [`secrets.yaml`](#secretsyaml)\n- [OTA updates](#ota-updates)\n- [Implementation notes](#implementation-notes)\n- [Troubleshooting](#troubleshooting)\n- [License](#license)\n\n---\n\n## Hardware Requirements\n\n| Component | Requirement |\n|---|---|\n| MCU board | **ESP32-S3-DevKitC-1** (v1.0 / v1.1). Use a variant **without octal-PSRAM** (`N8`, `N16`, `N8R2`), see note below |\n| ESPHome | **≥ 2024.12.4** (see `esphome.min_version`) |\n| Framework | Arduino (required by `esp32_rmt_led_strip` and the `pulse_counter` legacy mode used here) |\n| Fans | Standard **4-pin PWM PC fans** (Intel spec): GND / +12 V / Tach / PWM. Up to 6 tach, up to 4 PWM-controlled |\n| Fan power | External **+12 V** PSU. The ESP32 is powered from USB-C on the DevKitC-1. **A common ground is mandatory** (see [Power budget](#power-budget) below) |\n| Pull-ups | External **10 kΩ** pull-ups from each Tach line to +3.3 V on **fans 1–4** (see note below) |\n\n### Why \"no octal-PSRAM\"\n\nESP32-S3 modules with the `R8` suffix (8 MB octal PSRAM) wire **GPIO33 – GPIO37** to the SPI PSRAM data bus (`SPIIO4 – SPIIO7` + `SPIDQS`), per the [ESP32-S3 Hardware Design Guidelines](https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/schematic-checklist.html). Those pins **cannot be used as GPIO** on R8 boards.\n\nThis firmware touches **three** pins inside that reserved range:\n\n- **GPIO35** — PWM output for fan 3\n- **GPIO36** — tachometer input for fan 6\n- **GPIO37** — PWM output for fan 4\n\nSo on an `N16R8` / `N32R8` module the firmware will fail to boot or corrupt PSRAM access. Use a non-R8 variant (`N8`, `N16`, `N8R2` quad-PSRAM is fine), or remap fans 3, 4, and 6 to free pins.\n\n---\n\n## Pinout\n\n### Tachometer inputs (RPM)\n\n| Fan | GPIO | Method | Pull-up | HA entity |\n|---|---|---|---|---|\n| Fan 1 | GPIO5 | PCNT (hardware) | external 10 kΩ → 3.3 V | `Fan RPM 1 (GPIO5)` |\n| Fan 2 | GPIO6 | PCNT | external 10 kΩ → 3.3 V | `Fan RPM 2 (GPIO6)` |\n| Fan 3 | GPIO7 | PCNT | external 10 kΩ → 3.3 V | `Fan RPM 3 (GPIO7)` |\n| Fan 4 | GPIO8 | PCNT | external 10 kΩ → 3.3 V | `Fan RPM 4 (GPIO8)` |\n| Fan 5 | GPIO9 | software, 13 µs glitch filter | **internal** `INPUT_PULLUP` | `Fan RPM 5 (GPIO9)` |\n| Fan 6 | GPIO36 | software, 13 µs glitch filter | **internal** `INPUT_PULLUP` | `Fan RPM 6 (GPIO36)` |\n\nThe tach output of a PC fan is **open-collector / open-drain**, so it always needs a pull-up. Fans 1–4 declare the pin with the short syntax (`pin: GPIO5`), which does not propagate `INPUT_PULLUP` into the PCNT peripheral — that is why an **external resistor (4.7 – 10 kΩ to 3.3 V)** is required. Fans 5 and 6 use the long pin syntax and rely on the MCU's internal pull-up.\n\n### PWM outputs\n\n| Fan | GPIO | Frequency | Driver | HA entity |\n|---|---|---|---|---|\n| Fan 1 | GPIO17 | 25 kHz | LEDC | `Fan Speed 1 (GPIO17)` |\n| Fan 2 | GPIO18 | 25 kHz | LEDC | `Fan Speed 2 (GPIO18)` |\n| Fan 3 | **GPIO35** | 25 kHz | LEDC | `Fan Speed 3 (GPIO35)` |\n| Fan 4 | **GPIO37** | 25 kHz | LEDC | `Fan Speed 4 (GPIO37)` |\n\n`min_power: 0.1` clamps the duty cycle floor to 10 % so the fan always spins reliably and avoids stall noise on cold start.\n\n### Other peripherals\n\n| Function | GPIO | Notes |\n|---|---|---|\n| WS2812 LED ring (3 LEDs) | GPIO48 | RBG order, `chipset: ws2812`, `rmt_channel: 2`, `internal: true` — not exposed to HA |\n| I²C SDA | GPIO41 | 400 kHz |\n| I²C SCL | GPIO40 | Reserved for the optional SSD1306 OLED (block is commented out in YAML) |\n\n### Fan capabilities at a glance\n\nThis is the most important thing to understand before wiring anything:\n\n| Fan slot | Tach (RPM read) | PWM (speed control) | What you get in Home Assistant |\n|---|---|---|---|\n| **Fan 1** | GPIO5 | GPIO17 | RPM sensor + speed slider 0 – 100 % |\n| **Fan 2** | GPIO6 | GPIO18 | RPM sensor + speed slider 0 – 100 % |\n| **Fan 3** | GPIO7 | GPIO35 | RPM sensor + speed slider 0 – 100 % |\n| **Fan 4** | GPIO8 | GPIO37 | RPM sensor + speed slider 0 – 100 % |\n| **Fan 5** | GPIO9 | — *(no PWM channel)* | RPM sensor only — **fan runs at whatever fixed speed it is wired to** |\n| **Fan 6** | GPIO36 | — *(no PWM channel)* | RPM sensor only — **fan runs at whatever fixed speed it is wired to** |\n\n**What this means in practice for fans 5 and 6:**\n\n- If you wire the PWM pin straight to +12 V (or leave it floating on most fans), the fan runs at **100 % always**. There is no slider in HA to slow it down.\n- If you wire the PWM pin to GND, most modern fans interpret 0 % duty as \"stop\" or \"minimum\" — also a fixed state.\n- If you want variable speed on fans 5 and 6 too, you need an **external PWM source** for them (a separate 25 kHz signal from another MCU, a hardware fan controller, or a motherboard header). This firmware will only **read** their RPM in that case.\n- A common pattern: use fans 1 – 4 for case/radiator fans you want to control from HA, and assign fans 5 – 6 to a CPU-block pump tach and a PSU fan tach — both of which are typically not meant to be PWM-controlled anyway.\n\n#### Why is it 4 controlled and not 6?\n\nThe intuitive answer — \"because ESP32-S3 PWM hardware only supports 4 channels\" — is **wrong**. The LEDC peripheral on the ESP32-S3 has **8 PWM channels** and all of them can share a single 25 kHz timer, so the chip could comfortably drive **6 or even 8 PWM fans** at the same time ([ESP-IDF LEDC docs](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/ledc.html)).\n\nThe real reason this firmware ships with **4 PWM + 6 tach** is a **deliberate design choice** matching the most common PC-cooling layout:\n\n| Role in a typical PC | Wants speed control? | Wants RPM readback? |\n|---|---|---|\n| Case / radiator fans (3 – 4 of them) | **yes** — slow them down at idle, ramp them up under load | yes |\n| CPU water-block pump | usually no — runs at fixed speed for stable flow | **yes** — losing the pump = thermal emergency, must be monitored |\n| PSU fan | no — controlled by the PSU itself | **yes** — leading indicator of PSU dying |\n| GPU / SSD / chassis-monitor fans | no — controlled by their own card or BIOS | **yes** — useful telemetry |\n\nThat is a 4-PWM + 2-tach-only split, plus the 4 controlled fans also report their own RPM → **4 PWM + 6 tach total**. Exactly what this YAML implements.\n\nIf your build does not fit this pattern and you genuinely need all 6 fans speed-controlled, the silicon allows it — see the [extension recipe](#extend-to-6-pwm-controlled-fans). For the full silicon-vs-design analysis (LEDC channels, PCNT cap, INPUT_PULLUP quirk) see [Why this layout?](#why-this-layout-hardware-constraints) below.\n\n---\n\n## Why this layout? (hardware constraints)\n\nThe asymmetric \"**6 tach, only 4 PWM**\" layout and the split between PCNT (fans 1–4) and software pulse counting (fans 5–6) is sometimes attributed to \"the ESP32 can't handle that many fans.\" That is **not** the actual story — ESP32-S3 silicon could drive more. The real reasons are a stack of smaller constraints in the ESPHome layer and the standard PC use case.\n\n### What the silicon actually supports\n\n| Peripheral | ESP32-S3 silicon capacity | Implication for fan control |\n|---|---|---|\n| **LEDC (PWM)** | 8 channels, 4 timers, **low-speed mode only** ([ESP-IDF docs](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/ledc.html)) | All channels can share a single 25 kHz timer → in theory **8 PWM fans** at once |\n| **PCNT** | 8 units × 2 channels = **16 hardware channels** ([ESP-IDF docs](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/pcnt.html)) | Plenty for 6 fan tachs |\n| **RMT** | 4 TX + 4 RX channels | WS2812 strip consumes 1 TX, plenty left |\n\nSo the **chip itself is not the bottleneck**. What actually shapes the layout:\n\n### 1. ESPHome wraps PCNT with an 8-channel cap\n\nESPHome's `pulse_counter` component documents an explicit limit: *\"A maximum of 8 channels can be used\"* when `use_pcnt: true` ([ESPHome docs](https://esphome.io/components/sensor/pulse_counter.html)). This is a wrapper-level constraint, not a silicon one. For 6 fans it is **not binding**, but it caps any future expansion at 8 channels per device.\n\n### 2. PCNT's 13 µs glitch filter ceiling\n\nFrom the same ESPHome docs: *\"on the ESP32, when using the hardware pulse counter \\[`internal_filter`\\] can not be higher than 13 µs … for the ESP8266 or with `use_pcnt: false` you can use larger intervals too.\"* If you want a heavier debounce (for long, noisy fan cables — common in 19\" racks or 1U sleds), you must fall back to software counting.\n\n### 3. `INPUT_PULLUP` vs PCNT pin syntax\n\nPC-fan tachs are open-collector and need a pull-up. ESPHome's `pulse_counter` accepts two pin syntaxes:\n\n- **short** — `pin: GPIO5` — used by fans 1–4. Concise but does not carry a `mode:` (no internal pull-up), so each line needs an **external** 4.7 – 10 kΩ resistor on the board.\n- **expanded** — `pin: { number: GPIO9, mode: INPUT_PULLUP }` — used by fans 5–6. Cleanest way to enable the MCU's internal pull-up. In practice this pairs more reliably with `use_pcnt: false`, hence the software-counter fallback on those two channels.\n\nThis is the **specific reason** fans 5 and 6 dropped off PCNT: it was not a unit-count exhaustion, it was a deliberate choice to get an internal pull-up on those signals without adding more discrete components to the board.\n\n### 4. LEDC channels vs the 4-fan PWM choice\n\nLEDC has 8 channels on ESP32-S3, all able to share one 25 kHz timer, so **6 PWM outputs are perfectly feasible**. The reason this firmware exposes only 4 is the **typical case-fan use case**: enthusiast and SFF PC builds usually have 3–4 actively controlled case/radiator fans plus 2 \"report-only\" signals (CPU-block pump tach, PSU fan tach, or a non-PWM Molex fan). Putting the dial on 4 controlled + 6 monitored matches that pattern.\n\n### TL;DR\n\n| You might think… | …but the actual reason is |\n|---|---|\n| \"The chip can't drive 6 PWM fans\" | LEDC can do 8 at 25 kHz on a shared timer — the 4-PWM cap is a **design choice for typical PC fan setups**, not silicon |\n| \"PCNT ran out for fans 5–6\" | PCNT has 16 channels; the **8-channel ESPHome cap** is the real ceiling, and even that isn't hit at 6 fans |\n| \"Software counting was a forced fallback\" | It was a **pragmatic choice for clean internal pull-ups** on the open-collector tach lines (and a free filter \u003e 13 µs option for noisy installations) |\n\nIf you want to extend to 6 PWM outputs, add two more `ledc` outputs on free GPIOs (e.g. 15, 16) and two more `number.template` entities driving them — no chip changes required.\n\n---\n\n## Wiring a 4-pin PC fan\n\nStandard 4-pin PC-fan connector (looking into the fan-side socket):\n\n```\n        ┌─── Pin 1 GND       →  GND  (tied to both board GND and PSU GND)\n        │\n        ├─── Pin 2 +12V      →  +12 V from external PSU\n        │\n        ├─── Pin 3 TACH      →  ESP32 GPIO (RPM input) + pull-up to 3.3 V\n        │\n        └─── Pin 4 PWM       →  ESP32 GPIO (PWM output, 25 kHz)\n```\n\n**Important:**\n\n- A **common ground** between the ESP32 and the 12 V PSU is mandatory, otherwise the tach signal is just noise.\n- The ESP32 drives PWM at **3.3 V**. The Intel 4-Wire Fan Spec allows 0 – 5.25 V on the PWM input with a switching threshold around 1.8 V, so 3.3 V is a valid logic-high. The fan internally pulls up the PWM input, no external resistor needed on that line.\n- The tach line emits **two pulses per revolution** — that is why the filter is `rpm = x / 2.0`.\n\n---\n\n## Power budget\n\nThe ESP32 is fed from USB (≤ 500 mA at 5 V is plenty for the SoC + LED ring). The **fans run off the external 12 V PSU**, which you size yourself.\n\nRough current draw per fan at 12 V:\n\n| Fan class | Typical running current | Inrush at start |\n|---|---|---|\n| 120 mm case fan (e.g. Arctic P12) | 80 – 150 mA | up to 2× running |\n| 140 mm case fan (Noctua NF-A14) | 80 – 200 mA | up to 2× running |\n| 120 mm high-static-pressure (server / radiator) | 200 – 500 mA | up to 3× running |\n| 40/60 mm 1U server fan | 200 – 800 mA | up to 3× running |\n\n**Budget rule of thumb:** size the 12 V PSU for the **sum of inrush currents** of all fans you might spin up simultaneously. A 6-fan build of regular case fans is comfortably served by a **12 V / 2 A (24 W)** brick. A build with high-RPM radiator or server fans wants **12 V / 4 – 5 A** to survive simultaneous cold starts without a brownout.\n\nAlways tie the **PSU ground to the ESP32 ground** — otherwise the tach signal is just floating noise.\n\n---\n\n## Home Assistant entities\n\nExposed via the native API:\n\n### Sensors\n\n| Entity | Description |\n|---|---|\n| `sensor.fan_rpm_1` … `fan_rpm_6` | Fan speed in RPM |\n| `sensor.wifi_signal` | Wi-Fi strength as a **percentage** (custom mapping `(RSSI + 100) × 2`, clamped 0 – 100) |\n\n### Numbers (template, `restore_value: true`)\n\n| Entity | Range | Step | Default | Purpose |\n|---|---|---|---|---|\n| `number.led_brightness` | 0.0 – 1.0 | 0.01 | 0.2 | LED ring brightness |\n| `number.rpm_update_interval` | 1 – 60 s | 1 | 30 | Tach polling interval; restarts `rpm_update_loop` on change |\n| `number.fan_speed_1` … `fan_speed_4` | 0 – 100 % | 1 | 50 | PWM duty cycle of the corresponding fan |\n\n### Switch\n\n| Entity | Action |\n|---|---|\n| `switch.restart_rpm_fun` | Soft reboot of the device |\n\n---\n\n## LED indication\n\nThe WS2812 ring of 3 LEDs is declared `internal: true` — Home Assistant does not see it. It is a **local status indicator** only:\n\n| State | Colour / behaviour |\n|---|---|\n| Wi-Fi connected, fans idle | **Green**, steady |\n| Wi-Fi disconnected, fans idle | **Magenta**, blinking 500 ms on / 500 ms off |\n| Any fan spinning (RPM \u003e 0) | **Yellow**, blinking 200 ms on / 200 ms off (interleaved with the connection-state colour — see note below) |\n| Just (re)connected to Wi-Fi | Brief green + `init_pwm` re-applies saved PWM values |\n\n\u003e **Note on interleaving.** Two `interval:` blocks run in parallel: the **1 s block** sets the colour from Wi-Fi state, the **500 ms block** triggers the yellow fan-activity blink. When fans are spinning and Wi-Fi is connected, the visible result is **mostly yellow flashes with brief green flashes every second**, not pure yellow. This is by design — both signals stay observable at the same time.\n\nBrightness is controlled from HA via `number.led_brightness`.\n\n---\n\n## Installation\n\n### 1. Clone\n\n```bash\ngit clone https://github.com/ilia-ae/rpm-fun_esphome.git\ncd rpm-fun_esphome\n```\n\n### 2. Install ESPHome\n\n```bash\n# pip\npip install esphome\n\n# or Docker\ndocker pull ghcr.io/esphome/esphome\n```\n\n### 3. Create `secrets.yaml`\n\nPlace it next to `rpm-fun.yaml` (template below).\n\n### 4. Compile and flash\n\nFirst time over USB:\n\n```bash\nesphome run rpm-fun.yaml\n# or, with Docker:\ndocker run --rm -v \"${PWD}\":/config --device=/dev/ttyACM0 -it ghcr.io/esphome/esphome run rpm-fun.yaml\n```\n\nSubsequent updates **over the air**:\n\n```bash\nesphome upload rpm-fun.yaml --device rpm-fun.local\n```\n\n### 5. Add to Home Assistant\n\nThe device announces itself via mDNS as `rpm-fun.local`. Confirm the integration in **Settings → Devices \u0026 Services → ESPHome** and paste the `api_ha` key from `secrets.yaml`.\n\n---\n\n## `secrets.yaml`\n\nTemplate (create this file next to `rpm-fun.yaml`):\n\n```yaml\nwifi_ssid: \"YourWiFiSSID\"\nwifi_password: \"YourWiFiPassword\"\n\n# 32-byte base64 key for the native API encryption\n# Generate: `openssl rand -base64 32`\napi_ha: \"PUT_32BYTE_BASE64_KEY_HERE=\"\n\n# Password for OTA updates\nota_key: \"your-strong-ota-password\"\n\n# Password for the recovery Wi-Fi access point (fallback hotspot).\n# Minimum 8 characters.\nap_password: \"your-fallback-ap-password\"\n```\n\n\u003e A template lives at [`secrets.yaml.example`](secrets.yaml.example) — `cp secrets.yaml.example secrets.yaml` and fill it in. `secrets.yaml` is already in [`.gitignore`](.gitignore); **never commit it**.\n\n### Fallback hotspot\n\nIf the main Wi-Fi is unreachable at boot or drops out, the device exposes an open AP for recovery:\n\n| Field | Value |\n|---|---|\n| SSID | `Rpm-Fun Fallback Hotspot` |\n| Password | from `!secret ap_password` in your local `secrets.yaml` — **set your own**, do not leave the example value |\n| Captive-portal URL | opens automatically; manual fallback: \u003chttp://192.168.4.1\u003e |\n\nConnect any phone/laptop to the SSID, accept the captive-portal prompt (or open \u003chttp://192.168.4.1\u003e directly), and you get a web form to set new Wi-Fi credentials without re-flashing.\n\n---\n\n## OTA updates\n\nESPHome native OTA platform. Password is taken from `!secret ota_key`. To push an update:\n\n```bash\nesphome upload rpm-fun.yaml --device rpm-fun.local\n```\n\nESPHome will open a TCP session to the device (default port 3232) and stream the new binary.\n\n---\n\n## Implementation notes\n\n### Why fans 5 – 6 are on software counting\n\nSee the [Why this layout?](#why-this-layout-hardware-constraints) section above for the full story. Short version: this is **not** a PCNT capacity issue; it is a deliberate move to enable the MCU's internal `INPUT_PULLUP` on those two open-collector tach lines via the expanded pin syntax. If you want to switch them back to PCNT, change `use_pcnt: false` → `true`, drop the `mode: INPUT_PULLUP` from the pin block, add an external 4.7 – 10 kΩ pull-up to 3.3 V on the board, and remove `internal_filter` (it behaves differently under PCNT).\n\n### Why `init_pwm` fires on `on_connect`\n\nAfter a reset or brown-out, `restore_value: true` restores the target percentages in `number.fan_speed_*`, but ESPHome **does not** auto-call `set_action` on restore. The `init_pwm` script runs from `wifi.on_connect`, syncing the actual LEDC outputs back to the saved values. Without it, the fans would sit at LEDC's default duty until the user nudges the slider in HA.\n\n### The RPM polling loop\n\n`pulse_counter` with `update_interval: never` means **\"never auto-publish\"**. Polling is driven explicitly by the `rpm_update_loop` script (`mode: restart`), which runs `component.update` on all six sensors in sequence with a delay of `rpm_update_interval × 1000 ms`. Changing the interval in HA restarts the loop via `on_value: script.execute rpm_update_loop`.\n\n`mode: restart` matters: when the interval changes, the old loop is aborted and a new one starts with the new delay — no overlapping concurrent loops.\n\n### Wi-Fi → percent\n\nESPHome reports Wi-Fi signal in dBm by default. The lambda `quality = (RSSI + 100) × 2`, clamped to 0 – 100, gives a dashboard-friendly percentage (−50 dBm → 100 %, −100 dBm → 0 %).\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause / fix |\n|---|---|\n| Fan 5 or 6 spins at full speed and ignores HA | **Expected.** Fans 5 and 6 only have tachometer inputs in this firmware — there is no PWM channel for them. They run at whatever speed their power wiring dictates (typically 100 % off straight +12 V). See [Fan capabilities at a glance](#fan-capabilities-at-a-glance) and the [extension recipe](#extend-to-6-pwm-controlled-fans) if you need to add PWM control. |\n| Tach reads 0 on fans 1 – 4 | Missing external 4.7 – 10 kΩ pull-up from Tach to 3.3 V. Add the resistor. |\n| RPM jitters ± 50 at a steady speed | Filter too short or no common ground between PSU and ESP. Tie the grounds. |\n| Fan refuses to start, chirps | `min_power` too low, or the fan's PWM input is fussy about 3.3 V. Try raising `min_power` to `0.2`. |\n| Firmware hangs at boot | Module is `R8` (octal PSRAM); GPIO33 – 37 are reserved for PSRAM, and this firmware uses GPIO35 (PWM 3), GPIO36 (tach 6) and GPIO37 (PWM 4). Use a non-R8 variant or remap fans 3, 4, and 6. |\n| LED ring shows the wrong colour | Incorrect `rgb_order`. Try swapping `RBG` ↔ `GRB` ↔ `RGB` in the `light` block. |\n| Device not visible in HA | The `api.encryption.key` in `secrets.yaml` must be valid base64 of 32 bytes (`openssl rand -base64 32`). |\n| OTA fails | Check free space (≥ 1 MB on the ESP32-S3 `ota` partition). |\n\n---\n\n## Extensions\n\n### Extend to 6 PWM-controlled fans\n\nIf 4 controlled fans is not enough and you want fans 5 and 6 fully variable too, you need to add two more PWM outputs and two more sliders. ESP32-S3's LEDC has 8 channels total and they can all share the same 25 kHz timer, so the silicon is fine — see [Why this layout?](#why-this-layout-hardware-constraints).\n\nPick two free GPIOs (good candidates: GPIO15, GPIO16 — both safe, no strapping function, no PSRAM conflict on any S3 variant), then **add** to `rpm-fun.yaml`:\n\n```yaml\noutput:\n  # …existing 4 entries…\n  - platform: ledc\n    id: pwm_fan_5\n    pin: GPIO15\n    frequency: 25000Hz\n    min_power: 0.1\n  - platform: ledc\n    id: pwm_fan_6\n    pin: GPIO16\n    frequency: 25000Hz\n    min_power: 0.1\n\nnumber:\n  # …existing entries…\n  - platform: template\n    id: fan_speed_5\n    name: \"Fan Speed 5 (GPIO15)\"\n    min_value: 0\n    max_value: 100\n    step: 1\n    initial_value: 50\n    restore_value: true\n    optimistic: true\n    set_action:\n      - lambda: |-\n          id(pwm_fan_5).set_level(x / 100.0);\n          id(fan_speed_5).publish_state(x);\n  - platform: template\n    id: fan_speed_6\n    name: \"Fan Speed 6 (GPIO16)\"\n    # …same structure, swap 5 → 6, GPIO15 → GPIO16…\n```\n\nOptionally also extend the `init_pwm` script so the new outputs get restored on (re)connect:\n\n```yaml\nscript:\n  - id: init_pwm\n    then:\n      - lambda: |-\n          id(pwm_fan_1).set_level(id(fan_speed_1).state / 100.0);\n          id(pwm_fan_2).set_level(id(fan_speed_2).state / 100.0);\n          id(pwm_fan_3).set_level(id(fan_speed_3).state / 100.0);\n          id(pwm_fan_4).set_level(id(fan_speed_4).state / 100.0);\n          id(pwm_fan_5).set_level(id(fan_speed_5).state / 100.0);\n          id(pwm_fan_6).set_level(id(fan_speed_6).state / 100.0);\n```\n\nRe-flash, and HA picks up `Fan Speed 5` and `Fan Speed 6` automatically.\n\n### OLED display\n\nThe YAML contains a commented SSD1306 72×40 OLED block (I²C, address `0x3C`, on SDA = 41 / SCL = 40). Uncomment the `display:` block (the matching `font:` block is already present) to show six fan RPMs, SNTP time, and Wi-Fi quality on the screen.\n\n---\n\n## License\n\nMIT © [Ilia Arestov](https://ilia.ae/en/)\n\nSee [LICENSE](LICENSE) for the full text.\n\n---\n\n## References\n\n- [ESP-IDF — LED Control (LEDC) on ESP32-S3](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/ledc.html)\n- [ESP-IDF — Pulse Counter (PCNT) on ESP32-S3](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/pcnt.html)\n- [ESPHome — `pulse_counter` sensor](https://esphome.io/components/sensor/pulse_counter.html)\n- [ESPHome — `ledc` output](https://esphome.io/components/output/ledc/)\n- [Intel 4-Wire PWM Fan Specification](https://www.formfactors.org/) (background on the 25 kHz target and the open-collector tach signal)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filia-ae%2Frpm-fun_esphome","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Filia-ae%2Frpm-fun_esphome","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filia-ae%2Frpm-fun_esphome/lists"}