Add wifi to an older roomba

Thanks for looking into this. I used the hex view plugin and could send binary data, eg 128 as 0x80, and in hex that does indeed output that as 0x80 rather than sending ascii data. Apologies if I misunderstand the feature but I’m pretty sure it’s sending binary data as connecting the tx and rx pins does return me binary data when I send it using hex

I tried this, but I must have done something wrong because it’s not working. I need to check it.

And now with the new version of ESPHome I don’t want to update the code until there is a tested version.

You can’t just send the command. First you have to select proper operating mode, before Roomba can obey your commands. On startup it only sends out diagnostic messages and does not accept any commands except for mode change.

See documentation and source code in my repo:

For those in the near future I will share my findings here on how I got mine working.

substitutions:
  ### System variables ###
  TipoPlaca: d1_mini
  hostname: "hass-roomba" # Device hostname .local
  PrefixoNome: "Roomba -"
  id: "irobot"
  RedeWifi: !secret wifi_ssid
  SenhaWifi: !secret wifi_password
  SenhaWifiReconfig: !secret wifi_password
  EndConfig:  "hass-roomba.local"
  WifiOculto: 'False'
  WifiFastConnect: 'True'
  SenhaOTA: !secret ota_password # OTA update password
  # ------------------------------ BRC pin, (RX/TX) pin, polling ms, wake up
  init: 'RoombaComponent::instance(D5, id(uart_bus), 10000, true);'

esphome:
  name: $hostname
  comment: Roomba 870 IoT DIY integration
  friendly_name: ${hostname}
  includes:
    - esphome-roomba/Roomba.h
  platformio_options:
    build_flags: -D HAVE_HWSERIAL0

esp8266:
  board: ${TipoPlaca}

# Enable logging
logger:
  hardware_uart: UART1
  level: INFO
  
# Enable Home Assistant API
api:
  encryption:
    key: <your key here>

# Enable OTA updates
ota:
  - platform: esphome
    password: ${SenhaOTA}

wifi:
  networks:
    ssid: ${RedeWifi}
    password: ${SenhaWifi}
    hidden: ${WifiOculto}
  fast_connect: ${WifiFastConnect}
  use_address: ${EndConfig}

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "roomba-fallback"
    password: ${SenhaWifiReconfig}

#Habilita um AP Wifi para reconfigurar em caso de perda de conexão com a rede configurada
captive_portal:

# Serial Port
uart:
  id: uart_bus
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 115200

# Wemos D1 LED to check status
status_led:
  pin:
    number: GPIO2
    inverted: True

# Custom Roomba Component
external_components:
  - source:
      type: git
      url: https://github.com/robertklep/esphome-custom-component
    components: [ custom, custom_component ]

# Roomba custom sensors
time:
  - platform: homeassistant
    id: my_time
    on_time:
      - seconds: 0
        minutes: 0
        hours: 5
        then:
          - button.press: set_date

custom_component:
  - lambda: |-
      auto r = ${init}
      return {r};
    id: my_roomba

binary_sensor:
  - platform: custom
    lambda: |-
        auto r = ${init}
        return {r->vacuumSensor, r->virtualWallSensor, r->chargingSourcesSensor};
    binary_sensors:
      - name: "${PrefixoNome} Vacuum State"
        id: "${id}_vacuum"
      - name: "${PrefixoNome} Virtual Wall"
      - name: "${PrefixoNome} Charging Source Available"

sensor:
  - platform: wifi_signal
    name: "${PrefixoNome} WiFi Signal"
    update_interval: 60s
  - platform: custom
    lambda: |-
        auto r = ${init}
        return {r->voltageSensor, r->currentSensor, r->batteryChargeSensor, r->batteryCapacitySensor, r->batteryPercentSensor, r->batteryTemperatureSensor, r->driveSpeedSensor, r->rightMotorCurrentSensor, r->leftMotorCurrentSensor, r->mainBrushCurrentSensor, r->sideBrushCurrentSensor};
    sensors:
      - name: "${PrefixoNome} Voltage"
        unit_of_measurement: "V"
        icon: mdi:sine-wave
        accuracy_decimals: 2
      - name: "${PrefixoNome} Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 3
      - name: "${PrefixoNome} Charge"
        unit_of_measurement: "Ah"
        icon: mdi:battery-charging
        accuracy_decimals: 2
      - name: "${PrefixoNome} Capacity"
        unit_of_measurement: "Ah"
        icon: mdi:battery
        accuracy_decimals: 2
      - name: "${PrefixoNome} Battery"
        unit_of_measurement: "%"
        state_class: "measurement"
        device_class: battery
        icon: mdi:battery-outline
        accuracy_decimals: 0
      - name: "${PrefixoNome} Temperature"
        unit_of_measurement: "°C"
        icon: mdi:thermometer
        accuracy_decimals: 0
      - name: "${PrefixoNome} Drive Speed"
        unit_of_measurement: "mm/s"
        accuracy_decimals: 0
      - name: "${PrefixoNome} Right Motor Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Left Motor Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Main Brush Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Side Brush Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "${PrefixoNome} IP Address"
    ssid:
      name: "${PrefixoNome} SSID"
    bssid:
      name: "${PrefixoNome} BSSID"
    mac_address:
      name: "${PrefixoNome} MAC Address"
  - platform: custom
    lambda: |-
      auto r = ${init}
      return {r->chargingSensor, r->activitySensor, r->oiModeSensor, r->buttonsSensor};
    text_sensors:
      - name: "${PrefixoNome} Charging State"
      - name: "${PrefixoNome} Activity"
      - name: "${PrefixoNome} OI Mode"
        id: "${id}_oi_mode"
      - name: "${PrefixoNome} Pressed Button"

