[RELEASE] Roomba Scheduler

Roomba Scheduler is an application and device that enables local execution and scheduling of Roomba cleaning tasks in assistance with a Raspberry Pi running Dorita980 and Rest980. This application and device works with ALL WiFi enabled Roomba and Braava devices.

Highlights:

  • Complete step-by-step instructions for RPi and Hubitat configuration
  • Control multiple Roomba devices from a single RPi using multiple Roomba Scheduler installations
  • Roomba Device auto-creation; Device named based on user config in iRobot app
  • Pushover Notifications Start/Stop/Full Bin/Dock-Charging/Error/Battery Died
  • Advanced Cleaning Schedule for up to 10 cleaning times per selected days
  • Presence Schedule - auto start, delay and auto-dock
  • Advanced Options for non-900+ series Roomba devices
  • Roomba 900+ Series cleaning settings and Map
  • Dynamic Dashboard Tile
  • AppWatchDog2 Support

Dynamic Dashboard Tile:
dashboard

Device Information page:
image

Special thanks to @dman2306 for the initial code concept and permission to use his code to develop this application.

Donations are always appreciated:
https://paypal.me/aaronmward?locale.x=en_US

Roomba Scheduler Changes:

  • 1.2.7 - optimized scheduling code (thanks StepHack!), fixed additional scheduling bugs (thx dman2306)
  • 1.2.6 - fixed i7series result set for Roomba information
  • 1.2.5 - fixed restriction logic so restrictions work, more notification choices, UI updates
  • 1.2.4 - i7 series modifications to dock roomba correctly
  • 1.2.3 - added ability to restrict cleaning based on switch, turn off resticted switch if presence away options
  • 1.2.2 - added additional notification options for errors, add time-delay for notification of errors
  • 1.2.1 - fixed current day scheduling bug, minor tweaks and fixes
  • 1.2.0 - fixed scheduling bug
  • 1.1.9 - fixed notifcations for unknown error codes, couple additional bugs discovered in logic
  • 1.1.8 - added more error traps, error8 - bin issue attempt to restart cleaning, advanced presence options
  • 1.1.7 - fixed bug if unknown error occurs to continue monitoring
  • 1.1.6 - support for dashboard changes in CSS
  • 1.1.5 - full customization of notification messages
  • 1.1.4 - added ability to have multiple Roomba Schedulers
  • 1.1.3 - reduced device handler complexity, added support for device switch.on/off and options for off
  • 1.1.2 - fixed dead battery logic, added Roomba information page, added specific error codes to notifications, setup and config error checking
  • 1.1.1 - fixed notification options to respect user choice for what is notified
  • 1.1.0 - fixed global variables not being set
  • 1.0.9 - ability to set Roomba 900+ device settings, advanced docking options for non-900+ devices
  • 1.0.8 - determine if Roomba battery has died during docking
  • 1.0.7 - add duration for dashboard tile, minor grammar fixes
  • 1.0.6 - add all messages for dynamic dashboard tile
  • 1.0.5 - added bin full notifications, refined presence handler for additional cleaning scenarios, support for dynamic dashboard tile
  • 1.0.4 - added presence to start/dock roomba
  • 1.0.3 - changed frequency polling based on Roomba event. Also fixed Pushover notifications to occur no matter how Roomba events are changed
  • 1.0.2 - add ability for advanced scheduling multiple times per day
  • 1.0.1 - added ability for all WiFi enabled Roomba devices to be added and controlled
  • 1.0.0 - Inital concept from Dominick Meglio

Roomba Driver Changes:

  • 1.1.2 - additional CSS fixes to ensure of future dashboard changes won't affect tile
  • 1.1.1 - fixed CSS for dashboard update
  • 1.1.0 - support for switch control on/off, handler changes
  • 1.0.9 - error in namespace preventing creation of adding device
  • 1.0.8 - added logging option
  • 1.0.7 - added battery died notifications
  • 1.0.6 - cleaning duration
  • 1.0.5 - added all possible tile outcomes (cleaning, docking, stopped, error, dead battery)
  • 1.0.4 - added dashboard tile updates
  • 1.0.3 - moved all notifications back to app
  • 1.0.2 - modified how pushover works
  • 1.0.1 - added pushover notification capabilities
  • 1.0.0 - Inital concept from Dominick Meglio

AppWatchDog2 Supported

4 Likes

@aaron,

I think this is just what I am looking for. My Rooma I7+ is on its way.

However, could the app me modified a tad that instead of running on a schedule, start/stop it based on presence or HE mode?

Here is the reason for the request. I work for the railroad and my schedule is literally 24/7 most days. I do not have a set schedule, so setting a schedule in the app would do me no good. However, if we all leave the house then kick in and clean and when one of us arrives back home, dock and wait until we leave again.

Hopefully this makes sense. Thanks.

1 Like

1.0.3 - changed frequency polling based on Roomba event. Also fixed Pushover notifications to occur no matter how Roomba events are changed

Yes that is in the works. Currently I am just replacing my IFTTT integration. I am planning on having an option for presence and if "ALL" away begin cleaning. :slight_smile:

1 Like

Easier than expected....

1.0.4 - added presence to start/dock roomba

