/** * * Hubitat Import URL: https://raw.githubusercontent.com/PrayerfulDrop/Hubitat/master/Roomba/Roomba-app.groovy * * **************** iRobot Scheduler **************** * * Design Usage: * This app is designed to integrate any WiFi enabled Roomba or Braava devices to have direct local connectivity and integration into Hubitat. This application will create a Roomba/Braava device based on the * the name you gave your Roomba/Braava device in the cloud app. With this app you can schedule multiple cleaning times, automate cleaning when presence is away, receive notifications about status * of the Roomba/Braava (stuck, cleaning, died, etc) and also setup continous cleaning mode for non-900 series WiFi Roomba devices. * * Copyright 2019 Aaron Ward * * Special thanks to Dominick Meglio for creating the initial integration and giving me permission to use his code to create this application. * *------------------------------------------------------------------------------------------------------------------- * 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. * * * ------------------------------------------------------------------------------------------------------------------------------ * Donations are always appreciated: https://www.paypal.me/aaronmward * ------------------------------------------------------------------------------------------------------------------------------ * * Changes: * 1.3.6 - misc fixes * 1.3.5 - fixed spelling of "Braava" not "Brava". Added a setting to use a switch to override presence settings (great for a global pandemic!) * 1.3.4 - removed AppWatchDog, added last cleaning visibility, added ability to start a Braava device after successful docking/charging * 1.3.3 - changed app name to iRobot Scheduler and added contact sensors for cleaning schedule restrictions * 1.3.2 - Added basic support for Braava m6 (supports notifications for tank being empty instead of bin being full) * 1.3.1 - finalized logic fixes for unique use case scenarios (thx dman2306 for being a patient tester) * 1.3.0 - fixed additional logic for unique options set, optimized presence handler * 1.2.9 - fixed bug in notifications and battery % start * 1.2.8 - lowest battery option for start cleaning, new delay presence options, new day cleaning enforcement, restricted days for cleaning, more error checking, reset application state option, fixed updateDevices scheduling issue * 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 restricted 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 **/ def version() { version = "1.3.6" return version } definition( name: "iRobot Scheduler", namespace: "aaronward", author: "Aaron Ward", description: "Scheduling and local execution of iRobot services", category: "Misc", iconUrl: "", iconX2Url: "", iconX3Url: " ") preferences { page name: "mainPage", title: "", install: true, uninstall: true page name: "pageroombaInfo", title: "", install: false, uninstall: false, nextPage: "mainPage" page name: "pageroombaNotify", title: "", install: false, uninstall: false, nextPage: "mainPage" } def mainPage() { debug=false dynamicPage(name: "mainPage") { section(getFormat("title", "${getImage("Blank")}" + " ${app.label}")) { paragraph "
This application provides iRobot Roomba and Braava local integration and advanced scheduling.
" } section(getFormat("header-blue", " Rest980/Dorita980 Connectivity:")){ if(state.roombaName==null || state.error) paragraph "Rest980 Server cannot be reached - check IP Address" input "doritaIP", "text", title: "Rest980 Server IP Address:", description: "Rest980 Server IP Address:", required: true, submitOnChange: true, width: 6 input "doritaPort", "number", title: "Rest980 Server Port:", description: "Dorita Port", required: true, defaultValue: 3000, width: 6 if(state.roombaName!=null && state.roombaName.length() > 0) href "pageroombaInfo", title: "Information about my Roomba: ${state.roombaName}", description:"" } section(getFormat("header-blue", " Notification Device(s):")) { // PushOver Devices input "pushovertts", "bool", title: "Use 'Pushover' device(s)?", required: false, defaultValue: false, submitOnChange: true if(pushovertts == true) { input "pushoverdevice", "capability.notification", title: "PushOver Device(s):", required: true, multiple: true input "pushoverStart", "bool", title: "Notify when Roomba starts cleaning?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverBin", "bool", title: "Notify when Roomba's bin is full?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverTank", "bool", title: "Notify when Braava's tank is empty?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverStop", "bool", title: "Notify when Roomba stops cleaning?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverDead", "bool", title: "Notify when Roomba's Battery dies?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverDock", "bool", title: "Notify when Roomba is docked and charging?", required: false, defaultValue:false, submitOnChange: true, width: 6 input "pushoverError", "bool", title: "Notify when Roomba has an error?", required: false, defaultValue:false, submitOnChange: true, width: 6 href "pageroombaNotify", title: "Change default notification messages from ${state.roombaName}", description: "" } } section(getFormat("header-blue", " Cleaning Schedule:")) { paragraph "Cleaning schedule must be set for at least one day and one time.
Note: Roomba devices require at least 2 hours to have a full battery. Consider this when scheduling multiple cleaning times in a day." input "schedDay", "enum", title: "Select which days to schedule cleaning:", required: true, multiple: true, submitOnChange: true, options: [ "0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday", "4": "Thursday", "5": "Friday", "6": "Saturday" ] input "timeperday", "text", title: "Number of times per day to clean:", required: true, defaultValue: "1", submitOnChange:true, width: 6 paragraph "", width: 6 if(timeperday==null) timeperday="1" if(timeperday.toInteger()<1 || timeperday.toInteger()>10) { paragraph "Please enter a value between 1 and 10
" } else { for(i=0; i < timeperday.toInteger(); i++) { switch (i) { case 0: proper="First" break case 1: proper="Second" break case 2: proper="Third" break case 3: proper="Fourth" break case 4: proper="Fifth" break case 5: proper="Sixth" break case 6: proper="Seventh" break case 7: proper="Eighth" break case 8: proper="Nineth" break case 9: proper="Tenth" break } input "day${i}", "time", title: "${proper} time:",required: true, width: 6 } } } section(getFormat("header-blue", " Presence Options:")) { input "usePresence", "bool", title: "Use presence?", defaultValue: false, submitOnChange: true if(usePresence) { input "roombaPresence", "capability.presenceSensor", title: "Choose presence device(s):", multiple: true, required: true, submitOnChange: true input "roombaPresenceClean", "bool", title: "Immediately start cleaning if everyone leaves (outside of normal schedule)?", defaultValue: false, submitOnChange: true if(roombaPresenceCleandelay==null || roombaPresenceCleandelay=="") roombaPresenceCleandelay="5" if(roombaPresenceClean) { input "roombaPresenceCleandelay", "text", title: "Delay this many minutes prior to cleaning? (0-1440)", defaultValue: 5, range:"0-1440", submitOnChange: true if(roombaPresenceCleandelay.toInteger()<0 || roombaPresenceCleandelay.toInteger()>1440) paragraph "Error: Please enter number of minutes between 0 and 1440." } input "roombaPresenceDock", "bool", title: "Dock Roomba if someone arrives home?", defaultValue: false, submitonChange: true if(roombaPresenceDelay) paragraph getFormat("wordline", "Delay Cleaning when presence home:") input "roombaPresenceDelay", "bool", title: "If presence is home, delay the cleaning schedule?", defaultValue: false, submitOnChange: true if(roombaPresenceDelayTime==null || roombaPresenceDelayTime=="" ) roombaPresenceDelayTime = "90" if(roombaPresenceDelay) { input "roombaPresenceDelayTime", "text", title: "Minutes to delay cleaning schedule if people present? (0-1440)", defaultValue: 90, range:"0-1440", submitOnChange: true if(roombaPresenceDelayTime.toInteger()<0 || roombaPresenceDelayTime.toInteger()>1440) paragraph "Error: Please enter number of minutes between 0 and 1440." input "roombaDelayDay", "bool", title: "If presence still present, cancel cleaning but reschedule for next schedule cleaning time/day?", defaultValue: false, submitOnChange: true if(roombaDelayDay) { input "roombaafterday", "bool", title: "Would you like to reschedule a cleaning for any day (even outside of cleaning schedule) if cleaning cancelled for more than a certain number of days?", defaultValue: false, submitOnChange: true if(roombaafterday) { input "roombaaftertimeday", "text", title: "Number of days that can be missed before a cleaning must occur?", defaultValue: 0, submitOnChange: true input "roombarestrictSched", "enum", title: "Day(s) that should be restricted from rescheduling the missed cleaning after ${roombaaftertimeday} days occur?", required: false, multiple: true, submitOnChange: true, options: [ "0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday", "4": "Thursday", "5": "Friday", "6": "Saturday" ] } } } input "roombaIgnorePresenceSwitch", "capability.switch", title: "Ignore presence settings if the following switch is turned on" } } section(getFormat("header-blue", " Advanced Options:")) { paragraph "Roomba models below 900+ series do not have the ability to find a docking station prior to the battery dying. Options below provide similar functionality or at least a better chance for Roomba to dock before dying." input "roombaBatteryLevel", "enum", title: "Lowest battery percentage Roomba is allowed to start cleaning?", defaultValue:"60", required:false, multiple: false, submitOnChange:true, options: [ "80":"80%", "70":"70%", "60":"60%", "50":"50%", "40":"40%"] input "useTime", "bool", title: "Limit Roomba's cleaning time?", defaultValue: false, submitOnChange: true, width: 6 if(useTime) { input "roombaLimitTime", "text", title: "How many minutes to run (minimum 20)?", defaultValue: "60", required: true, submitOnChange: true if(roombaLimitTime==null) roombaLimitTime="20" if(roombaLimitTime.toInteger() < 20) { paragraph "Please enter a number greater than 20
" app?.updateSetting("roombaLimitTime",[value:"60",type:"text"]) } } input "useBattery", "bool", title: "Have Roomba dock based on battery percentage?", defaultValue: false, submitOnChange: true if(useBattery) { input "roombaBattery", "enum", title: "What percent to have Roomba begin docking?", defaultValue: "30", required: false, multiple: false, submitOnChange: true, options: [ "40": "40%", "30": "30%", "20": "20%", "10": "10%"] } input "roombaOff", "enum", title: "Do the following when Roomba's switch is turned 'Off'?", defaultValue: "dock", required: false, multiple: false, submitOnChange: true, options: [ "dock": "Dock", "stop": "Stop" ] paragraph "
Settings for Roomba 900+ series devices:" input "roomba900", "bool", title: "Configure 900+ options?", defaultValue: false, submitOnChange: true if(roomba900){ paragraph "See ${state.roombaName}'s Cleaning Map" input "roombacarpetBoost", "enum", title: "Select Carpet Boost option:", required: false, multiple: false, defaultValue: "auto", submitOnChange: true, options: [ "auto": "Auto", "performance": "Performance", "eco": "Eco" ] input "roombaedgeClean", "bool", title: "Set Edge Cleaning (On/Off):", defaultValue: false, submitOnChange: true input "roombacleaningPasses", "enum", title: "Select Cleaning Passes option:", required: false, multiple: false, defaultValue: "auto", submitOnChange: true, options: [ "auto": "Auto", "one": "Pass Once", "two": "Pass Twice" ] input "roombaalwaysFinish", "bool", title: "Set Always Finish Option (On/Off):", defaultValue: false, submitOnChange: true } paragraph "
Settings for Braava devices:" input "BraavaYes", "bool", title: "Start Braava(s) after iRobot is done cleaning?", required: false, defaultValue: false, submitOnChange: true if(BraavaYes) input "BraavaDevice", "capability.switch", title: "Select Braava robot(s) to turn on after iRobot docks:", required: false, multiple: true, defaultValue: null, submitOnChange: true } section(getFormat("header-blue", " Logging and Restrictions:")) { } section() { input "modesYes", "bool", title: "Enable restrictions?", required: true, defaultValue: false, submitOnChange: true if(modesYes) { input "restrictbySwitch", "capability.switch", title: "Use a switch to restrict cleaning schedule:", required: false, multiple: false, defaultValue: null, submitOnChange: true input "restrictbyContact", "capability.contactSensor", title: "Use a contact sensor(s) to restrict cleaning schedule:", required: false, multiple: true, defaultValue: null, submitOnChange: true input "pushoverRestrictions", "bool", title: "Send Pushover Msg if restrictions are on and Roomba tries to clean?", required: false, defaultValue:false, submitOnChange: true, width: 6 } if(modesYes && usePresence) input "turnoffSwitch", "bool", title: "Turn off switch if presence away?", required: false, multiple: false, defaultValue: false, submitOnChange: true, width: 6 paragraph "Note: resetting the application state settings should only be used if you are experiencing issues with Roomba starting correctly." input "resetApp", "bool", title: "Reset application state settings?", required: false, defaultValue:false, submitOnChange: true input "logEnable", "bool", title: "Debug Logging?", required: false, defaultValue: true, submitOnChange: true if(logEnable) input "logMinutes", "text", title: "Log for the following number of minutes (0=logs always on):", required: false, defaultValue:15, submitOnChange: true if(debug) input "init", "bool", title: "Initialize?", required: false, defaultVale:false, submitOnChange: true // For testing purposes if(app.init) { log.debug "Initalizing button clicked." try { initialize() } catch (any) { log.error "${any}" } app?.updateSetting("init",[value:"false",type:"bool"]) } if(app.resetApp) { app?.updateSetting("resetApp",[value:"false",type:"bool"]) resetApp() paragraph "Success! Roomba Scheduler's application state settings have been reset." } if(debug) { paragraph "
state.schedDelay: ${state.schedDelay} - state.lastcleaning: ${state.lastcleaning} - state.presence: ${state.presence}
state.errors: ${state.errors} - state.prevcleaning: ${state.prevcleaning} - state.DaysSinceLastCleaning: ${state.DaysSinceLastCleaning}" } paragraph getFormat("line") paragraph "
Developed by: Aaron Ward
v${version()}

PayPal Logo

Donations always appreciated!
" } } } def pageroombaInfo() { def result = executeAction("/api/local/info/state") def cleantime=false switch(state.cleaning) { case "cleaning": img = "roomba-clean.png" cleantime = true msg = state.cleaning.capitalize() break case "stopped": img = "roomba-stop.png" msg = state.cleaning.capitalize() break case "charging": img = "roomba-charge.png" msg = state.cleaning.capitalize() break case "docking": img = "roombadock.png" cleantime = true msg = state.cleaning.capitalize() break case "dead": img = "roomba-dead.png" msg = "Battery Died" break case "error": img = "roomba-error.png" msg = state.cleaning.capitalize() break case "idle": img = "roomba-stop.png" msg = state.cleaning.capitalize() break } if(result.data?.bin?.full) bin="Full" else bin="Empty" img = "https://raw.githubusercontent.com/PrayerfulDrop/Hubitat/master/Roomba/support/${img}" temp = "

  ${state.roombaName}

" temp += "

Roomba SKU: ${result.data.sku}

" if (result.data.mac != null) temp += "

Roomba MAC: ${result.data.mac}

" else if (result.data.hwPartsRev?.wlan0HwAddr != null) temp += "

Roomba MAC: ${result.data.hwPartsRev?.wlan0HwAddr}

" temp += "

Software Version: ${result.data.softwareVer}

" temp += "

Current State: ${msg}

" if(cleantime) temp += "

Elapsed Time: ${result.data.cleanMissionStatus.mssnM} minutes

" temp += "

Battery Status: ${result.data.batPct}%" if (result.data.bin != null) temp += "

Bin Status: ${bin}" else if (result.data.tankLvl != null) temp += "

Tank Level: ${result.data.tankLvl}%" temp += "

# of cleaning jobs: ${String.format("%,d",result.data.cleanMissionStatus.nMssn)}

" temp += "

Total Time Cleaning: ${String.format("%,d",result.data.bbrun.hr)} hours and ${result.data.bbrun.min} minutes

" temp += "

Last cleaning occured on: ${state.lastcleaningcycle}

" temp += "

Days since last cleaning: ${String.format("%,d", state.DaysSinceLastCleaning)}

" dynamicPage(name: "pageroombaInfo", title: "", nextPage: "mainPage", install: false, uninstall: false) { section(getFormat("title", "${getImage("Blank")}" + " ${app.label}")) { paragraph "
This application provides Roomba local integration and advanced scheduling.
" } section(getFormat("header-blue", " Device Information:")) { paragraph temp paragraph getFormat("line") paragraph "
Developed by: Aaron Ward
v${version()}

PayPal Logo

Donations always appreciated!
" } } } def pageroombaNotify() { dynamicPage(name: "pageroombaNotify", title: "", nextPage: "mainPage", install: false, uninstall: false) { section(getFormat("title", "${getImage("Blank")}" + " ${app.label}")) {} section(getFormat("header-green", " Notification Messages:")) { paragraph "Instructions to use variables:" paragraph "%device% = Roomba device's name

" //paragraph "%cleaningstatus% = Roomba device's current cleaning status

" input "pushoverStartMsg", "text", title: "Start Cleaning:", required: false, defaultValue:"%device% has started cleaning", submitOnChange: true input "pushoverStopMsg", "text", title: "Stop Cleaning:", required: false, defaultValue:"%device% has stopped cleaning", submitOnChange: true input "pushoverDockMsg", "text", title: "Docked and Charging::", required: false, defaultValue:"%device% is charging", submitOnChange: true input "pushoverBinMsg", "text", title: "Bin is Full:", required: false, defaultValue:"%device%'s bin is full", submitOnChange: true input "pushoverTankMsg", "text", title: "Tank is Empty:", required: false, defaultValue:"%device%'s tank is empty", submitOnChange: true input "pushoverDeadMsg", "text", title: "Battery dies:", required: false, defaultValue:"%device% battery has died", submitOnChange: true input "pushoverErrorMsg", "text", title: "Error:", required: false, defaultValue:"%device% has stopped because", submitOnChange: true input "pushoverErrorMsg2", "text", title: "Error2 - Both wheels are stuck:", required: false, defaultValue:"both wheels are stuck", submitOnChange: true input "pushoverErrorMsg3", "text", title: "Error3 - Left wheel is stuck:", required: false, defaultValue:"left wheel is stuck", submitOnChange: true input "pushoverErrorMsg4", "text", title: "Error4 - Right wheel is stuck:", required: false, defaultValue:"right wheel is stuck", submitOnChange: true input "pushoverErrorMsg5", "text", title: "Error5 - Roomba is wedged under something:", required: false, defaultValue: "it is wedged under something", submitOnChange: true input "pushoverErrorMsg7", "text", title: "Error7 - Bin is missing:", required: false, defaultValue:"cleaning bin is missing", submitOnChange: true input "pushoverErrorMsg16", "text", title: "Error16 - Stuck on object:", required: false, defaultValue:"stuck on an object", submitOnChange: true } } } def getRoombaSchedule() { def roombaSchedule = [] for(i=0; i 7) { tempday=1 } if(!restricteddaysofweek.contains(tempday.toString())) { foundschedule=true cleaningday = map2[tempday] weekday = map[tempday] } } } if(daysofweek.contains(day.toString()) && !foundschedule) { // Check when next scheduled cleaning time will be for(it in state.roombaSchedule) { if((it > current) && !foundschedule) { nextcleaning = it cleaningday = "*" weekday = "Today" foundschedule=true } } if(!foundschedule) { tempday = day while(!foundschedule) { tempday = tempday + 1 if(tempday>7) { tempday=1 } if(daysofweek.contains(tempday.toString())) { foundschedule=true cleaningday = map2[tempday] weekday = map[tempday] } } } } else { // Check when the next day we are cleaning tempday = day while(!foundschedule) { tempday = tempday + 1 if(tempday>7) { tempday=1 } if(daysofweek.contains(tempday.toString())) { foundschedule=true cleaningday = map2[tempday] weekday = map[tempday] } } } log.info "Next scheduled cleaning: ${weekday} at ${Date.parse("HH:mm", nextcleaning).format('h:mm a')}" schedule("0 ${Date.parse("HH:mm", nextcleaning).format('mm H')} ? * ${cleaningday} *", RoombaSchedStart) } def RoombaSchedStart() { def result = executeAction("/api/local/info/state") def device = getChildDevice("roomba:" + result.data.name) def presence = getPresence() // If Delay cleaning is selected if(debug) log.debug "Current variables: roombaPresenceDelay: ${roombaPresenceDelay} - presence: ${presence} - state.presence: ${state.presence}" if(((roombaPresenceDelay && presence) || state.presence) && (roombaIgnorePresenceSwitch == null || roombaIgnorePresenceSwitch.currentValue("switch") == "off")) { if(debug) log.debug "roomba PresenceDelay or Presence leave values equal true" if(state.presence==true) timer = roombaPresenceCleandelay else timer = roombaPresenceDelayTime if(!state.schedDelay && state.startDelayTime==null) { if(roombaPresenceDelay && state.presence) log.info "Presence has departed with delay start. Waiting ${timer} minute(s) before starting cleaning" else log.info "Roomba Schedule was initiated but presence is detected. Waiting ${timer} minutes before starting" def now = new Date() long temp = now.getTime() state.startDelayTime = temp state.schedDelay = true runIn(60,RoombaDelay) RoombaScheduler(false) } else { if(state.startDelayTime==null || state.startDelayTime == "") { log.warn "Application state is inaccurate. Initializing state variables." initialize() } else { long timeDiff def now = new Date() long unxNow = now.getTime() unxPrev = state.startDelayTime unxNow = unxNow/1000 unxPrev = unxPrev/1000 timeDiff = Math.abs(unxNow-unxPrev) timeDiff = Math.round(timeDiff/60) if(logEnable) log.debug "Time delay difference is currently: ${timeDiff.toString()} of ${timer} minute(s)" if(timeDiff <= timer.toInteger()-1) { runIn(60,RoombaDelay) } else { if(roombaDelayDay && state.DaysSinceLastCleaning.toInteger()>roombaaftertimeday.toInteger()-1) { RoombaScheduler(true) } else { RoombaScheduler(false) if(roombaDelayDay) log.debug "Delay time has expired, skip cleaning is selected due to presence is home. Current days since last cleaning: ${state.DaysSinceLastCleaning}" else { log.info "Delay time has expired. Starting expired cleaning schedule" device.start() } } updateDevices() state.schedDelay = false state.presence = false state.startDelayTime=null } } } } // Delay cleaning is not selected else { if(debug) log.debug "RoombaDelay or Immediate Presence values false...starting Roomba normal cleaning schedule" if(logEnable) "Starting Roomba normal cleaning schedule" device.start() updateDevices() RoombaScheduler(false) } } def RoombaDelay() { RoombaSchedStart() } // Device creation and status updhandlers def createChildDevices() { try { def result = executeAction("/api/local/info/state") if (result && result.data) { if (!getChildDevice("roomba:"+result.data.name)) addChildDevice("roomba", "Roomba", "roomba:" + result.data.name, 1234, ["name": result.data.name, isComponent: false]) } } catch (e) {log.error "Couldn't create child device due to connection issue." } } def cleanupChildDevices() { try { def result = executeAction("/api/local/info/state") for (device in getChildDevices()) { def deviceId = device.deviceNetworkId.replace("roomba:","") if (result.data.name != deviceId) deleteChildDevice(device.deviceNetworkId) } } catch (e) { log.error "Couldn't clean up child devices due to connection issue."} } def updateDevices() { try { def result = executeAction("/api/local/info/state") if (result && result.data) { def device = getChildDevice("roomba:" + result.data.name) device.sendEvent(name: "battery", value: result.data.batPct) if (result.data.bin != null) { if (!result.data.bin.present) device.sendEvent(name: "bin", value: "missing") if (result.data.bin.full) { device.sendEvent(name: "bin", value: "full") if(pushoverBin && state.sendBinNotification) { state.sendBinNotification = false pushNow(state.pushoverBinMsg) } } else { device.sendEvent(name: "bin", value: "good") state.sendBinNotification = true } } else if (result.data.tankLvl != null) { if (result.data.detectedPad.contains("Wet")) { if (!result.data.mopReady.tankPresent) device.sendEvent(name: "tank", value: "missing") else if (result.data.tankLvl == 0) { device.sendEvent(name: "tank", value: "empty") if(pushoverTank && state.sendTankNotification) { state.sendTankNotification = false pushNow(state.pushoverTankMsg) } } else { device.sendEvent(name: "tank", value: "good") state.sendTankNotification = true } } } def status = state.prevcleaning def msg = null switch (result.data.cleanMissionStatus.phase){ case "hmMidMsn": case "hmPostMsn": case "hmUsrDock": if(result.data.batPct == 0) { if(state.batterydead==false) { def now = new Date() long temp = now.getTime() state.starttime = temp state.batterydead = true status = "docking" } else { long timeDiff def now = new Date() long unxNow = now.getTime() unxPrev = state.starttime unxNow = unxNow/1000 unxPrev = unxPrev/1000 timeDiff = Math.abs(unxNow-unxPrev) timeDiff = Math.round(timeDiff/60) if(logEnable) log.debug "Checking how long since battery was at 0%. Time difference is currently: ${timeDiff.toString()} minute(s)" if(timeDiff > 10) { status = "dead" if(pushoverDead) msg=state.pushoverDeadMsg } else { status = "docking" } } } else { status = "docking" state.batterydead = false state.errors = false } break case "charge": status = "charging" if(pushoverDock) msg=state.pushoverDockMsg state.batterydead = false state.errors = false if(!state.docked) { if(BraavaYes) BraavaDevice.on() state.docked = true } // working on long daystimeDiff = 0 def daynow = new Date() long dayunxNow = daynow.getTime() dayunxPrev = state.lastcleaning dayunxNow = dayunxNow/1000/60/60/24 dayunxPrev = dayunxPrev/1000/60/60/24 daytimeDiff = Math.abs(dayunxNow-dayunxPrev) daytimeDiff = daytimeDiff.trunc() daytimeDiff = Math.round(daytimeDiff) state.DaysSinceLastCleaning = daytimeDiff break case "run": state.docked=false status = "cleaning" if(pushoverStart) msg=state.pushoverStartMsg state.batterydead = false //control Roomba docking based on Time or Battery % if(useTime && roombaTime.toInteger() >= result.data.cleanMissionStatus.mssnM.toInteger()) { device.dock() } if(useBattery && roombaBattery.toInteger() >= result.data.batPct.toInteger()) { device.dock() } state.errors = false break case "stop": status = state.prevcleaning if(result.data.cleanMissionStatus.notReady.toInteger() > 0) { if(state.errors==false && state.notified==false) { def errornow = new Date() long errortemp = errornow.getTime() state.errorstarttime = errortemp state.errors = true if(logEnable) log.warn "Detected possible cleaning error with ${state.roombaName}" } else { if(!state.cleaning.contains("error") && state.errors) { long errortimeDiff = 0 def errornow = new Date() long errorunxNow = errornow.getTime() errorunxPrev = state.errorstarttime errorunxNow = errorunxNow/1000 errorunxPrev = errorunxPrev/1000 errortimeDiff = Math.abs(errorunxNow-errorunxPrev) errortimeDiff = Math.round(errortimeDiff/60) if(logEnable) log.warn "Checking how long since error detected. Time difference is currently: ${errortimeDiff.toString()} minute(s)" if(errortimeDiff > 5) status = "error" } } if(status.contains("error")) { temp = state.pushoverErrorMsg switch(result.data.cleanMissionStatus.notReady) { case "2": temp += " ${state.pushoverErrorMsg2}" break case "3": temp += " ${state.pushoverErrorMsg3}" break case "4": temp += " ${state.pushoverErrorMsg4}" break case "5": temp += "${stat.pushoverErrorMsg5}" break case "7": temp += " ${state.pushoverErrorMsg7}" break case "8": temp += " has a bin error. Attempting to restart cleaning." device.resume pauseExecution(5000) device.resume break case "16": temp += " ${state.pushoverErrorMsg16}" break default: temp = "${state.roombaName} has an unknown error notReady:${result.data.cleanMissionStatus.notReady}" break } if(pushoverError) msg = temp status = "error" } } else { status = "idle" state.errors = false if(pushoverStop) msg=state.pushoverStopMsg } break } if(debug) log.trace "Before: state.cleaning: '${state.cleaning}' state.prevcleaning: '${state.prevcleaning}' state.notified: '${state.notified}'" state.cleaning = status device.sendEvent(name: "cleanStatus", value: status) if(debug) log.trace "Sending '${status}' to ${device} dashboard tile" device.roombaTile(state.cleaning, result.data.batPct, result.data.cleanMissionStatus.mssnM) if(!state.notified && !state.cleaning.contains(state.prevcleaning)) { if(msg!=null) { state.notified = true pushNow(msg) } state.prevcleaning = state.cleaning } else state.notified = false } } catch (e) { if(logEnable) log.error "iRobot cloud error. ${e} " if(logEnable) log.warn "Retrying updating devices in 30 seconds." } } def pushNow(msg) { // If user selects Pushover notifications then send message if (pushovertts) { if (logEnable) log.debug "Sending Pushover message: ${msg}" try {pushoverdevice.deviceNotification("${msg}")} catch (e) {log.error "Pushover device is not selected."} } } // Handlers def getPresence() { def presence = false if(roombaPresence.findAll { it?.currentPresence == "present"}) { presence = true } return presence } def switchHandler(evt) { if(evt.value == "on") { def result = executeAction("/api/local/info/state") def device = getChildDevice("roomba:" + result.data.name) if(result.data.cleanMissionStatus.phase.contains("run")) { pushNow("Restriction switch turned on while ${state.roombaName} was cleaning. Sending ${state.roombaName} home to dock.") device.dock() } } } def presenceHandler(evt) { try { if (roombaIgnorePresenceSwitch?.currentValue("switch") == "on") { if(logEnable) log.info "Presence arrived but override switch is on" return } def result = executeAction("/api/local/info/state") if (result && result.data) { def device = getChildDevice("roomba:" + result.data.name) def presence = getPresence() state.presence = false // Dock Roomba if presence is true and roombaPresenceDock is true if(presence && result.data.cleanMissionStatus.phase.contains("run") && roombaPresenceDock) { if(logEnable) log.info "Docking ${state.roombaName} based on presence options" state.startDelayTime = null state.schedDelay = false device.dock() RoombaScheduler(false) } // Reset restriction switch based on presence away and turnoffSwitch is true if(!presence && turnoffSwitch && restrictbySwitch.currentState("switch").value == "on") { log.info "Restriction switch '${restrictbySwitch.displayName} is ${restrictbySwitch.currentState("switch").value}. Presence away, turning off ${restrictbySwitch.displayName}" restrictbySwitch.off } // If roombaPresenceClean is true start cleaning based on a delay if(!presence && roombaPresenceClean && result.data.cleanMissionStatus.phase.contains("charge") || result.data.cleanMissionStatus.phase.contains("stop")) { if(logEnable) log.info "Presence cleaning option is selected AND presence has departed." state.presence = true state.startDelayTime = null state.schedDelay = false RoombaDelay() } // if RoombaPresenceDelay is true start cleaning based if presence departs and cleaning schedule still valid if(!presence && roombaPresenceDelay && state.schedDelay) { if(logEnable) log.info "RoombaPresenceDelay is true AND presence has departed." state.schedDelay = false state.startDelayTime = null device.start() RoombaScheduler(false) } //update status of Roomba updateDevices() } } catch (e) { log.error "iRobot communication error. ${e}" } } def getContacts() { def contacts = false if(restrictbyContact.findAll { it?.currentContact == "open"}) { contacts = true } return contacts } def handleDevice(device, id, evt) { try { def restrict = (modesYes && ((restrictbySwitch !=null && restrictbySwitch.currentState("switch").value == "on") || (restrictbyContact !=null && getContacts())) ) ? true : false def device_result = executeAction("/api/local/info/state") def result = "" switch(evt) { case "stop": result = executeAction("/api/local/action/stop") break case "start": if(!restrict) { if(device_result.data.cleanMissionStatus.phase.contains("run") || device_result.data.cleanMissionStatus.phase.contains("hmUsrDock") || device_result.data.batPct.toInteger()roombaBatteryLevel.toInteger()) { if(roomba900) { result = executeAction("/api/local/config/carpetBoost/${roombacarpetBoost}") if(roombaedgeClean) result = executeAction("/api/local/config/edgeClean/on") else result = executeAction("/api/local/config/edgeClean/off") result = executeAction("/api/local/config/cleaningPasses/${roombacleaningPasses}") if(roombaalwaysFinish) result = executeAction("/api/local/config/alwaysFinish/on") else result = executeAction("/api/local/config/alwaysFinish/off") } if(!device_result.data.cleanMissionStatus.phase.contains("run") || !device_result.data.cleanMissionStatus.phase.contains("hmUsrDock")) { result = executeAction("/api/local/action/start") setLastCycle() } else { result = executeAction("/api/local/action/pause") pauseExecution(1500) result = executeAction("/api/local/action/start") setLastCycle() } } else log.warn "${device} is currently not on the charging station. Cannot start cleaning." } } else { if(device_result.data.cleanMissionStatus.phase.contains("run")) { log.warn "Cleaning schedule for ${state.roombaName} is currently restricted. Turn off '${restrictbySwitch.displayName}'" if(pushoverRestrictions) pushNow("Current cleaning for ${state.roombaName} has been restricted. Sending ${state.roombaName} to dock.") result = executeAction("/api/local/action/pause") pauseExecution(1500) result = executeAction("/api/local/action/dock") } else if(pushoverRestrictions) pushNow("Cleaning schedule for ${state.roombaName} is currently restricted. Turn off '${restrictbySwitch.displayName}' to resume cleaning schedule.") } break case "resume": result = executeAction("/api/local/action/resume") break case "pause": result = executeAction("/api/local/action/pause") break case "dock": if(device_result.data.cleanMissionStatus.phase.contains("run") || device_result.data.cleanMissionStatus.phase.contains("hmUsrDock")) { result = executeAction("/api/local/action/pause") pauseExecution(1500) result = executeAction("/api/local/action/dock") } else { result = executeAction("/api/local/action/dock") } break case "off": if(roombaOff=="dock") { if(device_result.data.cleanMissionStatus.phase.contains("run") || device_result.data.cleanMissionStatus.phase.contains("hmUsrDock")) { result = executeAction("/api/local/action/pause") pauseExecution(1500) result = executeAction("/api/local/action/dock") } else { result = executeAction("/api/local/action/dock") } } else result = executeAction("/api/local/action/stop") break } } catch (e) { log.error "iRobot error. Cannot start action. ${e}" } } def setLastCycle() { def now = new Date() state.lastcleaningcycle=now.format("MM/dd/YYYY h:mm a") app.updateLabel("iRobot Scheduler - ${state.roombaName} - Last Cleaning: ${state.lastcleaningcycle}") state.lastcleaning=now.getTime() } def setStateVariables() { // Ensure variables are set def result = executeAction("/api/local/info/state") pushoverStartMsg="%device% has started cleaning" pushoverStopMsg ="%device% has stopped cleaning" pushoverDockMsg="%device% is charging" pushoverBinMsg="%device%'s bin is full" pushoverTankMsg="%device%'s tank is empty" pushoverDeadMsg="%device% battery has died" pushoverErrorMsg="%device% has stopped because" pushoverErrorMsg2="both wheels are stuck" pushoverErrorMsg3="left wheel is stuck" pushoverErrorMsg4="right wheel is stuck" pushoverErrorMsg5="it is wedged under something" pushoverErrorMsg7="cleaning bin is missing" pushoverErrorMsg16="stuck on an object" try { state.roombaName = result.data.name} catch (e) {state.roombaName = "RoombaUnknown"} try {state.pushoverStartMsg = pushoverStartMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverStartMsg = pushoverStartMsg} try {state.pushoverStopMsg = pushoverStopMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverStopMsg = pushoverStopMsg} try {state.pushoverDockMsg = pushoverDockMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverDockMsg = pushoverDockMsg} try {state.pushoverBinMsg = pushoverBinMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverBinMsg = pushoverBinMsg} try {state.pushoverTankMsg = pushoverTankMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverTankMsg = pushoverTankMsg} try {state.pushoverDeadMsg = pushoverDeadMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverDeadMsg = pushoverDeadMsg} try {state.pushoverErrorMsg = pushoverErrorMsg.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg = pushoverErrorMsg} try {state.pushoverErrorMsg2 = pushoverErrorMsg2.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg2 = pushoverErrorMsg2} try {state.pushoverErrorMsg3 = pushoverErrorMsg3.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg3 = pushoverErrorMsg3} try {state.pushoverErrorMsg4 = pushoverErrorMsg4.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg4 = pushoverErrorMsg4} try {state.pushoverErrorMsg4 = pushoverErrorMsg5.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg4 = pushoverErrorMsg5} try {state.pushoverErrorMsg7 = pushoverErrorMsg7.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg7 = pushoverErrorMsg7} try {state.pushoverErrorMsg16 = pushoverErrorMsg16.replace("%device%",state.roombaName)} catch (e) {state.pushoverErrorMsg16 = pushoverErrorMsg16} if(state.prevcleaning==null || state.prevcleaning=="") state.prevcleaning = "idle" state.notified = false if(state.batterydead==null) state.batterydead = false if(state.sendBinNotification==null) state.sendBinNotification = true if(state.sendTankNotification==null) state.sendTankNotification = true if(state.schedDelay==null) state.schedDelay = false state.errors = false if(state.lastcleaning==null) { def now = new Date() long nowtemp = now.getTime() state.lastcleaning=nowtemp } state.presence = false state.startDelayTime=null } def resetApp(){ if(logEnable) log.warn "Application state variables have been reset." state.schedDelay = false state.batterydead = false state.notified = false state.presence = false } def executeAction(path) { def params = [ uri: "http://${doritaIP}:${doritaPort}", path: "${path}", contentType: "application/json" ] def result = null try { httpGet(params) { resp -> result = resp } state.error = false } catch (e) { if(path.contains("carpetBoost")) log.warn "Roomba device does not support Carpet Boost options" else if(path.contains("edgeClean")) log.warn "Roomba device does not support Edge Clean options" else if(path.contains("cleaningPasses")) log.warn "Roomba device does not support Cleaning Passes options" else if(path.contains("alwaysFinish")) log.warn "Roomba device does not support Always Finish options" else if(path.contains("state")) { log.error "Rest980 Server not available: $e" state.error = true } } return result } //Application Handlers def getImage(type) { def loc = "" } def getFormat(type, myText=""){ if(type == "header-blue") return "
${myText}
" if(type == "line") return "\n
" if(type == "title") return "

${myText}

" if(type == "wordline") return "${myText}" } def logsOff(){ log.warn "Debug logging disabled." app?.updateSetting("logEnable",[value:"false",type:"bool"]) } // Start-up stuff def initialize() { if(usePresence) subscribe(roombaPresence, "presence", presenceHandler) if(modesYes) subscribe(restrictbySwitch, "switch", switchHandler) log.info "Initializing $app.label...scheduling jobs." setStateVariables() cleanupChildDevices() createChildDevices() getRoombaSchedule() RoombaScheduler(false) updateDevices() schedule("0/30 * * * * ?", updateDevices) app.updateLabel("iRobot Scheduler - ${state.roombaName}") if (logEnable && logMinutes.toInteger() != 0) { if(logMinutes.toInteger() !=0) log.warn "Debug messages set to automatically disable in ${logMinutes} minute(s)." runIn((logMinutes.toInteger() * 60),logsOff) } else { if(logEnable && logMinutes.toInteger() == 0) {log.warn "Debug logs set to not automatically disable." } } } def installed() { log.info "Installed with settings: ${settings}" state.error=false initialize() } def updated() { log.info "Updated with settings: ${settings}" initialize() } def uninstalled() { log.warn "Uninstalled app" for (device in getChildDevices()) { deleteChildDevice(device.deviceNetworkId) } } //imports import groovy.time.TimeCategory import hubitat.helper.RMUtils