[Tutorial] Control an RF433 ceiling fan (Cecotec & others) from Home Assistant — ESP32 + CC1101 + a custom integration

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.

:warning: 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.

:warning: 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)

  1. HACS → three-dots → Custom repositories
  2. Add https://github.com/dasimon135/ha-rf-fan — category Integration
  3. Install RF Fan, then restart Home Assistant
  4. Settings → Devices & Services → Add IntegrationRF Fan

Step 3 — Set up & learn your remote

In the config flow:

  1. Pick your ESPHome gateway (auto-detected).
  2. 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.
  3. 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 fan entity with speed control (+ direction and a natural preset if enabled)
  • a light entity
  • 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! :slightly_smiling_face: 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 :slight_smile:

Very nice. I solved the same problem without ESPhome. Propably interesting to check both approaches:

I have a CREATE WIND LARGE ceiling fan (the 152 cm wooden one). It’s sold in two variants: remote-only, and WiFi + remote. I have the remote-only version.

What it did:

  • Full control from HA: on/off, 6 speed presets, timers, summer/winter (rotation direction), beep on/off
  • Exposed as a native fan entity plus switch and button entities via MQTT
  • Two-way sync: the ESP32 also listens passively to the original remote, so HA stays in sync when someone uses the physical remote

One quirk of this remote: the code value drifts slightly on every press, so instead of exact matching I use nearest-neighbor matching against reference codes. There’s a small helper script to compute those from your own recordings, plus a sniffer/replay sketch to capture your remote’s codes (yours will differ from mine).

Documentation is bilingual (EN/DE) and includes wiring, protocol notes, and a troubleshooting guide covering the dead ends I hit along the way (the CC1101 async-serial-mode GDO0 config was the big one).

Repo: GitHub - jot-koehler/esp32-cc1101-fan-control: Home Assistant control for a WiFi-less 433 MHz fan via ESP32 + CC1101 — with MQTT and passive remote sync. · GitHub

— Built and debugged also with Claude :slight_smile:

guys, great info and integration! I may be tackling this soon, never done anything with ESP32 before.

while searching for hardware I ran across this; which has a lot of depth of info on the hardware options… Simple ESPSomfy RTS device · rstrouse/ESPSomfy-RTS Wiki · GitHub

Some questions…

  1. can a single controller interface into more than 1 fan? I have 2 fans I want to control
  2. do these also work with this? … ESP32 C3 Mini Plus ESP32-C3 SuperMini or Freenove ESP32-S3-WROOM (has USB-C)

thx!

Thanks, glad it’s useful!

1. Multiple fans on one gateway — Yes. The ESPHome gateway is just a dumb RF transmitter/receiver exposed as a transmit_rf_fan service; it doesn’t know or care how many fans you have. Each fan gets its own HA config entry (its own set of learned codes), and the integration passes a device identifier along with the action/code when it calls the service — same on the receive side with the esphome.rf_fan_received event. So one CC1101 + one ESP32 can happily drive both of your fans, as long as their RF codes don’t collide (in practice they won’t, since each remote has its own code space). You just run the guided learning twice, once per fan, against the same gateway.

2. Other boards (ESP32-C3 SuperMini / ESP32-S3-WROOM) — Both should work fine. The CC1101 talks SPI (SCK/MOSI/MISO/CSN) plus one GPIO for GDO0 (RX+TX data), and ESPHome lets you remap those pins to whatever’s free on your board — nothing in this project is tied to the classic ESP32 DevKit pinout, that’s just the reference wiring.

  • On the C3 SuperMini, just avoid the strapping pins (GPIO2, GPIO8, GPIO9) for CSN/GDO0/SCK etc. — pick any other free GPIOs and update esphome/rf_fan_example.yaml accordingly.
  • The ESP32-S3-WROOM (Freenove) has plenty of free GPIOs and no particular constraint here; USB-C is just a bonus for flashing.

Either way, keep wiring short (<10–15 cm) to avoid SPI glitches, and remember the CC1101 is 3.3 V only.

thx for the great reply. I found a mini on Ali that has a built-in wifi antenna connection, and includes the antenna also. LMK what you think.

and this RF module includes the 3db RF antenna

Nice picks, both are the right parts. Three things to check first:

CC1101 + 3 dB antenna: make sure both the module and the antenna are 433 MHz (they come in 315/433/868/915). A 3 dB antenna cut for the wrong band will actually make 433 worse.

ESP32 mini w/ external wifi antenna: on most of these the u.FL connector isn’t live by default — there’s a tiny 0‑ohm resistor you have to move from the PCB antenna to the external pad, or the antenna does nothing. Also check it has enough free GPIOs: CC1101 needs SPI + 2 pins (SCK/MISO/MOSI/CS/GDO0/GDO2). Plain ESP32 or S3 is safest; the C3 SuperMini gets tight.

And don’t mix the antennas up: u.FL = 2.4 GHz (wifi), SMA = 433 MHz (fans).

Green light if the RF side is clearly 433, the ESP32 antenna is switchable, and you’ve got the pins. :+1:

I dont see a CS on the mini’s diagram? ChatGPT says “If your board doesn’t expose GPIO5 (the traditional VSPI SS pin), simply choose another unused GPIO and configure your library to use it. Unlike SCK, MOSI, and MISO, CS can be almost any available GPIO on the ESP32. The ESP32’s GPIO matrix lets you assign the chip select signal to nearly any output pin in software”

It recommends mapping O5 or 07 to CS

Do you think this will work?

I dont know how to check if the resister needs to move. I would hope they would sell it ready to use the antenna that they include in the set :slight_smile: ?

Good catch on the pinout — no pin is labeled CS because there isn’t a dedicated one. On the ESP32‑C3 (like the classic ESP32), only SCK/MISO/MOSI are fixed (GPIO8/9/10 here) — CS can be routed to any free GPIO via the chip’s GPIO matrix, so your library config just needs to point at whichever pin you pick. GPIO7 works fine.

One thing to plan for: the CC1101 also needs GDO0 and GDO2 (interrupt pins), so budget 2 more free GPIOs beyond CS — e.g. GPIO4 and GPIO3 look open on this board. Avoid GPIO20/21 if you need UART/logging.

On the antenna resistor — no way to tell from the listing photos, honestly. Worth just testing once it arrives: do a wifi scan and compare RSSI with the antenna connected vs. unplugged. If there’s no difference, the trace antenna is still active and the resistor needs moving.

I bought it, so we’ll know in a week or so :wink:

btw, some images on Ali show the resistor position. I dont want to mess with surface mount soldering, hope it has proper placement. Not sure if the external antenna is necessary for most applications but it was cheap so might as well get the external.

@eleven_gorgon45

with your comment " CC1101 is a 3.3 V module — do not power it from 5 V."
… I think the when we power the ESP32 using USB, the ESP down-converts to 3.3v for that pin to output to the CC1101, yes?

the ESP32 Mini I received is different than the photos they posted. I was not sure if the internal or external was wired up. It looked like the internal. But until I connected the external antenna, it would not connect to my wifi. So even though it looks like the cap is directionally on the white built-in antenna, seems like it is connecting to the external. strange. Leaving photos here in case anyone needs them.

Also, I toned it out… Lines of the same color show continuity between pads / connector. So I thought I was going to need a jumper where the black arrow, seems like that is not needed.