Control Dooya RF433 Blinds with ESP32 + CC1101 via ESPHome

[Tutorial] Control Dooya RF433 Blinds with ESP32 + CC1101 via ESPHome

Introduction

Hey everyone!

Sharing my setup for controlling Dooya roller blinds (RF433 MHz motors) from Home Assistant using an ESP32 and a CC1101 module running ESPHome.

Dooya blinds (and rebrands like Zemismart, AM43, etc.) use RF 433 MHz remotes with a proprietary protocol. The idea: replace all DC90 remotes with a single ESP32 that transmits the same RF codes.

Result: 8 blinds controlled from HA, no cloud, no proprietary hub, for ~$10 worth of hardware.


Hardware

Component Approx. Price Source
ESP32 DevKit V1 (or clone) ~$4 AliExpress
CC1101 433MHz module (green E07, 8 pins) ~$3 AliExpress
Dupont jumper wires (female-female) ~$2 AliExpress
Micro-USB cable + 5V charger already have -

Total: ~$10

Optional for final installation

  • Plastic enclosure (~83x58x33mm)
  • PG7 cable gland (USB cable passthrough)
  • 3M 9448A double-sided tape (mounting inside enclosure)

Wiring: ESP32 + CC1101

Identify your CC1101 module

There are two common versions of the 8-pin CC1101 module. Check pin 2:

  • GREEN module (E07): Pin 2 = VCC (3.3V)
  • BLUE module (Standard): Pin 2 = GND

Wiring for GREEN module (E07) — most common on AliExpress

CC1101 Pin Name ESP32 GPIO Function
1 GND GND Ground
2 VCC 3V3 3.3V Power
3 GDO0 GPIO 4 RX/TX (RF data)
4 CSN GPIO 5 SPI Chip Select
5 SCK GPIO 18 SPI Clock
6 MOSI GPIO 23 SPI MOSI
7 MISO GPIO 19 SPI MISO
8 GDO2 (not used) -

Wiring for BLUE module (Standard)

CC1101 Pin Name ESP32 GPIO Function
1 VCC 3V3 3.3V Power
2 GND GND Ground
3 MOSI GPIO 23 SPI MOSI
4 SCLK GPIO 18 SPI Clock
5 MISO GPIO 19 SPI MISO
6 GDO2 (not used) -
7 GDO0 GPIO 4 RX/TX
8 CSN GPIO 5 SPI Chip Select

:warning: Warning: The CC1101 runs on 3.3V only. Do NOT connect it to 5V or you’ll fry it.


Step 1: Sniff your remote codes

Before you can transmit, you need to capture the RF codes from each Dooya remote. Each remote has a unique ID (24-bit) that is paired to the blind motor.

ESPHome RX config (sniffer)

:warning: Critical: Reception only works when remote_receiver is used alone, without remote_transmitter in the same config. This is a limitation of sharing GPIO4 between RX and TX.

substitutions:
  device_name: esp32-rf433-sniffer
  friendly_name: "RF433 Sniffer"

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: -100
    then:
      - lambda: id(mycc1101).recv();

esp32:
  board: esp32dev
  framework:
    type: esp-idf

external_components:
  - source: github://juanboro/esphome-radiolib-cc1101@main
    components: [ radiolib_cc1101 ]

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  fast_connect: true
  ap:
    ssid: "${device_name}-fallback"
    password: "12345678"

captive_portal:

logger:
  level: DEBUG

api:

ota:
  - platform: esphome
    password: "1234"

web_server:
  port: 80

spi:
  id: spi_bus
  type: single
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

radiolib_cc1101:
  id: mycc1101
  cs_pin: 5
  rx_pin:
    number: 4
    allow_other_uses: true
  frequency: 433.9205
  filter: 468khz
  bitrate: 5

# Receiver ONLY - NO remote_transmitter!
remote_receiver:
  id: rf_receiver
  pin:
    number: 4
    allow_other_uses: true
  dump: all

