Roomba 980 Wifi Connectivity Reverse engineering

Yeah is it literally “user:[password]” in 64? I might have used [user]:[password]

Changed to the latter and after resetting the Roomba it is working now! Thanks!

yes, literally. Do not add your actual user name in this string.

@M.a.S.e
I posted a device handler code which allows you to controll the roomba directly through ST hub.

Thank you all the guidance in this thread. Got it to work.

I got my blid/pass with dorita980 and can get the roomba to work through the simulator in the IDE. How do I get the device to show up in my phone app? I published the device handler.

EDIT: Nevermind. New to SmartThings here. Had to add the device. Thanks everybody for all the hard work.

Hi all,

here is another simple way to get stuff running without node.js and all the other things (although this is a grrrreeeeaaaat reference!). Use a browser plugin, like e.g. HttpRequester, and the send the commands to your Roomba 980:

1. Get the password:
(Replace 192.168.10.135 with your robots IP address)
(Place the robot on the home base and press the HOME button for about 2 seconds until a series of tones is played and the WIFI light flashes, then hurry to send the POST command)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 37

{"do":"get","args":["passwd"],"id":1}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":{"passwd":"a1bcdEF23GhIj4KL"},"id":1}

2. Get the username/blid:
(Replace 192.168.10.135 with your robots IP address)
(For the Authorization header, you must Base64 encode the string “user:”+ThePasswordReceivedFromStep1, e.g. Base64(user:a1bcdEF23GhIj4KL) -> dXNlcjphMWJjZEVGMjNHaElqNEtM)
(The decimal “blid” values in the response must be hex encoded to receive the username: 43,6,75,31,32,127,12,132 -> 2B064B1F207F0C84)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 34
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"get","args":["sys"],"id":2}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":{"umi":2,"pid":2,"blid":[43,6,75,31,32,127,12,132],"sw":"v1.2.9","cfg":0,"boot":4042,"main":4313,"wifi":517,"nav":"01.08.04","ui":2996,"audio":32,"bat":"lith"},"id":2}

3. Start the robots clean job:
(Replace 192.168.10.135 with your robots IP address)
(See comment of Step 2 for the Authorization header)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 49
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"set","args":["cmd" {"op":"start"}],"id":3}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":null,"id":3}

4. Pause the robots clean job:
(Replace 192.168.10.135 with your robots IP address)
(See comment of Step 2 for the Authorization header)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 49
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"set","args":["cmd" {"op":"pause"}],"id":4}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":null,"id":4}

5. Resume the robots clean job:
(Replace 192.168.10.135 with your robots IP address)
(See comment of Step 2 for the Authorization header)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 50
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"set","args":["cmd" {"op":"resume"}],"id":5}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":null,"id":5}

6. Stop the robots clean job:
(Replace 192.168.10.135 with your robots IP address)
(See comment of Step 2 for the Authorization header)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 48
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"set","args":["cmd" {"op":"stop"}],"id":6}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":null,"id":6}

7. Force the robot to return back to its home base:
(Replace 192.168.10.135 with your robots IP address)
(See comment of Step 2 for the Authorization header)

