├── .gitignore ├── ESPHomeRoombaComponent.h ├── LICENSE.md ├── README.md └── example ├── .gitignore ├── homeassistant-vacuum.yaml └── roomba.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | example/.esphome 3 | example/roomba 4 | example/secrets.yaml -------------------------------------------------------------------------------- /ESPHomeRoombaComponent.h: -------------------------------------------------------------------------------- 1 | #include "esphome.h" 2 | #include 3 | 4 | static const char *TAG = "component.Roomba"; 5 | class RoombaComponent : public PollingComponent, public CustomMQTTDevice 6 | { 7 | protected: 8 | uint8_t brcPin; 9 | uint32_t updateInterval; 10 | std::string stateTopic; 11 | std::string commandTopic; 12 | Roomba roomba; 13 | 14 | public: 15 | Sensor *distanceSensor; 16 | Sensor *voltageSensor; 17 | Sensor *currentSensor; 18 | Sensor *chargeSensor; 19 | Sensor *capacitySensor; 20 | BinarySensor *chargingBinarySensor; 21 | BinarySensor *dockedBinarySensor; 22 | BinarySensor *cleaningBinarySensor; 23 | 24 | static RoombaComponent* instance(const std::string &stateTopic, const std::string &commandTopic, uint8_t brcPin, uint32_t updateInterval) 25 | { 26 | static RoombaComponent* INSTANCE = new RoombaComponent(stateTopic, commandTopic, brcPin, updateInterval); 27 | return INSTANCE; 28 | } 29 | 30 | void setup() override 31 | { 32 | ESP_LOGD(TAG, "Setting up roomba."); 33 | pinMode(this->brcPin, OUTPUT); 34 | digitalWrite(this->brcPin, HIGH); 35 | 36 | this->roomba.start(); 37 | 38 | ESP_LOGD(TAG, "Attempting to subscribe to MQTT."); 39 | 40 | subscribe(this->commandTopic, &RoombaComponent::on_message); 41 | } 42 | 43 | void update() override 44 | { 45 | ESP_LOGD(TAG, "Attempting to update sensor values."); 46 | 47 | int16_t distance; 48 | uint16_t voltage; 49 | int16_t current; 50 | uint16_t charge; 51 | uint16_t capacity; 52 | uint8_t charging; 53 | bool cleaningState; 54 | bool dockedState; 55 | bool chargingState; 56 | bool publishJson; 57 | // Flush serial buffers 58 | while (Serial.available()) 59 | { 60 | Serial.read(); 61 | } 62 | 63 | uint8_t sensors[] = { 64 | Roomba::SensorDistance, // 2 bytes, mm, signed 65 | Roomba::SensorChargingState, // 1 byte 66 | Roomba::SensorVoltage, // 2 bytes, mV, unsigned 67 | Roomba::SensorCurrent, // 2 bytes, mA, signed 68 | Roomba::SensorBatteryCharge, // 2 bytes, mAh, unsigned 69 | Roomba::SensorBatteryCapacity // 2 bytes, mAh, unsigned 70 | }; 71 | uint8_t values[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; 72 | 73 | // Serial reading timeout -- https://community.home-assistant.io/t/add-wifi-to-an-older-roomba/23282/52 74 | bool success = this->roomba.getSensorsList(sensors, sizeof(sensors), values, sizeof(values)); 75 | if (!success) 76 | { 77 | ESP_LOGD(TAG, "Unable to read sensors from the roomba."); 78 | return; 79 | } 80 | 81 | distance = values[0] * 256 + values[1]; 82 | voltage = values[3] * 256 + values[4]; 83 | current = values[5] * 256 + values[6]; 84 | charge = values[7] * 256 + values[8]; 85 | capacity = values[9] * 256 + values[10]; 86 | charging = values[2]; 87 | 88 | cleaningState = current < -300; 89 | dockedState = current > -50; 90 | chargingState = charging == Roomba::ChargeStateReconditioningCharging || charging == Roomba::ChargeStateFullChanrging || charging == Roomba::ChargeStateTrickleCharging; 91 | 92 | // Only publish new states if there was a change 93 | if (this->distanceSensor->state != distance) { 94 | this->distanceSensor->publish_state(distance); 95 | } 96 | 97 | if (this->voltageSensor->state != voltage) { 98 | this->voltageSensor->publish_state(voltage); 99 | } 100 | 101 | if (this->currentSensor->state != current) { 102 | this->currentSensor->publish_state(current); 103 | } 104 | 105 | if (this->chargeSensor->state != charge) { 106 | this->chargeSensor->publish_state(charge); 107 | } 108 | 109 | if (this->capacitySensor->state != capacity) { 110 | this->capacitySensor->publish_state(capacity); 111 | } 112 | 113 | if (this->chargingBinarySensor->state != chargingState) { 114 | this->chargingBinarySensor->publish_state(chargingState); 115 | } 116 | 117 | if (this->dockedBinarySensor->state != dockedState) { 118 | this->dockedBinarySensor->publish_state(dockedState); 119 | } 120 | 121 | if (this->cleaningBinarySensor->state != cleaningState) { 122 | this->cleaningBinarySensor->publish_state(cleaningState); 123 | } 124 | 125 | static std::string lastBatteryLevel = "0.0"; 126 | static std::string lastState; 127 | std::string batteryLevel = value_accuracy_to_string(100.0 * ((1.0 * charge) / (1.0 * capacity)), 2); 128 | std::string state = cleaningState ? "cleaning" : 129 | dockedState ? "docked" : 130 | chargingState ? "idle" : 131 | "idle"; 132 | 133 | // Publish to the state topic a json document; necessary for the 'state' schema 134 | if (batteryLevel != lastBatteryLevel || state != lastState) { 135 | lastBatteryLevel = batteryLevel; 136 | lastState = state; 137 | 138 | publish_json(this->stateTopic, [=](JsonObject &root) { 139 | root["battery_level"] = parse_float(batteryLevel).value(); 140 | root["state"] = state; 141 | root["fan_speed"] = "off"; 142 | }); 143 | } 144 | } 145 | 146 | void on_message(const std::string &payload) 147 | { 148 | ESP_LOGD(TAG, "Got values %s", payload.c_str()); 149 | 150 | // Roomba Wakeup 151 | digitalWrite(this->brcPin, HIGH); 152 | delay(100); 153 | digitalWrite(this->brcPin, LOW); 154 | delay(500); 155 | digitalWrite(this->brcPin, HIGH); 156 | delay(100); 157 | 158 | if (payload == "turn_on" || payload == "turn_off" || 159 | payload == "start" || payload == "stop") 160 | { 161 | this->roomba.cover(); 162 | } 163 | else if (payload == "dock" || payload == "return_to_base") 164 | { 165 | this->roomba.dock(); 166 | } 167 | else if (payload == "locate") 168 | { 169 | this->roomba.playSong(1); 170 | } 171 | else if (payload == "spot" || payload == "clean_spot") 172 | { 173 | this->roomba.spot(); 174 | } 175 | else 176 | { 177 | ESP_LOGW(TAG, "Received unknown status payload: %s", payload.c_str()); 178 | this->status_momentary_warning("state", 5000); 179 | } 180 | 181 | delay(500); 182 | this->update(); 183 | } 184 | 185 | private: 186 | RoombaComponent(const std::string &stateTopic, const std::string &commandTopic, uint8_t brcPin, uint32_t updateInterval) : 187 | PollingComponent(updateInterval), roomba(&Serial, Roomba::Baud115200) 188 | { 189 | this->brcPin = brcPin; 190 | this->updateInterval = updateInterval; 191 | this->stateTopic = stateTopic; 192 | this->commandTopic = commandTopic; 193 | 194 | this->distanceSensor = new Sensor(); 195 | this->voltageSensor = new Sensor(); 196 | this->currentSensor = new Sensor(); 197 | this->chargeSensor = new Sensor(); 198 | this->capacitySensor = new Sensor(); 199 | this->chargingBinarySensor = new BinarySensor(); 200 | this->dockedBinarySensor = new BinarySensor(); 201 | this->cleaningBinarySensor = new BinarySensor(); 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2018 Dustin Brewer 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unsupported 2 | 3 | Due to a botched surgery, I no longer have a Roomba 650. Leaving it for for future DIYers. 4 | 5 | # Roomba Component for ESPHome 6 | 7 | A barebones bit of glue between ESPHome and Mike McCauley's Roomba library that enables control of a Roomba 650 via MQTT. 8 | 9 | Last tested with ESPHome 1.13.x and Mike McCauley's Roomba Library 1.3. 10 | 11 | The inspiration is [John Boiles' esp-roomba-mqtt project](https://github.com/johnboiles/esp-roomba-mqtt), but I want all my IoT devices driven by ESPHome. 12 | 13 | ## Hardware 14 | 15 | ## Wiring Guide 16 | John Boiles' wiring guide was the basis for my setup. 17 | 18 | The main difference is that John Boiles' repo uses a voltage divider on the Roomba's TX pin, while I used a PNP transistor. I used a transistor because the voltage divider was not working for me, and I found a Roomba Create document with the following information: 19 | 20 | > In some cases, the drive strength of a Roomba TX line is not enough to drive the RX line of another board (for example, in some revisions of Arduino). In this case, a simple PNP transistor (2N2907A, 2N3906, or 2N4403, among others) can be used to provide enough “drive” for the Arduino. 21 | > 22 | > Source: [Roomba Create® 2 to 5V Logic](http://www.irobot.com/~/media/MainSite/PDFs/About/STEM/Create/Create_2_to_5V_Logic.pdf) 23 | 24 | ## Alternative Wiring Guide 25 | One might find this [alternative wiring guide](https://i.stack.imgur.com/aaifY.jpg) easier to get started with. It has fewer parts required and uses a PNP transistor. 26 | 27 | ## Special Notes 28 | 29 | * Many PNP Transistors will work; **pay attention** to the pinout of the specific transistor you choose to use. 30 | * Many pins can be the BRC pin; update [roomba.yaml](example/roomba.yaml#L13) with the pin you choose. 31 | The example matches John Boiles' chosen pin D5/GPIO14. 32 | 33 | ## Placement 34 | 35 | The Wemos D1 mini is small enough to [fit into the compartment by one of the wheels](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/optimized/2X/a/a258c7253f8bd3fe76ad9e7aa1202b60bd113d74_2_496x600.jpg). 36 | _Source:_ [Add wifi to an older roomba](https://community.home-assistant.io/t/add-wifi-to-an-older-roomba/23282) community project thread on the Home Assistant forum. 37 | 38 | 39 | ## Software Setup/Use 40 | 41 | Take a look at the example directory for a fully working example. 42 | 43 | * - Contains the bits needed for ESPHome. 44 | * - Contains the bits needed to integrate as a "MQTT Vacuum" in Home Assistant. 45 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | **/.pioenvs/ 6 | **/.piolibdeps/ 7 | **/lib/ 8 | **/src/ 9 | **/platformio.ini 10 | /secrets.yaml 11 | -------------------------------------------------------------------------------- /example/homeassistant-vacuum.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - platform: mqtt 3 | name: "Roomba" 4 | schema: state 5 | supported_features: 6 | - start 7 | - stop 8 | - return_home 9 | - battery 10 | - clean_spot 11 | # IMPORTANT NOTE: If you change the substitution name value in roomba.yml, the availability_topic/state_topic/command_topic need to change as well. 12 | availability_topic: "roomba_001/status" 13 | state_topic: "roomba_001/state" 14 | command_topic: "roomba_001/command" -------------------------------------------------------------------------------- /example/roomba.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: roomba_001 3 | platform: ESP8266 4 | board: d1_mini 5 | includes: 6 | - ../ESPHomeRoombaComponent.h 7 | libraries: 8 | - Roomba=https://github.com/Apocrathia/Roomba 9 | 10 | substitutions: 11 | # IMPORTANT NOTE: If you change the substitution name value, the state_topic/command_topic need to change as well. 12 | name: "roomba_001" 13 | # state topic, command topic, BRC pin, polling interval in milliseconds 14 | init: 'RoombaComponent::instance("roomba_001/state", "roomba_001/command", D5, 1000);' 15 | 16 | wifi: 17 | ssid: !secret wifi_ssid 18 | password: !secret wifi_password 19 | 20 | mqtt: 21 | broker: !secret mqtt_broker 22 | discovery: true 23 | 24 | logger: 25 | baud_rate: 0 26 | level: info 27 | 28 | ota: 29 | 30 | custom_component: 31 | - lambda: |- 32 | auto r = ${init} 33 | return {r}; 34 | 35 | sensor: 36 | - platform: custom 37 | lambda: |- 38 | auto r = ${init} 39 | return {r->distanceSensor, r->voltageSensor, r->currentSensor, r->chargeSensor, r->capacitySensor}; 40 | 41 | sensors: 42 | - name: "${name} distance" 43 | unit_of_measurement: "mm" 44 | accuracy_decimals: 0 45 | - name: "${name} voltage" 46 | unit_of_measurement: "mV" 47 | accuracy_decimals: 0 48 | - name: "${name} current" 49 | unit_of_measurement: "mA" 50 | accuracy_decimals: 0 51 | - name: "${name} charge" 52 | unit_of_measurement: "mAh" 53 | accuracy_decimals: 0 54 | - name: "${name} capacity" 55 | unit_of_measurement: "mAh" 56 | accuracy_decimals: 0 57 | 58 | binary_sensor: 59 | - platform: status 60 | name: "${name} Status" 61 | 62 | - platform: custom 63 | lambda: |- 64 | auto r = ${init} 65 | return {r->chargingBinarySensor, r->dockedBinarySensor, r->cleaningBinarySensor}; 66 | 67 | binary_sensors: 68 | - name: "${name} charging" 69 | - name: "${name} docked" 70 | - name: "${name} cleaning" 71 | --------------------------------------------------------------------------------