How to sniff

  1. Flash this config to the ESP32
  2. Open the ESPHome logs (via dashboard or esphome logs)
  3. Press a button on your DC90 remote (Up, Stop, or Down)
  4. Look for a log line like:
Received Dooya: id=0x00D1C917, channel=5, button=1, check=1
  1. Write down id, channel, button, and check for each button
  2. Repeat for each remote

Dooya protocol

  • id: 24-bit, unique per remote (paired to the motor)
  • channel: usually 5 for single-channel remotes
  • button: 1 = Up, 3 = Down, 5 = Stop
  • check: usually equals button value

Step 2: Configure TX (transmitter)

Once you’ve captured all codes, switch to the TX config. Remove remote_receiver and add remote_transmitter instead.

ESPHome TX config

substitutions:
  device_name: esp32-blinds-rf433
  friendly_name: "Dooya Blinds RF433"

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}

esp32:
  board: esp32dev
  framework:
    type: esp-idf

external_components:
  - source: github://juanboro/esphome-radiolib-cc1101@main
    components: [ radiolib_cc1101 ]

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  fast_connect: true
  ap:
    ssid: "${device_name}-fallback"
    password: "12345678"

captive_portal:

logger:
  level: DEBUG

api:

ota:
  - platform: esphome
    password: "1234"

web_server:
  port: 80

# SPI config for CC1101
spi:
  id: spi_bus
  type: single
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

# RadioLib CC1101 - Dooya settings
radiolib_cc1101:
  id: mycc1101
  cs_pin: 5
  rx_pin:
    number: 4
    allow_other_uses: true
  frequency: 433.9205
  filter: 468khz
  bitrate: 5

# RF transmitter on GDO0 (GPIO4)
remote_transmitter:
  - id: rf_transmitter
    pin:
      number: 4
      allow_other_uses: true
    carrier_duty_percent: 100%
    non_blocking: false
    on_transmit:
      then:
        - lambda: id(mycc1101).xmit();
    on_complete:
      then:
        - lambda: id(mycc1101).recv();

# === Buttons per blind ===
# Duplicate this block for each blind, changing the name and ID
button:
  # --- Living Room Blind ---
  - platform: template
    name: "Living Room Blind - Up"
    id: blind_living_up
    icon: "mdi:arrow-up-bold-box"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917        # <-- Replace with YOUR remote's ID
          channel: 5
          button: 1
          check: 1
          repeat: { times: 5, wait_time: 1ms }

  - platform: template
    name: "Living Room Blind - Stop"
    id: blind_living_stop
    icon: "mdi:stop-circle"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917
          channel: 5
          button: 5
          check: 5
          repeat: { times: 5, wait_time: 1ms }

  - platform: template
    name: "Living Room Blind - Down"
    id: blind_living_down
    icon: "mdi:arrow-down-bold-box"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917
          channel: 5
          button: 3
          check: 3
          repeat: { times: 5, wait_time: 1ms }

Key points

  • repeat: { times: 5, wait_time: 1ms } sends the code 5 times for reliable motor reception
  • The on_transmit / on_complete lambdas switch the CC1101 between TX mode and idle — this is mandatory
  • ESP-IDF framework required: the Arduino framework does not work correctly for TX with the CC1101

Step 3: Home Assistant integration

Buttons automatically appear in HA via the ESPHome integration. You’ll get 3 buttons per blind: Up, Stop, Down.

Create a Cover entity (optional)

For a proper blind entity in HA with up/down/stop controls, create a template cover in your configuration.yaml:

cover:
  - platform: template
    covers:
      blind_living_room:
        device_class: shutter
        friendly_name: "Living Room Blind"
        open_cover:
          - action: button.press
            target:
              entity_id: button.living_room_blind_up
        close_cover:
          - action: button.press
            target:
              entity_id: button.living_room_blind_down
        stop_cover:
          - action: button.press
            target:
              entity_id: button.living_room_blind_stop