POST https://192.168.10.135/umi
Content-Type: application/json
Connection: close
User-Agent: aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0
Content-Encoding: identity
Accept: */*
Accept-Language: en-us
Host: 192.168.10.135
content-length: 48
Authorization: Basic dXNlcjphMWJjZEVGMjNHaElqNEtM

{"do":"set","args":["cmd" {"op":"dock"}],"id":7}

– Response: –

200 OK
Server:  Marvell-WM
Connection:  close
Transfer-Encoding:  chunked
Content-Type:  application/json

{"ok":null,"id":7}
4 Likes

Thanks for all the great info.

I am trying to send the commands with HttpRequester, but I am getting a response of 0.

I copied and pasted the data into the raw transaction but afte I execute it, it comes back with
– response –
0

Yes, I did change the IP address to that of my Roomba.

Any suggestion?

Sure. I have suggestions:
I guess what you did was just copying the whole request stuff that I showed in the “Content to Send”-TextBox section in HttpRequester. This way you will get a 0 response. You have to set each header separately, one-by-one, in the “Header”-tab.

So, you have to set the URL in the “URL”-dropdown-box, then the headers “Connection”, “User-Agent”, …, “content-length”, etc. in the “Headers”-tab along with the respective values. The only thing that you need to copy to the “Content to Send”-textbox is this:

{"do":"get","args":["passwd"],"id":1}

Don´t forget to insert this carriage-return before {"do…1}.

2 Likes

Hello,

Thanks for the reply.

I have tried entering them individually into the header tab, but still get a response of 0.

Any other suggestions?
Thanks again for the help.

Update I tried using Postman (Chrome ext) and it worked for me. Not sure why me and HttpRequester could not get along, but Thanks again!

Hi,

everything seems to look correct. And if you just enter the URL https://192.168.0.50/umi in your browser? What is the answer? Something like “File not found”? If not, then you seem to have network problems… Can you ping the robot under this address?

Kind regards,
Rene

Thanks for the fast response.

I ended up following your steps but using Postman (Chrome ext) instead of HttpRequester and it worked for me.

Thanks for the help!

I am getting the error “something went wrong: groovyx.net.http.HttpResponseException: Request contains unknown asset ID” when I run the simulator in Smart Things.

I am pretty confident I have the right AssetID. . ElPaso@irobot!xxx with the xxx replaced with the blid value I got.

I know my blid and password are correct because when I use a POST command I am able to start my roomba.

Any suggestions?

Update Changed my BLID to uppercase and now all is well.

I figured it out.

I was using lowercase characters in my BLID. Changed them to uppercase and now it works.

You guys do some great work on here. Keep it up!

Thanks for putting this together. I got this installed and working, but I am struggling to understand how to get this added to my devices in the smartthings app. I was able to get the vacuum running from the smartthings ide, but it isn’t appearing in the app. Do I need to have a smartapp set up? sorry for the noob question. Thanks for your help!

I’m not sure to understand your issue but maybe this will help (and my apologies if you already knew this) : once you created a device handler, you have to create a virtual device (in the devices menu in IDE, then click “new device”). You will be given the opportunity to pick the “driver” i.e. “device handler” you just created in a list of hundreds of devices (providing you didn’t forget to hit the publish button when you created the device handler). Once this is done, you should see it as a new device to be configured in the app, just like any other device. You might need to hit the “find new device” button, but most of the times now it shows itself in the list of devices directly (in the iphone app at least).
Hope this helps

Got it! Awesome! Thanks so much!

This worked great for me.

Questions: How responsive did everyone find this solution? I felt like it lagged really bad… (Not blaming the app itself, I realize there are many layers its passing through)

And lastly, has anyone looked into updating the app to show the “status” of what is running/happening to the Roomba? This implementation is purely sending commands. But doesn’t show us anything.

Just curious. Love it though, thanks guys!

This looks like great work thanks for all your efforts it is terrible that iRobot does not provide an API for this model.

For another alternative you can get a Roomba 805 from Costco (I was told by iRobot that 8xx series are the same they just have different accessories) for $400 (currently $350 on sales) and the Thinking Cleaner wifi module for $99. That makes the solution a few hundred cheaper and below is the latest 2.x code for smart things integration:

Not sure if you can modify the above code for the UI and status tile as @darrylb has suggested.

Hi all, I’ve improved upon Elfege’s device type to make a larger state machine out of the Roomba virtual switch so that it more closely follows the iRobot App experience.

Here is my unpublished github repo and here is the code block for copy/paste consumption:
Update 1: Included a ‘bin full’ status
Update 2: More error handling, included some additional valueTile, made pollingInterval a setting (default - 4min)
Update 3: Added the Consumable capability in order to allow CORE SmartApps to be used for notifications!

/**
*  Roomba 9xx - Virtual Switch
*
*  Copyright 2016 Steve-Gregory
*
*  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
*  in compliance with the License. You may obtain a copy of the License at:
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
*  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
*  for the specific language governing permissions and limitations under the License.
*
*/
metadata {
    definition (name: "Roomba 9xx - Virtual Switch", namespace: "Steve-Gregory", author: "Steve-Gregory") {
        capability "Switch"
        capability "Refresh"
        capability "Polling"
        capability "Consumable"
        capability "Configuration"

        command "dock"
        command "refresh"
        command "resume"
        command "pause"

        attribute "batteryLevel", "number"
        attribute "totalJobs", "number"
        attribute "totalJobHrs", "number"
        attribute "headline", "string"
        attribute "robotName", "string"
        attribute "preferences_set", "string"
        attribute "status", "string"
    }
}
// simulator metadata
simulator {
}
//Preferences
preferences {
        section("Roomba Credentials") {
            input title: "Roomba Credentials", description: "The username/password can be retrieved via node.js & dorita980", displayDuringSetup: true, type: "paragraph", element: "paragraph"
            input "roomba_username", "text", title: "Roomba username/blid", required: true, displayDuringSetup: true
            input "roomba_password", "password", title: "Roomba password", required: true, displayDuringSetup: true
            input "roomba_host", "string", title:"Roomba Host (Default: Use the Cloud)", defaultValue:""
        }
        section("Misc.") {
            input title: "Polling Interval", description: "This feature allows you to change the frequency of polling for the robot in minutes (1-59)", displayDuringSetup: true, type: "paragraph", element: "paragraph"
            input "pollInterval", "number", title: "Polling Interval", description: "Change polling frequency (in minutes)", defaultValue:4, range: "1..59", required: true, displayDuringSetup: true
        }
}
// Settings updated
def updated() {
    //log.debug "Updated settings ${settings}..
    schedule("0 0/${settings.pollInterval} * * * ?", poll)  // 4min polling is normal for irobots
    poll()
}
// Configuration
def configure() {
    log.debug "Configuring.."
    poll()
}
//Consumable
def setConsumableStatus(statusString) {
    log.debug "User requested setting the Consumable Status - ${statusString}"
    def status = device.latestValue("status")
    log.debug "Setting value based on last roomba state - ${status}"
    if(status == "bin-full") {
        return "maintenance_required"
    } else {
        return "good"
    }
}
//Refresh
def refresh() {
    log.debug "Executing 'refresh'"
    poll()
}
//Polling
def poll() {
    log.debug "Polling for status ----"
    sendEvent(name: "headline", value: "Polling the API", displayed: false)
    state.RoombaCmd = "getStatus"
    apiGet()
}
// UI tile definitions
tiles {

    multiAttributeTile(name:"CLEAN", type:"lighting", width: 6, height: 4, canChangeIcon: true) {
        tileAttribute("device.status", key: "PRIMARY_CONTROL") {
            attributeState "error", label: 'Error', icon: "st.switches.switch.off", backgroundColor: "#bc2323" // No action allowed here
            attributeState "bin-full", label: 'Bin Full', icon: "st.switches.switch.off", backgroundColor: "#bc2323" // No action allowed here
            attributeState "docked", label: 'Start Clean', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "starting"
            attributeState "docking", label: 'Docking', icon: "st.switches.switch.off", backgroundColor: "#ffa81e" // No action allowed here
            attributeState "starting", label: 'Starting Clean', icon: "st.switches.switch.off", backgroundColor: "#ffffff"
            attributeState "cleaning", label: 'Stop Clean', action: "stop", icon: "st.switches.switch.on", backgroundColor: "#79b821"
            attributeState "pausing", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821" // No action allowed here
            attributeState "paused", label: 'Send Home', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "docking"
            attributeState "resuming", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821" // No action allowed here
        }
        tileAttribute("device.headline", key: "SECONDARY_CONTROL") {
           attributeState "default", label:'${currentValue}'
        }
        tileAttribute("device.batteryLevel", key: "SLIDER_CONTROL") {
        }
    }
    standardTile("DOCK", "device.status", width: 2, height: 2) {
        state "docked", label: 'Docked', backgroundColor: "#79b821" // No action allowed here
        state "docking", label: 'Docking', backgroundColor: "#ffa81e" // No action allowed here
        state "starting", label: 'UnDocking', backgroundColor: "#ffa81e" // No action allowed here
        state "cleaning", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking"
        state "pausing", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking" // No action allowed here
        state "paused", label: 'Dock', action: "dock", backgroundColor: "#ffffff", nextState: "docking"
        state "resuming", label: 'Not on Dock', backgroundColor: "#ffffff", defaultState: true // No action allowed here
    }
    standardTile("PAUSE", "device.status", width: 2, height: 2) {
        state "docked", label: 'Pause', backgroundColor: "#ffffff", defaultState: true // No action allowed here
        state "docking", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
        state "starting", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
        state "cleaning", label: 'Pause', action: "pause", backgroundColor: "#ffffff"
        state "pausing", label: 'Pausing..', backgroundColor: "#79b821" // No action allowed here
        state "paused", label: 'Paused', backgroundColor: "#79b821" // No action allowed here
        state "resuming", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
    }
    standardTile("RESUME", "device.status", width: 2, height: 2) {
        state "docked", label: 'Resume', backgroundColor: "#ffffff", defaultState: true // No action allowed here
        state "docking", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "starting", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "cleaning", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "pausing", label: 'Resume', backgroundColor: "#79b821" // No action allowed here
        state "paused", label: 'Resume', action: "resume", backgroundColor: "#ffffff"
        state "resuming", label: 'Resuming..', backgroundColor: "#79b821" // No action allowed here
    }
    standardTile("refresh", "device.status", width: 6, height: 2, decoration: "flat") {
        state "default", label:'Refresh', action:"refresh.refresh", icon:"st.secondary.refresh"
    }
    valueTile("job_count", "device.totalJobs", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Number of Cleaning Jobs:\n${currentValue} jobs'
    }
    valueTile("job_hr_count", "device.totalJobHrs", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Total Job Time:\n${currentValue} hours'
    }
    valueTile("current_job_time", "device.runtimeMins", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Current Job Runtime:\n${currentValue} minutes'
    }
    main "CLEAN"
    details(["STATUS",
             "CLEAN", "DOCK", "PAUSE", "RESUME",
             "refresh", "job_count", "job_hr_count", "current_job_time"])
}
// Switch methods
def on() {
    def status = device.latestValue("status")
    log.debug "On based on state - ${status}"
    if(status == "paused") {
        resume()
    } else {
        start()
    }
}
def off() {
    def status = device.latestValue("status")
    log.debug "Off based on state - ${status}"
    if(status == "paused") {
        dock()
    } else {
        stop()
    }
}
// Actions
def start() {
    sendEvent(name: "status", value: "starting")
    state.RoombaCmd = "start"
    apiGet()
    runIn(30, poll)
}
def stop() {
    sendEvent(name: "status", value: "stopping")
    state.RoombaCmd = "stop"
    apiGet()
    runIn(30, poll)
}
def dock() {
    sendEvent(name: "status", value: "docking")
    state.RoombaCmd = "dock"
    apiGet()
    runIn(30, poll)
}
def pause() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    apiGet()
    runIn(30, poll)
}
def resume() {
    sendEvent(name: "status", value: "resuming")
    state.RoombaCmd = "resume"
    apiGet()
    runIn(30, poll)
}
// API methods
def parse(description) {
    def msg = parseLanMessage(description)
    def headersAsString = msg.header // => headers as a string
    def headerMap = msg.headers      // => headers as a Map
    def body = msg.body              // => request body as a string
    def status = msg.status          // => http status code of the response
    def json = msg.json              // => any JSON included in response body, as a data structure of lists and maps
    def xml = msg.xml                // => any XML included in response body, as a document tree structure
    def data = msg.data              // => either JSON or XML in response body (whichever is specified by content-type header in response)
}
def apiGet() {
    def request_query = ""
    def request_host = ""
    def encoded_str = "${roomba_username}:${roomba_password}".bytes.encodeBase64()

    //Handle prefrences
    if("${roomba_host}" == "" || "${roomba_host}" == "null") {
        request_host = "https://irobot.axeda.com"
    } else {
        log.debug "Using Roomba Host: ${roomba_host}"
        request_host = "${roomba_host}"
    }

    //Validation before calling the API
    if(!roomba_username || !roomba_password) {
        def new_status = "Username/Password not set. Configure required before using device."
        sendEvent(name: "headline", value: new_status, displayed: false)
        sendEvent(name: "preferences_set", value: "missing", displayed: false)
        return
    } else if(state.preferences_set != "missing") {
        sendEvent(name: "preferences_set", value: "ready", displayed: false)
    }

    state.AssetID = "ElPaso@irobot!${roomba_username}"
    state.Authorization = "${encoded_str}"

    // Path (No changes required)
    def request_path = "/services/v1/rest/Scripto/execute/AspenApiRequest"
    // Query manipulation
    if( state.RoombaCmd == "getStatus" || state.RoombaCmd == "accumulatedHistorical" || state.RoombaCmd == "missionHistory") {
        request_query = "?blid=${roomba_username}&robotpwd=${roomba_password}&method=${state.RoombaCmd}"
    } else {
        request_query = "?blid=${roomba_username}&robotpwd=${roomba_password}&method=multipleFieldSet&value=%7B%0A%20%20%22remoteCommand%22%20:%20%22${state.RoombaCmd}%22%0A%7D"
    }

    def requestURI = "${request_host}${request_path}${request_query}"
    def httpRequest = [
        method:"GET",
        uri: "${requestURI}",
        headers: [
            'User-Agent': 'aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0',
            Accept: '*/*',
            'Accept-Language': 'en-us',
            'ASSET-ID': state.AssetID,
        ]
    ]
    try {
        httpGet(httpRequest) { resp ->
            resp.headers.each {
                log.debug "${it.name} : ${it.value}"
            }
            log.debug "response contentType: ${resp.contentType}"
            log.debug "response data: ${resp.data}"
            parseResponseByCmd(resp, state.RoombaCmd)
        }
    } catch (e) {
        log.error "something went wrong: $e"
    }
}
def parseResponseByCmd(resp, command) {
    //Parsing
    def data = resp.data
    if(command == "getStatus") {
        setStatus(data)
    } else if(command == "accumulatedHistorical" ) {
        //readSummaryInfo -- same as getStatus but easier to parse
    } else if(command == "missionHistory") {
        //readMissionHistory -- get results about last 30 jobs -- Out of scope for device-type?
    }
}
def setStatus(data) {
    //TODO: Mine other data here later? add support for "percent completion"?
    def rstatus = data.robot_status
    def robotName = data.robotName
    def mission = data.mission
    def runstats = data.bbrun
    def cschedule = data.cleanSchedule
    def pmaint = data.preventativeMaintenance
    def robot_status = new groovy.json.JsonSlurper().parseText(rstatus)
    def robot_history = new groovy.json.JsonSlurper().parseText(mission)
    def runtime_stats = new groovy.json.JsonSlurper().parseText(runstats)
    def schedule = new groovy.json.JsonSlurper().parseText(cschedule)
    def maintenance = new groovy.json.JsonSlurper().parseText(pmaint)
    log.debug "Robot Status = ${robot_status}"
    log.debug "Robot History = ${robot_history}"
    log.debug "Runtime stats= ${runtime_stats}"
    log.debug "Robot schedule= ${schedule}"
    log.debug "Robot Maintenance= ${maintenance}"
    def current_cycle = robot_status['cycle']
    def current_charge = robot_status['batPct']
    def current_phase = robot_status['phase']
    def num_mins_running = robot_status['mssnM']
    def flags = robot_status['flags']  // Unknown what 'Flags' 0/1/2/5 mean?
    def readyCode = robot_status['notReady']
    def num_cleaning_jobs = robot_history['nMssn']
    def num_dirt_detected = runtime_stats['nScrubs']
    def total_job_time = runtime_stats['hr']
    

    def new_status = get_robot_status(current_phase, current_cycle, current_charge, readyCode)
    def roomba_value = get_robot_enum(current_phase, readyCode)

    log.debug("Robot updates -- ${roomba_value} + ${new_status}")
    //Set the state object
    if(roomba_value == "cleaning") {
        state.switch = "on"
    } else {
        state.switch = "off"
    }

    if(status == "bin-full") {
        state.consumableStatus = "maintenance_required"
    } else {
        state.consumableStatus = "good"
    }

    //send events, display final event
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "batteryLevel", value: current_charge, displayed: false)
    sendEvent(name: "headline", value: new_status, displayed: false)
    sendEvent(name: "status", value: roomba_value)
    sendEvent(name: "switch", value: state.switch)
    sendEvent(name: "consumableStatus", value: state.consumableStatus)
}
def get_robot_enum(current_phase, readyCode) {
    if(readyCode != 0) {
        if(readyCode == 16) {
            return "bin-full"
        } else {
            return "error"
        }
    } else if(current_phase == "charge") {
        return "docked"
    } else if(current_phase == "hmUsrDock") {
        return "docking"
    } else if(current_phase == "pause" || current_phase == "stop") {
        return "paused"
    } else if(current_phase == "run") {
        return "cleaning"
    } else {
        //"Stuck" phase falls into this category.
        log.error "Unknown phase - Raw 'robot_status': ${status}. Add to 'get_robot_enum'"
        return "error"
    }
}
def parse_not_ready_status(readyCode) {
    def robotName = device.latestValue("robotName")

    if(readyCode == 16) {
      return "${robotName} bin is full. Empty bin to continue."
    } else if(readyCode == 7) {
      return "${robotName} is not upright. Place robot on flat surface to continue."
    } else if (readyCode == 1) {
      return "${robotName} is stuck. Move robot to continue."
    } else {
      return "${robotName} returned notReady=${readyCode}. See iRobot app for details."
    }
}
def get_robot_status(current_phase, current_cycle, current_charge, readyCode) {
    log.debug "Enter get_robot_status"

    def robotName = device.latestValue("robotName")

    if(readyCode != 0) {
      return parse_not_ready_status(readyCode)
    } else if(current_phase == "charge") {
        if (current_charge == 100) {
            return "${robotName} is Docked/Fully Charged"
        } else {
            return "${robotName} is Docked/Charging"
        }
    } else if(current_phase == "hmUsrDock") {
        return "${robotName} is returning home"
    } else if(current_phase == "run") {
        return "${robotName} is cleaning (${current_cycle} cycle)"
    } else if(current_phase == "pause" || current_phase == "stop") {
        return "Paused - 'Dock' or 'Resume'?"
    }

    log.error "Unknown phase - ${current_phase}."
    return "Error - refresh to continue. Code changes required if problem persists."
}

