Small bit of help with Roomba 980

So I ported the Roomba 980 code over from ST and I got everything working (phew) except battery... attached is the code. Anyone have any ideas as to why I don’t see battery status?

MY BAD @ogiewon kindly pointed out that I did not paste the full driver. Lesson: Don't try and copy/paste from an iPad... it's a pain.

/**

  • iRobot Roomba v2.0
    *. 900 series - Virtual Switch
  • Copyright 2016 Steve-Gregory
  • Modified by Adrian Caramaliu to add support for v2 local API
  • 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.

*/

/Known NotReady states/
def getRoombaStates() {
def ROOMBA_READY = 0
def ROOMBA_STUCK = 1
def ROOMBA_BIN_FULL = 16
def ROOMBA_NOT_UPRIGHT = 7
def ROOMBA_IN_THE_DARK = 8
def ROOMBA_STATES = ['ready': ROOMBA_READY, 'stuck': ROOMBA_STUCK, 'full': ROOMBA_BIN_FULL, 'tilted': ROOMBA_NOT_UPRIGHT, 'dark': ROOMBA_IN_THE_DARK]
return ROOMBA_STATES
}
metadata {
definition (name: "Roomba980", namespace: "ady624", author: "Steve Gregory & Adrian Caramaliu") {
capability "Battery"
capability "Switch"
capability "Refresh"
capability "Polling"
capability "Consumable"
capability "Timed Session"
capability "Configuration"

    command "dock"
    command "resume"
    command "pause"
    command "cancel"
    command "pauseAndDock"

    attribute "totalJobs", "number"
    attribute "totalJobHrs", "number"
    attribute "headline", "string"
    attribute "robotName", "string"
    attribute "preferences_set", "string"
    attribute "status", "string"
    //For ETA heuristic
    attribute "lastSqft", "number"
    attribute "lastRuntime", "number"
    attribute "lastDate", "string"
}

}
// simulator metadata
simulator {
}
//Preferences
preferences {
section("Cloud Roomba API Type") {
input "localAPI", "bool", title: "Use a local REST gateway for Roomba", description: "Enable this if you have installed a local REST gateway for Roomba, you will need to provide the IP of that gateway", displayDuringSetup: true
}
section("Roomba Local Settings") {
input type: "paragraph", title: "Fill these parameters if using a local REST gateway"
input "roomba_host", "string", title:"IP of Roomba local REST Gateway", displayDuringSetup: true
input "roomba_port", "number", range: "1..65535", defaultValue: 3000, title:"IP of Roomba local REST Gateway", displayDuringSetup: true
}
section("Roomba Cloud Credentials") {
input type: "paragraph", title: "Please fill in the Roomba credentials below if using a Cloud connection to your robot", description: "The username/password can be retrieved via node.js & dorita980", displayDuringSetup: true
input "roomba_username", "text", title: "Roomba username/blid", displayDuringSetup: true
input "roomba_password", "password", title: "Roomba password", displayDuringSetup: true
}
section("Misc.") {
input type: "paragraph", title: "Polling Interval [minutes]", description: "This feature allows you to change the frequency of polling for the robot in minutes (1-59)"
input "pollInterval", "number", title: "Polling Interval", description: "Change polling frequency (in minutes)", defaultValue:4, range: "1..59", required: true, displayDuringSetup: true
}
}
// UI tile definitions
tiles {
multiAttributeTile(name:"CLEAN", type:"generic", width: 6, height: 4, canChangeIcon: true) {
tileAttribute("device.status", key: "PRIMARY_CONTROL") {
attributeState "error", label: 'Error', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
attributeState "bin-full", label: 'Bin Full', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
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"
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", nextState: "pausing"
attributeState "pausing", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821"
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"
}
tileAttribute("device.headline", key: "SECONDARY_CONTROL") {
attributeState "default", label:'${currentValue}'
}
}
valueTile("DOCK", "device.status", width: 2, height: 2) {
state "docked", label: 'Docked', backgroundColor: "#79b821"
state "docking", label: 'Docking', backgroundColor: "#ffa81e"
state "starting", label: 'UnDocking', backgroundColor: "#ffa81e"
state "cleaning", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
state "pausing", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
state "paused", label: 'Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
state "resuming", label: 'Not on Dock', backgroundColor: "#ffffff", defaultState: true, action: "dock"
}
valueTile("PAUSE", "device.status", width: 2, height: 2) {
state "docked", label: 'Pause', backgroundColor: "#ffffff", defaultState: true
state "docking", label: 'Pause', backgroundColor: "#ffffff"
state "starting", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
state "cleaning", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
state "pausing", label: 'Pausing..', backgroundColor: "#79b821"
state "paused", label: 'Paused', backgroundColor: "#79b821"
state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
state "resuming", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
}
valueTile("RESUME", "device.status", width: 2, height: 2) {
state "docked", label: 'Resume', backgroundColor: "#ffffff", defaultState: true
state "docking", label: 'Resume', backgroundColor: "#ffffff"
state "starting", label: 'Resume', backgroundColor: "#ffffff"
state "cleaning", label: 'Resume', backgroundColor: "#ffffff"
state "pausing", label: 'Resume', backgroundColor: "#79b821", action: "resume"
state "paused", label: 'Resume', backgroundColor: "#ffffff", action: "resume"
state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
state "resuming", label: 'Resuming..', backgroundColor: "#79b821"
}
standardTile("refresh", "device.status", width: 4, height: 2, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("battery", "device.battery", width: 2, height: 2, decoration: "flat") {
state "default", label:'Battery ${currentValue}%'
}

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'
}
valueTile("current_job_sqft", "device.runtimeSqft", width: 3, height: 1, decoration: "flat") {
    state "default", label:'Current Job Sqft:\n${currentValue} ft'
}
valueTile("current_job_time_estimated", "device.timeRemaining", width: 3, height: 1, decoration: "flat") {
    state "default", label:'Estimated Completion Time:\n${currentValue} minutes'
}
valueTile("current_job_sqft_estimated", "device.sqftRemaining", width: 3, height: 1, decoration: "flat") {
    state "default", label:'Estimated Sqft Remaining:\n${currentValue} ft'
}
main "CLEAN"
details(["STATUS",
         "CLEAN", "DOCK", "PAUSE", "RESUME",
         "refresh",
         "battery",
         "current_job_time", "current_job_time_estimated",
         "current_job_sqft", "current_job_sqft_estimated",
         "job_hr_count", "job_count"
         ])

}
// 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()
}
//Timed Session
def setTimeRemaining(timeNumber) {
log.debug "User requested setting the Time remaining to ${timeNumber}"
return
}
//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(roomba_value == "bin-full") {
    // Optionally this could be 'replace'?
    state.consumable = "maintenance_required"
} else if(roomba_value == "error"){
    state.consumable = "missing"
} else {
    state.consumable = "good"
}
return state.consumable

}
//Refresh
def refresh() {
log.debug "Executing 'refresh'"
return poll()
}
//Polling
def pollHistory() {
log.debug "Polling for missionHistory ----"
sendEvent(name: "headline", value: "Polling history API", displayed: false)
state.RoombaCmd = "missionHistory"
return localAPI ? null : apiGet()
}
def poll() {
//Get historical data first
pollHistory()
//Then poll for current status
log.debug "Polling for status ----"
sendEvent(name: "headline", value: "Polling status API", displayed: false)
state.RoombaCmd = "getStatus"
return localAPI ? local_poll() : apiGet()
}
// Switch methods
def on() {
// Always start roomba
def status = device.latestValue("status")
log.debug "On based on state - ${status}"
if(status == "paused") {
return resume()
} else {
return start()
}
}
def off() {
// Always return to dock..

def status = device.latestValue("status")
log.debug "Off based on state - ${status}"
if(status == "paused") {
	return dock()
} else {
    return pauseAndDock()
}

}
// Timed Session
def start() {
sendEvent(name: "status", value: "starting")
state.RoombaCmd = "start"
runIn(15, poll)
return localAPI ? local_start() : apiGet()
}
def stop() {
sendEvent(name: "status", value: "stopping")
state.RoombaCmd = "stop"
runIn(15, poll)
return localAPI ? local_stop() : apiGet()
}
def pauseAndDock() {
sendEvent(name: "status", value: "pausing")
state.RoombaCmd = "pause"
return localAPI ? local_pauseAndDock() : apiGet()
}
def pause() {
sendEvent(name: "status", value: "pausing")
state.RoombaCmd = "pause"
runIn(15, poll)
return localAPI ? local_pause() : apiGet()
}
def cancel() {
return off()
}

