Generic Tuya Smart Vacuum (Eufy, Ionvac) ESPHome Config

Sharing my ESPHome configurations for the Eufy RoboVac 30C and Tzumi Ionvac SmartClean 2000, which are based on the Tuya wifi module. There may be room for improvement. Guessing several generic vacuums will have similar configuration files.

Eufy Robovac 30C
ESP8266EX
TYWE1S Module

esphome:
  name: esphome-eufy-robovac-30c
  friendly_name: ESPHome RoboVac 30C
  min_version: 2025.11.0
  name_add_mac_suffix: false
  on_boot:
    priority: 800
    then:
      - delay: 5s
      - logger.log: "MCU Waiting for heartbeat..."

esp8266:
  board: esp01_1m

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: Eufy-Robovac Fallback Hotspot
    password: Redacted

captive_portal:

web_server:

# Tuya supports sending time to the application processor and the vacuum does use it (not sure for why though) so let's configure a time reference
time:
  - platform: homeassistant
    id: time_source

# UART will be used to talk to the application processor, so we need to disable any logging over it
logger:
  baud_rate: 0

# UART to talk to the application processor
uart:
  id: vacuum_uart
  rx_pin: GPIO13
  tx_pin: GPIO15
  baud_rate: 115200
  debug: null

# the TuyaMCU object
tuya:
  id: vacuum_tuya
  uart_id: vacuum_uart
  time_id: time_source

sensor:
  - platform: tuya
    name: Battery
    sensor_datapoint: 104
    unit_of_measurement: "%"
    icon: mdi:battery
    device_class: battery
    state_class: measurement
    entity_category: diagnostic

button:
  - platform: template
    name: Start cleaning
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(5, 0);

  - platform: template
    name: Start spot cleaning
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(5, 2);

  - platform: template
    name: Pause
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(2, false);

  - platform: template
    name: Resume
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(2, true);

  - platform: template
    name: Return to dock
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(101, true);

  - platform: template
    name: Nav Forward
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 0);

  - platform: template
    name: Nav Back
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 1);

  - platform: template
    name: Nav Right
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 2);

  - platform: template
    name: Nav Left
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 3);

select:
  - platform: tuya
    id: nav_dp
    enum_datapoint: 3
    options:
      0: "Forward"
      1: "Back"
      2: "Right"
      3: "Left"

  - platform: tuya
    name: "Cleaning Mode"
    enum_datapoint: 5
    options:
      0: Auto
      1: Edge
      2: Spot
      3: Small Room
    entity_category: config

  - platform: tuya
    id: status_dp
    enum_datapoint: 15
    options:
      0: Running
      1: Idle
      2: Sleeping
      3: Charging
      4: Charged
      5: Docking
    on_value: &publish_state_sensor
      then:
        - text_sensor.template.publish:
            id: state_sensor
            state: !lambda 'return (id(pause_dp).state == "Paused" && id(status_dp).state == "Idle") ? "Paused" : id(status_dp).state;'

  - platform: tuya
    name: Fan speed
    enum_datapoint: 102
    entity_category: config
    options:
      0: Standard
      1: BoostIQ
      2: Max

  - platform: tuya
    id: pause_dp
    enum_datapoint: 2
    options:
      0: Paused
      1: Resume
    on_value: *publish_state_sensor

text_sensor:
  - platform: template
    name: State
    entity_category: diagnostic
    id: state_sensor

  - platform: tuya
    name: "Error Code"
    sensor_datapoint: 106
    entity_category: diagnostic
    icon: mdi:alert-circle

switch:
  - platform: tuya
    name: Locate
    icon: mdi:map-marker
    entity_category: config
    switch_datapoint: 103

  - platform: tuya
    name: Power
    switch_datapoint: 2
    icon: mdi:power

Tzumi Ionvac SmartClean 2000
RTL8710BN
WR3 Module

I had to use the framework 1.9.2 to overcome a LibreTiny bug in the RX communication from the MCU. It may be fixed in more recent versions of ESPHome.

esphome:
  name: esphome-smartclean-2000
  friendly_name: ESPHome SmartClean 2000

rtl87xx:
  board: wr3
  framework:
    version: 1.9.2

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: Esphome-Smartclean Fallback
    password: "Redacted"

