QuietCool Whole House Fan (RF "glass" remote) integration

Hi all,

I just installed a QuietCool whole house fan and was disappointed that I wasn't able to easily integrate it into Home Assistant. So I did what any nerd trying to avoid real work would do and fired up URH and reverse engineered the remote's RF protocol to build an ESPHome controller for it.

The entire process and sample component YAML are documented in my git repo.

I used an ESP32 and a CC1101 radio module (both available from lots of places online for less than $10 USD each) and was able to get it to work using the stock components in ESPHome (CC1101 and Template Fan) and also provided the code to the Arduino proof-of-concept.

Major kudos to Caleb Crome who reverse engineered an older version and got me heading down the right path.

Happy to answer any questions if you have them!

Cheers,
-Thadd

6 Likes

Hi there, I’m excited to see this as I just picked up one of these: https://www.athom.tech/blank-1/esphome-rf433-ir-remote-controller, and I attempted hacking the RF signals but failed in my attempts.

Any idea if your code could be adapted for this controller?

There’s a good chance it can. I have a generic 433 radio and I’ll do some testing. It might need a custom component though.

Wow. I have been checking quietcools website for YEARS waiting for this functionality. Nice work thadd. Thank you so much!

Really easy to set up. Highly recommend

2 Likes

Hi @thadd I just want to say thanks for contributing your work on this.

I've managed to get it booting and got my remote id but it does not seem able to turn the fan on or off.

You mention in your repo your friend having the non-eco model. That may be what mine is. Do you happen to know what changes are needed to get it working on his model?

EDIT: Based on your screenshots stating that your friend's signal capture was a higher frequency I tried this:

cc1101:
  cs_pin: 4
  gdo0_pin: 3
  frequency: 433.94MHz

Now it works! :smiley:

1 Like

Glad you got it working! It’s probably not a difference between the models but with the centering on the CC1101 chips. I read some folks had to tweak the target frequency a bit to get things to work so I’ll make a note of trying that in docs in case other run into the same issue.

Thanks for testing it and figuring that out!

1 Like

I've been searching for a year for a way to automate my whole house fan with the QuietCool glass remote, thank you for putting this together. It grabbed my remote ID immediately after flashing but wasn't able to successfully send commands out of the box. Taking a cue from this discussion thread I started incrementing the frequency and landed on a working solution after a couple tries.

cc1101:
  cs_pin: 15
  gdo0_pin: 5
  frequency: 433.96MHz
1 Like

Thanks for the feedback and I'm glad you got it working!

I've updated the repo with instructions for setting the frequency so others won't have to hunt for it.

I also added some lambdas in the on and off triggers to accommodate the fact that the fan turns on to high speed even if you turn it off from low.

It might be useful to have the other verified frequencies listed but commented out in the component.yaml file:

  frequency: 433.92MHz
#  frequency: 433.94MHz
#  frequency: 433.96MHz
1 Like

Good call, updated!

1 Like

Thank you for this! I literly just installed my fan last weekend and was activly trying to reverse engineer the packet. You saved me a lot of time!

1 Like

I got an ESP32 and CC1101 now, wired em up, and I've been banging my head on this for a week. Tried frequencies between .90 and .98 in 0.01 increments. Just not seeing anything in the logs. I do notice that at ~.94, the remote seems to be unable to communicate with the fan when it is nearby, suggesting that the CC1101 is interfering with the remote. Unfortunately, even though moving the remote away from the CC1101 gets rid of the interference and allows the remote to operate, I still do not see the remote ID printed in the logs.

I also see this in the logs non-stop:

[07:48:27.264][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.273][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.281][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.292][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.298][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.307][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.315][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.324][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.333][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.342][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.351][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.360][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1
[07:48:27.369][W][cc1101:207]: Invalid packet: rx_bytes 64, payload_length 159
[07:48:27.379][W][cc1101:207]: Invalid packet: rx_bytes 23, payload_length 1

I can suppress it by increasing the log level, but then I just see silence.

Any ideas on what else I could try?

I would try the Arduino version in the repo. You can download the IDE from Arduino's website and that file should load up and run on your ESP32. It'll be easier to debug than ESPHome if only because it compiles and runs in seconds instead of minutes. It sounds like the interface is working fine since it's doing some reading, but still double check your wiring. Some ESP32s have discrepancies between the pin numbering printed on the board and which GPIO they're actually connected to.

Once the Arduino sketch is loaded on the ESP32, you can hit "R" when connected to the ESP in the serial console from the Arduino IDE and then try waking your remote and pushing buttons. If it can read your remote it should spit out the ID and the command it saw to the console.

Is your QuietCool newer or older? You have the glass remote (not the one with physical buttons, just touch surfaces)? Try also unplugging your fan if it's easy to do. It could be that the controller is echoing responses that are messing up the ID reader.

Are you in the USA? It might be that they have different frequencies in different countries.

My QuietCool is about 2 years old, glass remote (just a touch surface).

I can unplug the fan (it's on a smart plug in the attic), are you saying to try and get the remote ID with the fan entirely unpowered?

And yes I'm in the USA.

Yeah, exactly. I put a software-defined radio right next to the fan and picked up a response message. It was super faint and there’s no way the antenna inside the remote can pick it up. I think they intended the protocol originally to have a confirmation but couldn’t get it working.

Anyway, eliminating potential messages from the fan itself if yours transmits louder won’t hurt and could help.

But the important thing is the arduino version should be more forgiving if the message is faint because he frequency is off.

Another thing that occurred to me is to try tweaking the baud rate too (line 67 in component.yaml or 42 in the arduino sketch). When I captured things it seemed just a touch under 2400 and so it's possible some part of it is a bit off or being a bit particular.

Thadd, thanks for the original legwork here — your URH writeup is what convinced
me this was tractable. A couple of things I found might help unstick people on
the frequency/baud tuning:

@James_Guillochon — your hunch that the capture looked “just under 2400” lines
up with what the remote’s firmware actually configures: it’s exactly 2400 bps
(2-FSK, ±10 kHz deviation, sync 2D D4, no CRC). Locking that in should clear
the “Invalid packet” warnings. What helped me most on the frequency front was
using a Semtech LoRa radio in raw FSK instead of a CC1101 — the packet engine
does sync-word detection in hardware, so it’s far less sensitive to the exact
center frequency and there’s no per-chip 433.92/.94/.96 hunting.

One protocol note for your “turns on High when you press Off from Low” lambdas
(post #8): speed is a 2-bit field (command >> 4) & 0x03 and the 0x80 bit is
masked off, so 80 and 90 are both valid Off — one means “off, no
remembered speed,” the other “off, remembered Low.” Handling 80 fixed a
stuck-state issue on one of my two units, which emit different Off bytes despite
identical firmware.

For what it’s worth, I spent the day with the LoRa boards I had on hand building
a self-learning version — you pair it by pressing your glass remote twice and it
captures the 4-byte sender ID over the air (no sniffing or firmware dump to
onboard), and it’s bi-directional so pressing the physical remote updates the HA
entity too. Repo’s here if it’s useful:

Happy to compare notes.

Hi all! I was able to load this onto an ESP8266 I had lying around and got it transmitting with 433.96MHz. However, one thing I noticed is that the fan will start seemingly at randomly when the router is offline. Typically it is 10-20 min from when the router is offline, but has been as long as an hour.

I can only assume that something is causing it to transmit the start command when it is searching for the router to reconnect to. Would it be possible to prevent transmitting any commands until the device is confirmed connected to the router?

# ---------- CC1101 ----------
cc1101:
  cs_pin: 5
  gdo0_pin: 27
  frequency: 433.94MHz          # start at 433.94, then try 433.94 and 433.96
  modulation_type: 2-FSK
  symbol_rate: 2400
  fsk_deviation: 10kHz
  packet_mode: true
  packet_length: 0              # variable length
  sync_mode: 16/16
  sync0: 0xD4
  sync1: 0x2D
  num_preamble: 4               # 8 bytes of preamble
  filter_bandwidth: 102kHz      # tighter than default 203 kHz
  carrier_sense_above_threshold: true
  carrier_sense_rel_thr: +10dB
  magn_target: 33dB
  on_packet:
    then:
      - lambda: |-
          ESP_LOGI("quietcool", "GOOD packet len=%d  data: %02X %02X %02X %02X %02X %02X",
                   x.size(),
                   x.size()>0 ? x[0]:0, x.size()>1 ? x[1]:0,
                   x.size()>2 ? x[2]:0, x.size()>3 ? x[3]:0,
                   x.size()>4 ? x[4]:0, x.size()>5 ? x[5]:0);

I’m also US, have the glass remote, but I’m running the 3 speed model. It has been installed for 15 months, no idea how old the stock was when I had it installed.

I added this update to narrow the filter a bit and hone in on valid packets. As soon as I added this the remote ID popped out immediately. So far I can get my setup to listen, but it does not appear to be working on the Tx side. Not sure if I’m not actually sending data, or if my CC1101 is too far out of spec for the hub to listen. Too late to work on it more tonight, but thought I’d share my minor success.

Now that I had a few hours of sleep I have it working. I ended up using 433.91MHz.

Thadd, thank you so much for your contribution. I have been trying unsuccessfully to get this working on my own for a year on and off. It’s very satisfying to be able to use my phone to run the fan. I could have purchased dozens of new remotes with the time I spent on this, but the dopamine hit of getting it done on my own has been very satisfying.

For anyone else with a 3 speed fan sold as Stealth Pro X (dealer only installation) the following works for me:

fan:
  - platform: template
    name: "QuietCool WHF"
    id: house_fan
    speed_count: 3
    restore_mode: RESTORE_DEFAULT_OFF

    on_turn_on:
      - script.execute:
          id: send_command
          cmd: 0x9F          # On
      - lambda: id(house_fan).speed = 1;

    on_turn_off:
      - script.execute:
          id: send_command
          cmd: 0x80          # Off
      - lambda: id(house_fan).speed = 0;

    on_speed_set:
      - if:
          condition:
            lambda: 'return x == 0;'
          then:
            - script.execute:
                id: send_command
                cmd: 0x80    # Off
      - if:
          condition:
            lambda: 'return x == 1;'
          then:
            - script.execute:
                id: send_command
                cmd: 0x1F    # Low
      - if:
          condition:
            lambda: 'return x == 2;'
          then:
            - script.execute:
                id: send_command
                cmd: 0x2F    # Medium
      - if:
          condition:
            lambda: 'return x == 3;'
          then:
            - script.execute:
                id: send_command
                cmd: 0x3F    # High
1 Like