// Actions
def dock() {
sendEvent(name: "status", value: "docking")
state.RoombaCmd = "dock"
runIn(15, poll)
return localAPI ? local_dock() : apiGet()
}
def resume() {
sendEvent(name: "status", value: "resuming")
state.RoombaCmd = "resume"
runIn(15, poll)
return localAPI ? local_resume() : apiGet()
}
// API methods
def parse(description) {
log.trace "GOT HERE"
def msg = parseLanMessage(description)
log.trace "GOT MSG $msg"
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() {
if (local) return
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 ->
        log.debug "response Headers:" + resp.headers.collect { "${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) {
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") {
setMissionHistory(data)
}
}
def convertDate(dateStr) {
return Date.parse("yyyyMMdd H:m", dateStr)
}
def setMissionHistory(data) {
def lastRuntime = -1
def lastSqft = -1
def lastDate = ""
def mstatus = data.status
def robot_history = data.missions

robot_history.sort{ convertDate(it.date) }.each{ mission ->
    if(mission.done == 'ok') {
        lastSqft = mission.sqft
        lastRuntime = mission.runM
        lastDate = mission.date
    }
}

state.lastRuntime = lastRuntime
state.lastSqft = lastSqft
state.lastDate = lastDate

sendEvent(name: "lastRuntime", value: state.lastRuntime, displayed: false)
sendEvent(name: "lastSqft", value: state.lastSqft, displayed: false)
sendEvent(name: "lastDate", value: state.lastDate, displayed: false)

}

def setStatus(data) {
def rstatus = data.robot_status
def robotName = data.robotName
state.robotName = 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 current_sqft = robot_status['sqft']
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"
}

/* Consumable state-changes */
if(roomba_value == "bin-full") {
    state.consumable = "maintenance_required"
} else if(roomba_value == "error"){
    state.consumable = "missing"
} else {
    state.consumable = "good"
}

/* Timed Session state-changes */
if(roomba_value == "cleaning") {
    state.sessionStatus = "running"
} else if (roomba_value == "paused") {
    state.sessionStatus = "paused"
} else if (roomba_value == "docked" || roomba_value == "docking") {
    state.sessionStatus = "canceled"
} else {
    state.sessionStatus = "stopped"
}

/* Misc. state-changes */
if(state.lastRuntime == -1) {
    state.timeRemaining = -1
} else {
    state.timeRemaining = state.lastRuntime - num_mins_running
}
if(state.lastSqft == -1) {
    state.sqftRemaining = -1
} else {
    state.sqftRemaining = state.lastSqft - current_sqft
}

/*send events, display final event*/
sendEvent(name: "robotName", value: robotName, displayed: false)
sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
sendEvent(name: "consumable", value: state.consumable)

}