Note: Without RF state feedback, the blind position is unknown. The cover will always show “unknown” state but commands work fine.


Enclosure (final installation)

Once everything works on the breadboard:

  1. Solder wires directly onto the ESP32 pins (component side)
  2. Cut the protruding pins on the back with flush cutters (or bend them flat if you want to keep the breadboard option)
  3. Mount the ESP32 and CC1101 inside the enclosure with 3M 9448A double-sided tape
  4. Drill a hole for the USB cable (PG7 cable gland = clean and solid)
  5. The CC1101 antenna can stay inside if the enclosure is plastic (RF signal passes through)

Placement

Place the enclosure centrally relative to your blinds for optimal range. The CC1101 has a range of ~30-50m indoors, more than enough for a house.


Gotchas & tips

:cross_mark: RX + TX together = doesn’t work

remote_receiver and remote_transmitter share the same GPIO4. When both are in the same config, reception stops working, even when not transmitting. Solution: two separate YAML files — one for sniffing, one for transmitting.

:cross_mark: Arduino framework = unstable TX

With the Arduino framework, TX freezes after a few transmissions. Switch to ESP-IDF, it’s the only stable config.

:white_check_mark: Dooya frequency = 433.9205 MHz

This is NOT standard 433.92 MHz. The exact frequency is 433.9205 MHz. If your blinds don’t respond, check this setting.

:white_check_mark: allow_other_uses: true is mandatory

GPIO4 is used by both radiolib_cc1101 (rx_pin) and remote_transmitter (pin). You must add allow_other_uses: true on both declarations, otherwise ESPHome refuses to compile.

:white_check_mark: The repeat matters

Dooya motors need to receive the code multiple times to react. repeat: { times: 5 } is a good balance between reliability and speed.


External component used

  • esphome-radiolib-cc1101 by juanboro
  • Based on the RadioLib library, enables CC1101 usage in OOK direct mode with ESPHome
  • The Dooya protocol is natively supported by ESPHome (remote_transmitter.transmit_dooya)

Conclusion

This works flawlessly for my setup of 8 Dooya blinds. The CC1101 is far more reliable than a basic 433 MHz transmitter because it allows precise frequency tuning (433.9205 MHz vs generic 433.92).

Total cost is ridiculously low (~$10) compared to proprietary Zigbee/WiFi solutions, and everything stays 100% local via Home Assistant.

Feel free to ask if you have any questions! :slightly_smiling_face:


Tested with: ESPHome 2025.x / ESP-IDF 5.x / ESP32 DevKit v1 / CC1101 E07 green module / Home Assistant 2025.x+

Excellent write up. Thanks for sharing.
I have a project for this. Great guide !

Hello, it is a very good guide, I used it on an ESP32-S3-WROOM1 N16R8 - it needs special yaml configuration - and it works. :wink:

Now I just opened closed and stopped a shutter, and my goal is I want control a BIDI Dooya motor… I will write about the project.

Awesome write-up!
I tried to dot his for my motionblinds CMD-02-P but with no luck. I don’t have a remote, so I tried to send different pairing codes, but with no luck.

The user manual in my blinds states some CM-11 type remotes, and I suspect they might differ from yours.

If anyone finds luck pairing with their CM remote or have any tips. Please share.

Very nice writeup! With HA supporting more and more RF i decided to try to get my awning into HA and I will follow your guide :slight_smile:

One thing i thougth off, ESP32 supports "cover" entity, so why not use that instead of three different buttons?

Cover has a "Time based cover" where you specify the duration to open/close, I guess that can guestimate how open/closed it is, right?

Shame HA doesnt natively support time based cover, but I guess we can hack together something if we really wanted to

Edit 1:
When i get to do this I will try/test with time based cover, can report back what my findings are :slight_smile:

Filthy AI generation of Cover instead of buttons, with 15 seconds open/close duration:

cover:
  - platform: time_based
    name: "Living Room Blind"
    id: living_room_blind
    device_class: blind

    open_duration: 15s
    close_duration: 15s

    open_action:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917
          channel: 5
          button: 1
          check: 1
          repeat:
            times: 5
            wait_time: 1ms

    close_action:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917
          channel: 5
          button: 3
          check: 3
          repeat:
            times: 5
            wait_time: 1ms

    stop_action:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x00D1C917
          channel: 5
          button: 5
          check: 5
          repeat:
            times: 5
            wait_time: 1ms

    assumed_state: true

Edit 2: I got it working once but then suddenly stopped responding, both to the sniffing and the transmitting, might be loose solder, will try with dupont-cables, hopefully not friend anything ...

As for the Cover:
time-based works but UX is better without it, with just a blind cover... as the timings is very guestimated

Edit 3:
I changed my wires to dupont-wires and this config here seems to work perfectly fine.
Note: I had chatGPT help me with some small tweaks (delay on_boot, advanced-tag in esp32.framework, baud_rate: 0 in logger, 2ms wait_time)

Anyhow - THANK YOU for your writeup! saved me A LOT of time.
Leaving a few tags for anyone google´ing a solution: awning / markise / terrassemarkise / Dooya DC1600 ( Dooya DC 1600 ) remote fjernkontroll

esphome:
  name: esphome-web-6f75d0
  friendly_name: esp32_RF433
  min_version: 2025.11.0
  name_add_mac_suffix: false
  on_boot:
    priority: -100
    then:
      - delay: 500ms
      - lambda: |-
          id(mycc1101).recv();

substitutions:
  device_name: esphome-web-6f75d0
  friendly_name: "esp32_RF433"

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    advanced:
      minimum_chip_revision: 3.1
      sram1_as_iram: true
preferences:
  flash_write_interval: 1min

external_components:
  - source: github://juanboro/esphome-radiolib-cc1101@main
    components: [ radiolib_cc1101 ]

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  fast_connect: true
  ap:
    ssid: "${device_name}-fallback"
    password: "12345678"

captive_portal:

logger:
  level: DEBUG
  baud_rate: 0

  
api:

ota:
  - platform: esphome
    password: "1234"

web_server:
  port: 80

spi:
  id: spi_bus
  type: single
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

radiolib_cc1101:
  id: mycc1101
  cs_pin: 5
  rx_pin:
    number: 4
    allow_other_uses: true
  frequency: 433.9205
  filter: 468khz
  bitrate: 5

# Receiver ONLY - NO remote_transmitter!
#remote_receiver:
#  id: rf_receiver
#  pin:
#    number: 4
#    allow_other_uses: true
#  dump: all

# RF transmitter on GDO0 (GPIO4)
remote_transmitter:
  - id: rf_transmitter
    pin:
      number: 4
      allow_other_uses: true
    carrier_duty_percent: 100%
    non_blocking: true
    on_transmit:
      then:
        - lambda: id(mycc1101).xmit();
    on_complete:
      then:
        - delay: 10ms
        - lambda: |-
            id(mycc1101).recv();


button:
  - platform: template
    name: "Markise - Opp"
    id: terrassemarkise_esp32_opp
    icon: "mdi:arrow-up-bold-box"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x006B45A2
          channel: 161
          button: 1
          check: 1
          repeat: { times: 5, wait_time: 2ms }

  - platform: template
    name: "Markise - Stop"
    id: terrassemarkise_esp32_stop
    icon: "mdi:stop-circle"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x006B45A2
          channel: 161
          button: 5
          check: 5
          repeat: { times: 5, wait_time: 2ms }

  - platform: template
    name: "Markise - Ned"
    id: terrassemarkise_esp32_ned
    icon: "mdi:arrow-down-bold-box"
    on_press:
      - remote_transmitter.transmit_dooya:
          transmitter_id: rf_transmitter
          id: 0x006B45A2
          channel: 161
          button: 3
          check: 3
          repeat: { times: 5, wait_time: 2ms }

  - platform: template
    name: "Reset CC1101"

    on_press:
      then:
        - delay: 200ms
        - lambda: |-
            id(mycc1101).recv();