button:
  - platform: restart
    id: restart_button
    name: "${PrefixoNome} Restart"

  - platform: template
    name: "${PrefixoNome} Clean"
    id: clean
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("clean");

  - platform: template
    name: "${PrefixoNome} Max Clean"
    id: max_clean
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("max_clean");
  
  - platform: template
    name: "${PrefixoNome} Dock"
    id: dock
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("dock");
  
  - platform: template
    name: "${PrefixoNome} Stop"
    id: stop
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("stop");
  
  - platform: template
    name: "${PrefixoNome} Wake Up"
    id: wakeup
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("wakeup");

  - platform: template
    name: "${PrefixoNome} Wake Up on Dock"
    id: wakeup_on_dock
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("wake_on_dock");
  
  - platform: template
    name: "${PrefixoNome} Sleep"
    id: roomba_sleep
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("sleep");

  - platform: template
    name: "${PrefixoNome} Locate"
    id: locate
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("locate");
  
  - platform: template
    name: "${PrefixoNome} Clean Spot"
    id: clean_spot
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("clean_spot");

  - platform: template
    name: "${PrefixoNome} Forward"
    id: go_forward
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_forward");
  
  - platform: template
    name: "${PrefixoNome} Full Speed"
    id: go_max
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_max");
  
  - platform: template
    name: "${PrefixoNome} Go Faster"
    id: go_faster
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_faster");

  - platform: template
    name: "${PrefixoNome} Go Slower"
    id: go_slower
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_slower");

  - platform: template
    name: "${PrefixoNome} Turn Left"
    id: go_left
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_left");

  - platform: template
    name: "${PrefixoNome} Turn Right"
    id: go_right
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_right");

  - platform: template
    name: "${PrefixoNome} Stop Driving"
    id: stop_driving
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("stop_driving");

  - platform: template
    name: "${PrefixoNome} Reverse"
    id: go_reverse
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_reverse");

  - platform: template
    name: "${PrefixoNome} Rotate Left"
    id: rotate_left
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("rotate_left");

  - platform: template
    name: "${PrefixoNome} Rotate Right"
    id: rotate_right
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("rotate_right");
  
  - platform: template
    name: "${PrefixoNome} Restart Roomba"
    id: restart_roomba
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("reset");
  
  - platform: template
    name: "${PrefixoNome} Poweroff Roomba"
    id: poweroff_roomba
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("poweroff");

  - platform: template
    name: "${PrefixoNome} Set Date"
    id: set_date
    on_press:
      then:
        - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("set_date");