def get_robot_enum(current_phase, readyCode) {
def ROOMBA_STATES = getRoombaStates()

if(readyCode != ROOMBA_STATES['ready']) {
    if(readyCode == ROOMBA_STATES['full']) {
        return "bin-full"
    } else if(readyCode != ROOMBA_STATES['dark']) {
        return "error"
    }
}

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 = state.robotName
def ROOMBA_STATES = getRoombaStates()

if(readyCode == ROOMBA_STATES['full']) {
  return "${robotName}'s bin is full. Empty bin to continue."
} else if(readyCode == ROOMBA_STATES['tilted']) {
  return "${robotName} is not upright. Place robot on flat surface to continue."
} else if (readyCode == ROOMBA_STATES['stuck']) {
  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) {
def robotName = state.robotName
def ROOMBA_STATES = getRoombaStates()

// 0 and 8 are "okay to run"
if(readyCode != ROOMBA_STATES['ready'] && readyCode != ROOMBA_STATES['dark']) {
  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."

}

/* local REST gw support */

def lanEventHandler(evt) {
log.trace "GOT HERE"
def description = evt.description
def hub = evt?.hubId
def parsedEvent = parseLanMessage(description)
log.trace "RECEIVED LAN EVENT: $parsedEvent"

/*
//ping response
if (parsedEvent.data && parsedEvent.data.service && (parsedEvent.data.service == "hch")) {
def msg = parsedEvent.data
if (msg.result == "pong") {
//log in successful to local server
log.info "Successfully contacted local server"
atomicState.hchPong = true
}
}

*/    

}

private local_get(path, cbk) {
def host = "$roomba_host:$roomba_port"
new hubitat.device.HubAction("""GET $path HTTP/1.1\r\nHOST: $host\r\n\r\n""", hubitat.device.Protocol.LAN, null, [callback: cbk])
}

void local_dummy_cbk(hubitat.device.HubResponse hubResponse) {
}

void local_poll_cbk(hubitat.device.HubResponse hubResponse) {
def data = hubResponse.json
def current_charge = data.batPct
def robotName = data.name
state.robotName = robotName
def mission = data.cleanMissionStatus
def current_cycle = mission.cycle
def current_phase = mission.phase
def current_sqft = mission.sqft
def num_mins_running = mission.mssnM
def readyCode = mission.notReady
def num_cleaning_jobs = mission.nMssn
def num_dirt_detected = data.bbrun.nScrubs
def total_job_time = data.bbrun.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"
}   

/* Consumable state-changes */
if(roomba_value == "bin-full") {
    state.consumable = "maintenance_required"
} else if(roomba_value == "error"){
    state.consumable = "missing"
} else {
    state.consumable = "good"
}

/* Timed Session state-changes */
if(roomba_value == "cleaning") {
    state.sessionStatus = "running"
} else if (roomba_value == "paused") {
    state.sessionStatus = "paused"
} else if (roomba_value == "docked" || roomba_value == "docking") {
    state.sessionStatus = "canceled"
} else {
    state.sessionStatus = "stopped"
}

/* Misc. state-changes */
if(state.lastRuntime == -1) {
    state.timeRemaining = -1
} else {
    state.timeRemaining = (state.lastRuntime ?: num_mins_running) - num_mins_running
}
if(state.lastSqft == -1) {
    state.sqftRemaining = -1
} else {
    state.sqftRemaining = (state.lastSqft ?: current_sqft) - current_sqft
}

/*send events, display final event*/
sendEvent(name: "robotName", value: robotName, displayed: false)
sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
sendEvent(name: "consumable", value: state.consumable)    

}

private local_poll() {
local_get('/api/local/info/state', 'local_poll_cbk')
}

private local_start() {
local_get('/api/local/action/start', 'local_dummy_cbk')
}

private local_stop() {
local_get('/api/local/action/stop', 'local_dummy_cbk')
}

private local_pause() {
local_get('/api/local/action/pause', 'local_dummy_cbk')
}

private local_resume() {
local_get('/api/local/action/resume', 'local_dummy_cbk')
}

private local_dock() {
local_get('/api/local/action/dock', 'local_dummy_cbk')
}

private local_pauseAndDock() {
local_get('/api/local/action/pause', 'local_dummy_cbk')
wait(100)
local_get('/api/local/action/dock', 'local_dummy_cbk')
}

Most of your code didn’t make it into your post. You can edit your original post, paste in the complate driver code, then highlight it, and finally press the Preformatted Text Menu Button which looks like “</>”. This will make it much easier for others to read your code, as well as copy it.

BUMP NUDGE?!?! Any ideas why I can't see my battery status?

It's been a while but it would be great if someone could look at my code to help me figure out why I can't see battery level as part of the device.

I would really like to push an automation that would return the roomba to the dock when battery is < 20%

Your code is for the most part unreadable. You should edit your post and start over. Delete all the code from your post, paste in a fresh copy of the code. Make sure you highlight all of the code, then click the </> button to properly format in a way that is more readable. It looks like only a section of your code was highlighted when the button was pushed.

/**
*  iRobot Roomba v2.0
*. 900 series - Virtual Switch
*
*  Copyright 2016 Steve-Gregory
*  Modified by Adrian Caramaliu to add support for v2 local API
*
*  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.
*
*/

/*Known NotReady states*/
def getRoombaStates() {
    def ROOMBA_READY = 0
    def ROOMBA_STUCK = 1
    def ROOMBA_BIN_FULL = 16
    def ROOMBA_NOT_UPRIGHT = 7
    def ROOMBA_IN_THE_DARK = 8
    def ROOMBA_STATES = ['ready': ROOMBA_READY, 'stuck': ROOMBA_STUCK, 'full': ROOMBA_BIN_FULL, 'tilted': ROOMBA_NOT_UPRIGHT, 'dark': ROOMBA_IN_THE_DARK]
    return ROOMBA_STATES
}
metadata {
    definition (name: "Roomba980", namespace: "ady624", author: "Steve Gregory & Adrian Caramaliu") {
        capability "Battery"
        capability "Switch"
        capability "Refresh"
        capability "Polling"
        capability "Consumable"
        capability "Timed Session"
        capability "Configuration"

        command "dock"
        command "resume"
        command "pause"
        command "cancel"
        command "pauseAndDock"

        attribute "totalJobs", "number"
        attribute "totalJobHrs", "number"
        attribute "headline", "string"
        attribute "robotName", "string"
        attribute "preferences_set", "string"
        attribute "status", "string"
        //For ETA heuristic
        attribute "lastSqft", "number"
        attribute "lastRuntime", "number"
        attribute "lastDate", "string"
    }
}
// simulator metadata
simulator {
}
//Preferences
preferences {
    section("Cloud Roomba API Type") {
        input "localAPI", "bool", title: "Use a local REST gateway for Roomba", description: "Enable this if you have installed a local REST gateway for Roomba, you will need to provide the IP of that gateway", displayDuringSetup: true
    }
    section("Roomba Local Settings") {
    	input type: "paragraph", title: "Fill these parameters if using a local REST gateway"
        input "roomba_host", "string", title:"IP of Roomba local REST Gateway", displayDuringSetup: true
        input "roomba_port", "number", range: "1..65535", defaultValue: 3000, title:"IP of Roomba local REST Gateway", displayDuringSetup: true
    }
    section("Roomba Cloud Credentials") {
        input type: "paragraph", title: "Please fill in the Roomba credentials below if using a Cloud connection to your robot", description: "The username/password can be retrieved via node.js & dorita980", displayDuringSetup: true
        input "roomba_username", "text", title: "Roomba username/blid", displayDuringSetup: true
        input "roomba_password", "password", title: "Roomba password", displayDuringSetup: true
    }
    section("Misc.") {
        input type: "paragraph", title: "Polling Interval [minutes]", description: "This feature allows you to change the frequency of polling for the robot in minutes (1-59)"
        input "pollInterval", "number", title: "Polling Interval", description: "Change polling frequency (in minutes)", defaultValue:4, range: "1..59", required: true, displayDuringSetup: true
    }
}
// UI tile definitions
tiles {
    multiAttributeTile(name:"CLEAN", type:"generic", width: 6, height: 4, canChangeIcon: true) {
        tileAttribute("device.status", key: "PRIMARY_CONTROL") {
            attributeState "error", label: 'Error', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
            attributeState "bin-full", label: 'Bin Full', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
            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"
            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", nextState: "pausing"
            attributeState "pausing", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821"
            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"
        }
        tileAttribute("device.headline", key: "SECONDARY_CONTROL") {
           attributeState "default", label:'${currentValue}'
        }
    }
    valueTile("DOCK", "device.status", width: 2, height: 2) {
        state "docked", label: 'Docked', backgroundColor: "#79b821"
        state "docking", label: 'Docking', backgroundColor: "#ffa81e"
        state "starting", label: 'UnDocking', backgroundColor: "#ffa81e"
        state "cleaning", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "pausing", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "paused", label: 'Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Not on Dock', backgroundColor: "#ffffff", defaultState: true, action: "dock"
    }
    valueTile("PAUSE", "device.status", width: 2, height: 2) {
        state "docked", label: 'Pause', backgroundColor: "#ffffff", defaultState: true
        state "docking", label: 'Pause', backgroundColor: "#ffffff"
        state "starting", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
        state "cleaning", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
        state "pausing", label: 'Pausing..', backgroundColor: "#79b821"
        state "paused", label: 'Paused', backgroundColor: "#79b821"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
    }
    valueTile("RESUME", "device.status", width: 2, height: 2) {
        state "docked", label: 'Resume', backgroundColor: "#ffffff", defaultState: true
        state "docking", label: 'Resume', backgroundColor: "#ffffff"
        state "starting", label: 'Resume', backgroundColor: "#ffffff"
        state "cleaning", label: 'Resume', backgroundColor: "#ffffff"
        state "pausing", label: 'Resume', backgroundColor: "#79b821", action: "resume"
        state "paused", label: 'Resume', backgroundColor: "#ffffff", action: "resume"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Resuming..', backgroundColor: "#79b821"
    }
    standardTile("refresh", "device.status", width: 4, height: 2, decoration: "flat") {
        state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
    }
    valueTile("battery", "device.battery", width: 2, height: 2, decoration: "flat") {
        state "default", label:'Battery ${currentValue}%'
    }

	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'
    }
    valueTile("current_job_sqft", "device.runtimeSqft", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Current Job Sqft:\n${currentValue} ft'
    }
    valueTile("current_job_time_estimated", "device.timeRemaining", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Estimated Completion Time:\n${currentValue} minutes'
    }
    valueTile("current_job_sqft_estimated", "device.sqftRemaining", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Estimated Sqft Remaining:\n${currentValue} ft'
    }
    main "CLEAN"
    details(["STATUS",
             "CLEAN", "DOCK", "PAUSE", "RESUME",
             "refresh",
             "battery",
             "current_job_time", "current_job_time_estimated",
             "current_job_sqft", "current_job_sqft_estimated",
             "job_hr_count", "job_count"
             ])
}
// 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()
}
//Timed Session
def setTimeRemaining(timeNumber) {
    log.debug "User requested setting the Time remaining to ${timeNumber}"
    return
}
//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(roomba_value == "bin-full") {
        // Optionally this could be 'replace'?
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }
    return state.consumable
}
//Refresh
def refresh() {
    log.debug "Executing 'refresh'"
    return poll()
}
//Polling
def pollHistory() {
    log.debug "Polling for missionHistory ----"
    sendEvent(name: "headline", value: "Polling history API", displayed: false)
    state.RoombaCmd = "missionHistory"
    return localAPI ? null : apiGet()
}
def poll() {
    //Get historical data first
    pollHistory()
    //Then poll for current status
    log.debug "Polling for status ----"
    sendEvent(name: "headline", value: "Polling status API", displayed: false)
    state.RoombaCmd = "getStatus"
    return localAPI ? local_poll() : apiGet()
}
// Switch methods
def on() {
    // Always start roomba
    def status = device.latestValue("status")
    log.debug "On based on state - ${status}"
    if(status == "paused") {
	    return resume()
    } else {
	    return start()
    }
}
def off() {
    // Always return to dock..

    def status = device.latestValue("status")
    log.debug "Off based on state - ${status}"
    if(status == "paused") {
    	return dock()
    } else {
	    return pauseAndDock()
    }
}
// Timed Session
def start() {
    sendEvent(name: "status", value: "starting")
    state.RoombaCmd = "start"
    runIn(15, poll)
	return localAPI ? local_start() : apiGet()
}
def stop() {
    sendEvent(name: "status", value: "stopping")
    state.RoombaCmd = "stop"
    runIn(15, poll)
    return localAPI ? local_stop() : apiGet()
}
def pauseAndDock() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    return localAPI ? local_pauseAndDock() : apiGet()
}
def pause() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    runIn(15, poll)
    return localAPI ? local_pause() : apiGet()
}
def cancel() {
	return off()
}