This is brilliant work. I got the RFX433 to control my Dooya blinds about a year ago, and I'm happy with it, works very well. However this CC1101 option is a fraction of the cost, love seeing stuff like this.

Hi,
Stumbled on this thread after hitting a similar problem with a Naterial-branded awning (same Dooya 433 MHz protocol under the hood). Took a slightly different angle that turned out to be complementary to yours with bi-directional state sync with original remote, so sharing in case it helps the next person:

Repo: GitHub - cpecorari/esp32-naterial-awning: ESP32 + CC1101 controller for Naterial - Dooya 433.88 MHz awning motors. Full TX from HA + RX state sync from the physical remote. · GitHub

Different choices in our build:

  • Single YAML with remote_transmitter + remote_receiver on separate pins (GPIO12 / GPIO27 → GDO0 / GDO2), no allow_other_uses gymnastics
  • Mainline cc1101 component (merged into ESPHome core some time around 2026.1), not radiolib_cc1101
  • Arduino framework — no TX freezes for us so far, but yours is the safer bet if ESP-IDF works for you
  • Raw frame capture + replay (no need to decode the Dooya structured fields — just dump what the remote sends and store it)
  • Bidirectional state sync: when the physical remote is pressed, the cover state in HA updates automatically. The catch is breaking the echo loop (calling cover.open from binary_sensor.on_press would re-fire open_action which would re-transmit). Solved with a rx_trigger_ms timestamp global and a 3 s guard in each cover action.

The REVERSE_ENGINEERING.md in the repo documents 10 gotchas we hit. Two are worth flagging here because they cost real time:

  1. ESPHome CLI versions 2025.x and 2026.1.x – 2026.3.x silently compile a non-transmitting binary — same YAML, same chip, no RF emitted, no errors in logs. Building via the ESPHome dashboard add-on inside HA at >= 2026.4.5 works. We never isolated the exact regression but it's reproducible.
  2. Naterial-branded remotes send two distinct RF frames per direction button press (probably a header + command pattern). One binary_sensor only catches one of the two, so you need two matchers per direction button (and only one for STOP).

Massive thanks for the original write-up — it was the closest thing to a starting point when we landed on this problem.

-- Massively coded and debugged with Claude :slight_smile:

Hey, sorry for the late reply, just saw this. Bad news I’m afraid: I don’t think this approach will work for your motor. The CMD-02-P is a Motionblinds motor (Coulisse), and those use a bidirectional 433 MHz protocol, not the old one-way Dooya protocol my DC90 remotes speak. So even if you brute-force pairing codes, the frames transmit_dooya sends are just not what your motor is listening for. The CM-11 remotes from your manual are part of that same Motionblinds ecosystem.

Your best options are probably the official Motionblinds integration in HA with their WiFi bridge (works well from what I’ve read), or getting hold of an actual CM-series remote to pair the motor first, though that still won’t make it talk to a CC1101 with my config.

Happy to be proven wrong if someone has decoded the Motionblinds RF protocol, but I haven’t seen it done yet.

This is great stuff, thanks for sharing! The bidirectional sync is the part I never got around to. I do something very similar on another RF gateway (echo-guard with a timestamp global, same idea with a few seconds of grace) and it really is the trick that makes RX+TX on one device livable.

The CLI vs dashboard compile regression is a nasty one, good catch. Silent failures like that can eat an entire weekend.

I’ll add a link to your repo in the OP since the raw capture + state sync angle complements this tutorial nicely.

Glad it saved you some time! And thanks for reporting back on the time-based cover, that matches my experience: it works, but the guesstimated position ends up more annoying than three honest buttons. Nice touch leaving the search tags too.