captive_portal:

web_server:

# Tuya supports sending time to the application processor and the vacuum does use it (not sure for why though) so let's configure a time reference
time:
  - platform: homeassistant
    id: time_source

# UART will be used to talk to the application processor, so we need to disable any logging over it
logger:
  baud_rate: 0

# UART to talk to the application processor
uart:
  id: vacuum_uart
  tx_pin: PA23
  rx_pin: PA18
  baud_rate: 115200
  debug: null

# the TuyaMCU object
tuya:
  id: vacuum_tuya
  uart_id: vacuum_uart
  time_id: time_source

sensor:
  - platform: tuya
    name: Battery
    sensor_datapoint: 6
    unit_of_measurement: "%"
    icon: mdi:battery
    device_class: battery
    state_class: measurement
    entity_category: diagnostic

  - platform: tuya
    name: Clean Time Last
    sensor_datapoint: 17
    unit_of_measurement: minutes
    icon: mdi:timer-sync
    device_class: duration
    state_class: total_increasing
    entity_category: diagnostic

  - platform: tuya
    name: Clean Time Total
    sensor_datapoint: 108
    unit_of_measurement: minutes
    icon: mdi:timer-sync
    device_class: duration
    state_class: total_increasing
    entity_category: diagnostic

  - platform: tuya
    name: Use Side Brush
    sensor_datapoint: 7
    unit_of_measurement: "%"
    icon: mdi:broom
    device_class: battery
    state_class: measurement
    entity_category: diagnostic

  - platform: tuya
    name: Use Main Brush
    sensor_datapoint: 8
    unit_of_measurement: "%"
    icon: mdi:broom
    device_class: battery
    state_class: measurement
    entity_category: diagnostic

  - platform: tuya
    name: Use Filter
    sensor_datapoint: 9
    unit_of_measurement: "%"
    icon: mdi:air-filter
    device_class: battery
    state_class: measurement
    entity_category: diagnostic

button:
# Not working
  - platform: template
    name: Reset side brush
    icon: mdi:broom
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(10, true);

# Not working
  - platform: template
    name: Reset main brush
    icon: mdi:broom
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(11, true);

# Not working
  - platform: template
    name: Reset filter
    icon: mdi:air-filter
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(12, true);

  - platform: template
    name: Start cleaning
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 1);

  - platform: template
    name: Start spot cleaning
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_enum_datapoint_value(3, 3);

  - platform: template
    name: Pause
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(2, 0);

  - platform: template
    name: Resume
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(2, 1);

  - platform: template
    name: Return to dock
    entity_category: config
    on_press:
      - lambda: id(vacuum_tuya).force_set_boolean_datapoint_value(3, 5);

select:
  - platform: tuya
    name: "Mode"
    enum_datapoint: 3
    options:
      0: Standby
      1: Auto
      2: Edge
      3: Spot
      4: PartialBow
      5: Dock
    entity_category: config

  - platform: tuya
    name: "Direction"
    enum_datapoint: 4
    options:
      0: "Forward"
      1: "180"
      2: "Left"
      3: "Right"
      4: "Stop"

  - platform: tuya
    id: status_dp
    enum_datapoint: 5
    options:
      0: Idle
      1: Charging
      2: Running
      3: Docking
      4: Fault
      9: Docked
    on_value: &publish_state_sensor
      then:
        - text_sensor.template.publish:
            id: state_sensor
            state: !lambda 'return (id(pause_dp).state == "Paused" && id(status_dp).state == "Idle") ? "Paused" : id(status_dp).state;'

  - platform: tuya
    id: pause_dp
    enum_datapoint: 2
    options:
      1: Paused
      2: Resume
    on_value: *publish_state_sensor

  - platform: tuya
    name: Fan speed
    enum_datapoint: 14
    entity_category: config
    options:
      1: Normal
      2: Strong

text_sensor:
  - platform: template
    name: State
    entity_category: diagnostic
    id: state_sensor

switch:
  - platform: tuya
    name: Power
    switch_datapoint: 2
    icon: mdi:power 

  - platform: tuya
    name: Locate
    icon: mdi:map-marker
    entity_category: config
    switch_datapoint: 13

Thanks to Rjevski for his config esphome-eufy-robovac-g10-hybrid.