[Custom integration] Dooya RF Covers — RF433 blinds and covers via ESPHome + CC1101

:round_pushpin: News and support live on GitHub

I maintain this integration alone and unpaid, so everything is tracked in one place: report a bug or ask a question.

New versions no longer need announcing here: HACS offers you the update, release notes included. Otherwise, the RSS feed https://github.com/dasimon135/ha-dooya/releases.atom, or Watch → Releases on the repository.

This thread stays open for user-to-user help. The README is the reference and stays current — this post does not.

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.

Update, October 2026: the original config below no longer compiles. It uses the external radiolib_cc1101 component, which pulls RadioLib without a pinned version, and RadioLib no longer builds against the ESP-IDF that current ESPHome ships:

fatal error: driver/ledc.h: No such file or directory

A node you already flashed keeps working; the error only appears the next time you compile it. For a new build, or before your next update, use the built-in cc1101 config in Updated wiring and config (2026) near the end. It needs no external component, and with TX and RX on separate pins a single YAML does both jobs.

  • If you want a real cover entity with a position instead of three buttons, there is now an integration for that: see Going further at the end.

The original radiolib_cc1101 config is kept below for reference only.


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

:warning: The PCB colour is not a reliable indicator — some blue boards carry the green pinout and the other way round, depending on the manufacturer. The only dependable test is to look at pin 2 on your own module:

  • Pin 2 = VCC → use Pinout A below
  • Pin 2 = GND → use Pinout B below

Pinout A — pin 2 = VCC (often the green E07, but not always)

CC1101 Pin Name ESP32 GPIO Function
1 GND GND Ground
2 VCC 3V3 3.3V Power
3 GDO0 GPIO 4 RF data — TX on the unified node, RX on the sniffer
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 GPIO 16 RF data in — unified node only

Pinout B — pin 2 = GND

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 GPIO 16 RF data in — unified node only
7 GDO0 GPIO 4 RF data — TX on the unified node, RX on the sniffer
8 CSN GPIO 5 SPI Chip Select

The GDO2 wire is only needed for the unified node (esphome/dooya-node.yaml), which transmits on GDO0 and receives on GDO2. The original sniffer further down this post receives on GDO0/GPIO 4 and leaves GDO2 unconnected — if that is the one you flashed, skip that wire. Wiring the unified node from a table that says not used gives you a node that transmits perfectly and hears nothing.

: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 with radiolib_cc1101: the Arduino framework does not work correctly for TX in this setup (see the gotchas at the end for the nuance)

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.

This is still true of a bare ESPHome cover: the Dooya protocol used here is one-way, so the motor never tells you where it is. It can be worked around by timing the travel rather than measuring it — @Haningback tried ESPHome’s time_based cover in post #5 and concluded the guessed position was more annoying than three honest buttons, which matches my own experience. The integration linked in Going further takes the same idea further: it measures your real opening and closing times with a calibration assistant instead of having you guess them, and it resyncs when it hears the physical remote.


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

:warning: RX + TX together — depends on your wiring

With the single-pin radiolib_cc1101 setup described above, remote_receiver and remote_transmitter share GPIO4, and reception stops working as soon as both are declared, even when not transmitting. Two separate YAML files — one for sniffing, one for transmitting — is the workaround for that setup.

This is not a hardware limitation. Wire GDO0 and GDO2 to two different GPIOs and TX and RX coexist in a single YAML, with no allow_other_uses gymnastics at all. @cpecorari reported this in post #7 with GPIO12/GPIO27, and it is what I run myself now (GPIO4 for TX, GPIO16 for RX) with the mainline cc1101 component.

:warning: Arduino framework = unstable TX (with radiolib_cc1101)