// Actions
def dock() {
    sendEvent(name: "status", value: "docking")
    state.RoombaCmd = "dock"
    runIn(15, poll)
	return localAPI ? local_dock() : apiGet()
}
def resume() {
    sendEvent(name: "status", value: "resuming")
    state.RoombaCmd = "resume"
    runIn(15, poll)
    return localAPI ? local_resume() : apiGet()
}
// API methods
def parse(description) {
	log.trace "GOT HERE"
    def msg = parseLanMessage(description)
    log.trace "GOT MSG $msg"
    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() {
	if (local) return
    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 ->
            log.debug "response Headers:" + resp.headers.collect { "${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) {
    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") {
        setMissionHistory(data)
    }
}
def convertDate(dateStr) {
    return Date.parse("yyyyMMdd H:m", dateStr)
}
def setMissionHistory(data) {
    def lastRuntime = -1
    def lastSqft = -1
    def lastDate = ""
    def mstatus = data.status
    def robot_history = data.missions

    robot_history.sort{ convertDate(it.date) }.each{ mission ->
        if(mission.done == 'ok') {
            lastSqft = mission.sqft
            lastRuntime = mission.runM
            lastDate = mission.date
        }
    }

    state.lastRuntime = lastRuntime
    state.lastSqft = lastSqft
    state.lastDate = lastDate

    sendEvent(name: "lastRuntime", value: state.lastRuntime, displayed: false)
    sendEvent(name: "lastSqft", value: state.lastSqft, displayed: false)
    sendEvent(name: "lastDate", value: state.lastDate, displayed: false)
}

def setStatus(data) {
    def rstatus = data.robot_status
    def robotName = data.robotName
	state.robotName = 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 current_sqft = robot_status['sqft']
    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"
    }

    /* Consumable state-changes */
    if(roomba_value == "bin-full") {
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }

    /* Timed Session state-changes */
    if(roomba_value == "cleaning") {
        state.sessionStatus = "running"
    } else if (roomba_value == "paused") {
        state.sessionStatus = "paused"
    } else if (roomba_value == "docked" || roomba_value == "docking") {
        state.sessionStatus = "canceled"
    } else {
        state.sessionStatus = "stopped"
    }

    /* Misc. state-changes */
    if(state.lastRuntime == -1) {
        state.timeRemaining = -1
    } else {
        state.timeRemaining = state.lastRuntime - num_mins_running
    }
    if(state.lastSqft == -1) {
        state.sqftRemaining = -1
    } else {
        state.sqftRemaining = state.lastSqft - current_sqft
    }

    /*send events, display final event*/
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
    sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
    sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
    sendEvent(name: "consumable", value: state.consumable)
}