Here are some screenshots of the app in action:
Switch (Text alternates between status’ and 'battery level’


Running roomba

Pausing Roomba

Docking Roomba

Docking Roomba via master-button (doesn’t always work)

Recent Activity

Settings

CoRE SmartApp integration via Consumable Capability:

ToDo before release:

- Handle common errors (Bin full, stuck, “not upright”)
- CoRE SmartApp example for ‘bin full’ notifications
- Work on “refresh after actions” Update:30sec after running command, API will poll, in addition to the schedule() poll configurable in settings, this should work for most people
- Work on the main button being able to start/pause/dock Update: Fixed
- Use ‘password’ input for the password in preferences
- Include historical information as valueTile
- Including settings manipulation from the device

#Notes for future contributions/contributors:
Including Settings manipulation: (API scraped from iRobot using Fiddler+proxy)

#Edge cleaning OFF
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":1,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}
#Edge Cleaning ON
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}


#Finish cleaning when bin is full
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":1,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Don't finish job, wait for bin to be emptied, then press clean to resume

blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Cleaning passes
##One pass
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":1}
##Two pass
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":1,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":1}
##Automatic
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Carpet Boost
##Automatic
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}##Performance
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":1,"binPause":0,"vacHigh":1,"noPP":0,"ecoCharge":0,"noAutoPasses":0}
##ECO mode
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":1,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

# Update cleaning schedule (All days set to "No Cleaning" at 9am)
blid=...&robotpwd=...&method=multipleFieldSet&value={ "cleanSchedule" : { "cycle" : ["none", "none", "none", "none", "none", "none", "none"], "h" : [9, 9, 9, 9, 9, 9, 9], "m" : [0, 0, 0, 0, 0, 0, 0] } }
5 Likes

Looks good.

Any way to get a full bin push notification or event?