With radiolib_cc1101 on the Arduino framework, my TX froze after a few transmissions. Switching to ESP-IDF fixed it, and ESP-IDF is still what I recommend. @cpecorari (post #7) runs Arduino with the mainline cc1101 component and reports no freezes, so this looks specific to the external component rather than to Arduino itself.

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

Note: this offset was needed with radiolib_cc1101. With the mainline cc1101 component I set a plain 433.92MHz and everything responds. Either way, if your blinds don’t react this is still the first setting to nudge.

:white_check_mark: allow_other_uses: true is mandatory (single-pin setup)

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. This disappears entirely if you put RX and TX on separate pins.

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

:warning: A silent non-transmitting build (reported, not reproduced by me)

@cpecorari reports in post #7 that the ESPHome CLI on versions 2025.x and 2026.1.x – 2026.3.x compiles a binary that emits nothing at all: same YAML, same chip, no RF, no error in the logs. Building through the ESPHome dashboard add-on inside Home Assistant on ≥ 2026.4.5 works. The exact regression was never isolated. I have not hit this myself, but if you are convinced your config is right and nothing comes out of the antenna, try the dashboard before you re-solder anything.

:warning: Some remotes send two frames per button

Still from post #7: Naterial-branded remotes send two distinct RF frames for each direction button (header + command, most likely). A single binary_sensor matcher only catches one of the two, so you need two matchers per direction button — and only one for STOP.


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)

Since ESPHome 2025.12 this external component is optional: a cc1101 component is now part of ESPHome core. See the updated config below.


Updated wiring and config (2026)

Since ESPHome 2025.12 there is a native cc1101 component in ESPHome core. It replaces the external radiolib_cc1101 component, and because you no longer share one GPIO between RX and TX, a single YAML does both jobs.

Wiring changes: keep SPI as described above (CS → GPIO5, SCK → GPIO18, MOSI → GPIO23, MISO → GPIO19), then wire GDO0 → GPIO4 for transmission and GDO2 → GPIO16 for reception. Pin 8 of the module is the one you now need — the wiring tables near the top give it as GDO2 → GPIO 16 for exactly this reason.

A complete, commented node config using this — TX + RX, permanent listening, one node per RF zone — is here:

RF range tip while you are at it: replace the module’s coil antenna with a straight 17.3 cm wire (a 433 MHz quarter wave), and keep the node high up and away from reinforced concrete. It costs nothing and it was the single biggest reliability improvement I made.


Going further

Two projects grew out of this thread, and which one you want depends on what you need.

Raw capture and replay, with state sync from the physical remote — @cpecorari’s build for a Naterial-branded (Dooya 433.88 MHz) awning. It captures and replays raw frames instead of decoding the Dooya fields, and updates the cover state in Home Assistant when someone presses the physical remote. The REVERSE_ENGINEERING.md in the repo documents ten gotchas they hit, several of which apply well beyond Naterial:

A cover entity with a position, instead of three buttons — ha-dooya, a custom integration that turns the node above into one proper cover entity per blind, with a position estimated from your measured travel times.


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–2026.x / ESP-IDF 5.x / ESP32 DevKit v1 / CC1101 E07 green module / Home Assistant 2025.x+
Also reported working on ESP32-S3-WROOM1 N16R8 by @tzsolt (post #3).

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.

Following up on my own tutorial, because the thread kept circling back to the
same limitation and I ended up doing something about it.

The write-up at the top gives you three buttons per blind and no idea where the
blind actually is. @Haningback tried ESPHome’s time_based cover in post #5 and
concluded the guessed position was more annoying than three honest buttons —
which matched my experience exactly, and is why the original post says the state
will always show “unknown”.

So I wrote a custom integration for it: ha-dooya.

https://github.com/dasimon135/ha-dooya

What you get

One proper cover entity per blind, instead of three buttons:

  • Open / close / stop, plus set position — partial opening from the
    Home Assistant UI, from scripts, from voice
  • Estimated position from your real travel times. There’s a calibration
    assistant that measures your opening and closing times with a stopwatch
    rather than asking you to guess them, which is the part that makes the
    difference against a hand-tuned time_based cover
  • Resync when the physical remote is used — the node listens permanently,
    and a press on your DC90 updates the position in Home Assistant. Same
    echo-guard idea @cpecorari described in post #7
  • Recalibration as buttons on the device page (mark as open, closed, or set
    a known position), for when the estimate has drifted
  • Favorite position — per-blind, one button press, like the real remotes
  • Broadcast channel 0 — one entity that opens or closes every blind paired
    with the remote in a single RF frame
  • Automatic detection: press UP on your physical remote and it reads the
    blind’s ID for you. Manual entry too, if you already know it
  • Entities go unavailable when the ESPHome node is offline, instead of
    silently pretending to work
  • A Lovelace card is bundled (custom:dooya-cover-card) — animated shutter
    with position, presets and recalibration. Nothing extra to install
  • Works with the OEM rebrands too: Dooya, Cherub, Raex, Zemismart and the other
    clones on the same protocol

Being clear about what it is not: the protocol is still one-way, so the
position remains an estimate, and the entity is assumed_state. Nothing here
reads the motor’s real position, because the motor never tells anyone.

What you need to change on the ESP32

Your tutorial YAML will not work as-is — this is the part to read before you
install anything.

The integration doesn’t press your template buttons. It calls a native ESPHome
API service named transmit_dooya, and it listens for an
esphome.dooya_received event coming back from a remote_receiver with
dump: dooya. So you need:

  • an ESPHome node exposing an api: service named transmit_dooya
  • “Allow the device to perform Home Assistant actions” enabled in the
    ESPHome integration options — labelled Allow service calls on older Home
    Assistant versions. Without it the received events are dropped silently,
    which is a fun half hour
  • ideally the mainline cc1101 component with TX and RX on separate pins, so
    one node both transmits and listens

A complete commented node config is in the repo, and it’s the one I run:
https://github.com/dasimon135/ha-dooya/blob/a82df4cd6bc05784e56af7b687cf20f08b4c589a/esphome/dooya-node.yaml

One node per RF zone if you have reinforced concrete walls — assign each blind
to its nearest node in the integration’s options.

Requires Home Assistant 2026.5+.

Installing

It’s not in the HACS default store yet — the inclusion request is still pending
on their side. Until then:

  1. HACS → three-dot menu → Custom repositories
  2. Add https://github.com/dasimon135/ha-dooya as an Integration
  3. Install “Dooya RF Covers”, restart
  4. Settings → Devices & Services → Add integration → Dooya RF Covers

Current version is v0.8.0, which came out of a deliberate bug hunt — four real
bugs found and fixed, and the test suite roughly doubled to cover the parts
that had none. There’s a diagnostics
download on the device page; attaching it to an issue makes bugs enormously
easier to chase, and every bug worth fixing in this project so far was reported
by someone who had no idea what was wrong and just pasted the error.

Happy to answer questions here or in the repo’s issues.

Hey, sorry, this one slipped past me for far too long.

Two separate things in your message, so let me take them one at a time.

The ESP32-S3. Great to know it works there, that comes up regularly. You
mentioned it needs a special YAML configuration — could you paste yours? Even
just the esp32: and pin blocks would help. I only ever tested this on a plain
DevKit v1, so I have nothing useful to say to the next person who shows up with
an S3, and right now your post is the only evidence in the thread that it works
at all. I’ll link it from the original post once it’s here.

The BIDI motor. This is where I’d rather be honest than encouraging: I don’t
think you’ll get there with this setup. The one-way Dooya protocol my DC90
remotes speak — the one transmit_dooya encodes — is not what a bidirectional
motor is listening for. It expects a two-way exchange with acknowledgements, so
brute-forcing pairing codes with a one-way transmitter won’t get a response.

It’s very likely the same wall @kamerat hit in post #4 with a Motionblinds
CMD-02-P (my answer is in post #8). As far as I know the Motionblinds hardware
is Dooya-built, which would make your BIDI motor and their CMD-02-P the same
family — same protocol, same problem. If that’s right, the realistic path is the
official Motionblinds integration with their WiFi bridge, which people report
working well, rather than a CC1101.

If you do want to poke at it anyway, one thing has improved since March: the
cc1101 component is now in ESPHome core (since 2025.12,
https://esphome.io/components/cc1101.html) and it has a packet mode with an
on_packet trigger. That gets you raw frames even when no protocol decoder
recognises them, which is the right starting point for reverse engineering
something the dooya dumper can’t read. I’d genuinely like to be proven wrong
here — nobody seems to have decoded that protocol publicly yet, and if you get
anywhere I’ll link it from the original post.

Hi all,

quite new to HA and ESPHome and I stumbled onto this great tutorial since I planned to do the same with some Dooya blinds.

What gets me a bit confused from the first post is the evolution from separate rx/tx to the combined version, where I cannot find for example where to set the sniffed remote id when transmitting. I tried starting with the combined config but cannot quite make sense of all of it so I will probably first try to get sniffing working.

Wiring wise I should be up and running but in the meanwhile it’d be great if you could clarify where the remote id goes into the unified config.

Thanks!

Welcome, and good question — the answer is that the remote ID doesn’t go into the
node config at all any more. That’s the bit that changed between the two
versions, and I never spelled it out.

In the old TX-only config, each template button hardcoded one blind, so the ID
had to live in the YAML. The unified node doesn’t do that. It exposes a single
API service whose ID is a runtime parameter:

api:
  services:
    - service: transmit_dooya
      variables:
        dooya_id: int   # 24-bit shutter identifier
        channel: int    # 0 = broadcast
        btn: int        # 1=up, 3=down, 5=stop
        check: int

So the node knows nothing about your blinds; the caller supplies the ID on every
call. That caller is either the ha-dooya integration (it stores the ID per cover
— and it can learn it for you: press UP on your physical remote during setup and
it reads the ID off the air), or a plain Home Assistant action calling
esphome.<your node name>_transmit_dooya with the four values in data:.

One thing that will save you a step: you don’t need a separate sniffer build.
The same node listens permanently — the remote_receiver has dump: - dooya
and an on_dooya: handler that logs

received id=00D1C917 ch=0 btn=1 chk=1

for every frame it hears. So flash the unified config once, open the ESPHome
logs, press a button on your DC90, and read your ID straight off. Capture and
transmit from one build.

Two traps worth knowing before you get there:

  • In the ESPHome integration’s options, “Allow the device to perform Home
    Assistant actions” (labelled “Allow service calls” on older versions) must be
    enabled, or the received events are dropped silently. That one costs people
    half an hour.
  • TX and RX want separate pins on the CC1101.

And if you’d rather stay with plain ESPHome buttons instead of the integration,
that’s still perfectly valid — keep the tutorial’s template buttons with a
literal id: and just add the receiver block for sniffing.

Thanks for the fast reply and the clarification, it makes much more sense now. I’ll probably start with the ha-dooya integration then given that it should be pretty much plug and play.

I have tried sniffing but so far any button press is not recorded on the ESPHome logs: I will review again the wiring but I suspect it’s correct (3.3V/GND should be fine as well according to the scheme found on the AliExpress item page), but still…

Again kudos for the work!

The wiring tables in the first post are wrong for the config I pointed you at. Both of them list GDO2 as “(not used)” — pin 8 in the green-module table, pin 6 in the blue one. That was true of the original sniffer in the tutorial, which receives on GDO0/GPIO4 and genuinely does not need GDO2. It is not true of the unified node: that one transmits on GDO0 → GPIO4 and receives on GDO2 → GPIO16. If you wired from the table and flashed the unified node, the receiver is listening on a pin with nothing attached to it, which is exactly the silence you are describing. I will fix both tables.

So the first question is which one you flashed:

  • Unified node — it has a cc1101: block and a remote_receiver: with pin: number: 16. Then run one wire from module GDO2 to GPIO16 and try again. That is the whole fix.
  • The tutorial’s sniffer — it has radiolib_cc1101: with rx_pin: number: 4, and remote_receiver also on number: 4, with dump: all. There GDO2 really is unused, so something else is wrong and the next point is where I would look.

Second, and this is the one I would check before anything else: do not trust the pinout drawing on the AliExpress listing. There are two incompatible pinouts on these 8-pin CC1101 boards, and the PCB colour does not reliably tell you which you have — some blue boards carry the green pinout and the other way round. The only dependable test is pin 2 on the board itself: pin 2 = VCC means the first table, pin 2 = GND means the second. Get those two crossed and the module is either unpowered or shorted across the supply, and you get precisely this: a device that boots, logs happily, and hears nothing. (The first post says “check pin 2” but still leads with the colour — my fault again, the French version of this tutorial has the caveat and this one does not. I will port it over.)

To separate “the radio is deaf” from “the radio is fine but the frames are not Dooya”, put dump: all on the remote_receiver temporarily and press any 433 MHz remote you own, not just the blind one:

  • nothing at all in the log → the radio is not receiving. Wiring, power or SPI. Recheck pin 2 first, then CS/SCK/MOSI/MISO on GPIO 5/18/23/19.
  • raw lines appear but no received id=… → the radio is fine and it is a decoding question, which is a much better place to be. Paste a raw dump here and we will read it together.

One thing that is not your problem, so you do not go hunting for it: the “Allow the device to perform Home Assistant actions” setting I mentioned last time only drops events on the way from ESPHome to Home Assistant. You are missing them in the ESPHome log itself, upstream of that.

Two updates, and one retraction.

The tables are fixed. Both wiring tables in the first post now give GDO2 → GPIO 16 instead of (not used), with a note saying the wire is for the unified node only. You were the one who found it, by wiring exactly what the table said.

And I owe you a correction on the other trap I listed. In my reply above I told you that “Allow the device to perform Home Assistant actions” must be enabled or the received events are dropped silently. That is wrong, and my follow-up only half-took it back by saying it drops them “on the way from ESPHome to Home Assistant”. It does not drop them there either.

I checked it in the Home Assistant source rather than from memory — homeassistant/components/esphome/manager.py, verified against 2026.8.3. The option gates the opposite direction: a device calling into Home Assistant’s own services. For a bus event the if service.is_event: branch fires before that check is ever reached, so esphome.dooya_received arrives whatever the option is set to. Node-exposed actions like transmit_dooya are not gated by it either — they are registered unconditionally.

So: leave that option wherever it is, it has no bearing on any of this. If frames do not arrive, the node’s own ESPHome log is the place to look, because a decoded frame is printed there before it is published — which tells you which of the two halves is silent. That correction is in the README and in the integration’s own repair text since 7 September; the forum posts were the last place still carrying it.

Sorry for the half-hour that one probably cost you on top of the GDO2 wire.

Hi again,

extremely thankful for all the clarifications and the help in debugging this.
IIRC in the updated section of the first post both TX and RX pins were already mentioned, in any case I was already aware of this from a slightly related LLM chat on CC1101 + ESP32 integration, so I had them both wired.

Now, whether they are wired correctly I’m not sure: as I mentioned, I am relying on AliExpress item’s page diagram, since the PCB as such carries not much info: the only indications are ‘1/2’ and ‘7/8’ on the left and right side of the pinout (no specific mention of GND).

A caveat on your suggestions: I could not set

dump:
  - all

alone or along with dooya, since allegedly it’s not recognised by ESPHome builder (at least in the unified config with the native component).

I have set the general verbosity to DEBUG and I can see this in the logs:

[23:09:35.099][C][remote_receiver.esp32:120]: Remote Receiver:
[23:09:35.099][C][remote_receiver.esp32:120]:   Clock resolution: 1000000 hz
[23:09:35.099][C][remote_receiver.esp32:120]:   RMT symbols: 192
[23:09:35.099][C][remote_receiver.esp32:120]:   Filter symbols: 0
[23:09:35.099][C][remote_receiver.esp32:120]:   Receive symbols: 192
[23:09:35.099][C][remote_receiver.esp32:120]:   Tolerance: 25%
[23:09:35.099][C][remote_receiver.esp32:120]:   Carrier frequency: 0 hz
[23:09:35.099][C][remote_receiver.esp32:120]:   Carrier duty: 100%
[23:09:35.099][C][remote_receiver.esp32:120]:   Filter out pulses shorter than: 50 us
[23:09:35.099][C][remote_receiver.esp32:120]:   Signal is done after 10000 us of no changes
[23:09:35.100][C][remote_receiver.esp32:131]:   Pin: GPIO16
[23:09:35.100][C][remote_transmitter:060]: Remote Transmitter:
[23:09:35.101][C][remote_transmitter:061]:   Clock resolution: 1000000 hz
[23:09:35.101][C][remote_transmitter:061]:   RMT symbols: 64
[23:09:35.101][C][remote_transmitter:131]:   Pin: GPIO4

Is this rx 0 hz worth double checking? In the builder it’s set at default value of 433.92Mhz.

In any case it still won’t receive anything and to be honest I’m starting to question the pinout: can you suggest a way to determine VCC/GND without shorting or frying the cc1101?

Thanks!

The 0 Hz line is not your radio. It belongs to the ESP32’s receiver: it is the carrier it would strip from an incoming signal, and 0 means none. The 433.92 MHz is set in the cc1101: block, which prints its own Frequency line in the same boot log.

dump: all has to be written as one value on its own line, not as a list item. ESPHome refuses “- all”, and all already includes the Dooya decoder:

remote_receiver:
  id: rf_receiver
  pin:
    number: 16
  dump: all

Keep your on_dooya: block under it as it is.

Before reaching for a multimeter, that boot log can already tell you whether the CC1101 is powered and wired. At startup ESPHome asks the chip for its ID over SPI, and the CC1101: block shows the answer as Chip ID:

  • if the last two digits are 00, or the first two are FF, nothing answered: power, GND, or one of CS/SCK/MOSI/MISO is wrong. You will usually also see “Failed to verify CC1101.”
  • any other value means the chip is powered the right way round and talking, so VCC/GND is settled and the problem is further along

If it did not answer and you want to find GND without powering anything: unplug the module, put the meter on continuity, hold one probe on the board’s ground copper and touch pin 1, then pin 2, with the other. The one that beeps is GND. I don’t have your board, so check that the copper you picked really is the ground plane before trusting the beep.

Could you paste the CC1101: block from the boot log, and whatever shows up with dump: all when you press a button on the remote?

Thanks for the hint on the Chip ID from the log, it looks like indeed the board is powered and negotiating properly:

[17:37:04.529][C][spi:066]: SPI bus:
[17:37:04.533][C][spi:131]:   CLK Pin: GPIO18
[17:37:04.533][C][spi:131]:   SDI Pin: GPIO19
[17:37:04.533][C][spi:131]:   SDO Pin: GPIO23
[17:37:04.538][C][spi:074]:   Using HW SPI: SPI2_HOST
[17:37:04.576][C][cc1101:241]: CC1101:
[17:37:04.576][C][cc1101:241]:   Chip ID: 0x0014
[17:37:04.576][C][cc1101:241]:   Frequency: 433919840 Hz
[17:37:04.576][C][cc1101:241]:   Channel: 0
[17:37:04.576][C][cc1101:241]:   Modulation: ASK/OOK
[17:37:04.576][C][cc1101:241]:   Symbol Rate: 4996 baud
[17:37:04.576][C][cc1101:241]:   Filter Bandwidth: 203125.0 Hz
[17:37:04.576][C][cc1101:241]:   Output Power: 9.9 dBm
[17:37:04.907][C][cc1101:131]:   CS Pin: GPIO5

However, dumping all traffic gets me flooded in a lot of ‘remote: pronto’ and ‘remote: beo4’ lines (plenty per second) and if I push any remote button (e.g. stop) I cannot discern it from any other entry of the log.
Having said that, is it possible that my remote is a newer model/works on a slightly different frequency?

On its back, the label reads:

Model: DD1620H
433.05-434.79 MHz

It’s a fifteen-channel newish remote (from March this year).