def get_robot_enum(current_phase, readyCode) {
    def ROOMBA_STATES = getRoombaStates()

    if(readyCode != ROOMBA_STATES['ready']) {
        if(readyCode == ROOMBA_STATES['full']) {
            return "bin-full"
        } else if(readyCode != ROOMBA_STATES['dark']) {
            return "error"
        }
    }

    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 = state.robotName
    def ROOMBA_STATES = getRoombaStates()

    if(readyCode == ROOMBA_STATES['full']) {
      return "${robotName}'s bin is full. Empty bin to continue."
    } else if(readyCode == ROOMBA_STATES['tilted']) {
      return "${robotName} is not upright. Place robot on flat surface to continue."
    } else if (readyCode == ROOMBA_STATES['stuck']) {
      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) {
    def robotName = state.robotName
    def ROOMBA_STATES = getRoombaStates()

    // 0 and 8 are "okay to run"
    if(readyCode != ROOMBA_STATES['ready'] && readyCode != ROOMBA_STATES['dark']) {
      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."
}




/* local REST gw support */

def lanEventHandler(evt) {
	log.trace "GOT HERE"
    def description = evt.description
    def hub = evt?.hubId
	def parsedEvent = parseLanMessage(description)
	log.trace "RECEIVED LAN EVENT: $parsedEvent"
	
/*   
    //ping response
    if (parsedEvent.data && parsedEvent.data.service && (parsedEvent.data.service == "hch")) {
	    def msg = parsedEvent.data
        if (msg.result == "pong") {
        	//log in successful to local server
            log.info "Successfully contacted local server"
			atomicState.hchPong = true
        }   	
    }
    
    */    
}

private local_get(path, cbk) {
	def host = "$roomba_host:$roomba_port"
	new hubitat.device.HubAction("""GET $path HTTP/1.1\r\nHOST: $host\r\n\r\n""", hubitat.device.Protocol.LAN, null, [callback: cbk])
}

void local_dummy_cbk(hubitat.device.HubResponse hubResponse) {
}

void local_poll_cbk(hubitat.device.HubResponse hubResponse) {
    def data = hubResponse.json
    def current_charge = data.batPct
    def robotName = data.name
	state.robotName = robotName    
    def mission = data.cleanMissionStatus
    def current_cycle = mission.cycle
    def current_phase = mission.phase
    def current_sqft = mission.sqft
    def num_mins_running = mission.mssnM
    def readyCode = mission.notReady
    def num_cleaning_jobs = mission.nMssn
    def num_dirt_detected = data.bbrun.nScrubs
    def total_job_time = data.bbrun.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"
    }   

    /* Consumable state-changes */
    if(roomba_value == "bin-full") {
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }

    /* Timed Session state-changes */
    if(roomba_value == "cleaning") {
        state.sessionStatus = "running"
    } else if (roomba_value == "paused") {
        state.sessionStatus = "paused"
    } else if (roomba_value == "docked" || roomba_value == "docking") {
        state.sessionStatus = "canceled"
    } else {
        state.sessionStatus = "stopped"
    }

    /* Misc. state-changes */
    if(state.lastRuntime == -1) {
        state.timeRemaining = -1
    } else {
        state.timeRemaining = (state.lastRuntime ?: num_mins_running) - num_mins_running
    }
    if(state.lastSqft == -1) {
        state.sqftRemaining = -1
    } else {
        state.sqftRemaining = (state.lastSqft ?: current_sqft) - current_sqft
    }

    /*send events, display final event*/
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
    sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
    sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
    sendEvent(name: "consumable", value: state.consumable)    
}

private local_poll() {
	local_get('/api/local/info/state', 'local_poll_cbk')
}

private local_start() {
	local_get('/api/local/action/start', 'local_dummy_cbk')
}

private local_stop() {
	local_get('/api/local/action/stop', 'local_dummy_cbk')
}

private local_pause() {
	local_get('/api/local/action/pause', 'local_dummy_cbk')
}

private local_resume() {
	local_get('/api/local/action/resume', 'local_dummy_cbk')
}

private local_dock() {
	local_get('/api/local/action/dock', 'local_dummy_cbk')
}

private local_pauseAndDock() {
	local_get('/api/local/action/pause', 'local_dummy_cbk')
    wait(100)
	local_get('/api/local/action/dock', 'local_dummy_cbk')
}

Any ideas why the battery does not show up as a Current State?

Can you show a screen shot of this live logging output?

After refreshing the device

[dev:321](http://192.168.7.201/logs#dev321)2018-07-07 21:13:51.039:traceGOT MSG [mac:B827EB496CA9, ip:c0a807dc, port:bb8, headers:[HTTP/1.1 200 OK:null, X-Powered-By:Express, Content-Type:application/json; charset=utf-8, Content-Length:2611, ETag:W/"a33-oYAsO2nQ7J12XKef/LiqkSzEWJo", Date:Sun, 08 Jul 2018 01:13:50 GMT, Connection:keep-alive], body:{"netinfo":{"dhcp":true,"addr":3232237537,"mask":4294967040,"gw":3232237313,"dns1":1263225675,"dns2":1263225932,"bssid":"14:22:db:16:a6:a6","sec":4},"wifistat":{"wifi":1,"uap":false,"cloud":1},"wlcfg":{"sec":7,"ssid":"536962656E696B"},"mac":"80:a5:89:0a:1d:34","country":"US","cloudEnv":"prod","svcEndpoints":{"svcDeplId":"v011"},"cleanMissionStatus":{"cycle":"none","phase":"stop","expireM":0,"rechrgM":0,"error":0,"notReady":15,"mssnM":89,"sqft":0,"initiator":"manual","nMssn":0},"mapUploadAllowed":true,"localtimeoffset":-240,"utctime":1530991222,"pose":{"theta":-2,"point":{"x":-10,"y":0}},"batPct":0,"dock":{"known":false},"bin":{"present":true,"full":false},"audio":{"active":false},"language":0,"noAutoPasses":false,"noPP":false,"ecoCharge":false,"vacHigh":false,"binPause":false,"carpetBoost":true,"openOnly":false,"twoPass":true,"schedHold":false,"lastCommand":{"command":"pause","time":1530980745,"initiator":"manual"},"langs":[{"en-US":0},{"fr-FR":1},{"es-ES":2},{"de-DE":3},{"it-IT":4}],"bbnav":{"aMtrack":91,"nGoodLmrks":7,"aGain":35,"aExpo":22},"bbpanic":{"panics":[8,9,8,9,8]},"bbpause":{"pauses":[0,16,4,4,16,1,17,1,6,3]},"bbmssn":{"nMssn":838,"nMssnOk":18,"nMssnC":474,"nMssnF":192,"aMssnM":40,"aCycleM":30},"bbrstinfo":{"nNavRst":160,"nMobRst":3,"causes":"2b59"},"cap":{"pose":1,"ota":2,"multiPass":2,"carpetBoost":1,"pp":1,"binFullDetect":1,"langOta":1,"maps":1,"edge":1,"eco":1,"svcConf":1},"hardwareRev":1,"sku":"R98----","batteryType":"lith","soundVer":"5","uiSwVer":"4582","navSwVer":"01.12.01#1","wifiSwVer":"20992","mobilityVer":"5806","bootloaderVer":"3580","umiVer":"6","softwareVer":"v2.4.6-3","tz":{"events":[{"dt":1522598400,"off":-240},{"dt":1541311201,"off":-300},{"dt":1552201201,"off":-240}],"ver":5},"timezone":"America/New_York","name":"Roomba","cleanSchedule":{"cycle":["none","none","none","none","none","none","none"],"h":[10,12,9,12,9,12,10],"m":[30,0,0,0,0,0,30]},"bbchg3":{"avgMin":307,"hOnDock":7446,"nAvail":950,"estCap":12311,"nLithChrg":248,"nNimhChrg":0,"nDocks":2799},"bbchg":{"nChgOk":242,"nLithF":0,"aborts":[0,0,0]},"bbswitch":{"nBumper":56194,"nClean":6463,"nSpot":4061,"nDock":2799,"nDrops":5547},"bbrun":{"hr":248,"min":20,"sqft":1080,"nStuck":401,"nScrubs":510,"nPicks":2295,"nPanics":2558,"nCliffsF":12207,"nCliffsR":58107,"nMBStll":37,"nWStll":48,"nCBump":1},"bbsys":{"hr":8349,"min":10},"signal":{"rssi":-34,"snr":52},"connected":true,"deploymentState":0,"echo":false,"mssnNavStats":{"nMssn":838,"gLmk":20,"lmk":35,"reLc":1,"mTrk":255,"plnEr":"none","sfkdp":0,"kdp":0,"mpSt":"ok","nmc":1,"nmmc":0,"nrmc":0},"deploymentId":"swu1529033564","disconnect":1}, header:HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 2611 ETag: W/"a33-oYAsO2nQ7J12XKef/LiqkSzEWJo" Date: Sun, 08 Jul 2018 01:13:50 GMT Connection: keep-alive , status:200, json:[country:US, disconnect:1, bbpanic:[panics:[8, 9, 8, 9, 8]], bbpause:[pauses:[0, 16, 4, 4, 16, 1, 17, 1, 6, 3]], bin:[present:true, full:false], ecoCharge:false, tz:[ver:5, events:[[dt:1522598400, off:-240], [dt:1541311201, off:-300], [dt:1552201201, off:-240]]], language:0, echo:false, netinfo:[gw:3232237313, sec:4, dns2:1263225932, bssid:14:22:db:16:a6:a6, dns1:1263225675, addr:3232237537, dhcp:true, mask:4294967040], bbmssn:[aCycleM:30, aMssnM:40, nMssnOk:18, nMssnC:474, nMssn:838, nMssnF:192], deploymentId:swu1529033564, bootloaderVer:3580, dock:[known:false], sku:R98----, mssnNavStats:[sfkdp:0, mTrk:255, plnEr:none, nmc:1, kdp:0, nmmc:0, mpSt:ok, nrmc:0, lmk:35, gLmk:20, nMssn:838, reLc:1], signal:[rssi:-34, snr:52], navSwVer:01.12.01#1, softwareVer:v2.4.6-3, bbsys:[min:10, hr:8349], utctime:1530991222, pose:[theta:-2, point:[x:-10, y:0]], bbswitch:[nBumper:56194, nSpot:4061, nClean:6463, nDrops:5547, nDock:2799], lastCommand:[initiator:manual, time:1530980745, command:pause], deploymentState:0, cleanMissionStatus:[phase:stop, rechrgM:0, initiator:manual, expireM:0, sqft:0, notReady:15, mssnM:89, error:0, cycle:none, nMssn:0], noPP:false, hardwareRev:1, name:Roomba, localtimeoffset:-240, schedHold:false, binPause:false, uiSwVer:4582, cloudEnv:prod, timezone:America/New_York, carpetBoost:true, soundVer:5, umiVer:6, batPct:0, mac:80:a5:89:0a:1d:34, wifistat:[cloud:1, wifi:1, uap:false], wlcfg:[sec:7, ssid:536962656E696B], svcEndpoints:[svcDeplId:v011], cap:[pp:1, eco:1, edge:1, maps:1, pose:1, multiPass:2, carpetBoost:1, langOta:1, ota:2, binFullDetect:1, svcConf:1], cleanSchedule:[h:[10, 12, 9, 12, 9, 12, 10], cycle:[none, none, none, none, none, none, none], m:[30, 0, 0, 0, 0, 0, 30]], wifiSwVer:20992, mobilityVer:5806, twoPass:true, bbrun:[nCliffsF:12207, nPanics:2558, min:20, nStuck:401, nCliffsR:58107, nMBStll:37, sqft:1080, nPicks:2295, hr:248, nScrubs:510, nWStll:48, nCBump:1], langs:[[en-US:0], [fr-FR:1], [es-ES:2], [de-DE:3], [it-IT:4]], audio:[active:false], bbchg3:[nAvail:950, nLithChrg:248, avgMin:307, nNimhChrg:0, nDocks:2799, hOnDock:7446, estCap:12311], vacHigh:false, bbnav:[aMtrack:91, nGoodLmrks:7, aGain:35, aExpo:22], connected:true, bbchg:[nLithF:0, nChgOk:242, aborts:[0, 0, 0]], mapUploadAllowed:true, noAutoPasses:false, bbrstinfo:[causes:2b59, nNavRst:160, nMobRst:3], batteryType:lith, openOnly:false], data:[country:US, disconnect:1, bbpanic:[panics:[8, 9, 8, 9, 8]], bbpause:[pauses:[0, 16, 4, 4, 16, 1, 17, 1, 6, 3]], bin:[present:true, full:false], ecoCharge:false, tz:[ver:5, events:[[dt:1522598400, off:-240], [dt:1541311201, off:-300], [dt:1552201201, off:-240]]], language:0, echo:false, netinfo:[gw:3232237313, sec:4, dns2:1263225932, bssid:14:22:db:16:a6:a6, dns1:1263225675, addr:3232237537, dhcp:true, mask:4294967040], bbmssn:[aCycleM:30, aMssnM:40, nMssnOk:18, nMssnC:474, nMssn:838, nMssnF:192], deploymentId:swu1529033564, bootloaderVer:3580, dock:[known:false], sku:R98----, mssnNavStats:[sfkdp:0, mTrk:255, plnEr:none, nmc:1, kdp:0, nmmc:0, mpSt:ok, nrmc:0, lmk:35, gLmk:20, nMssn:838, reLc:1], signal:[rssi:-34, snr:52], navSwVer:01.12.01#1, softwareVer:v2.4.6-3, bbsys:[min:10, hr:8349], utctime:1530991222, pose:[theta:-2, point:[x:-10, y:0]], bbswitch:[nBumper:56194, nSpot:4061, nClean:6463, nDrops:5547, nDock:2799], lastCommand:[initiator:manual, time:1530980745, command:pause], deploymentState:0, cleanMissionStatus:[phase:stop, rechrgM:0, initiator:manual, expireM:0, sqft:0, notReady:15, mssnM:89, error:0, cycle:none, nMssn:0], noPP:false, hardwareRev:1, name:Roomba, localtimeoffset:-240, schedHold:false, binPause:false, uiSwVer:4582, cloudEnv:prod, timezone:America/New_York, carpetBoost:true, soundVer:5, umiVer:6, batPct:0, mac:80:a5:89:0a:1d:34, wifistat:[cloud:1, wifi:1, uap:false], wlcfg:[sec:7, ssid:536962656E696B], svcEndpoints:[svcDeplId:v011], cap:[pp:1, eco:1, edge:1, maps:1, pose:1, multiPass:2, carpetBoost:1, langOta:1, ota:2, binFullDetect:1, svcConf:1], cleanSchedule:[h:[10, 12, 9, 12, 9, 12, 10], cycle:[none, none, none, none, none, none, none], m:[30, 0, 0, 0, 0, 0, 30]], wifiSwVer:20992, mobilityVer:5806, twoPass:true, bbrun:[nCliffsF:12207, nPanics:2558, min:20, nStuck:401, nCliffsR:58107, nMBStll:37, sqft:1080, nPicks:2295, hr:248, nScrubs:510, nWStll:48, nCBump:1], langs:[[en-US:0], [fr-FR:1], [es-ES:2], [de-DE:3], [it-IT:4]], audio:[active:false], bbchg3:[nAvail:950, nLithChrg:248, avgMin:307, nNimhChrg:0, nDocks:2799, hOnDock:7446, estCap:12311], vacHigh:false, bbnav:[aMtrack:91, nGoodLmrks:7, aGain:35, aExpo:22], connected:true, bbchg:[nLithF:0, nChgOk:242, aborts:[0, 0, 0]], mapUploadAllowed:true, noAutoPasses:false, bbrstinfo:[causes:2b59, nNavRst:160, nMobRst:3], batteryType:lith, openOnly:false]]

[dev:321](http://192.168.7.201/logs#dev321)2018-07-07 21:13:51.015:traceGOT HERE

[dev:321](http://192.168.7.201/logs#dev321)2018-07-07 21:13:50.743:debugPolling for status ----

[dev:321](http://192.168.7.201/logs#dev321)2018-07-07 21:13:50.736:debugPolling for missionHistory ----

[dev:321](http://192.168.7.201/logs#dev321)2018-07-07 21:13:50.728:debugExecuting 'refresh'

The battery event that needs to be parsed in the app comes in via robot_status. Refresh doesn't return that message content.
We're looking for a Debug line of "Robot status = blablabla..." without that your battery level won't be updated.
It looks as if the incoming message is different than what the app is looking for.

Give this a try

/**
*  iRobot Roomba v2.0
*. 900 series - Virtual Switch
*
*  Copyright 2016 Steve-Gregory
*  Modified by Adrian Caramaliu to add support for v2 local API
*
*  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.
*
*/

/*Known NotReady states*/
def getRoombaStates() {
    def ROOMBA_READY = 0
    def ROOMBA_STUCK = 1
    def ROOMBA_BIN_FULL = 16
    def ROOMBA_NOT_UPRIGHT = 7
    def ROOMBA_IN_THE_DARK = 8
    def ROOMBA_STATES = ['ready': ROOMBA_READY, 'stuck': ROOMBA_STUCK, 'full': ROOMBA_BIN_FULL, 'tilted': ROOMBA_NOT_UPRIGHT, 'dark': ROOMBA_IN_THE_DARK]
    return ROOMBA_STATES
}
metadata {
    definition (name: "Roomba980", namespace: "ady624", author: "Steve Gregory & Adrian Caramaliu") {
        capability "Battery"
        capability "Switch"
        capability "Refresh"
        capability "Polling"
        capability "Consumable"
        capability "Timed Session"
        capability "Configuration"

        command "dock"
        command "resume"
        command "pause"
        command "cancel"
        command "pauseAndDock"

        attribute "totalJobs", "number"
        attribute "totalJobHrs", "number"
        attribute "headline", "string"
        attribute "robotName", "string"
        attribute "preferences_set", "string"
        attribute "status", "string"
        //For ETA heuristic
        attribute "lastSqft", "number"
        attribute "lastRuntime", "number"
        attribute "lastDate", "string"
    }
}
// simulator metadata
simulator {
}
//Preferences
preferences {
    section("Cloud Roomba API Type") {
        input "localAPI", "bool", title: "Use a local REST gateway for Roomba", description: "Enable this if you have installed a local REST gateway for Roomba, you will need to provide the IP of that gateway", displayDuringSetup: true
    }
    section("Roomba Local Settings") {
    	input type: "paragraph", title: "Fill these parameters if using a local REST gateway"
        input "roomba_host", "string", title:"IP of Roomba local REST Gateway", displayDuringSetup: true
        input "roomba_port", "number", range: "1..65535", defaultValue: 3000, title:"IP of Roomba local REST Gateway", displayDuringSetup: true
    }
    section("Roomba Cloud Credentials") {
        input type: "paragraph", title: "Please fill in the Roomba credentials below if using a Cloud connection to your robot", description: "The username/password can be retrieved via node.js & dorita980", displayDuringSetup: true
        input "roomba_username", "text", title: "Roomba username/blid", displayDuringSetup: true
        input "roomba_password", "password", title: "Roomba password", displayDuringSetup: true
    }
    section("Misc.") {
        input type: "paragraph", title: "Polling Interval [minutes]", description: "This feature allows you to change the frequency of polling for the robot in minutes (1-59)"
        input "pollInterval", "number", title: "Polling Interval", description: "Change polling frequency (in minutes)", defaultValue:4, range: "1..59", required: true, displayDuringSetup: true
    }
}
// UI tile definitions
tiles {
    multiAttributeTile(name:"CLEAN", type:"generic", width: 6, height: 4, canChangeIcon: true) {
        tileAttribute("device.status", key: "PRIMARY_CONTROL") {
            attributeState "error", label: 'Error', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
            attributeState "bin-full", label: 'Bin Full', icon: "st.switches.switch.off", backgroundColor: "#bc2323"
            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"
            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", nextState: "pausing"
            attributeState "pausing", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821"
            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"
        }
        tileAttribute("device.headline", key: "SECONDARY_CONTROL") {
           attributeState "default", label:'${currentValue}'
        }
    }
    valueTile("DOCK", "device.status", width: 2, height: 2) {
        state "docked", label: 'Docked', backgroundColor: "#79b821"
        state "docking", label: 'Docking', backgroundColor: "#ffa81e"
        state "starting", label: 'UnDocking', backgroundColor: "#ffa81e"
        state "cleaning", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "pausing", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "paused", label: 'Dock', backgroundColor: "#ffffff", nextState: "docking", action: "dock"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Not on Dock', backgroundColor: "#ffffff", defaultState: true, action: "dock"
    }
    valueTile("PAUSE", "device.status", width: 2, height: 2) {
        state "docked", label: 'Pause', backgroundColor: "#ffffff", defaultState: true
        state "docking", label: 'Pause', backgroundColor: "#ffffff"
        state "starting", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
        state "cleaning", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
        state "pausing", label: 'Pausing..', backgroundColor: "#79b821"
        state "paused", label: 'Paused', backgroundColor: "#79b821"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Pause', backgroundColor: "#ffffff", action: "pause"
    }
    valueTile("RESUME", "device.status", width: 2, height: 2) {
        state "docked", label: 'Resume', backgroundColor: "#ffffff", defaultState: true
        state "docking", label: 'Resume', backgroundColor: "#ffffff"
        state "starting", label: 'Resume', backgroundColor: "#ffffff"
        state "cleaning", label: 'Resume', backgroundColor: "#ffffff"
        state "pausing", label: 'Resume', backgroundColor: "#79b821", action: "resume"
        state "paused", label: 'Resume', backgroundColor: "#ffffff", action: "resume"
        state "bin-full", label: 'Bin full', backgroundColor: "#bc2323"
        state "resuming", label: 'Resuming..', backgroundColor: "#79b821"
    }
    standardTile("refresh", "device.status", width: 4, height: 2, decoration: "flat") {
        state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
    }
    valueTile("battery", "device.battery", width: 2, height: 2, decoration: "flat") {
        state "default", label:'Battery ${currentValue}%'
    }

	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'
    }
    valueTile("current_job_sqft", "device.runtimeSqft", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Current Job Sqft:\n${currentValue} ft'
    }
    valueTile("current_job_time_estimated", "device.timeRemaining", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Estimated Completion Time:\n${currentValue} minutes'
    }
    valueTile("current_job_sqft_estimated", "device.sqftRemaining", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Estimated Sqft Remaining:\n${currentValue} ft'
    }
    main "CLEAN"
    details(["STATUS",
             "CLEAN", "DOCK", "PAUSE", "RESUME",
             "refresh",
             "battery",
             "current_job_time", "current_job_time_estimated",
             "current_job_sqft", "current_job_sqft_estimated",
             "job_hr_count", "job_count"
             ])
}
// 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()
}
//Timed Session
def setTimeRemaining(timeNumber) {
    log.debug "User requested setting the Time remaining to ${timeNumber}"
    return
}
//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(roomba_value == "bin-full") {
        // Optionally this could be 'replace'?
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }
    return state.consumable
}
//Refresh
def refresh() {
    log.debug "Executing 'refresh'"
    return poll()
}
//Polling
def pollHistory() {
    log.debug "Polling for missionHistory ----"
    sendEvent(name: "headline", value: "Polling history API", displayed: false)
    state.RoombaCmd = "missionHistory"
    return localAPI ? null : apiGet()
}
def poll() {
    //Get historical data first
    pollHistory()
    //Then poll for current status
    log.debug "Polling for status ----"
    sendEvent(name: "headline", value: "Polling status API", displayed: false)
    state.RoombaCmd = "getStatus"
    return localAPI ? local_poll() : apiGet()
}
// Switch methods
def on() {
    // Always start roomba
    def status = device.latestValue("status")
    log.debug "On based on state - ${status}"
    if(status == "paused") {
	    return resume()
    } else {
	    return start()
    }
}
def off() {
    // Always return to dock..

    def status = device.latestValue("status")
    log.debug "Off based on state - ${status}"
    if(status == "paused") {
    	return dock()
    } else {
	    return pauseAndDock()
    }
}
// Timed Session
def start() {
    sendEvent(name: "status", value: "starting")
    state.RoombaCmd = "start"
    runIn(15, poll)
	return localAPI ? local_start() : apiGet()
}
def stop() {
    sendEvent(name: "status", value: "stopping")
    state.RoombaCmd = "stop"
    runIn(15, poll)
    return localAPI ? local_stop() : apiGet()
}
def pauseAndDock() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    return localAPI ? local_pauseAndDock() : apiGet()
}
def pause() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    runIn(15, poll)
    return localAPI ? local_pause() : apiGet()
}
def cancel() {
	return off()
}

