Vacuum cleaner has jobs to be done

After I gained root access to my vacuum cleaner robot, a new world of opportunities emerges. This is the great aspect of owning your own home devices. And it is geometric. After you add a new liberated device to your home mesh, you can start thinking on all the combinations among your other devices, even if they won’t make any sense. Vacuum cleaner + thermostat? So you clean the room when it reaches some temperature? It doesn’t seem to go anywhere. Vacuum cleaner + roller shutters, so you clean after you close them? Neither it does. Vacuum cleaner + mobile presence? Hmm.. that might make sense!

But beyond the solutions looking for a problem, I am really committed to find real, practical use cases. And I found 2 great cases for squeezing the new liberated capabilities:

  • Starting house cleanup after we leave the house. It takes some time for the robot to clean the house. And you want it to do it while you are not at home. But not every time you leave, it depends on the last time you cleaned it, how long you are going to be out… So you want to tell the robot, Hey! We are leaving the house shortly! When we leave, start cleaning the house!
  • Choosing what rooms to clean. This fits in my house cleaning routine, usually on the weekend. I start organizing rooms, making the beds, cleaning the dust… After I finish one room, I want to send the robot to that room. If I finish the next room, I want to queue it after the other. This capability is not available in the robot nor in the app. You can tell the robot to clean several rooms at once, but not queuing rooms on the fly

Controlling the robot

After you root the robot, there are several alternatives to control it:

  • python-miio. A command line tool for communicating with Xiaomi devices. It supports many different gadgets, from light bulbs to air purifiers, and vacuum cleaners. You need to get an authentication token, which can be be obtained from the robot if you have rooted it, or from the android app. In the case of It doesn’t work very well with the Viomi V7, since it doesn’t get track of the sequence id. You need to reboot the robot to use the command line tool again
  • OpenHAB Xiaomi Mi IO Binding There is one Xiaomi addon for OpenHAB, which works pretty well. It also needs the authentication token to communicate with the robot. The basic commands are already available, and you can always send custom commands, which people have extracted from the binaries. The problem with this option is the robot state availability. It does not work in a push way, from the robot to OpenHAB. You need to configure the interval in which OpenHAB will ask to robot, and will update the OpenHAB things with the new values. This is not very practical to implement real time things, like updating cleaning rooms queue.
  • Valetudo Valetudo provides 2 more ways to communicate with the robot
    • REST API. Valetudo exposes a complete Swagger enabled API documented API with all the capabilities of the robot
    • MQTT You can also configure a MQTT server, and Valuetudo will publish all the robot information to the MQTT server. It will also listen for commands. This is great for push notifications, you can subscribe to MQTT topics, instead of pulling the information from the robot. Valetudo will publish robot states in real time

Implementation

With these tools, I implemented the 2 functionalities I described before in my kitchen dashboard

Cleanup after we leave the house

I added a new button in the dashboard. When you push it, it just publishes a true value on a MQTT topic. A rule in OpenHAB will check this value when our presence group turns off. That is, we left the house. If this value is set, it will tell the the robot to start cleaning the house

rule "start when leaving"
when
  Item our_presence changed to OFF
then
  if (robotito_start_when_leaving.state == ON) {
    robotito_commands.sendCommand("set_mode_withroom [0, 1, 0]")
    robotito_start_when_leaving.sendCommand(OFF)
  }
end

House map

I implemented a map of my house in the kitchen dashboard. This way, I can just touch the rooms in the map to send the robot to clean them. There is a MQTT channel for this, pending_rooms, where a comma-separated list of rooms ids gathers the rooms that need to be cleaned

A set of rules in OpenHAB watches for the changes in both, the robot state (via Valetudo MQTT) and the pending_rooms channel. The set of rules allows to enqueue rooms, remove rooms in the middle of the clean, or canceling and send the robot to the dock.

In order to build the set of rooms, I needed to draw the states of the robot. This was very useful to program the rules later

Flowchart after pending_rooms changes
Flowchart after robot state changes

I programmed the rules using the native OpenHAB rules DSL. It is based in Xtend. It was a real mess to program the rules. I hate Xtend. I find it a real nightmare. It is very limited. It took me several hours just to figure out how to do an Array unshift. I tried to install JS or Python rules engines, but it required to many additional steps

Anyway, I manage to get the functionality with the following rules file

