DC ceiling fan RF remote RH787T

The following is relevant to anyone who wants to mimic the RF signals from an RH787T ceiling fan remote control unit using an ESP board, an RF transmitter, and Esphome.

Here are examples of the remote:

Brands which have used this remote at some point in time include:

  • Regency
  • Hunter
  • Harbor Breeze
  • Westinghouse
  • Honeywell
  • Mercator

Get the codes for your specific remote by setting up an ESP device with a 433 MHz remote receiver and looking at dumps of rc_switch:

remote_receiver:
  - id: receiver
    dump: rc_switch

You will hopefully see dump lines like the following:

[12:11:06][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='001010101010101000100000'
[12:11:10][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='001010101010101011000000'
[12:11:14][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='001010101010101010011000'

Esphome guesses protocol 6 but this isn’t quite correct. None of Esphome’s preset protocols match this remote so we’ll need to manually configure a protocol. Without further ado, here are the protocol settings appropriate for this RF remote:

button:
  - platform: template
    name: "Button Name"
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101010000000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6

The above protocol settings (including the repeat count) were chosen to match the behaviour of the real remote. This was validated by using a second ESP device as a receiver (with dump set to raw) and comparing what it is seeing from the first ESP device versus the actual remote.

Also for reference, here are the rc_switch codes for my particular remote. I do not know to what extent different remotes have the same or similar codes, but I’m led to believe that units are assigned different prefixes and some units have dip switches to change them. (Mine does not have dip switches.)

'001010101010101000100000' - 0
'001010101010101011000000' - 1
'001010101010101010011000' - 2
'001010101010101001000000' - 3
'001010101010101001010000' - 4
'001010101010101010010100' - 5
'001010101010101010000000' - 6
'001010101010101011010000' - Reverse
'001010101010101011100000' - Set

Hope this helps someone.

BE AWARE that some versions of this remote apparently operate at 303.947 MHz and not 433.92 MHz. If you can’t read anything from the remote with a 433 MHz receiver, this is probably the reason. You can also verify this by opening the remote unit and looking for the resonator. A 433 MHz device will likely have a small silver button component labelled “R433M”.

If you have a 303 MHz remote, life is going to be more difficult for you, because transceivers which can operate at that frequency are not common. The most promising unit is the CC1101 but this isn’t currently supported by Esphome. I’ve had some limited success making a custom component with the library SmartRC-CC1101-Driver-Lib but it’s still a bit janky as a transmitter.

Here is an FCC report for the 303 MHz version. There’s a page showing internal photographs. The circuit board is completely different to mine, with a different arrangement of chips. The plastic case is identical save for colour. But since the circuit differences are substantial, I suspect the RF protocol differences are more than just the resonator frequency.

If you have this remote, please post in here with any attempts, successes or failures you’ve had trying to speak this remote’s language.

4 Likes

Here is this fan expressed as a fully resolved Home Assistant entity in an Esphome package:

rf.yaml

substitutions:
  device_name: 'rf'
  device_comment: "located next to front door"
  entity_prefix: "RF"

packages:
  device: !include _devices/sonoff_rf.yaml
  office-fan: !include _entity/office-fan.yaml
  core: !include _config/core.yaml

office-fan.yaml

output:
  # This does nothing.
  # Required because the fan "speed" component demands a float output
  - platform: template
    id: fanoutput
    type: float
    write_action:
      - lambda: ""

fan:
  - platform: speed
    output: fanoutput
    id: office_fan
    name: "Office Ceiling Fan"
    speed_count: 6
    on_turn_off:
      - lambda: |-
          id(fan_0).press();
    on_speed_set:
      - lambda: |-
          if      (id(office_fan).state == 0) { /* Fan is off, do nothing */ }
          else if (id(office_fan).speed == 1) { id(fan_1).press(); }
          else if (id(office_fan).speed == 2) { id(fan_2).press(); }
          else if (id(office_fan).speed == 3) { id(fan_3).press(); }
          else if (id(office_fan).speed == 4) { id(fan_4).press(); }
          else if (id(office_fan).speed == 5) { id(fan_5).press(); }
          else if (id(office_fan).speed == 6) { id(fan_6).press(); }

button:
  - platform: template
    id: fan_0
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101000100000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_1
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101011000000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_2
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101010011000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_3
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101001000000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_4
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101001010000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_5
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101010010100'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_6
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101010000000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6
  - platform: template
    id: fan_reverse
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '001010101010101011010000'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6

binary_sensor:
  - platform: remote_receiver
    id: fan_0_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 0;
          if (!id(office_fan).state) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_off(); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101000100000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_1_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 1;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101011000000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_2_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 2;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101010011000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_3_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 3;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101001000000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_4_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 4;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101001010000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_5_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 5;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101010010100'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true
  - platform: remote_receiver
    id: fan_6_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 6;
          if (id(office_fan).state && id(office_fan).speed == SETSPEED) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_on(); call.set_speed(SETSPEED); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101010000000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true

sonoff-rf.yaml

substitutions:
  pin_led: GPIO13 #inverted
  pin_tx: GPIO1
  pin_rx: GPIO3

esphome:
  platform: ESP8266
  board: esp01_1m

api:
  services:
    - service: learn
      then:
        - rf_bridge.learn

remote_receiver:
  pin:
    number: 4
  dump: rc_switch
  tolerance: 60%
  filter: 100us
  idle: 4ms

remote_transmitter:
  id: transmitter
  pin: 
    number: 5
  carrier_duty_percent: 100%

status_led:
  pin:
    number: ${pin_led}
    inverted: yes

uart:
  tx_pin: ${pin_tx}
  rx_pin: ${pin_rx}
  baud_rate: 19200

rf_bridge:
  on_code_received:
    then:
      - homeassistant.event:
          event: esphome.rf_code_received
          data:
            sync: !lambda 'char buffer [10];return itoa(data.sync,buffer,16);'
            low: !lambda 'char buffer [10];return itoa(data.low,buffer,16);'
            high: !lambda 'char buffer [10];return itoa(data.high,buffer,16);'
            code: !lambda 'char buffer [10];return itoa(data.code,buffer,16);'

core.yaml

substitutions:
  device_type: "Generic"
  device_comment: ""

esphome:
  name:  ${device_name}
  comment: "${device_type}: ${device_comment}"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true # necessary because it's a hidden ssid

ota:
  password: !secret ota_password

logger:
  baud_rate: 0

api:
  password: !secret hass_password
  reboot_timeout: 0s

sensor:
  - platform: uptime
    name: ${entity_prefix} Node Uptime
    update_interval: 15min
  - platform: wifi_signal
    name: ${entity_prefix} Wifi Signal
    update_interval: 1min
    filters:
      - filter_out: nan
      # exclude spurious readings
      - lambda: |- 
          if (x < -20) return x;
          else return {};
      - or:
        - throttle: 30min
        - delta: 5

binary_sensor:
  - platform: status
    name: ${entity_prefix} Node Status

button:
  - platform: restart
    name: ${entity_prefix} Node Restart
    entity_category: "diagnostic"

Hi there, this is really useful and timely. I have a Lucci DC fan and have done the first stage of dumping the remote codes. I hadn’t got as far as mimicing these with the transmit module.

I’m doing this on an Wemos D1 ESP32 dev board, not a sonoff. I was wondering if you could explain a few things?

What happens if I mimic the codes using protocol 6? Does it not work without your custom protocol settings? These are the codes I read with this yaml:

remote_receiver:
  pin: GPIO16
  tolerance: 50%
  filter: 250us
  idle: 4ms  
  dump: rc_switch
# Lucci 433MHz remote button codes
#
# LIGHT
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'
#
# POWER
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000000'
#
# CHANGE DIRECTION
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000001110'
#
# NATURAL WIND
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000111010'
#
# SPEED 1
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000110'
#
# SPEED 2
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000101'
#
# SPEED 3
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000100'
#
# SPEED 4
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000011'
#
# SPEED 5
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000010'
#
# SPEED 6
# [D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000000001'
#

I’m not sure how you’ve structured this package. Which is the base yaml file, is it rf.yaml? Since I’m not using a Sonoff, I assume I don’t need the api serives rf_bridge learn or the UART section. Are there any other adaptions I would need to make?

Thanks in advance!

You don’t have to follow my personal scheme, but here’s how I do things:

rf.yaml represents the singular physical device you intend to flash. It should be renamed to reflect your own naming scheme.

_config/core.yaml are the common defaults for all my esphome devices. It defines security, networking settings and the home assistant configuration.

_devices/sonoff_rf.yaml is the hardware specification. If you had multiple of the same device, this package would be common to all of them. Or if you’re not using a Sonoff device, you’d use a different file. In order for the fan to work, the hardware specification needs to have the ESP board settings and define a remote_receiver and remote_transmitter.

_entity/office-fan.yaml is everything to do with controlling my specific ceiling fan. The concept of a ceiling fan doesn’t exist in any other yaml file.

1 Like

In my case, I noticed that protocol 6 had the right binary structure but its replay speed was too slow. I was able to determine the pulse speed of 330 by setting up a second esphome receiver that dumps in pure raw (not rc_switch raw) and comparing the raw captures from the remote and from my esphome transmitter.

I actually created a crufty makeshift visualiser to help with the comparison. The format is very simple but doesn’t appear to be formally documented anywhere. I don’t know what the time unit is, I presume it’s microseconds. Positive numbers represent signal high for X microseconds, negative numbers represent signal low for -X microseconds.

1 Like

One more question, in office-fan.yaml what this binary_sensor section for? I’m guessing you are intercepting when the real remote control buttons are pressed and retransmitting them to the fan?

binary_sensor:
  - platform: remote_receiver
    id: fan_0_rcv
    on_press:
      - lambda: |-
          int SETSPEED = 0;
          if (!id(office_fan).state) {
            ESP_LOGI("remote receiver", "====== ignored RF fan %d", SETSPEED);
          }
          else {
            auto call = id(office_fan).turn_off(); call.perform();
          }
    rc_switch_raw:
      code: '001010101010101000100000'
      protocol: 
        pulse_length: 330
        sync: [23,1]
        zero: [1,2]
        one: [2,1]
        inverted: true

etc

Unfortunately I can’t get the fan to respond to transmit commands using this, and pressing the button in the front end in HA.

remote_transmitter:
  id: transmitter
  pin: GPIO17
  carrier_duty_percent: 100%

button:
  - platform: template
    name: Fan light
    id: fan_light
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          code: '100111010001110000011010'
          protocol: 
            pulse_length: 330
            sync: [23,1]
            zero: [1,2]
            one: [2,1]
            inverted: true
          repeat: 6

If I use another ESP32 as a receiver and dump the raw codes, this is what I see when I press the original remote control “Light On” button:

[17:12:04][D][remote.raw:028]: Received Raw: 24, -6210, 60, -4773, 32, -315, 80, -206, 35, -305, 69, -334, 22, -212, 56, -164, 25, -41, 26, -442, 60, -766, 57, -78, 45, -765, 44, -180, 31, -265, 49, -131, 31, -485, 39, -266, 63, -397, 41, -224, 31, -42, 42, -666, 27, -704, 27, -404, 
[17:12:04][D][remote.raw:028]:   78, -115, 21, -277, 35, -130, 38, -97, 53, -294, 48, -209, 55, -6175, 47, -9331, 55, -169, 52, -65, 107, -102, 24, -34, 40, -80, 117, -36, 102, -43, 41, -170, 157, -18, 59, -61, 47, -210, 16, -64, 128, -32, 79, -74, 27, -183, 40, -43, 33, -233, 19, 
[17:12:04][D][remote.raw:028]:   -34, 19, -51, 18, -73, 38, -92, 99, -90, 50, -415, 198, -28, 25, -211, 107, -307, 47, -71, 31, -72, 79, -37, 31, -249, 182, -49, 23, -26, 117, -70, 80, -32, 32, -58, 28, -52, 20, -4725, 17, -96, 42, -88, 128, -45, 40, -159, 144, -5045, 10, -8133, 22, 
[17:12:04][D][remote.raw:028]:   -156, 23, -20, 56, -83, 269, -88, 24, -27, 66, -42, 244, -44, 58, -32, 63, -27, 493, -65, 110, -39, 60, -31, 103, -38, 75, -41, 31, -47, 153, -78, 27, -32, 74, -59, 36, -26, 205, -111, 281, -115, 43, -23, 84, -35, 86, -101, 82, -85, 59, -110, 264, -24, 
[17:12:04][D][remote.raw:028]:   32, -39, 30, -63, 31, -113, 158, -51, 73, -144, 18, -33, 53, -46, 18, -128, 446, -48, 154, -67, 97, -172, 32, -18113, 19, -194, 66, -81, 176, -17, 179, -25, 58, -61, 246, -45, 163, -37, 546, -40, 373, -23, 68, -44, 33, -50, 236, -23, 123, -43, 33, -31, 
[17:12:04][D][remote.raw:028]:   24, -44, 343, -42, 67, -20, 82, -95, 43, -68, 132, -30, 287, -104, 248, -41, 117, -48, 276, -34, 325, -56, 73, -66, 79, -36, 156, -85, 30, -26, 141, -43, 190, -54, 163, -18368, 50, -137, 147, -35, 313, -38, 112, -49, 1085, -13, 473, -39, 78, -39, 361, 
[17:12:04][D][remote.raw:028]:   -59, 85, -25, 441, -50, 50, -26, 21, -37, 1368, -100, 451, -17, 161, -29, 97, -50, 58, -1093, 46, -36, 440, -15, 41, -12, 187, -40, 80, -42, 57, -988, 21, -382, 18, -3160, 16, -3643, 32, -8392, 80, -103, 157, -18, 292, -56, 118, -37, 1109, -31, 584, 
[17:12:04][D][remote.raw:041]:   -42, 297, -36, 112, -44, 1746, -69, 636, -110, 63, -253, 33, -34, 295, -71, 191, -24, 77, -41, 81, -4469, 86, -23, 1326, -37, 77, -2979

How do these relate to your transmitter codes and protocol numbers?

If I dump “rc_switch” I see this in the logs:

[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='1001110100011100000110'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='1001110100011100000110'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'
[17:21:36][D][remote.rc_switch:259]: Received RCSwitch Raw: protocol=6 data='100111010001110000011010'

So I’m not sure how the raw numbers relate to protocol 6 code “100111010001110000011010”?

Thanks for your help.

Yes. More importantly, I’m updating the state in Home Assistant so that it’s in sync.

1 Like

You’d need to post examples of this ESP32 receiving both your original remote and your mimicked signal. The rc_switch dumps are not useful for protocol analysis. I did have a quick look at the dump you provided provide and it’s pretty awful. Looks more like noise than an actual remote signal. But even through the noise it’s clear that it’s a very different signal to the RH787T.

FYI, this thread is supposed to be about the RH787T remote. It’s not surprising that my code didn’t work for you, as your remote is not a RH787T.

I realised that I stupidly had not connected antennas to the modules (I believe 1/4 wave is ok), so I will do that and capure the codes again. Hopefully there is less noise. Also I have the ESP32 very close to the 433MHz receiver, may need some separation there too.

FYI, this thread is supposed to be about the RH787T remote. It’s not surprising that my code didn’t work for you, as your remote is not a RH787T.

sorry about that. I know many times these electronics items all come out of the same factory and are rebranded but use the same electronics, so I kind of assumed my fan would be similar or identical to the list in your original post.

Maybe I will start a new thread and post my questions there

I’m one of the unlucky ones with the 303Mhz variant.



Don’t know if I should even attempt to make it smart. I love the fan but not sure if I want to keep it if I can’t automate it.

1 Like

Count me in your boat with the 303Mhz controller. I’ve spent like 2 days trying to read non-existent 433Mhz on my Sonoff Bridge. Ughh!. Do they sell replacements w/ a 303Mhz controller? We just need 9 GPIO inputs that switch to its ground plane then throw this whole thing in a project box. I just tested it’s very easy to jump each button to ground. The NodeMCU board can do that, no?

Meanwhile the original remote can be used as needed. I had planned to get this thing moved into HA and use some kind of 2 button Zigbee wall switch.

2 Likes

Hope someone has success with the 303MHz version.I just started researching and I’m happy I found this thread.

1 Like

@simondotau great work! Do you mind if I ask what RF transmitter you are using?

I have tried to get this working with the Sonoff RF 433 Bridge without any success - no . I fear I would have to hack it via this process: Hardware Itead Sonoff RF Bridge Direct Hack · xoseperez/espurna Wiki · GitHub

For the last few years I have been using a Bond Bridge to send commands to my fans, which is fine. But I’ve always wanted to listen to the remotes so I can keep Home Assistant in sync too. Congrats on getting it done.

Incidentally my remotes do have DIP switches. They are driving this “Airfusion” DC fan. https://www.beaconlighting.com.au/airfusion-fraser-dc-fan-only-in-white

If you go full on insane with a 303Mhz remote here is your solution: The ceiling fan that pushed me too far

Here’s the relay-free option of 303Mhz remote I came up with.