// Actions
def dock() {
    sendEvent(name: "status", value: "docking")
    state.RoombaCmd = "dock"
    runIn(15, poll)
	return localAPI ? local_dock() : apiGet()
}
def resume() {
    sendEvent(name: "status", value: "resuming")
    state.RoombaCmd = "resume"
    runIn(15, poll)
    return localAPI ? local_resume() : apiGet()
}
// API methods
def parse(description) {
	log.trace "GOT HERE"
    def msg = parseLanMessage(description)
    log.trace "GOT MSG $msg"
    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() {
	if (local) return
    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 ->
            log.debug "response Headers:" + resp.headers.collect { "${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) {
    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") {
        setMissionHistory(data)
    }
}
def convertDate(dateStr) {
    return Date.parse("yyyyMMdd H:m", dateStr)
}
def setMissionHistory(data) {
    def lastRuntime = -1
    def lastSqft = -1
    def lastDate = ""
    def mstatus = data.status
    def robot_history = data.missions

    robot_history.sort{ convertDate(it.date) }.each{ mission ->
        if(mission.done == 'ok') {
            lastSqft = mission.sqft
            lastRuntime = mission.runM
            lastDate = mission.date
        }
    }

    state.lastRuntime = lastRuntime
    state.lastSqft = lastSqft
    state.lastDate = lastDate

    sendEvent(name: "lastRuntime", value: state.lastRuntime, displayed: false)
    sendEvent(name: "lastSqft", value: state.lastSqft, displayed: false)
    sendEvent(name: "lastDate", value: state.lastDate, displayed: false)
}

def setStatus(data) {
    def rstatus = data.robot_status
    def robotName = data.robotName
	state.robotName = 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 current_sqft = robot_status['sqft']
    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"
    }

    /* Consumable state-changes */
    if(roomba_value == "bin-full") {
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }

    /* Timed Session state-changes */
    if(roomba_value == "cleaning") {
        state.sessionStatus = "running"
    } else if (roomba_value == "paused") {
        state.sessionStatus = "paused"
    } else if (roomba_value == "docked" || roomba_value == "docking") {
        state.sessionStatus = "canceled"
    } else {
        state.sessionStatus = "stopped"
    }

    /* Misc. state-changes */
    if(state.lastRuntime == -1) {
        state.timeRemaining = -1
    } else {
        state.timeRemaining = state.lastRuntime - num_mins_running
    }
    if(state.lastSqft == -1) {
        state.sqftRemaining = -1
    } else {
        state.sqftRemaining = state.lastSqft - current_sqft
    }

    /*send events, display final event*/
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
    sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
    sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
    sendEvent(name: "consumable", value: state.consumable)
}

