Skip to content

v0.1.0 — confirmed-authority QuietCool control

Choose a tag to compare

@btli btli released this 28 Jul 00:15
· 59 commits to main since this release

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.