Skip to content

Releases: joyfulhouse/esphome-quietcool

v0.3.0

Choose a tag to compare

@btli btli released this 17 Aug 21:53
v0.3.0
1edf133

Added

  • Passive remote-state synchronization: OEM remote key-presses overheard on air now update the confirmed fan state, with verification queries and passive-evidence consensus guarding against misclassification (#37).
  • Periodic heartbeat queries with deterministic jitter: the controller re-confirms the fan's true state every few minutes, so any missed RF evidence self-corrects without operator action (#37).
  • SX127x RX watchdog in quietcool-cpp-lora32.yaml: bounds the variable-length packet-engine wedge (phantom packet from a truncated burst tail or corrupted length byte) to seconds by re-arming RX (standby → rx) after 5 s of radio silence. Diagnosed and validated on a production unit (#39).

Fixed

  • Passive-epoch response handling: incomplete or abandoned passive responses recover instead of stalling the confirmation state machine; stale passive ambiguity and expired responses are cleaned up at epoch boundaries.
  • Passive evidence and response consensus isolated from each other; abandoned observations publish their authority loss.
  • Terminal OEM recovery re-arms after passive activity.
  • Heartbeat scheduling survives degradation and passive-sync interleavings.

Full changelog: v0.2.0...v0.3.0

v0.2.0 — Fan Timer, restored diagnostics, core follow-ups

Choose a tag to compare

@btli btli released this 02 Aug 15:57

Features

  • Fan Timer select (None / 1 / 2 / 4 / 8 / 12 hours). Every option — None included — transmits an energizing command: a duration on a stopped fan starts it at LOW (matching the OEM remote), and None means no timer / run continuously, which restarts a fan whose timer expired. Off (stop) is deliberately unrepresentable in the timer vocabulary. Speed-aware: composes from confirmed state against the confirmed speed band, so a two-speed fan can never be sent MED.
  • Eight restored RF diagnostics, disabled by default: Last TX Command, Last Valid RX Frame, Last Confirmed Fan State, Fan Speed Capability, Remote Sender ID, TX / RX Valid / RX Rejected counters. The rejected counter doubles as a live RF-interference meter (it found a real household interferer during field validation).

Fixes (#35)

  • An OEM-set timer that expires unobserved now invalidates its stale display at the observation-time upper bound and re-queries, instead of showing the old program forever.
  • Controller Fault publishes off from boot (previously unknown until a fault) and is hardened against mid-setup degradation ordering.
  • Authority delivery re-checks read a cheap revision accessor instead of constructing full core snapshots twice per delivery.

Verification

Two multi-model adversarial review campaigns (Fable 5, Opus 5, GPT-5.6-sol, Gemini 3.1 Pro): 16 rounds for the timer feature and 3 rounds for the follow-ups, each ending with all four engines clean. Every fix wave landed with named mutation kills; ASan/UBSan clean. Field-validated on two production controllers, including a live timer round-trip (0x91 on → confirmed 11 s → 0x90 off → confirmed 10 s) through active RF interference.

Upgrade notes

Declare the new select platform (see quietcool-cpp-example.yaml) and note AUTO_LOAD now includes select. ESPHome ≥ 2026.7.0; esp-idf with a 16 KB loop-task stack remains REQUIRED.

Installing

Install from your ESPHome Device Builder by sourcing github://joyfulhouse/esphome-quietcool@v0.2.0 — no repo checkout needed. Full steps: INSTALL.md (read it on main; this tag's copy predates the Builder-first rewrite).

v0.1.0 — confirmed-authority QuietCool control

Choose a tag to compare

@btli btli released this 28 Jul 00:15

First tagged release. The C++ external component is the product; both of the
maintainer's whole-house fan controllers run this code in production.

This is a 0.x release on purpose. The code is running two real fans and
every change in it was adversarially reviewed and hardware-verified, but the
deployment is one household on one board family, and the configuration surface
may still change. Treat the YAML options and the persisted NVS layout as
unstable until 1.0.

What it is

ESPHome firmware that drives QuietCool whole-house / gable attic fans over
their native 433.92 MHz 2-FSK link — no cloud, no OEM hub, no BLE. The protocol
was reverse-engineered from the OEM handheld remote's firmware and SDR captures;
this is an independent, clean-room implementation.

The RF confirmation core is a platform-free C++ library (components/quietcool/)
with host-run test suites, wrapped by a thin ESPHome adapter. Its central rule:
the fan entity publishes only confirmed authority. Hearing a command is not
proof the fan acted on it, so a heard frame is diagnostics-only and never moves
the safety-facing entity.

Highlights in this release

  • Speed bands are positional, not identity. The wire nibbles have fixed
    meanings (LOW 0x9F, MED 0xAF, HIGH 0xBF) and a given fan supports a
    subset. A 2-speed fan is {LOW, HIGH}, so Home Assistant's top level must map
    to HIGH — mapping it by identity sent MED, a speed such a fan does not have,
    which stopped the fan instead of running it.
  • Speed capability survives reboots. The confirmed capability is persisted in
    NVS and seeds the band at restore, before any RF round-trip, closing a window a
    real power blip exposed. The record's schema version is deliberately unchanged
    so old and new firmware still read each other's records — a version bump would
    make a rollback fail closed and boot unprovisioned.
  • The listed band and the command band are separate. Home Assistant caches
    supported_speed_count at connect and only refreshes on reconnect, so the
    listed band latches monotonically non-increasing: a stale cache survives
    narrowing (the top level clamps to HIGH) but not widening. While capability is
    unknown, the command band is 2 — a band that structurally cannot form MED.
    No unsupported speed can reach the fan even before it has taught the device
    anything.
  • Learn is refused while a sender is bound. Re-pairing is deliberately two
    steps, Forget then Learn, so one accidental button press can no longer open a
    window in which a working binding is replaced.
  • Pairing docs state the real limit. The ambiguity guard can only fire when
    the intended fan is among the senders heard; if it stays silent and a lone
    foreign fan transmits, that fan gets bound. So Learn must be run while
    actively triggering the target fan
    — procedure, not a tip.
  • Repository restructured. The C++ build is the documented path; the two
    ~150 KB monolithic YAML configs are frozen under legacy/.

Hardware

  • LilyGO TTGO LoRa32 V2.1 (SX1278) — verified reference board, running in
    production on two controllers.
  • Heltec / HiLetgo ESP32 LoRa V3 (SX1262) — config- and compile-validated;
    awaiting hardware verification.

Requires ESPHome ≥ 2026.7.0 and the ESP-IDF framework with a 16 KB loop task
stack. The Arduino default of 8 KB overflowed into FreeRTOS kernel structures
during RF exchanges and crash-looped a controller; do not shrink it.

How this was verified

Host suites run on every push — 217 core tests, the same suite again under
ASan/UBSan, 51 adapter tests against ESPHome stubs, and 147 config regression
tests. Every fix in this release was implemented in isolation and then reviewed
adversarially by four independent engines (Fable, Claude Opus, Codex
gpt-5.6-sol, Gemini 3.1 Pro), looping until all attested clean — seven rounds
for the capability work. Findings were mutation-tested rather than trusted: a
claim only counted once breaking the property made a named test fail.

The capability-persistence fix was then confirmed on real hardware in a way no
test could stage: after a restart, the controller's boot query happened to
exhaust without RF consensus, and the unit still advertised the correct 2-speed
band — with no confirmation that boot, the value could only have come from NVS.

What 1.0 still needs

  • Hardware verification on the SX1262 board, not just compile validation.
  • Soak time beyond the current deployment: two fans, one household, one board.
  • The two known residuals below closed, or consciously accepted.
  • A settled configuration surface — the YAML options and the NVS record layout
    are still free to change in 0.x.

Known residuals

Both low severity, both found by the review loop and filed rather than rushed:

  • #32remembered_speed survives re-binding to a different fan.
  • #33 — the echo guard keys on the current outbound byte only, so a
    mid-transaction re-aim leaves the previous byte unsuppressed.

Safety note

This drives a whole-house fan. Open a window before starting one — there is a
backdraft hazard with combustion appliances. Flashing and the boot status query
are non-energizing; a status query cannot start the fan.