def get_robot_enum(current_phase, readyCode) {
    def ROOMBA_STATES = getRoombaStates()

    if(readyCode != ROOMBA_STATES['ready']) {
        if(readyCode == ROOMBA_STATES['full']) {
            return "bin-full"
        } else if(readyCode != ROOMBA_STATES['dark']) {
            return "error"
        }
    }

    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 = state.robotName
    def ROOMBA_STATES = getRoombaStates()

    if(readyCode == ROOMBA_STATES['full']) {
      return "${robotName}'s bin is full. Empty bin to continue."
    } else if(readyCode == ROOMBA_STATES['tilted']) {
      return "${robotName} is not upright. Place robot on flat surface to continue."
    } else if (readyCode == ROOMBA_STATES['stuck']) {
      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) {
    def robotName = state.robotName
    def ROOMBA_STATES = getRoombaStates()

    // 0 and 8 are "okay to run"
    if(readyCode != ROOMBA_STATES['ready'] && readyCode != ROOMBA_STATES['dark']) {
      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."
}

private local_get(path, cbk) {
	//def host = "$roomba_host:$roomba_port"
	//new hubitat.device.HubAction("""GET $path HTTP/1.1\r\nHOST: $host\r\n\r\n""", hubitat.device.Protocol.LAN, null, [callback: cbk])
    
    httpGet([
        uri: "http://$roomba_host:$roomba_port$path"
    ]){ resp -> 
        "$cbk"(resp.data)
    }   
}

void local_dummy_cbk(data) {
}

void local_poll_cbk(data) {
    def current_charge = data.batPct
    def robotName = data.name
	state.robotName = robotName    
    def mission = data.cleanMissionStatus
    def current_cycle = mission.cycle
    def current_phase = mission.phase
    def current_sqft = mission.sqft
    def num_mins_running = mission.mssnM
    def readyCode = mission.notReady
    def num_cleaning_jobs = mission.nMssn
    def num_dirt_detected = data.bbrun?.nScrubs ?: -1
    def total_job_time = data.bbrun?.hr ?: -1
    

    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"
    }   

    /* Consumable state-changes */
    if(roomba_value == "bin-full") {
        state.consumable = "maintenance_required"
    } else if(roomba_value == "error"){
        state.consumable = "missing"
    } else {
        state.consumable = "good"
    }

    /* Timed Session state-changes */
    if(roomba_value == "cleaning") {
        state.sessionStatus = "running"
    } else if (roomba_value == "paused") {
        state.sessionStatus = "paused"
    } else if (roomba_value == "docked" || roomba_value == "docking") {
        state.sessionStatus = "canceled"
    } else {
        state.sessionStatus = "stopped"
    }

    /* Misc. state-changes */
    if(state.lastRuntime == -1) {
        state.timeRemaining = -1
    } else {
        state.timeRemaining = (state.lastRuntime ?: num_mins_running) - num_mins_running
    }
    if(state.lastSqft == -1) {
        state.sqftRemaining = -1
    } else {
        state.sqftRemaining = (state.lastSqft ?: current_sqft) - current_sqft
    }

    /*send events, display final event*/
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "runtimeSqft", value: current_sqft, displayed: false)
    sendEvent(name: "timeRemaining", value: state.timeRemaining, displayed: false)
    sendEvent(name: "sqftRemaining", value: state.sqftRemaining, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "battery", 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: "sessionStatus", value: state.sessionStatus)
    sendEvent(name: "consumable", value: state.consumable)    
}