switch:
  - platform: template
    name: "${PrefixoNome} Vacuum"
    lambda: |-
      return id(${id}_vacuum).state;
    turn_on_action:
      - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("vacuum_on");
    turn_off_action:
      - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("vacuum_off");
  
  - platform: template
    name: "${PrefixoNome} Safe Mode"
    lambda: |-
      if (id(${id}_oi_mode).state == "safe") {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("safe");
    turn_off_action:
      - lambda: |-
            static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("passive");

For hardware I am using a cheap generic buck converter to get the 3v3, a Wemos D1 Mini, 2N3906 to shift the level of the Roomba TX to ESP RX. Searching in this topic you can find the schematic from @wburgers at #165

Example of it working at the time of writing:

Can you also show your Roomba.h file? Thank you!

#include "esphome.h"

#define ROOMBA_READ_TIMEOUT 200

class RoombaComponent : public UARTDevice, public CustomAPIDevice, public PollingComponent { 
	public:
		//Sensor *distanceSensor;
		Sensor *voltageSensor;
		Sensor *currentSensor;
		Sensor *batteryChargeSensor;
		Sensor *batteryCapacitySensor;
		Sensor *batteryPercentSensor;
		Sensor *batteryTemperatureSensor;
		TextSensor *chargingSensor;
		TextSensor *activitySensor;
		Sensor *driveSpeedSensor;
		TextSensor *oiModeSensor;
		Sensor *rightMotorCurrentSensor;
		Sensor *leftMotorCurrentSensor;
        Sensor *mainBrushCurrentSensor;
        Sensor *sideBrushCurrentSensor; 
        BinarySensor *vacuumSensor;
		BinarySensor *virtualWallSensor;
		BinarySensor *chargingSourcesSensor;
		TextSensor *buttonsSensor;

		static RoombaComponent* instance(uint8_t brcPin, UARTComponent *parent, uint32_t updateInterval, bool lazy650Enabled) {
			static RoombaComponent* INSTANCE = new RoombaComponent(brcPin, parent, updateInterval, lazy650Enabled);
			return INSTANCE;
		}

		void setup() override {
			if (this->lazy650Enabled) {
				// High-impedence on the BRC_PIN
				// see https://github.com/johnboiles/esp-roomba-mqtt/commit/fa9af14376f740f366a9ecf4cb59dec2419deeb0#diff-34d21af3c614ea3cee120df276c9c4ae95053830d7f1d3deaf009a4625409ad2R140
				pinMode(this->brcPin, INPUT);
			} else {
				pinMode(this->brcPin, OUTPUT);
				digitalWrite(this->brcPin, HIGH);
			}

			register_service(&RoombaComponent::on_command, "command", {"command"});
		}

    	void update() override {
			if (this->lazy650Enabled) {
				long now = millis();
				// Wakeup the roomba at fixed intervals
				if (now - lastWakeupTime > 50000) {
					ESP_LOGD("roomba", "Time to wakeup");
					lastWakeupTime = now;
					if (!wasCleaning) {
						if (wasDocked) {
							wake_on_dock();
						} else {
							brc_wakeup();
						}
					} else {
						brc_wakeup();
					}
				}
			}

			uint8_t charging;
			uint16_t voltage;
			int16_t current;
			uint16_t batteryCharge;
			uint16_t batteryCapacity;
			int16_t batteryTemperature;
			int16_t rightMotorCurrent;
			int16_t leftMotorCurrent;
            int16_t mainBrushCurrent;
            int16_t sideBrushCurrent;
			uint8_t virtualWall;
			uint8_t chargingSources;
			uint8_t buttons;

			flush();

			uint8_t sensors[] = {
				SensorChargingState,
				SensorVoltage,
				SensorCurrent,
				SensorBatteryCharge,
				SensorBatteryCapacity,
				SensorBatteryTemperature,
				SensorOIMode,
				SensorRightMotorCurrent,
				SensorLeftMotorCurrent,
                SensorMainBrushCurrent,
                SensorSideBrushCurrent,
				SensorVirtualWall,
				SensorChargingSourcesAvailable,
				SensorButtons,
			};

			uint8_t values[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

			bool success = getSensorsList(sensors, sizeof(sensors), values, sizeof(values));
			if (!success) {
				ESP_LOGD("roomba", "Could not get sensor values from serial");
				return;
			}

			charging = values[0];
			voltage = values[1] * 256 + values[2];
			current = values[3] * 256 + values[4];
			batteryCharge = values[5] * 256 + values[6];
      		batteryCapacity = values[7] * 256 + values[8];
			batteryTemperature = values[9];
			std::string oiMode = get_oimode(values[10]);
			rightMotorCurrent = values[11] * 256 + values[12]; 
			leftMotorCurrent = values[13] * 256 + values[14]; 
            mainBrushCurrent = values[15] * 256 + values[16];
            sideBrushCurrent = values[17] * 256 + values[18];
			virtualWall = values[19];
			chargingSources = values[20];
			buttons = values[21];

			std::string activity = get_activity(charging, current);
			wasCleaning = activity == "Cleaning";
			wasDocked = activity == "Docked";

			float voltageData = 0.001 * roundf(voltage * 100) / 100;
			if (this->voltageSensor->state != voltageData) {
				this->voltageSensor->publish_state(voltageData);
			}

			float currentData = 0.001 * roundf(current * 100) / 100;
			if (this->currentSensor->state != currentData) {
				this->currentSensor->publish_state(currentData);
			}

			float charge = 0.001 * roundf(batteryCharge * 100) / 100;
			if (this->batteryChargeSensor->state != charge) {
				this->batteryChargeSensor->publish_state(charge);
			}

			float capacity = 0.001 * roundf(batteryCapacity * 100) / 100;
			if (this->batteryCapacitySensor->state != capacity) {
				this->batteryCapacitySensor->publish_state(capacity);
			}

			float battery_level = 100.0 * ((1.0 * charge) / (1.0 * capacity));
			if (this->batteryPercentSensor->state != battery_level) {
				this->batteryPercentSensor->publish_state(battery_level);
			}

			if (this->batteryTemperatureSensor->state != batteryTemperature) {
				this->batteryTemperatureSensor->publish_state(batteryTemperature);
			}

			if (this->chargingState != charging) {
				this->chargingState = charging;
				this->chargingSensor->publish_state(ToString(charging));
			}

			if (activity.compare(this->activitySensor->state) != 0) {
				this->activitySensor->publish_state(activity);
			}

			if (this->driveSpeedSensor->state != this->speed) {
				this->driveSpeedSensor->publish_state(this->speed);
			}

			if (oiMode.compare(this->oiModeSensor->state) != 0) {
				this->oiModeSensor->publish_state(oiMode);
			}

			float rightMotorCurrentData = 0.001 * (rightMotorCurrent * 100) / 100;
            if(this->rightMotorCurrentSensor->state != rightMotorCurrentData) {
				this->rightMotorCurrentSensor->publish_state(rightMotorCurrentData);
			}

			float leftMotorCurrentData = 0.001 * (leftMotorCurrent * 100) / 100;
            if(this->leftMotorCurrentSensor->state != leftMotorCurrentData) {
				this->leftMotorCurrentSensor->publish_state(leftMotorCurrentData);
			}

            float mainBrushCurrentData = 0.001 * (mainBrushCurrent * 100) / 100;
            if(this->mainBrushCurrentSensor->state != mainBrushCurrentData) {
				this->mainBrushCurrentSensor->publish_state(mainBrushCurrentData);
			}

            float sideBrushCurrentData = 0.001 * (sideBrushCurrent * 100) / 100;
            if(this->sideBrushCurrentSensor->state != sideBrushCurrentData) {
				this->sideBrushCurrentSensor->publish_state(sideBrushCurrentData);
			}

			if (virtualWall == 1) {
				this->virtualWallSensor->publish_state(true);
			} else {
				this->virtualWallSensor->publish_state(false);
			}

			if (chargingSources == 0) {
				this->chargingSourcesSensor->publish_state(false);
			} else {
				this->chargingSourcesSensor->publish_state(true);
			}

			if (!wasDocked) {
				if (buttons == 1) {
					this->buttonsSensor->publish_state("Clean");
				} else if (buttons == 2) {
					this->buttonsSensor->publish_state("Spot");
				} else if (buttons == 4) {
					this->buttonsSensor->publish_state("Dock");
				} else {
					this->buttonsSensor->publish_state("None");
				}
			} else {
				this->buttonsSensor->publish_state("None");
			}

		}

        // this function can be called from the Roomba yaml file as 
        // static_cast< RoombaComponent*> (id(my_roomba).get_component(0))->send_command("go_forward");
        void send_command(std::string command) {
            on_command(command);
        }

	private:
		uint8_t brcPin;
		uint8_t chargingState;
		int lastWakeupTime = 0;
		bool wasCleaning = false;
		bool wasDocked = false;
		int16_t speed = 0;

		bool lazy650Enabled = false;

		RoombaComponent(uint8_t brcPin, UARTComponent *parent, uint32_t updateInterval, bool lazy650Enabled) : UARTDevice(parent), PollingComponent(updateInterval) {
			this->brcPin = brcPin;
			this->lazy650Enabled = lazy650Enabled;
			this->voltageSensor = new Sensor();
			this->currentSensor = new Sensor();
			this->batteryChargeSensor = new Sensor();
			this->batteryCapacitySensor = new Sensor();
			this->batteryPercentSensor = new Sensor();
			this->batteryTemperatureSensor = new Sensor();
			this->rightMotorCurrentSensor = new Sensor();
			this->leftMotorCurrentSensor = new Sensor();
			this->mainBrushCurrentSensor = new Sensor();
            this->sideBrushCurrentSensor = new Sensor();

			this->chargingSensor = new TextSensor();
			this->activitySensor = new TextSensor();

			this->driveSpeedSensor = new Sensor();
			this->oiModeSensor = new TextSensor();
            this->vacuumSensor = new BinarySensor();
			this->virtualWallSensor = new BinarySensor();
			this->chargingSourcesSensor = new BinarySensor();
			this->buttonsSensor = new TextSensor();

		}

		typedef enum {
			Sensors7to26					= 0,  //00
			Sensors7to16					= 1,  //01
			Sensors17to20					= 2,  //02
			Sensors21to26					= 3,  //03
			Sensors27to34					= 4,  //04
			Sensors35to42					= 5,  //05
			Sensors7to42					= 6,  //06
			SensorBumpsAndWheelDrops		= 7,  //07
			SensorWall						= 8,  //08
			SensorCliffLeft					= 9,  //09
			SensorCliffFrontLeft			= 10, //0A
			SensorCliffFrontRight			= 11, //0B
			SensorCliffRight				= 12, //0C
			SensorVirtualWall				= 13, //0D
			SensorOvercurrents				= 14, //0E
			//SensorUnused1					= 15, //0F
			//SensorUnused2					= 16, //10
			SensorIRByte					= 17, //11
			SensorButtons					= 18, //12
			SensorDistance					= 19, //13
			SensorAngle						= 20, //14
			SensorChargingState				= 21, //15
			SensorVoltage					= 22, //16
			SensorCurrent					= 23, //17
			SensorBatteryTemperature		= 24, //18
			SensorBatteryCharge				= 25, //19
			SensorBatteryCapacity			= 26, //1A
			SensorWallSignal				= 27, //1B
			SensoCliffLeftSignal			= 28, //1C
			SensoCliffFrontLeftSignal		= 29, //1D
			SensoCliffFrontRightSignal		= 30, //1E
			SensoCliffRightSignal			= 31, //1F
			SensorUserDigitalInputs			= 32, //20
			SensorUserAnalogInput			= 33, //21
			SensorChargingSourcesAvailable	= 34, //22
			SensorOIMode					= 35, //23
			SensorSongNumber				= 36, //24
			SensorSongPlaying				= 37, //25
			SensorNumberOfStreamPackets		= 38, //26
			SensorVelocity					= 39, //27
			SensorRadius					= 40, //28
			SensorRightVelocity				= 41, //29
			SensorLeftVelocity				= 42, //2A
			SensorLeftMotorCurrent			= 54, //36
			SensorRightMotorCurrent			= 55, //37
            SensorMainBrushCurrent          = 56, //38
            SensorSideBrushCurrent          = 57, //39
		} SensorCode;

		typedef enum {
			ChargeStateNotCharging				= 0,
			ChargeStateReconditioningCharging	= 1,
			ChargeStateFullCharging				= 2,
			ChargeStateTrickleCharging			= 3,
			ChargeStateWaiting					= 4,
			ChargeStateFault					= 5,
		} ChargeState;

        typedef enum {
            ResetCmd        = 7,   //07
			PowerOffCmd		= 133, //85
            StartCmd        = 128, //80
            StopCmd         = 173, //AD
            SafeCmd         = 131, //83
            FullCmd         = 132, //84
            CleanCmd        = 135, //87
			MaxCleanCmd		= 136, //88
            SpotCmd         = 134, //86
            DockCmd         = 143, //8F
            PowerCmd        = 133, //85
            DriveCmd        = 137, //89
            MotorsCmd       = 138, //8A
            LEDAsciiCMD     = 164, //A4
            SongCmd         = 140, //8C
            PlayCmd         = 141, //8D
            SensorsListCmd  = 149, //95
			SetDateCmd		= 168, //A8
        } Commands;      
            
		void brc_wakeup() {
			if (this->lazy650Enabled) {
				ESP_LOGD("roomba", "brc_wakeup");
				pinMode(this->brcPin, OUTPUT);
				digitalWrite(this->brcPin, LOW);
				delay(200);
				pinMode(this->brcPin, OUTPUT);
				delay(200);
				start_oi(); // Start
			} else {
				digitalWrite(this->brcPin, LOW);
				delay(1000);
				digitalWrite(this->brcPin, HIGH);
				delay(100);
			}
		}

		void on_command(std::string command) {
			if (command == "clean") {
				clean();
			} else if (command == "max_clean") {
				maxClean();
			} else if (command == "dock") {
                displayString("DOCK");
				dock();
			} else if (command == "locate") {
				locate();
                displayString("LOC ");
			} else if (command == "clean_spot") {
				spot();
			} else if (command == "stop") {
				stop();
			} else if (command == "wakeup") {
				brc_wakeup();
			} else if (command == "wake_on_dock") {
				wake_on_dock();
			} else if (command == "sleep") {
				sleep();
			} else if (command == "go_max") {
	            ESP_LOGI("roomba", "go max");
				alter_speed(1000);
			} else if (command == "go_forward") {
	            ESP_LOGI("roomba", "go forward");
                this->speed = 200;
                displayString("FWD ");
				drive(this->speed, 0);
			} else if (command == "go_reverse") {
	            ESP_LOGI("roomba", "go reverse");
                this->speed = -200;
                displayString("REV ");
				drive(this->speed, 0);
			} else if (command == "go_faster") {
	            ESP_LOGI("roomba", "go faster");
				alter_speed(100);
			} else if (command == "go_slower") {
	            ESP_LOGI("roomba", "slow it down");
				alter_speed(-100);
			} else if (command == "go_right") {
	            ESP_LOGI("roomba", "go right");
                this->speed = 200;
                displayString("RITE");
				drive(this->speed, -250);
			} else if (command == "go_left") {
	            ESP_LOGI("roomba", "go left");
                this->speed = 200;
                displayString("LEFT");
				drive(this->speed, 250);
			} else if (command == "rotate_right") {
	            ESP_LOGI("roomba", "rotate_right");
                this->speed = 200;
                displayString("ROTR");
				drive(this->speed, 0xFFFF);
			} else if (command == "rotate_left") {
	            ESP_LOGI("roomba", "rotate_left");
                this->speed = 200;
                displayString("ROTL");
				drive(this->speed, 0x0001);
			} else if (command == "stop_driving") {
	            ESP_LOGI("roomba", "STOP");
				this->speed = 0;
                displayString("STOP");
				drive(0,0);
			} else if (command == "drive" || command == "safe") {
	            ESP_LOGI("roomba", "DRIVE (Safe) mode");
				this->speed = 0;
				safeMode();
			} else if (command == "passive") {
	            ESP_LOGI("roomba", "passive mode");
				start_oi();
            } else if (command == "vacuum_on") {
                ESP_LOGI("roomba", "vacuum_on");
                displayString("VAC ");
                vacuum_on();
            } else if (command == "vacuum_off") {
                ESP_LOGI("roomba", "vacuum_off");
                displayString("OFF ");
                vacuum_off();
            } else if (command == "reset") {
                ESP_LOGI("roomba", "reset");
                reset();
			} else if (command == "poweroff") {
                ESP_LOGI("roomba", "poweroff");
                powerOff();
			} else if (command == "set_date") {
				ESP_LOGI("roomba", "set_date");
				setDate();
            } else {
                ESP_LOGE("roomba", "unrecognized command %s", command.c_str());
			}            
            //ESP_LOGI("roomba", "ACK %s", command.c_str());
    	}

		void start_oi() {
			write(StartCmd);
		}

        void reset() {
            write(ResetCmd);
        }

		void powerOff() {
			write(PowerOffCmd);
		}

		void locate() {
			uint8_t song[] = {62, 12, 66, 12, 69, 12, 74, 36};
			safeMode();
			delay(500);
			setSong(0, song, sizeof(song));
			playSong(0);
		}
   
		void setSong(uint8_t songNumber, uint8_t data[], uint8_t len){
			write(SongCmd);
			write(songNumber);
			write(len >> 1); // 2 bytes per note
			write_array(data, len);
		}

		void playSong(uint8_t songNumber){
			write(PlayCmd);
			write(songNumber);
		}

        void vacuum_on() {
            write(MotorsCmd);
            write(7); // Main Brush on, Vacuum on, Side brush on
            this->vacuumSensor->publish_state(true);
        }

        void vacuum_off() {
            write(MotorsCmd);
            write(0); // Main Brush off, Vacuum off, Side brush off
            this->vacuumSensor->publish_state(false);
        }

        void displayString(std::string mystring){
            write(LEDAsciiCMD);
            uint8_t asciiValue0 = (int)mystring[0];
            write(asciiValue0);
            uint8_t asciiValue1 = (int)mystring[1];
            write(asciiValue1);
            uint8_t asciiValue2 = (int)mystring[2];
            write(asciiValue2);
            uint8_t asciiValue3 = (int)mystring[3];
            write(asciiValue3);
            delay(50);         
        }

		void wake_on_dock() {
			ESP_LOGD("roomba", "wake_on_dock");
			brc_wakeup();
			// Some black magic from @AndiTheBest to keep the Roomba awake on the dock
			// See https://github.com/johnboiles/esp-roomba-mqtt/issues/3#issuecomment-402096638
			delay(10);
			write(CleanCmd); // Clean
			delay(150);
			write(DockCmd); // Dock
		}

		void alter_speed(int16_t step) {
			int16_t speed = this->speed + step;

			if (speed > 500)
				speed = 500;
			else if (speed < -500)
				speed = -500;

			this->speed = speed;
			this->driveSpeedSensor->publish_state(speed);
			this->drive(this->speed, 0);
		}

		void drive(int16_t velocity, int16_t radius) {
			write(DriveCmd);
			write((velocity & 0xff00) >> 8);
			write(velocity & 0xff);
			write((radius & 0xff00) >> 8);
			write(radius & 0xff);
		}

		void safeMode() {
			write(SafeCmd);
		}

		std::string get_oimode(uint8_t mode) {
			switch(mode) {
				case 0: return "off";
				case 1: return "passive";
				case 2: return "safe";
				case 3: return "full";
				default: return "unknown";
			}
		}

		void clean() {
			write(CleanCmd);
		}

		void maxClean() {
			write(MaxCleanCmd);
		}

		void dock() {
			write(DockCmd);
		}

		void spot() {
			write(SpotCmd);
		}

		void stop() {
			if (wasCleaning) {
				write(CleanCmd);
			}
		}

		void sleep() {
			write(PowerCmd);
		}

		void flush() {
			while (available())
			{
				read();
			}
		}

		void setDate() {
			auto time_component = id(my_time).now();

			if (time_component.is_valid()) {
				int day = (time_component.day_of_week) - 1;
				int hour = time_component.hour;
				int minute = time_component.minute;
			
				ESP_LOGI("roomba", "Setting current time: %d %02d:%02d", day, hour, minute);
				
				write(SetDateCmd);
				delay(50);
				write(day);
				delay(50);
				write(hour);
				delay(50);
				write(minute);
			} else {
				ESP_LOGI("roomba", "Time is not valid yet");
			}
		}

		bool getSensorsList(uint8_t* packetIDs, uint8_t numPacketIDs, uint8_t* dest, uint8_t len){
			write(SensorsListCmd);
			write(numPacketIDs);
			write_array(packetIDs, numPacketIDs);
			return getData(dest, len);
		}

		bool getData(uint8_t* dest, uint8_t len) {
			while (len-- > 0) {
				unsigned long startTime = millis();
				while (!available()) {
					yield();
					// Look for a timeout
					if (millis() > startTime + ROOMBA_READ_TIMEOUT)
					return false;
				}
				*dest++ = read();
			}
			return true;
		}

		std::string get_activity(uint8_t charging, int16_t current) {
			bool isCharging = charging == ChargeStateReconditioningCharging || charging == ChargeStateFullCharging || charging == ChargeStateTrickleCharging;
			
			if (current > -50)
				return "Docked";
			else if (isCharging)
				return "Charging";
			else if (current < -300)
				return "Cleaning";
			return "Lost";
		}

		inline const char* ToString(uint8_t chargeState) {
			switch (chargeState) {
				case ChargeStateNotCharging:			return "Not Charging";
				case ChargeStateReconditioningCharging:	return "Reconditioning Charging";
				case ChargeStateFullCharging:			return "Full Charging";
				case ChargeStateTrickleCharging:		return "Trickle Charging";
				case ChargeStateWaiting:				return "Waiting";
				case ChargeStateFault:					return "Fault";
				default:								return "Unknown Charging State";
			}
		}
};
1 Like

Hello,
Does this still work on ESPHome version 2025.7.3?
Thank you.

Not completely as-is. You’ll either need a workaround to enable custom components as an external component (there is an example of someone doing that in this thread using GitHub - robertklep/esphome-custom-component: Brings back support for custom ESPHome components) or wrap the old custom component into its own external component.

Well, for those in the future, I am using the custom component as said above and the custom Roomba.h that I sent in my last message. Current running on ESPHome 2025.11.0 with some tweaks to my YAML file:

substitutions:
#(...)
  init: 'RoombaComponent::instance(D5, id(uart_bus), 10000, true);'

esphome:
  name: $hostname
  comment: Roomba 870 IoT DIY integration
  friendly_name: ${hostname}
  includes:
    - esphome-roomba/Roomba.h
  platformio_options:
    build_flags: -D HAVE_HWSERIAL0

#(...)

# Serial Port
uart:
  id: uart_bus
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 115200

# Custom Roomba Component
external_components:
  - source:
      type: git
      url: https://github.com/robertklep/esphome-custom-component
    components: [ custom, custom_component ]

custom_component:
  - lambda: |-
      auto r = ${init};
      return {r};
    components:
      - id: my_roomba

binary_sensor:
  - platform: custom
    lambda: |-
        auto r = ${init}
        return {r->vacuumSensor, r->virtualWallSensor, r->chargingSourcesSensor};
    binary_sensors:
      - name: "${PrefixoNome} Vacuum State"
        id: "${id}_vacuum"
      - name: "${PrefixoNome} Virtual Wall"
      - name: "${PrefixoNome} Charging Source Available"

sensor:
  - platform: custom
    lambda: |-
        auto r = ${init}
        return {r->voltageSensor, r->currentSensor, r->batteryChargeSensor, r->batteryCapacitySensor, r->batteryPercentSensor, r->batteryTemperatureSensor, r->driveSpeedSensor, r->rightMotorCurrentSensor, r->leftMotorCurrentSensor, r->mainBrushCurrentSensor, r->sideBrushCurrentSensor};
    sensors:
      - name: "${PrefixoNome} Voltage"
        unit_of_measurement: "V"
        icon: mdi:sine-wave
        accuracy_decimals: 2
      - name: "${PrefixoNome} Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 3
      - name: "${PrefixoNome} Charge"
        unit_of_measurement: "Ah"
        icon: mdi:battery-charging
        accuracy_decimals: 2
      - name: "${PrefixoNome} Capacity"
        unit_of_measurement: "Ah"
        icon: mdi:battery
        accuracy_decimals: 2
      - name: "${PrefixoNome} Battery"
        unit_of_measurement: "%"
        state_class: "measurement"
        device_class: battery
        icon: mdi:battery-outline
        accuracy_decimals: 0
      - name: "${PrefixoNome} Temperature"
        unit_of_measurement: "°C"
        icon: mdi:thermometer
        accuracy_decimals: 0
      - name: "${PrefixoNome} Drive Speed"
        unit_of_measurement: "mm/s"
        accuracy_decimals: 0
      - name: "${PrefixoNome} Right Motor Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Left Motor Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Main Brush Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2
      - name: "${PrefixoNome} Side Brush Current"
        unit_of_measurement: "A"
        icon: mdi:lightning-bolt
        accuracy_decimals: 2

text_sensor:
  - platform: custom
    lambda: |-
      auto r = ${init}
      return {r->chargingSensor, r->activitySensor, r->oiModeSensor, r->buttonsSensor};
    text_sensors:
      - name: "${PrefixoNome} Charging State"
      - name: "${PrefixoNome} Activity"
      - name: "${PrefixoNome} OI Mode"
        id: "${id}_oi_mode"
      - name: "${PrefixoNome} Pressed Button"

button:
  - platform: template
    name: "${PrefixoNome} Clean"
    id: clean
    on_press:
      then:
      - lambda: |-
          auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
          roomba->send_command("clean");

  - platform: template
    name: "${PrefixoNome} Max Clean"
    id: max_clean
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("max_clean");
  
  - platform: template
    name: "${PrefixoNome} Dock"
    id: dock
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("dock");
  
  - platform: template
    name: "${PrefixoNome} Stop"
    id: stop
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("stop");
  
  - platform: template
    name: "${PrefixoNome} Wake Up"
    id: wakeup
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("wakeup");

  - platform: template
    name: "${PrefixoNome} Wake Up on Dock"
    id: wakeup_on_dock
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("wake_on_dock");

  - platform: template
    name: "${PrefixoNome} Sleep"
    id: roomba_sleep
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("sleep");

  - platform: template
    name: "${PrefixoNome} Locate"
    id: locate
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("locate");  
            
  - platform: template
    name: "${PrefixoNome} Clean Spot"
    id: clean_spot
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("clean_spot");

  - platform: template
    name: "${PrefixoNome} Forward"
    id: go_forward
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_forward");

  - platform: template
    name: "${PrefixoNome} Full Speed"
    id: go_max
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_max");

  - platform: template
    name: "${PrefixoNome} Go Faster"
    id: go_faster
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_faster");

  - platform: template
    name: "${PrefixoNome} Go Slower"
    id: go_slower
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_slower");

  - platform: template
    name: "${PrefixoNome} Turn Left"
    id: go_left
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_left");

  - platform: template
    name: "${PrefixoNome} Turn Right"
    id: go_right
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_right");

  - platform: template
    name: "${PrefixoNome} Stop Driving"
    id: stop_driving
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("stop_driving");

  - platform: template
    name: "${PrefixoNome} Reverse"
    id: go_reverse
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("go_reverse");

  - platform: template
    name: "${PrefixoNome} Rotate Left"
    id: rotate_left
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("rotate_left");

  - platform: template
    name: "${PrefixoNome} Rotate Right"
    id: rotate_right
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("rotate_right");
  
  - platform: template
    name: "${PrefixoNome} Restart Roomba"
    id: restart_roomba
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("reset");
  
  - platform: template
    name: "${PrefixoNome} Poweroff Roomba"
    id: poweroff_roomba
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("poweroff");

  - platform: template
    name: "${PrefixoNome} Set Date"
    id: set_date
    on_press:
      then:
        - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("set_date");

switch:
  - platform: template
    name: "${PrefixoNome} Vacuum"
    lambda: |-
      return id(${id}_vacuum).state;
    turn_on_action:
      - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("vacuum_on");
    turn_off_action:
      - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("vacuum_off");
  
  - platform: template
    name: "${PrefixoNome} Safe Mode"
    lambda: |-
      if (id(${id}_oi_mode).state == "safe") {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("safe");
    turn_off_action:
      - lambda: |-
            auto roomba = static_cast<RoombaComponent*>(id(my_roomba));
            roomba->send_command("passive");

*Note: just sharing the important part for the roomba, adapt to your implementation if you plan on using it

1 Like