// Commands
var stop = "set_mode [0]"
var home = "set_charge [1]"

var currentRoom = ''
var willClean = false

rule "pendingrooms changed"
when
  Item robotito_pendingrooms changed
then
  var rooms = robotito_pendingrooms.state.toString
  logError("robotito", "robotito_pendingrooms changed to: " + rooms)
  var state = valetudo_robotito_status.state.toString

  if (state == "returning") {
    if (rooms == "") {
      logError("robotito", "state is returning and no pending rooms. pass")
    } else {
      logError("robotito", "state is returning and there are pending rooms. stopping")

      robotito_commands.sendCommand(stop)
    }
    
    return;
  }

  if (state == "idle" || state == "docked" || state == "stop") {
    if (rooms == "") {
      logError("robotito", "rooms are empty. pass")

      return;
    }
    
    var firstRoom = rooms.split(",").get(0)

    logError("robotito", "state was: " + state + ". cleaning first pending room: " + firstRoom)

    currentRoom = firstRoom
    
    robotito_commands.sendCommand("set_mode_withroom [0, 1, 1, " + currentRoom + "]")

    return;
  }

  if (state == "cleaning") {
    var list = rooms.split(",")
    var firstRoom = list.get(0)

    if (firstRoom == currentRoom) {
      logError("robotito", "state was cleaning and firstRoom is currentRoom: " + currentRoom + ". pass")
      return;
    }

    if (rooms == "") {
      logError("robotito", "state was cleaning and firstRoom is empty. sending home")

      currentRoom = ""

      robotito_commands.sendCommand(home)
    } else {
      logError("robotito", "state was cleaning and firstRoom is not empty nor currentRoom. stopping to change room")

      currentRoom = ""

      robotito_commands.sendCommand(stop)
    }
   return;
  }

  logError("robotito", "unknown robotito state: " + state)
end

rule "state changed"
when
  Item valetudo_robotito_status changed
then
  var state = valetudo_robotito_status.state.toString
  var rooms = robotito_pendingrooms.state.toString

  logError("smarthome.event", "robotito state changed to " + state)


  if (state == "cleaning") {
    willClean = false
    logError("robotito", "robotito state changed to cleaning. pass")
   
    return;
  }

  if (state == "returning") {
    if (willClean) {
      logError("robotito", "state changed to returning, but will clean. pass")
      willClean = false
      return;
    }

    if (rooms == "") {
      logError("robotito", "robotito state changed to returning, and no rooms pending. pass")
      
      return;
    }

    if (rooms.split(",").size() < 2) {
      logError("robotito", "robotito state changed to returning, and room " + rooms + " was just cleaned. reseting pending rooms and pass")

      currentRoom = ""
      robotito_pendingrooms.sendCommand("")

      return;
    }

    logError("robotito", "robotito state changed to returning, and there are pending rooms: " + rooms + " stopping")

    robotito_commands.sendCommand(stop)
  
    return;
  }

  if (state == "idle") {
    if (rooms == "") {
      logError("robotito", "robotito state changed to idle, and no rooms pending. pass")
    } else {
      willClean = true
      logError("robotito", "willClean set to true")

      if (currentRoom == "") {
        var firstRoom = rooms.split(",").get(0)

        logError("robotito", "state was idle, but currentRoom was empty. start cleaning first pending room: " + firstRoom)

        currentRoom = firstRoom
    
        robotito_commands.sendCommand("set_mode_withroom [0, 1, 1, " + currentRoom + "]")
      } else {
        logError("robotito", "robotito state changed to idle, there was currentRoom: " + currentRoom + " and there are pending rooms: " + rooms + ". jumping to next room")
        var list = rooms.split(",")
        var newPendingRooms = ""
        for (var i = 1; i < list.size(); i++) {
          newPendingRooms = newPendingRooms + list.get(i)

          logError("robotito", "i: " + i)
          logError("robotito", "setting newPendingRooms: " + newPendingRooms)
          if (i != list.size() - 1) {
            newPendingRooms = newPendingRooms + ","
          }
        }
  
        robotito_pendingrooms.sendCommand(newPendingRooms)
      }
    }

    return;
  }

  logError("robotito", "robotito state changed to " + state + ". not addressed")
end

These two functionalities are pretty handy and show up the benefits of controlling your things. Besides the peace of knowing what it is going on inside your own devices.