private local_poll() {
	local_get('/api/local/info/state', 'local_poll_cbk')
}

private local_start() {
	local_get('/api/local/action/start', 'local_dummy_cbk')
}

private local_stop() {
	local_get('/api/local/action/stop', 'local_dummy_cbk')
}

private local_pause() {
	local_get('/api/local/action/pause', 'local_dummy_cbk')
}

private local_resume() {
	local_get('/api/local/action/resume', 'local_dummy_cbk')
}

private local_dock() {
	local_get('/api/local/action/dock', 'local_dummy_cbk')
}

private local_pauseAndDock() {
	local_get('/api/local/action/pause', 'local_dummy_cbk')
    wait(100)
	local_get('/api/local/action/dock', 'local_dummy_cbk')
}
4 Likes

THANK YOU!!!

I just picked up this robot and wondered if you would share the rules you setup for your bot? Thank you.

I actually stopped trying to implement Roomba into HE. In my experience, my automations with roomba were such that did not require “immediate responses”.

That being said, I created automations in IFTTT to start the vacuum when mode was set to ‘Away’ and dock the Vacuum when Presence changes.

I'm confused. So I enable the Room as a virtual device.

I've put in my blid and password. Where do I define the IP address of the Roomba??

So just using the blid and password.

It's not turning on....

The steps for install have been the following.

1.) Set up a new drivers code
2.) Create new virtual device using Roomba 980
3.) Punch in the blid and password
4.) Try turning on. Not working...

You also have to run rest server locally.
On raspberry pi for example

Sounded like it was optional? Do you have any links to instructions for this?

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.