Introduction
Hey everyone!
Following my Dooya RF433 blinds write-up, here’s the same ESP32 + CC1101 approach applied to a 433 MHz ceiling fan (mine is a Cecotec, but it’s fully generic).
This time I went a step further and built a dedicated Home Assistant custom integration — “RF Fan” — so you don’t have to hand-sniff and paste codes. HA learns each remote button for you through a guided flow, and you get proper fan and light entities, plus optional timers, colour temperature (Kelvin), sound, reverse direction and natural-airflow.
Result: full fan + light control from HA, no cloud, no proprietary hub, for ~$10 of hardware. Works with any RF fan, not just Cecotec.
Hardware
| Component | Approx. Price | Source |
|---|---|---|
| ESP32 DevKit V1 (or clone) | ~$4 | AliExpress |
| CC1101 433 MHz module (green E07, 8 pins) | ~$3 | AliExpress |
| Dupont jumper wires (female-female) | ~$2 | AliExpress |
| 433 MHz antenna (or a ~17 cm wire) | cents | — |
| Micro-USB cable + 5 V charger | already have | — |
Total: ~$10. A small plastic enclosure is nice for the final install.
The CC1101 is a 3.3 V module — do not power it from 5 V.
Wiring: ESP32 + CC1101
There are two common 8-pin CC1101 modules. Check pin 2: green E07 = VCC, blue Standard = GND.
GREEN module (E07) — most common on AliExpress
| CC1101 Pin | Name | ESP32 GPIO |
|---|---|---|
| 1 | GND | GND |
| 2 | VCC | 3V3 |
| 3 | GDO0 | GPIO 4 (RF data, RX and TX) |
| 4 | CSN | GPIO 5 |
| 5 | SCK | GPIO 18 |
| 6 | MOSI | GPIO 23 |
| 7 | MISO | GPIO 19 |
| 8 | GDO2 | not used |
BLUE module (Standard) — same GPIOs, only VCC/GND (pins 1↔2) and the data pins are laid out differently; match by name, not pin number.
Step 1 — Flash the ESPHome gateway
The gateway does two jobs: it transmits the fan’s RF frames (via a transmit_rf_fan service) and listens so Home Assistant can learn your buttons (it fires an rf_fan_received event). The radio is driven by the excellent esphome-radiolib-cc1101 external component at 433.92 MHz.
A complete, working example is in the repo: esphome/rf_fan_example.yaml.
GPIO 4 is shared between RX and TX. The example handles this (it drops back into receive mode after each transmit), so a short watchdog keeps the receiver alive. Keep that part.
Step 2 — Install the RF Fan integration (HACS)
- HACS → three-dots → Custom repositories
- Add
https://github.com/dasimon135/ha-rf-fan— category Integration - Install RF Fan, then restart Home Assistant
- Settings → Devices & Services → Add Integration → RF Fan
Step 3 — Set up & learn your remote
In the config flow:
- Pick your ESPHome gateway (auto-detected).
- Declare what your remote can do — number of speeds, light style (none / single toggle / separate on & off), and tick any of: reverse direction, natural airflow, colour temperature, sleep timers, sound. You’re only asked for the buttons you actually have.
- Choose Guided learning and press each button on the physical remote when prompted — the gateway captures the frame, HA stores it. No YAML. (Manual paste is also available.)
Need to add a capability later? Use ⋮ → Reconfigure on the integration entry — it learns only the new buttons and keeps the codes you already captured.
What you get in HA
- a
fanentity with speed control (+ direction and a natural preset if enabled) - a
lightentity - a colour-temperature select (+ a small calibrate button)
- a sound switch, and timer buttons (1/2/4/8 h)
State is assumed (the fan sends nothing back), but it’s restored across restarts, and if you use the physical remote the integration tries to follow along via RX.
A nice dashboard card (Mushroom)
A single, unified card with everything (needs Mushroom + vertical-stack-in-card from HACS):
Mushroom card YAML (adjust entity IDs to yours)
type: custom:vertical-stack-in-card
cards:
- type: custom:mushroom-fan-card
entity: fan.living_room_fan
name: Living Room Fan
icon_animation: true
show_percentage_control: true
show_oscillate_control: false
collapsible_controls: false
- type: grid
columns: 2
square: false
cards:
- type: custom:mushroom-light-card
entity: light.living_room_fan_light
name: Light
icon_color: amber
show_brightness_control: false
- type: custom:mushroom-select-card
entity: select.living_room_fan_color_temperature
name: Colour
icon: mdi:thermometer-lines
- type: custom:mushroom-chips-card
alignment: center
chips:
- type: template
entity: fan.living_room_fan
icon: mdi:fan
content: Normal
icon_color: "{{ 'blue' if state_attr('fan.living_room_fan','preset_mode') != 'natural' else 'disabled' }}"
tap_action:
action: perform-action
perform_action: fan.set_preset_mode
target: { entity_id: fan.living_room_fan }
data: { preset_mode: normal }
- type: template
entity: fan.living_room_fan
icon: mdi:weather-windy
content: Natural
icon_color: "{{ 'blue' if state_attr('fan.living_room_fan','preset_mode') == 'natural' else 'disabled' }}"
tap_action:
action: perform-action
perform_action: fan.set_preset_mode
target: { entity_id: fan.living_room_fan }
data: { preset_mode: natural }
- type: template
entity: fan.living_room_fan
icon: mdi:rotate-right
content: Forward
icon_color: "{{ 'blue' if state_attr('fan.living_room_fan','direction') != 'reverse' else 'disabled' }}"
tap_action:
action: perform-action
perform_action: fan.set_direction
target: { entity_id: fan.living_room_fan }
data: { direction: forward }
- type: template
entity: fan.living_room_fan
icon: mdi:rotate-left
content: Reverse
icon_color: "{{ 'blue' if state_attr('fan.living_room_fan','direction') == 'reverse' else 'disabled' }}"
tap_action:
action: perform-action
perform_action: fan.set_direction
target: { entity_id: fan.living_room_fan }
data: { direction: reverse }
- type: custom:mushroom-chips-card
alignment: center
chips:
- type: entity
entity: switch.living_room_fan_sound
icon: mdi:volume-high
content_info: name
tap_action: { action: toggle }
- type: entity
entity: button.living_room_fan_timer_1h
icon: mdi:timer-outline
content_info: name
tap_action:
action: perform-action
perform_action: button.press
target: { entity_id: button.living_room_fan_timer_1h }
- type: entity
entity: button.living_room_fan_timer_2h
icon: mdi:timer-outline
content_info: name
tap_action:
action: perform-action
perform_action: button.press
target: { entity_id: button.living_room_fan_timer_2h }
- type: entity
entity: button.living_room_fan_timer_4h
icon: mdi:timer-outline
content_info: name
tap_action:
action: perform-action
perform_action: button.press
target: { entity_id: button.living_room_fan_timer_4h }
- type: entity
entity: button.living_room_fan_timer_8h
icon: mdi:timer-outline
content_info: name
tap_action:
action: perform-action
perform_action: button.press
target: { entity_id: button.living_room_fan_timer_8h }
- type: entity
entity: button.living_room_fan_recalibrate_color
icon: mdi:target-variant
content_info: name
tap_action:
action: perform-action
perform_action: button.press
target: { entity_id: button.living_room_fan_recalibrate_color }
Honest limitations
- Assumed state — no RF acknowledgement from the fan; commands are one-way (HA tracks a best-effort state).
- RX can be noisy depending on your local 433 MHz environment; TX is reliable.
- Toggle/relative buttons (light toggle, colour cycle, direction, natural) are handled so a single tap = a single action.
Conclusion
Same spirit as the Dooya build — ~$10 of hardware, 100% local, no cloud. The difference here is a real custom integration doing the heavy lifting (guided learning, proper entities, reconfigure, state restore), so it should be approachable even if you’ve never sniffed an RF code.
Code (integration + ESPHome example): GitHub - dasimon135/ha-rf-fan: Generic Home Assistant integration for RF (433 MHz) fans — ESPHome gateway, guided code learning, optional light/timers/kelvin/sound/direction. · GitHub
Feel free to ask if you have any questions!
And if you try it on a different RF fan, I’d love to hear whether it works for you.
Tested with: ESPHome 2026.x / ESP-IDF 5.x / ESP32 DevKit v1 / CC1101 E07 green module / Home Assistant 2026.5+
— Built and debugged with Claude ![]()





