RF hacking my cloud-controlled ceiling fan

Hacker News Top News

Summary

A developer reverse-engineers the RF remote of a Dreo cloud-controlled ceiling fan to enable local control via Home Assistant, bypassing cloud dependency and documenting the process of decoding and replaying commands.

No content available
Original Article
View Cached Full Text

Cached at: 06/30/26, 09:39 PM

# RF Hacking my Cloud-Controlled Ceiling Fan · Sam Wilkinson Source: [https://samwilkinson.io/posts/2026-06-24-rf-hacking-dreo](https://samwilkinson.io/posts/2026-06-24-rf-hacking-dreo) ## RF Hacking my Cloud\-Controlled Ceiling Fan 2026\.06\.24 · 8 min readWhen we moved into our current place, we knew pretty quickly we'd want to change our bedroom ceiling fan\. That thing easily covered a quarter of our ceiling, wobbled like crazy, and I could not figure out what any of the three \(\!\) pull chains did\. It had to go\. After some searching,[this fan](https://www.amazon.com/dp/B0CXPR24K2?th=1)\(the CLF513S\) from Dreo seemed like a good option\. It's reasonably priced, looks quite nice, and most importantly, it's a normal size\. I've been on a smart home kick over the past few years, with[Home Assistant](https://www.home-assistant.io/)as my core platform, so I wanted to be able to add the new fan to my setup\. Unfortunately, Dreo's[smart home integration](https://github.com/dreo-team/hass-dreoverse)is cloud\-only out of the box\. They do get bonus points, though, for maintaining an official Home Assistant integration\. I really try to avoid cloud\-based smart home controls whenever possible, for a number of reasons: - Internet outages stop them from working\. - You often get locked into ad\-filled proprietary apps, and integrations to get around that can be flaky\. - It just generally feels weird to have external servers control physical things in your home? The fan was sleek and affordable enough that I held my nose and bought it, with the hope of figuring out local control once I had it installed\. ## Choosing a Path to Local Control When exploring my options for local control, I found this[excellent project](https://github.com/ouaibe/dreo-cloudcutter)by`ouaibe`on GitHub where they used some very impressive firmware reverse engineering to get[ESPHome](https://esphome.io/)running on a different Dreo fan\. While I could*try*to follow in their footsteps and attempt to replicate their methods on my ceiling fan, the prospect of bricking something hard\-wired into my ceiling put me off\. The fan's only local control mechanism was its remote, so I wanted to try the "universal remote" path: decode the remote's commands and replay them\.[Universal remotes](https://www.theverge.com/podcast/949620/harmony-universal-remote-version-history)are famously reliable products, so clearly I was on the right track\. ![Dreo Remote](https://samwilkinson.io/assets/remote.png) The remote did not require line of sight, so it was not infrared like a TV remote\. Looking up the[FCC ID](https://apps.fcc.gov/oetcf/eas/reports/ViewExhibitReport.cfm?mode=Exhibits&RequestTimeout=500&calledFromFrame=N&application_id=ToNOqr8hEMB%2BC0rFFA0IiA%3D%3D&fcc_id=2A3SYDRCF01)listed on the back confirmed it was an RF transmitter\. To enable local control through Home Assistant, there were three steps I needed to take: 1. Decode the remote's RF commands\. 2. Build something to replay those commands\. 3. Let Home Assistant trigger those replays\. With the approach settled, the first step was understanding what the remote was actually sending\. ## Decoding Commands To decode a \(simple\) RF control scheme, you need to figure out three things: 1. The[carrier frequency](https://en.wikipedia.org/wiki/Carrier_wave)· The base frequency used to communicate\. 2. The[modulation method](https://en.wikipedia.org/wiki/Signal_modulation#Digital_modulation_methods)· How 1s and 0s are being conveyed through that carrier frequency\. 3. The payload\(s\) · The actual 1s and 0s being sent by the remote\. Having found the carrier frequency in the manufacturer's FCC documentation \(433\.92 MHz, a common frequency for remotes\), the next step was figuring out the modulation method\. Two common digital modulation methods for remotes are: - **Amplitude Shift Keying**\(**ASK**\) · The transmitter shifts the amplitude of the carrier up and down to represent 1 and 0\. A simple version is**On\-Off Keying \(OOK\)**, where the carrier just turns on and off\. - **Frequency Shift Keying**\(**FSK**\) · The transmitter shifts the signal frequency slightly above and below the carrier frequency to represent 1 and 0\. It's the same basic idea as AM vs FM radio, but with digital signals instead of analog audio\. The FCC documentation*says*the remote uses FSK, but I wanted to verify the actual signal rather than take that at face value\. Often these documents use generic values so they aren't the most reliable source\. I used an[RTL\-SDR](https://www.rtl-sdr.com/buy-rtl-sdr-dvb-t-dongles/)\(with[gqrx](https://www.gqrx.dk/)\) to capture RF spectrum data while pressing some of the remote buttons\. This made it clear that the remote was using ASK \(OOK specifically\): bursts of high amplitude at the carrier frequency with silence in between\. Below is a waterfall plot, rotated sideways, showing the spectrum captured when pressing the Fan On/Off button twice\. ![Waterfall](https://samwilkinson.io/assets/remote-fan-on-off_raw_waterfall.png) Looking at the capture in more detail with the plot below, the signal appeared to use short and long bursts of amplitude, kind of like Morse code's dots and dashes\. ![OOK Bits](https://samwilkinson.io/assets/ook_bit_comparison.png) Zooming in on one of the button presses, I saw that a specific pattern was being repeated five times in quick succession\. The plot below shows one of those repetitions, which I could then decode into logical 1s and 0s\. I repeated this process for each button I wanted to replicate \(fan on/off, fan speed presets, light on/off, and brightness up/down\)\. ![On Off Command Decoded](https://samwilkinson.io/assets/remote-fan-on-off_decoded.png) Each command followed the same structure, with a single packet repeated five times with an 8\.8 ms gap between repetitions: - A**preamble**of 8 logical 0s\. - A shared 20\-bit**synchronization**phrase\. - A unique 13\-bit**payload**specifying the command\. For example, the Fan On/Off button's payload was: `0111110010000` As mentioned above, the logical bits were encoded as short OOK pulse patterns, with`1000`representing a logical`0`and`1110`representing a logical`1`\. Each pulse in those patterns was transmitted for 300 μs, so each logical bit \(four pulses\) took 1\.2 ms to transmit\. That means the Fan On/Off payload above was transmitted as: `1000 1110 1110 1110 1110 1110 1000 1000 1110 1000 1000 1000 1000` All the decoded commands can be found in the[project's repo](https://github.com/scwilkinson/dreo-rf-control)\. ## Replaying Commands To replay these commands, I wired a[Xiao ESP32\-C6](https://www.amazon.com/dp/B0D2NKVB34?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_1&th=1)board to an[RFM69HCW](https://www.adafruit.com/product/3071)433 MHz RF transceiver\. I've been using Seeed Studio's Xiao boards a lot recently; they're absolutely tiny and great for keeping projects compact\. The ESP32 is completely covered by the transceiver breakout\. ![RFAN Circuit in Open Enclosure](https://samwilkinson.io/assets/rfan-enclosure-open.png) As far as I could tell, the packet structure I had decoded was proprietary and not supported by existing libraries like RadioHead\. Rather than asking the radio library to understand the protocol, I used the transceiver in a raw transmit mode and manually toggled its data pin with the pulse timings I had decoded from the original remote\. To replay a command, the microcontroller needs to: 1. Configure the radio over SPI: frequency, modulation, power settings, and so on\. 2. Put the radio into transmit mode\. 3. Toggle the radio's data pin using the pulse timings for the command\. 4. Put the radio back to sleep until it is needed again\. This is where the RTL\-SDR earned its keep a second time\. Without it, debugging was mostly a matter of flashing firmware, walking into the bedroom, and seeing whether the fan had responded\. With the SDR, I could actually compare my generated signal against the original remote capture\. The most annoying issue was timing\. The fan was surprisingly picky: a command that looked right in code could still be ignored if the pulse timing or inter\-packet gap was slightly off\. The last hurdle was a self\-inflicted bug in my radio/SPI handling which caused the ESP32 to drop its Wi\-Fi connection\. Once that was fixed, and after quite a bit of running between my office and bedroom to test commands, the ESP32 was reliably replaying the same commands as the original remote\! ## Connecting to Home Assistant Now that I had the ability to replay the commands I wanted to use, I needed a way to trigger them from Home Assistant\. A simple solution was MQTT, a lightweight messaging protocol that is widely used in smart home devices\. MQTT works by having devices publish messages to named topics, while other devices subscribe to those topics and react when messages arrive\. ![MQTT Diagram](https://samwilkinson.io/assets/mqtt-code-block.png) I configured the ESP32 to subscribe to a set of MQTT topics, with each topic corresponding to a specific fan or light command\. Home Assistant could then publish messages to these topics whenever a user interacted with the fan controls in the dashboard\. When the ESP32 receives a message, it replays the appropriate RF command\. Since the fan doesn't send anything back to the transmitter, the ESP32 has no way to tell what the fan is actually doing\. To get around this, when the device first powers on, it assumes a default state: light off, medium brightness, and fan off\. As it sends commands, it updates this internal state accordingly\. The state is saved to persistent storage, so the default state is only needed for the first boot\. Very occasionally the state can go out of sync with the fan, for example if commands are sent while the fan is powered off at the wall\. When that happens, I just use the remote to bring the fan back into the state Home Assistant expects\. ## Wrapping Up ![RFAN in Enclosure](https://samwilkinson.io/assets/rfan-enclosure.png) After printing a little case for the transmitter, including the obligatory accent color for the antenna, it was finally ready to go\. The best sign a project like this has worked is that no one has to think about it anymore\. I've had the little transmitter sitting on a shelf in my office for nearly a year now, and my partner and I happily control our fan with Siri every day\. If you're interested in replicating this approach, check out the[project's repo](https://github.com/scwilkinson/dreo-rf-control)\.

Similar Articles

An iroh powered smart fan

Hacker News Top

The article demonstrates how to build a smart fan using an ESP32 microcontroller and the iroh networking library, enabling peer-to-peer control without a cloud service.

Reverse-engineering is cheap now

Simon Willison's Blog

The article discusses how coding agents have dramatically reduced the cost and effort of reverse-engineering home devices, making automation projects more accessible and less risky.

Hacking your PC using your speaker without ever touching it

Hacker News Top

A security researcher reverse-engineers the Creative Sound Blaster Katana V2X firmware, uncovering vulnerabilities that allow attackers within 15 meters to turn the speaker into a covert spying tool and Rubber Ducky without physical access.