I'm using this similar local device for about year
what is a major difference?

/**
*  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')
}

Edit your message and select ALL your code. Then click 'Preformatted' followed by a click of 'Blockquote'

That will 'encapsulate' your code into a much more easily read/copied wad of text. :slight_smile:

42%20PM

Nice !! :slight_smile:

It's not perfect because you did it in two selection events..

Next time, copy your code, paste it into the message, then select (once) the entire code set and click those two buttons one after the other, don't deselect between.

You're getting the > in front of each line because the two button clicks got interpretted as two vs one 'Preformatted-Blockquote' :smiley:

This is ME paying back whoever in this Community taught me.. 18 months ago.

1 Like

thnx looks much better

with 3x edits :slight_smile:

1 Like

The biggest differences are:

  • Roomba Scheduler App/Driver is local execution and requires no cloud connectivity
  • Roomba Scheduler enables more advanced scheduling verses creating multiple RM rule actions
  • Roomba Scheduler provides presence based cleaning schedules
  • Roomba Scheduler is an app with a light-weight driver
  • Roomba Scheduler provides Pushover notifications
  • Roomba Scheduler works with all WiFi enabled Roomba devices
  • AppWatchDog2 ability to see when app/driver needs to be updated

Everyone is free to use what they want. I don’t own a 900+ Roomba but wanted more than RM rules, IFTTT reliance and better scheduling capabilities.

somehow it doesn't work for me
I setup app and changed my working device to this new and just errors pop up.

also, I think all could be done in the device and not splitting it to device/app
for a couple of reasons:

  1. more things to maintain for updates
  2. instead of any kind of app I really wanna use rule machine for everything,
    a. you can do almost everything with it
    b. because if anything is broken support gonna help you with RM, not with 3rd party apps
[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:50.764 [error](http://192.168.1.25/device/edit/206)groovy.lang.MissingMethodException: No signature of method: user_driver_aaronward_Roomba_1185.setConsumableStatus() is applicable for argument types: () values: [] (setConsumableStatus)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:50.012 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 81 (resume)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:49.723 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 77 (pause)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:49.351 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 73 (stop)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:48.732 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 69 (start)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:48.325 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 73 (stop)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:47.782 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 77 (pause)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:47.151 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 85 (dock)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:05:00.346 [error](http://192.168.1.25/device/edit/206)groovy.lang.MissingMethodException: No signature of method: user_driver_aaronward_Roomba_1185.poll() is applicable for argument types: () values: [] Possible solutions: dock(), wait(), run(), pause(), run(), stop() (poll)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:02:22.966 [error](http://192.168.1.25/device/edit/206)java.lang.ArrayIndexOutOfBoundsException: 1 on line 85 (dock)

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:00:00.434 [debug](http://192.168.1.25/device/edit/206)Robot updates -- docked + Roomba is Docked/Fully Charged

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:00:00.250 [debug](http://192.168.1.25/device/edit/206)Polling for status ----

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 19:00:00.234 [debug](http://192.168.1.25/device/edit/206)Polling for missionHistory ----

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 18:55:00.272 [debug](http://192.168.1.25/device/edit/206)Robot updates -- docked + Roomba is Docked/Fully Charged

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 18:55:00.116 [debug](http://192.168.1.25/device/edit/206)Polling for status ----

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 18:55:00.108 [debug](http://192.168.1.25/device/edit/206)Polling for missionHistory ----

[dev:206](http://192.168.1.25/logs/past#dev206)2019-09-16 18:50:00.248 [debug](http://192.168.1.25/device/edit/206)Robot updates -- docked + Roomba is Docked/Fully Charged

old device

new device, not configure , refresh?

Please read the readme.md on GitHub. There are requirements for this to work. The device gets created from the app. The app needs to connect to the Rest980 service locally hosted.

And no you cannot do anything I have created in just a device driver.

dunno, since you can't control it except for start, stop and dock by rest980 design.
How much more functionality is added?
Pretty much I wanna start it when everybody leaves the house
send it to dock if anybody arrives and start it if anything happens anyway by certain time.
I don't think there is so much more you can do. RM do that pretty good.

so improvement is: pushover and AppWatchDog2
cost is: have app + driver

Also support for Roomba devices outside of the 900 series.

Again to each their own. I developed this to assist others like me who can’t use the driver that is posted for the 900 series.

Also makes it easier than to build a lot of rules and finally no cloud reliance.

Lastly, I would not have invested this amount of time for something that is irrelevant. This is for me and sharing back with the community.

Agreed. More solutions are generally better for the community.
And I strongly believe that somebody will choose this Roomba implementation as their first choice.

1 Like

1.0.5 - added bin full notifications, refined presence handler for additional cleaning scenarios, support for dynamic dashboard tile

1.0.6 - add all messages for dynamic dashboard tile

now it starting to be suddenly more interesting.
bin notifications.... cool

How about the possibility to assign physical or virtual switch when Roomba gets stuck somewhere to play a melody so you don't have to search every corner and under furniture?

now I can ask: Alexa ask Roomba where is Roomba. But who wanna talk like that?
A simple press of a button or harmony remote and Roomba beeps. :smiley: