From 0b88a20f251281cd4b5d243208613051a32b0c05 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 7 Apr 2016 22:08:44 -0500 Subject: [PATCH 01/50] Updated structure to enable github integration --- .../copy-ninja/RainMachine.SmartDevice.groovy | 0 .../copy-ninja/RainMachine.SmartApp.groovy | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename RainMachine.SmartDevice.groovy => devicetypes/copy-ninja/RainMachine.SmartDevice.groovy (100%) rename RainMachine.SmartApp.groovy => smartapps/copy-ninja/RainMachine.SmartApp.groovy (100%) diff --git a/RainMachine.SmartDevice.groovy b/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy similarity index 100% rename from RainMachine.SmartDevice.groovy rename to devicetypes/copy-ninja/RainMachine.SmartDevice.groovy diff --git a/RainMachine.SmartApp.groovy b/smartapps/copy-ninja/RainMachine.SmartApp.groovy similarity index 100% rename from RainMachine.SmartApp.groovy rename to smartapps/copy-ninja/RainMachine.SmartApp.groovy From 17c5f9058caac53311bb12c1185ed90fc0fd0514 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 7 Apr 2016 22:15:47 -0500 Subject: [PATCH 02/50] LAN-connected code! Read-only callouts are now done via LAN calls Major rewrite due to nature of async calls Switched to using AtomicState Added "Last Refresh" attribute to child device tiles --- .../copy-ninja/RainMachine.SmartDevice.groovy | 46 +- .../copy-ninja/RainMachine.SmartApp.groovy | 670 +++++++++++++----- 2 files changed, 529 insertions(+), 187 deletions(-) diff --git a/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy b/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy index 9fc5795..82e274a 100644 --- a/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy +++ b/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy @@ -1,8 +1,8 @@ /** * RainMachine Smart Device * - * Author: Jason Mok - * Date: 2014-12-20 + * Author: Jason Mok/Brian Beaird + * Date: 2016-04-07 * *************************** * @@ -35,7 +35,8 @@ metadata { capability "Refresh" capability "Polling" - attribute "runTime", "number" + attribute "runTime", "number" + attribute "lastRefresh", "string" //command "pause" //command "resume" @@ -68,9 +69,12 @@ metadata { valueTile("runTime", "device.runTime", inactiveLabel: false, decoration: "flat") { state("runTimeValue", label:'${currentValue} mins', backgroundColor:"#ffffff") } + valueTile("lastRefresh", "device.lastRefresh", height: 1, width: 3, inactiveLabel: false, decoration: "flat") { + state("lastRefreshValue", label:'Last refresh: ${currentValue}', backgroundColor:"#ffffff") + } main "contact" - details(["contact","refresh","stopAll","runTimeControl","runTime"]) + details(["contact","refresh","stopAll","runTimeControl","runTime","lastActivity","lastRefresh"]) } } @@ -80,7 +84,7 @@ def installed() { poll() } -def parse(String description) {} +//def parse(String description) {} // turn on sprinkler def open() { @@ -95,8 +99,9 @@ def close() { // refresh status def refresh() { - parent.refresh() - poll() + sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) + parent.refresh() + //poll() } //resume sprinkling @@ -112,9 +117,14 @@ def pause() { // update status def poll() { log.info "Polling.." - deviceStatus(parent.getDeviceStatus(this)) + //deviceStatus(parent.getDeviceStatus(this)) + //def lastRefresh = parent.getDeviceLastRefresh(this) + //log.debug "Last refresh: " + lastRefresh + //sendEvent("name":"lastRefresh", "value": lastRefresh) } + + // stop everything def stopAll() { parent.sendStopAll() @@ -123,7 +133,25 @@ def stopAll() { // update the run time for manual zone void setRunTime(runTimeSecs) { - sendEvent("name":"runTime", "value": runTimeSecs) + sendEvent("name":"runTime", "value": runTimeSecs) +} + +def updateDeviceLastRefresh(lastRefresh){ + log.debug "Last refresh: " + lastRefresh + + def refreshDate = new Date() + def hour = refreshDate.format("h", location.timeZone) + def minute =refreshDate.format("m", location.timeZone) + def ampm =refreshDate.format("a", location.timeZone) + //def finalString = refreshDate.getDateString() + ' ' + hour + ':' + minute + ampm + + def finalString = new Date().format('MM/d/yyyy hh:mm',location.timeZone) + + sendEvent(name: "lastRefresh", value: finalString, display: false , displayed: false) +} + +def updateDeviceStatus(status){ + deviceStatus(status) } // update status diff --git a/smartapps/copy-ninja/RainMachine.SmartApp.groovy b/smartapps/copy-ninja/RainMachine.SmartApp.groovy index c93c577..ab2e61e 100644 --- a/smartapps/copy-ninja/RainMachine.SmartApp.groovy +++ b/smartapps/copy-ninja/RainMachine.SmartApp.groovy @@ -1,8 +1,8 @@ /** * RainMachine Service Manager SmartApp * - * Author: Jason Mok - * Date: 2014-12-20 + * Author: Jason Mok/Brian Beaird + * Date: 2016-3-15 * *************************** * @@ -45,75 +45,258 @@ definition( iconX3Url: "http://smartthings.copyninja.net/icons/RainMachine@3x.png" ) -preferences { - page(name: "prefLogIn", title: "RainMachine") - page(name: "prefListProgramsZones", title: "RainMachine") +preferences { + page(name: "prefLogIn", title: "RainMachine") + page(name: "prefLogInWait", title: "RainMachine") + page(name: "prefListProgramsZones", title: "RainMachine") + } /* Preferences */ def prefLogIn() { - def showUninstall = ip_address != null && password != null && ip_port != null - return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefListProgramsZones", uninstall:showUninstall, install: false) { + + //RESET ALL THE THINGS + atomicState.initialLogin = false + atomicState.loginResponse = null + atomicState.zonesResponse = null + atomicState.programsResponse = null + + def showUninstall = ip_address != null && password != null + return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ - input("ip_address", "text", title: "IP Address/Host Name", description: "IP Address/Host Name of RainMachine") - input("ip_port", "text", title: "Port Number", description: "Forwarded port RainMachine") + input("ip_address", "text", title: "IP Address/Host Name", description: "IP Address/Host Name of RainMachine") } section("Login Credentials"){ input("password", "password", title: "Password", description: "RainMachine password") } section("Server Polling"){ input("polling", "int", title: "Polling Interval (in minutes)", description: "in minutes", defaultValue: 5) - } + } } } -def prefListProgramsZones() { - if (forceLogin()) { - return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", install:true, uninstall:true) { - section("Select which programs to use"){ - input(name: "programs", type: "enum", required:false, multiple:true, metadata:[values:getProgramList()]) - } - section("Select which zones to use"){ - input(name: "zones", type: "enum", required:false, multiple:true, metadata:[values:getZoneList()]) - } - } - } +def prefLogInWait() { + log.debug "Logging in...waiting..." + "Current login response: " + atomicState.loginResponse + + doLogin() + + //Wait up to 20 seconds for login response + def i = 0 + while (i < 5){ + pause(2000) + if (atomicState.loginResponse != null){ + log.debug "Got a response! Let's go!" + i = 5 + } + i++ + } + + log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse + + //Connection issue + if (atomicState.loginResponse == null){ + log.debug "Unable to connect" + return dynamicPage(name: "prefLogInWait", title: "Log In", uninstall:false, install: false) { + section() { + paragraph "Unable to connect to Rainmachine. Check your local IP and try again" + } + } + } + + //Bad login credentials + if (atomicState.loginResponse == "Bad Login"){ + log.debug "Bad Login show on form" + return dynamicPage(name: "prefLogInWait", title: "Log In", uninstall:false, install: false) { + section() { + paragraph "Bad username/password. Click back and try again." + } + } + } + + //Login Success! + if (atomicState.loginResponse == "Success"){ + getZonesAndPrograms() + + //Wait up to 10 seconds for login response + i = 0 + while (i < 5){ + pause(2000) + if (atomicState.zonesResponse == "Success" && atomicState.programsResponse == "Success" ){ + log.debug "Got a zone response! Let's go!" + i = 5 + } + i++ + } + + log.debug "Done waiting on zones/programs. zone response: " + atomicState.zonesResponse + " programs response: " + atomicState.programsResponse + + return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", install:true, uninstall:true) { + section("Select which programs to use"){ + input(name: "programs", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ProgramList]) + } + section("Select which zones to use"){ + input(name: "zones", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ZoneList]) + } + } + } + + else{ + return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", uninstall:false, install: false) { + section() { + paragraph "Problem getting zone/program data. Click back and try again." + } + } + + } + +} + + +def parseLoginResponse(response){ + + log.debug "Reset login info!" + atomicState.access_token = "" + atomicState.expires_in = "" + + atomicState.loginResponse = 'Received' + + if (response.statusCode == 2){ + atomicState.loginResponse = 'Bad Login' + } + + log.debug "token was " + response.access_token + if (response.access_token != null){ + log.debug "Saving token" + atomicState.access_token = response.access_token + log.debug "Login token newly set to: " + atomicState.access_token + atomicState.expires_in = now() + response.expires_in + } + atomicState.loginResponse = 'Success' + log.debug "Login response set to: " + atomicState.loginResponse + log.debug "Login token was set to: " + atomicState.access_token +} + + +def parse(evt) { + + //log.debug "Evt: " + evt + //log.debug "Dev: " + evt.device + //log.debug "Name: " + evt.name + //log.debug "Source: " + evt.source + def description = evt.description + def hub = evt?.hubId + + //log.debug "cp desc: " + description + + def msg = parseLanMessage(evt.description) + + def headersAsString = msg.header // => headers as a string + def headerMap = msg.headers // => headers as a Map + def body = msg.body // => request body as a string + def status = msg.status // => http status code of the response + def json = msg.json // => any JSON included in response body, as a data structure of lists and maps + def xml = msg.xml // => any XML included in response body, as a document tree structure + def data = msg.data // => either JSON or XML in response body (whichever is specified by content-type header in response) + + + if (status == 200 && Body != "OK") { + + def slurper = new groovy.json.JsonSlurper() + def result = slurper.parseText(body) + + if (result.zones){ + log.debug "Zone response detected!" + log.debug "result: " + result + atomicState.zonesResponse = "Success" + getZoneList(result.zones) + } + + if (result.programs){ + log.debug "Program response detected!" + log.debug "result: " + result + atomicState.programsResponse = "Success" + log.debug "Set program status to: " + atomicState.programsResponse + getProgramList(result.programs) + } + + if (result.statusCode != null){ + log.debug "Login response detected!" + log.debug "result: " + result + parseLoginResponse(result) + } + + } + else if (status == 401){ + log.debug "result: " + body + atomicState.expires_in = now() - 500 + atomicState.access_token = "" + atomicState.loginResponse = 'Bad Login' + } + else if (status != 411 && body != null){ + log.debug "Unexpected response! " + status + " " + body + "evt " + description + } + + +} + + +def doLogin(){ + atomicState.loginResponse = null + return doCallout("POST", "/api/4/auth/login", "{\"pwd\": \"" + password + "\",\"remember\": 1 }") +} + +def getZonesAndPrograms(){ + atomicState.zonesResponse = null + atomicState.programsResponse = null + log.debug "Getting zones and programs using token: " + atomicState.access_token + doCallout("GET", "/api/4/zone?access_token=" + atomicState.access_token , "") + doCallout("GET", "/api/4/program?access_token=" + atomicState.access_token , "") } /* Initialization */ def installed() { log.info "installed()" log.debug "Installed with settings: " + settings - unschedule() - forceLogin() - initialize() + //unschedule() } def updated() { log.info "updated()" log.debug "Updated with settings: " + settings - state.polling = [ + atomicState.polling = [ last: now(), runNow: true ] - unschedule() - unsubscribe() - login() + //unschedule() + unsubscribe() initialize() } def uninstalled() { def delete = getAllChildDevices() delete.each { deleteChildDevice(it.deviceNetworkId) } -} +} + + +def updateMapData(){ + def combinedMap = [:] + combinedMap << atomicState.ProgramData + combinedMap << atomicState.ZoneData + atomicState.data = combinedMap +} def initialize() { - log.info "initialize()" - - // Get initial device status in state.data - refresh() - - def progZones = [] + log.info "initialize()" + + //Merge Zone and Program data into single map + //atomicState.data = [:] + + def combinedMap = [:] + combinedMap << atomicState.ProgramData + combinedMap << atomicState.ZoneData + atomicState.data = combinedMap + + def selectedItems = [] def programList = [:] def zoneList = [:] def delete @@ -121,23 +304,23 @@ def initialize() { // Collect programs and zones if (settings.programs) { if (settings.programs[0].size() > 1) { - progZones = settings.programs + selectedItems = settings.programs } else { - progZones.add(settings.programs) + selectedItems.add(settings.programs) } - programList = getProgramList() + programList = atomicState.ProgramList } if (settings.zones) { if (settings.zones[0].size() > 1) { - settings.zones.each { dni -> progZones.add(dni)} + settings.zones.each { dni -> selectedItems.add(dni)} } else { - progZones.add(settings.zones) + selectedItems.add(settings.zones) } - zoneList = getZoneList() + zoneList = atomicState.ZoneList } // Create device if selected and doesn't exist - progZones.each { dni -> + selectedItems.each { dni -> def childDevice = getChildDevice(dni) def childDeviceAttrib = [:] if (!childDevice) { @@ -151,113 +334,135 @@ def initialize() { } // Delete child devices that are not selected in the settings - if (!progZones) { + if (!selectedItems) { delete = getAllChildDevices() } else { delete = getChildDevices().findAll { - !progZones.contains(it.deviceNetworkId) + !selectedItems.contains(it.deviceNetworkId) } } delete.each { deleteChildDevice(it.deviceNetworkId) } + //Update data for child devices + pollAllChild() + + + //Subscribes to sunrise and sunset event to trigger refreshes + subscribe(location, "sunrise", monitorTheMonitor) + subscribe(location, "sunset", monitorTheMonitor) + subscribe(location, "mode", monitorTheMonitor) + subscribe(location, "sunriseTime", monitorTheMonitor) + subscribe(location, "sunsetTime", monitorTheMonitor) + + //Reset monitoring timestamp + atomicState.lastMonitored = now() + // Schedule polling - schedule("0 0/" + (settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1 + " * * * ?", refresh ) + schedulePoll() + schedule("19 0/" + 5 + " * * * ?", monitorPoll ) + } + /* Access Management */ -private forceLogin() { - //Reset token and expiry - state.auth = [ - expires_in: now() - 500, - access_token: "" - ] - state.polling = [ - last: now(), - runNow: true - ] - state.data = [:] - state.pause = [:] - return doLogin() +private loginTokenExists(){ + log.debug "Checking for token: " + return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) } -private login() { - if (!(state.auth.expires_in > now())) { - return doLogin() - } else { - return true - } -} -private doLogin() { - // TODO: make call through hub later... - apiPost("/api/4/auth/login",[pwd: settings.password, remember: 1]) { response -> - if (response.status == 200) { - state.auth.expires_in = now() + response.data.expires_in - state.auth.access_token = response.data.access_token - return true - } else { - return false - } - } +def doCallout(calloutMethod, urlPath, calloutBody){ + subscribe(location, null, parse, [filterEvents:false]) + log.info "Calling out to " + ip_address + urlPath + + //192.168.1.74:8 + + def httpRequest = [ + method: calloutMethod, + path: urlPath, + headers: [ + HOST: ip_address + ":80", + "Content-Type": "application/json", + Accept: "*/*", + ], + body: calloutBody + ] + + def hubAction = new physicalgraph.device.HubAction(httpRequest) + //log.debug "hubaction: " + hubAction + return sendHubCommand(hubAction) } + // Listing all the programs you have in RainMachine -def getProgramList() { - def programsList = [:] - apiGet("/api/4/program") { response -> - if (response.status == 200) { - response.data.programs.each { program -> - if (program.uid) { - def dni = [ app.id, "prog", program.uid ].join('|') - def endTime = 0 //TODO: calculate time left for the program - programsList[dni] = program.name - state.data[dni] = [ - status: program.status, - endTime: endTime - ] - log.debug "Prog: " + dni + " Status : " + state.data[dni].status - } - } - } - } - return programsList +def getProgramList(programs) { + //atomicState.ProgramData = [:] + def tempList = [:] + + def programsList = [:] + programs.each { program -> + if (program.uid) { + def dni = [ app.id, "prog", program.uid ].join('|') + def endTime = 0 //TODO: calculate time left for the program + + programsList[dni] = program.name + + tempList[dni] = [ + status: program.status, + endTime: endTime, + lastRefresh: now() + ] + + log.debug "Prog: " + dni + " Status : " + tempList[dni] + + } + } + atomicState.ProgramList = programsList + atomicState.ProgramData = tempList + + log.debug "temp list reviewed! " + atomicState.ProgramList + log.debug "atomic data reviewed! " + atomicState.ProgramData + + //log.debug "atomic data reviewed! " + atomicState.data + //pollAllChild() } // Listing all the zones you have in RainMachine -def getZoneList() { - def zonesList = [:] - apiGet("/api/4/zone") { response -> - if (response.status == 200) { - response.data.zones.each { zone -> - def dni = [ app.id, "zone", zone.uid ].join('|') - def endTime = now + ((zone.remaining?:0) * 1000) - zonesList[dni] = zone.name - state.data[dni] = [ - status: zone.state, - endTime: endTime - ] - log.debug "Zone: " + dni + " Status : " + state.data[dni].status - } - } - } - return zonesList +def getZoneList(zones) { + atomicState.ZoneData = [:] + def tempList = [:] + def zonesList = [:] + zones.each { zone -> + def dni = [ app.id, "zone", zone.uid ].join('|') + def endTime = now + ((zone.remaining?:0) * 1000) + zonesList[dni] = zone.name + tempList[dni] = [ + status: zone.state, + endTime: endTime, + lastRefresh: now() + ] + log.debug "Zone: " + dni + " Status : " + tempList[dni] + } + atomicState.ZoneList = zonesList + atomicState.ZoneData = tempList + log.debug "State zone list: " + atomicState.zonesList } // Updates devices -def updateDeviceData() { +def updateDeviceData() { log.info "updateDeviceData()" // automatically checks if the token has expired, if so login again if (login()) { // Next polling time, defined in settings - def next = (state.polling.last?:0) + ( (settings.polling.toInteger() > 0 ? settings.polling.toInteger() : 1) * 60 * 1000) - log.debug "last: " + state.polling.last - log.debug "now: " + now() + def next = (atomicState.polling.last?:0) + ( (settings.polling.toInteger() > 0 ? settings.polling.toInteger() : 1) * 60 * 1000) + log.debug "last: " + atomicState.polling.last + log.debug "now: " + new Date( now() * 1000 ) log.debug "next: " + next - log.debug "RunNow: " + state.polling.runNow - if ((now() > next) || (state.polling.runNow)) { + log.debug "RunNow: " + atomicState.polling.runNow + if ((now() > next) || (atomicState.polling.runNow)) { // set polling states - state.polling = [ + atomicState.polling = [ last: now(), runNow: false ] @@ -273,11 +478,14 @@ def updateDeviceData() { } def pollAllChild() { - // get all the children and send updates + // get all the children and send updates def childDevice = getAllChildDevices() childDevice.each { - log.debug "Polling " + it.deviceNetworkId - it.poll() + log.debug "Updating children " + it.deviceNetworkId + //sendAlert("Trying to set last refresh to: " + atomicState.data[it.deviceNetworkId].lastRefresh) + it.updateDeviceStatus(atomicState.data[it.deviceNetworkId].status) + it.updateDeviceLastRefresh(atomicState.data[it.deviceNetworkId].lastRefresh) + //it.poll() } } @@ -293,67 +501,93 @@ private getChildType(child) { if (childType == "zone") { return "zone" } } -/* api connection */ -// HTTP GET call -private apiGet(apiPath, callback = {}) { - def apiParams = [ - uri: "http://" + settings.ip_address + ":" + settings.ip_port, - path: apiPath, - contentType: "application/json", - query: [ access_token: state.auth.access_token ] - ] - log.debug "HTTP GET : " + apiParams - try { - httpGet(apiParams) { response -> - if (response.data.ErrorMessage) { - log.debug "API Error: $response.data" - } - callback(response) - } - } - catch (Error e) { - log.debug "API Error: $e" - } -} -// HTTP POST call -def apiPost(apiPath, apiBody, callback = {}) { - def apiParams = [ - uri: "http://" + settings.ip_address + ":" + settings.ip_port, - path: apiPath, - contentType: "application/json", - query: [ access_token: state.auth.access_token ], - body: apiBody - ] - log.debug "HTTP POST : " + apiParam - try { - httpPostJson("http://" + settings.ip_address + ":" + settings.ip_port + apiPath + "?access_token=" + state.auth.access_token, apiBody) { response -> - if (response.data.ErrorMessage) { - log.debug "API Error: $response.data" - } - callback(response) - } - } - catch (Error e) { - log.debug "API Error: $e" - } -} + /* for SmartDevice to call */ // Refresh data -def refresh() { - log.info "refresh()" - // Set initial polling run - state.polling = [ +def refresh() { + log.info "refresh()" + + atomicState.polling = [ last: now(), runNow: true ] - state.data = [:] + //atomicState.data = [:] + + + + //If login token exists and is valid, reuse it and callout to refresh zone and program data + if (loginTokenExists()){ + log.debug "Existing token detected" + getZonesAndPrograms() + + //Wait up to 10 seconds before cascading results to child devices + def i = 0 + while (i < 5){ + pause(2000) + if (atomicState.zonesResponse == "Success" && atomicState.programsResponse == "Success" ){ + log.debug "Got a zone response! Let's go!" + updateMapData() + pollAllChild() + return true + } + log.debug "Current zone response: " + atomicState.zonesResponse + "Current pgm response: " + atomicState.programsResponse + i++ + } + + if (atomicState.zonesResponse == null){ + log.debug "Unable to get zone data while trying to refresh" + return false + } + + if (atomicState.programsResponse == null){ + log.debug "Unable to get zone data while trying to refresh" + return false + } + + } + + //If not, get a new token then refresh + else{ + log.debug "Need new token" + doLogin() + + //Wait up to 20 seconds for successful login + def i = 0 + while (i < 5){ + pause(2000) + if (atomicState.loginResponse != null){ + log.debug "Got a response! Let's go!" + i = 5 + } + i++ + } + log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse + + + if (atomicState.loginResponse == null){ + log.debug "Unable to connect while trying to refresh zone/program data" + return false + } + + + if (atomicState.loginResponse == "Bad Login"){ + log.debug "Bad Login while trying to refresh zone/program data" + return false + } + + + if (atomicState.loginResponse == "Success"){ + log.debug "Got a zone response! Let's go!" + refresh() + } + + } //Update Devices - updateDeviceData() + //updateDeviceData() - pause(1000) - pollAllChild() + } // Get single device status @@ -361,15 +595,28 @@ def getDeviceStatus(child) { log.info "getDeviceStatus()" //tries to get latest data if polling limitation allows //updateDeviceData() - return state.data[child.device.deviceNetworkId].status + return atomicState.data[child.device.deviceNetworkId].status } +// Get single device refresh timestamp +def getDeviceLastRefresh(child) { + log.info "getDeviceStatus()" + //tries to get latest data if polling limitation allows + //updateDeviceData() + return atomicState.data[child.device.deviceNetworkId].lastRefresh +} + + + + + + // Get single device ending time def getDeviceEndTime(child) { //tries to get latest data if polling limitation allows updateDeviceData() - if (state.data[child.device.deviceNetworkId]) { - return state.data[child.device.deviceNetworkId].endTime + if (atomicState.data[child.device.deviceNetworkId]) { + return atomicState.data[child.device.deviceNetworkId].endTime } } @@ -388,7 +635,7 @@ def sendCommand(child, apiCommand, apiTime) { //Checks for any active running sprinklers before allowing another program to run if (childType == "program") { if (apiCommand == "start") { - state.data.each { dni, data -> if ((data.status == 1) || (data.status == 2)) { zonesActive = true }} + atomicState.data.each { dni, data -> if ((data.status == 1) || (data.status == 2)) { zonesActive = true }} if (!zonesActive) { apiPost(apiPath, [pid: childUID]) commandSuccess = true @@ -425,3 +672,70 @@ def sendStopAll() { refresh() return true } + +def monitorPoll(){ + try { + log.debug "Monitoring the poll...Last poll stamp: " + atomicState.polling.last + if (now() > atomicState.polling.last + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1)*100000*2){ + log.debug "RainMachine polling schedule needs reboot!" + sendAlert("RainMachine schedule is dead! Restart!") + reSchedulePoll() + } + atomicState.lastMonitored = now() + } catch (Error e) { + log.debug "Error in RainMachine monitorPoll: $e" + sendAlert("Error in RainMachine monitorPoll: $e") + } + +} + +private schedulePoll() { + log.debug "Creating RainMachine schedule. Setting was " + settings.polling + unschedule() + schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", refresh ) + log.debug "RainMachine schedule successfully started!" +} + +private reSchedulePoll() { + try { + log.debug "Attempting to recreate the RainMachine schedule..." + schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", refresh ) + log.debug "RainMachine schedule successfully restarted!" + sendAlert("RainMachine schedule successfully restarted!") + } catch (Error e) { + log.debug "Error restarting RainMachine schedule: $e" + sendAlert("Error restarting RainMachine schedule: $e") + } +} + +//Last line of defense against SDSS +public monitorTheMonitor(evt){ + try { + log.debug "Event " + evt.displayName + " triggered monitoring the rainmachine monitor...Last poll stamp: " + atomicState.lastMonitored + if (now() > atomicState.lastMonitored + 480000){ + log.debug "RainMachine monitor schedule needs reboot!" + sendAlert("RainMachine monitor schedule is dead! Restart!") + //reScheduleMonitor() + } + } catch (Error e) { + log.debug "Error in RainMachine monitorPoll: $e" + sendAlert("Error in RainMachine monitorPoll: $e") + } +} + + +private reScheduleMonitor() { + try { + log.debug "Attempting to recreate the RainMachine monitor..." + schedule("19 0/" + 5 + " * * * ?", monitorPoll ) + log.debug "RainMachine monitor successfully restarted!" + sendAlert("RainMachine monitor successfully restarted!") + } catch (Error e) { + log.debug "Error restarting RainMachine monitor: $e" + sendAlert("Error restarting RainMachine monitor: $e") + } +} + +def sendAlert(alert){ + sendSms("615-828-5772", "Alert: " + alert) +} From c58fc9e34ebf9c4278fcdfe193e1149147c97625 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 09:57:09 -0500 Subject: [PATCH 03/50] More github structuring --- .../{ => rainmachine.src}/RainMachine.SmartDevice.groovy | 0 .../copy-ninja/{ => rainmachine.src}/RainMachine.SmartApp.groovy | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename devicetypes/copy-ninja/{ => rainmachine.src}/RainMachine.SmartDevice.groovy (100%) rename smartapps/copy-ninja/{ => rainmachine.src}/RainMachine.SmartApp.groovy (100%) diff --git a/devicetypes/copy-ninja/RainMachine.SmartDevice.groovy b/devicetypes/copy-ninja/rainmachine.src/RainMachine.SmartDevice.groovy similarity index 100% rename from devicetypes/copy-ninja/RainMachine.SmartDevice.groovy rename to devicetypes/copy-ninja/rainmachine.src/RainMachine.SmartDevice.groovy diff --git a/smartapps/copy-ninja/RainMachine.SmartApp.groovy b/smartapps/copy-ninja/rainmachine.src/RainMachine.SmartApp.groovy similarity index 100% rename from smartapps/copy-ninja/RainMachine.SmartApp.groovy rename to smartapps/copy-ninja/rainmachine.src/RainMachine.SmartApp.groovy From d662675c38a792b91d4691679e0cfe2260b221d4 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 09:59:25 -0500 Subject: [PATCH 04/50] Even more github integration changes --- .../{RainMachine.SmartDevice.groovy => rainmachine.groovy} | 0 .../{RainMachine.SmartApp.groovy => rainmachine.groovy} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename devicetypes/copy-ninja/rainmachine.src/{RainMachine.SmartDevice.groovy => rainmachine.groovy} (100%) rename smartapps/copy-ninja/rainmachine.src/{RainMachine.SmartApp.groovy => rainmachine.groovy} (100%) diff --git a/devicetypes/copy-ninja/rainmachine.src/RainMachine.SmartDevice.groovy b/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy similarity index 100% rename from devicetypes/copy-ninja/rainmachine.src/RainMachine.SmartDevice.groovy rename to devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy diff --git a/smartapps/copy-ninja/rainmachine.src/RainMachine.SmartApp.groovy b/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy similarity index 100% rename from smartapps/copy-ninja/rainmachine.src/RainMachine.SmartApp.groovy rename to smartapps/copy-ninja/rainmachine.src/rainmachine.groovy From ead16664dd76f7ce08a45016a340b994bf9e0fbf Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 10:02:42 -0500 Subject: [PATCH 05/50] Added support for start/stop commands --- .../rainmachine.src/rainmachine.groovy | 144 +++++++++++++++--- .../rainmachine.src/rainmachine.groovy | 136 ++++++++++++++--- 2 files changed, 238 insertions(+), 42 deletions(-) diff --git a/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy b/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy index 82e274a..99ef284 100644 --- a/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy +++ b/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird - * Date: 2016-04-07 + * Date: 2016-04-08 * *************************** * @@ -30,13 +30,14 @@ * */ metadata { - definition (name: "RainMachine", namespace: "copy-ninja", author: "Jason Mok") { + definition (name: "RainMachine", namespace: "copy-ninja", author: "Jason Mok/Brian Beaird") { capability "Valve" capability "Refresh" capability "Polling" attribute "runTime", "number" attribute "lastRefresh", "string" + attribute "lastStarted", "string" //command "pause" //command "resume" @@ -50,7 +51,14 @@ metadata { standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff") state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#1e9cbb") - state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + //state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + + //state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e", nextState: "open") + //state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e", nextState: "closed") + + } /* standardTile("pausume", "device.switch", inactiveLabel: false, decoration: "flat") { state("resume", label:'resume', action:"pause", icon:"st.sonos.play-icon", nextState:"pause") @@ -81,25 +89,47 @@ metadata { // installation, set default value def installed() { runTime = 5 - poll() + //poll() } //def parse(String description) {} // turn on sprinkler def open() { - parent.sendCommand(this, "start", (device.currentValue("runTime") * 60)) - poll() + sendEvent(name: "contact", value: "opening", display: true, displayed: false) + parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) + //poll() } // turn off sprinkler def close() { - parent.sendCommand(this, "stop", (device.currentValue("runTime") * 60)) - poll() + sendEvent(name: "contact", value: "closing", display: true, displayed: false) + parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) + //poll() } // refresh status -def refresh() { - sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) +def refresh() { + //def deviceName = device.displayName + //def message = deviceName + " finished watering. Run time: " + //log.debug message + //log.debug "zone? " + deviceName.contains("Zone") + + //if (parent.sendPush && deviceName.contains("Zone")) { +// log.debug "do the push" +// parent.sendPushMessage(message) +// } + + //log.debug "found lastStarted as : " + device.currentValue("lastStarted") + + //def lastStarted = device.currentValue("lastStarted") + //log.debug "long it: " + lastStarted.toLong() + //sendEvent(name: "lastStarted", value: now(), display: false , displayed: false) + + + + //log.debug "Old Device Status: " + device.currentValue("contact") + //log.debug "Last started: " + device.currentValue("lastStarted") + sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) parent.refresh() //poll() } @@ -127,8 +157,11 @@ def poll() { // stop everything def stopAll() { - parent.sendStopAll() - poll() + sendEvent(name: "contact", value: "closing", display: true, displayed: false) + parent.sendCommand2(this, "stopall", (device.currentValue("runTime") * 60)) + + //parent.sendStopAll() + //poll() } // update the run time for manual zone @@ -136,7 +169,7 @@ void setRunTime(runTimeSecs) { sendEvent("name":"runTime", "value": runTimeSecs) } -def updateDeviceLastRefresh(lastRefresh){ +def updateDeviceLastRefresh(lastRefresh){ log.debug "Last refresh: " + lastRefresh def refreshDate = new Date() @@ -146,8 +179,7 @@ def updateDeviceLastRefresh(lastRefresh){ //def finalString = refreshDate.getDateString() + ' ' + hour + ':' + minute + ampm def finalString = new Date().format('MM/d/yyyy hh:mm',location.timeZone) - - sendEvent(name: "lastRefresh", value: finalString, display: false , displayed: false) + sendEvent(name: "lastRefresh", value: finalString, display: false , displayed: false) } def updateDeviceStatus(status){ @@ -156,16 +188,82 @@ def updateDeviceStatus(status){ // update status def deviceStatus(status) { - log.debug "Current Device Status: " + status - if (status == 0) { - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") - //sendEvent(name: "pausume", value: "resume") + log.debug "Old Device Status: " + device.currentValue("contact") + log.debug "New Device Status: " + status + + + if (status == 0) { //Device has turned off + + //Go ahead and mark the valve as closed + def oldStatus = device.currentValue("contact") + sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + + //If device has just recently closed, send notification + if (oldStatus != 'closed' && oldStatus != null){ + + //Take note of how long it ran and send notification + log.debug "lastStarted: " + device.currentValue("lastStarted") + def lastStarted = device.currentValue("lastStarted") + def lastActivityValue = "Unknown." + + if (lastStarted != null){ + lastActivityValue = "" + long lastStartedLong = lastStarted.toLong() + + log.debug "lastStarted converted: " + lastStarted + + + def diffTotal = now() - lastStartedLong + def diffDays = (diffTotal / 86400000) as long + def diffHours = (diffTotal % 86400000 / 3600000) as long + def diffMins = (diffTotal % 86400000 % 3600000 / 60000) as long + + if (diffDays == 1) lastActivityValue += "${diffDays} Day " + else if (diffDays > 1) lastActivityValue += "${diffDays} Days " + + if (diffHours == 1) lastActivityValue += "${diffHours} Hour " + else if (diffHours > 1) lastActivityValue += "${diffHours} Hours " + + if (diffMins == 1 || diffMins == 0 ) lastActivityValue += "${diffMins} Min" + else if (diffMins > 1) lastActivityValue += "${diffMins} Mins" + } + + def deviceName = device.displayName + def message = deviceName + " finished watering. Run time: " + lastActivityValue + log.debug message + + if (parent.prefSendPush && deviceName.contains("Zone")) { + parent.sendAlert(message) + //parent.sendPushMessage(message) + } + + + //sendEvent(name: "pausume", value: "resume") + } + + } - if (status == 1) { - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") - //sendEvent(name: "pausume", value: "pause") + if (status == 1) { //Device has turned on + log.debug "Zone turned on!" + + //Go ahead and mark the valve as closed + def oldStatus = device.currentValue("contact") + sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + + //If device has just recently opened, take note of time + if (oldStatus != 'open' && oldStatus != null){ + //Take note of current time the zone started + def refreshDate = new Date() + def hour = refreshDate.format("h", location.timeZone) + def minute =refreshDate.format("m", location.timeZone) + def ampm =refreshDate.format("a", location.timeZone) + def finalString = new Date().format('MM/d/yyyy hh:mm',location.timeZone) + sendEvent(name: "lastStarted", value: now(), display: false , displayed: false) + log.debug "stored lastStarted as : " + device.currentValue("lastStarted") + //sendEvent(name: "pausume", value: "pause") + } } - if (status == 2) { + if (status == 2) { //Device is pending sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") //sendEvent(name: "pausume", value: "pause") } diff --git a/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy b/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy index ab2e61e..1af85d3 100644 --- a/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy +++ b/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy @@ -72,6 +72,9 @@ def prefLogIn() { section("Server Polling"){ input("polling", "int", title: "Polling Interval (in minutes)", description: "in minutes", defaultValue: 5) } + section("Push Notifications") { + input "prefSendPush", "bool", required: false, title: "Push notifications when zones finish?" + } } } @@ -204,30 +207,42 @@ def parse(evt) { def slurper = new groovy.json.JsonSlurper() def result = slurper.parseText(body) + //Zone response if (result.zones){ log.debug "Zone response detected!" - log.debug "result: " + result - atomicState.zonesResponse = "Success" + log.debug "zone result: " + result getZoneList(result.zones) } + //Program response if (result.programs){ log.debug "Program response detected!" - log.debug "result: " + result - atomicState.programsResponse = "Success" - log.debug "Set program status to: " + atomicState.programsResponse + log.debug "program result: " + result getProgramList(result.programs) } + //Figure out the other response types if (result.statusCode != null){ - log.debug "Login response detected!" - log.debug "result: " + result - parseLoginResponse(result) + + //Login response + if (result.access_token != null){ + log.debug "Login response detected!" + log.debug "result: " + result + parseLoginResponse(result) + } + + //Generic error from one of the command methods + else if (result.statusCode != 0) { + log.debug "Error status detected! One of the last calls just failed!" + } + else{ + log.debug "Remote command successfully processed by Rainmachine controller." + } } } else if (status == 401){ - log.debug "result: " + body + log.debug "401 - bad login detected! result: " + body atomicState.expires_in = now() - 500 atomicState.access_token = "" atomicState.loginResponse = 'Bad Login' @@ -283,6 +298,7 @@ def updateMapData(){ combinedMap << atomicState.ProgramData combinedMap << atomicState.ZoneData atomicState.data = combinedMap + log.debug "new data list: " + atomicState.data } def initialize() { @@ -365,8 +381,8 @@ def initialize() { /* Access Management */ -private loginTokenExists(){ - log.debug "Checking for token: " +public loginTokenExists(){ + log.debug "Checking for token: " return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) } @@ -374,6 +390,7 @@ private loginTokenExists(){ def doCallout(calloutMethod, urlPath, calloutBody){ subscribe(location, null, parse, [filterEvents:false]) log.info "Calling out to " + ip_address + urlPath + //sendAlert("Calling out to " + ip_address + urlPath + " body: " + calloutBody) //192.168.1.74:8 @@ -413,7 +430,7 @@ def getProgramList(programs) { lastRefresh: now() ] - log.debug "Prog: " + dni + " Status : " + tempList[dni] + //log.debug "Prog: " + dni + " Status : " + tempList[dni] } } @@ -422,6 +439,7 @@ def getProgramList(programs) { log.debug "temp list reviewed! " + atomicState.ProgramList log.debug "atomic data reviewed! " + atomicState.ProgramData + atomicState.programsResponse = "Success" //log.debug "atomic data reviewed! " + atomicState.data //pollAllChild() @@ -441,11 +459,13 @@ def getZoneList(zones) { endTime: endTime, lastRefresh: now() ] - log.debug "Zone: " + dni + " Status : " + tempList[dni] + //log.debug "Zone: " + dni + " Status : " + tempList[dni] } atomicState.ZoneList = zonesList atomicState.ZoneData = tempList - log.debug "State zone list: " + atomicState.zonesList + log.debug "Temp zone list: " + zonesList + log.debug "State zone list: " + atomicState.ZoneList + atomicState.zonesResponse = "Success" } // Updates devices @@ -483,6 +503,10 @@ def pollAllChild() { childDevice.each { log.debug "Updating children " + it.deviceNetworkId //sendAlert("Trying to set last refresh to: " + atomicState.data[it.deviceNetworkId].lastRefresh) + if (atomicState.data[it.deviceNetworkId] == null){ + sendAlert("Refresh problem on ID: " + it.deviceNetworkId) + sendAlert("data list: " + atomicState.data) + } it.updateDeviceStatus(atomicState.data[it.deviceNetworkId].status) it.updateDeviceLastRefresh(atomicState.data[it.deviceNetworkId].lastRefresh) //it.poll() @@ -526,7 +550,7 @@ def refresh() { while (i < 5){ pause(2000) if (atomicState.zonesResponse == "Success" && atomicState.programsResponse == "Success" ){ - log.debug "Got a zone response! Let's go!" + log.debug "Got a good RainMachine response! Let's go!" updateMapData() pollAllChild() return true @@ -536,12 +560,14 @@ def refresh() { } if (atomicState.zonesResponse == null){ - log.debug "Unable to get zone data while trying to refresh" + sendAlert("Unable to get zone data while trying to refresh") + log.debug "Unable to get zone data while trying to refresh" return false } if (atomicState.programsResponse == null){ - log.debug "Unable to get zone data while trying to refresh" + sendAlert("Unable to get program data while trying to refresh") + log.debug "Unable to get program data while trying to refresh" return false } @@ -578,7 +604,7 @@ def refresh() { if (atomicState.loginResponse == "Success"){ - log.debug "Got a zone response! Let's go!" + log.debug "Got a login response for refreshing! Let's go!" refresh() } @@ -620,6 +646,78 @@ def getDeviceEndTime(child) { } } +def sendCommand2(child, apiCommand, apiTime) { + //If login token exists and is valid, reuse it and callout to refresh zone and program data + if (loginTokenExists()){ + log.debug "Existing token detected for sending command" + + def childUID = getChildUID(child) + def childType = getChildType(child) + def apiPath = "/api/4/" + childType + "/" + childUID + "/" + apiCommand + "?access_token=" + atomicState.access_token + //doCallout("GET", "/api/4/zone?access_token=" + atomicState.access_token , "") + + //Stop Everything + if (apiCommand == "stopall") { + apiPath = "/api/4/watering/stopall"+ "?access_token=" + atomicState.access_token + doCallout("POST", apiPath, "{\"all\":" + "\"true\"" + "}") + } + //Zones will require time + else if (childType == "zone") { + doCallout("POST", apiPath, "{\"time\":" + apiTime + "}") + } + + //Programs will require pid + else if (childType == "program") { + doCallout("POST", apiPath, "{\"pid\":" + childUID + "}") + } + + //Forcefully get the latest data after waiting for 5 seconds + pause(8000) + refresh() + } + + //If not, get a new token then refresh + else{ + log.debug "Need new token" + doLogin() + + //Wait up to 20 seconds for successful login + def i = 0 + while (i < 5){ + pause(2000) + if (atomicState.loginResponse != null){ + log.debug "Got a response! Let's go!" + i = 5 + } + i++ + } + log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse + + + if (atomicState.loginResponse == null){ + log.debug "Unable to connect while trying to refresh zone/program data" + return false + } + + + if (atomicState.loginResponse == "Bad Login"){ + log.debug "Bad Login while trying to refresh zone/program data" + return false + } + + + if (atomicState.loginResponse == "Success"){ + log.debug "Got a login response for sending command! Let's go!" + sendCommand2(child, apiCommand, apiTime) + } + + } + +} + + + + // Send command to start or stop def sendCommand(child, apiCommand, apiTime) { def childUID = getChildUID(child) @@ -655,7 +753,7 @@ def sendCommand(child, apiCommand, apiTime) { } //Forcefully get the latest data after waiting for 2 seconds - pause(2000) + pause(5000) refresh() return commandSuccess From 57cd6fd3664d03552ea1ad6000afa830efe7d7e5 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 10:07:26 -0500 Subject: [PATCH 06/50] Updated namespace --- .../{copy-ninja => brbeaird}/rainmachine.src/rainmachine.groovy | 2 +- .../{copy-ninja => brbeaird}/rainmachine.src/rainmachine.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename devicetypes/{copy-ninja => brbeaird}/rainmachine.src/rainmachine.groovy (99%) rename smartapps/{copy-ninja => brbeaird}/rainmachine.src/rainmachine.groovy (99%) diff --git a/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy similarity index 99% rename from devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy rename to devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 99ef284..fd000bf 100644 --- a/devicetypes/copy-ninja/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -30,7 +30,7 @@ * */ metadata { - definition (name: "RainMachine", namespace: "copy-ninja", author: "Jason Mok/Brian Beaird") { + definition (name: "RainMachine", namespace: "brbeaird", author: "Jason Mok/Brian Beaird") { capability "Valve" capability "Refresh" capability "Polling" diff --git a/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy similarity index 99% rename from smartapps/copy-ninja/rainmachine.src/rainmachine.groovy rename to smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 1af85d3..0639dfc 100644 --- a/smartapps/copy-ninja/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -36,7 +36,7 @@ */ definition( name: "RainMachine", - namespace: "copy-ninja", + namespace: "brbeaird", author: "Jason Mok", description: "Connect your RainMachine to control your irrigation", category: "SmartThings Labs", From 5ef19df3a8be5a5dd3f88e1bb640e9e89f489f36 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 10:18:04 -0500 Subject: [PATCH 07/50] Code Cleanup --- .../rainmachine.src/rainmachine.groovy | 140 +----------------- 1 file changed, 4 insertions(+), 136 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 0639dfc..d5863a3 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -362,21 +362,8 @@ def initialize() { //Update data for child devices pollAllChild() - - //Subscribes to sunrise and sunset event to trigger refreshes - subscribe(location, "sunrise", monitorTheMonitor) - subscribe(location, "sunset", monitorTheMonitor) - subscribe(location, "mode", monitorTheMonitor) - subscribe(location, "sunriseTime", monitorTheMonitor) - subscribe(location, "sunsetTime", monitorTheMonitor) - - //Reset monitoring timestamp - atomicState.lastMonitored = now() - // Schedule polling schedulePoll() - schedule("19 0/" + 5 + " * * * ?", monitorPoll ) - } @@ -392,8 +379,6 @@ def doCallout(calloutMethod, urlPath, calloutBody){ log.info "Calling out to " + ip_address + urlPath //sendAlert("Calling out to " + ip_address + urlPath + " body: " + calloutBody) - //192.168.1.74:8 - def httpRequest = [ method: calloutMethod, path: urlPath, @@ -504,8 +489,9 @@ def pollAllChild() { log.debug "Updating children " + it.deviceNetworkId //sendAlert("Trying to set last refresh to: " + atomicState.data[it.deviceNetworkId].lastRefresh) if (atomicState.data[it.deviceNetworkId] == null){ - sendAlert("Refresh problem on ID: " + it.deviceNetworkId) - sendAlert("data list: " + atomicState.data) + log.debug "Refresh problem on ID: " + it.deviceNetworkId + //sendAlert("Refresh problem on ID: " + it.deviceNetworkId) + //sendAlert("data list: " + atomicState.data) } it.updateDeviceStatus(atomicState.data[it.deviceNetworkId].status) it.updateDeviceLastRefresh(atomicState.data[it.deviceNetworkId].lastRefresh) @@ -609,10 +595,6 @@ def refresh() { } } - - //Update Devices - //updateDeviceData() - } @@ -633,10 +615,6 @@ def getDeviceLastRefresh(child) { } - - - - // Get single device ending time def getDeviceEndTime(child) { //tries to get latest data if polling limitation allows @@ -716,77 +694,6 @@ def sendCommand2(child, apiCommand, apiTime) { } - - -// Send command to start or stop -def sendCommand(child, apiCommand, apiTime) { - def childUID = getChildUID(child) - def childType = getChildType(child) - def commandSuccess = false - def zonesActive = false - def apiPath = "/api/4/" + childType + "/" + childUID + "/" + apiCommand - def apiBody = [] - - //Try to get the latest data first - updateDeviceData() - - //Checks for any active running sprinklers before allowing another program to run - if (childType == "program") { - if (apiCommand == "start") { - atomicState.data.each { dni, data -> if ((data.status == 1) || (data.status == 2)) { zonesActive = true }} - if (!zonesActive) { - apiPost(apiPath, [pid: childUID]) - commandSuccess = true - } else { - commandSuccess = false - } - } else { - apiPost(apiPath, [pid: childUID]) - commandSuccess = true - } - } - - //Zones will require time - if (childType == "zone") { - apiPost(apiPath, [time: apiTime]) - commandSuccess = true - } - - //Forcefully get the latest data after waiting for 2 seconds - pause(5000) - refresh() - - return commandSuccess -} - -//Stop everything -def sendStopAll() { - def apiPath = "/api/4/watering/stopall" - def apiBody = [all: "true"] - apiPost(apiPath, apiBody) - - //Forcefully get the latest data after waiting for 2 seconds - pause(2000) - refresh() - return true -} - -def monitorPoll(){ - try { - log.debug "Monitoring the poll...Last poll stamp: " + atomicState.polling.last - if (now() > atomicState.polling.last + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1)*100000*2){ - log.debug "RainMachine polling schedule needs reboot!" - sendAlert("RainMachine schedule is dead! Restart!") - reSchedulePoll() - } - atomicState.lastMonitored = now() - } catch (Error e) { - log.debug "Error in RainMachine monitorPoll: $e" - sendAlert("Error in RainMachine monitorPoll: $e") - } - -} - private schedulePoll() { log.debug "Creating RainMachine schedule. Setting was " + settings.polling unschedule() @@ -794,46 +701,7 @@ private schedulePoll() { log.debug "RainMachine schedule successfully started!" } -private reSchedulePoll() { - try { - log.debug "Attempting to recreate the RainMachine schedule..." - schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", refresh ) - log.debug "RainMachine schedule successfully restarted!" - sendAlert("RainMachine schedule successfully restarted!") - } catch (Error e) { - log.debug "Error restarting RainMachine schedule: $e" - sendAlert("Error restarting RainMachine schedule: $e") - } -} - -//Last line of defense against SDSS -public monitorTheMonitor(evt){ - try { - log.debug "Event " + evt.displayName + " triggered monitoring the rainmachine monitor...Last poll stamp: " + atomicState.lastMonitored - if (now() > atomicState.lastMonitored + 480000){ - log.debug "RainMachine monitor schedule needs reboot!" - sendAlert("RainMachine monitor schedule is dead! Restart!") - //reScheduleMonitor() - } - } catch (Error e) { - log.debug "Error in RainMachine monitorPoll: $e" - sendAlert("Error in RainMachine monitorPoll: $e") - } -} - - -private reScheduleMonitor() { - try { - log.debug "Attempting to recreate the RainMachine monitor..." - schedule("19 0/" + 5 + " * * * ?", monitorPoll ) - log.debug "RainMachine monitor successfully restarted!" - sendAlert("RainMachine monitor successfully restarted!") - } catch (Error e) { - log.debug "Error restarting RainMachine monitor: $e" - sendAlert("Error restarting RainMachine monitor: $e") - } -} def sendAlert(alert){ - sendSms("615-828-5772", "Alert: " + alert) + //sendSms("555-555-5555", "Alert: " + alert) } From 55bda3222291ff860a723a68afd3eb3ba2083f9d Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sat, 9 Apr 2016 10:19:10 -0500 Subject: [PATCH 08/50] Code cleanup --- .../rainmachine.src/rainmachine.groovy | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index fd000bf..e636f18 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -108,27 +108,7 @@ def close() { } // refresh status -def refresh() { - //def deviceName = device.displayName - //def message = deviceName + " finished watering. Run time: " - //log.debug message - //log.debug "zone? " + deviceName.contains("Zone") - - //if (parent.sendPush && deviceName.contains("Zone")) { -// log.debug "do the push" -// parent.sendPushMessage(message) -// } - - //log.debug "found lastStarted as : " + device.currentValue("lastStarted") - - //def lastStarted = device.currentValue("lastStarted") - //log.debug "long it: " + lastStarted.toLong() - //sendEvent(name: "lastStarted", value: now(), display: false , displayed: false) - - - - //log.debug "Old Device Status: " + device.currentValue("contact") - //log.debug "Last started: " + device.currentValue("lastStarted") +def refresh() { sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) parent.refresh() //poll() @@ -233,8 +213,8 @@ def deviceStatus(status) { log.debug message if (parent.prefSendPush && deviceName.contains("Zone")) { - parent.sendAlert(message) - //parent.sendPushMessage(message) + //parent.sendAlert(message) + parent.sendPushMessage(message) } From f1581c00a34ec0f65b199b7df59b071a55cd357b Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sun, 10 Apr 2016 15:57:33 -0500 Subject: [PATCH 09/50] Tile and scheduler updates Improved tile responsiveness when changing from active/inactive Re-enabled scheduler --- .../rainmachine.src/rainmachine.groovy | 14 ++++++---- .../rainmachine.src/rainmachine.groovy | 27 ++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index e636f18..409f673 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -52,8 +52,8 @@ metadata { state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff") state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#1e9cbb") //state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") - state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") - state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A", nextState: "open") + state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A", nextState: "closed") //state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e", nextState: "open") //state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e", nextState: "closed") @@ -98,12 +98,14 @@ def installed() { def open() { sendEvent(name: "contact", value: "opening", display: true, displayed: false) parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) + //parent.sendCommand3(this, 1) //poll() } // turn off sprinkler def close() { sendEvent(name: "contact", value: "closing", display: true, displayed: false) parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) + //parent.sendCommand3(this, 0) //poll() } @@ -176,7 +178,7 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + //sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ @@ -220,6 +222,7 @@ def deviceStatus(status) { //sendEvent(name: "pausume", value: "resume") } + sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") } @@ -228,10 +231,10 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + //sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") //If device has just recently opened, take note of time - if (oldStatus != 'open' && oldStatus != null){ + if (oldStatus != 'open'){ //Take note of current time the zone started def refreshDate = new Date() def hour = refreshDate.format("h", location.timeZone) @@ -242,6 +245,7 @@ def deviceStatus(status) { log.debug "stored lastStarted as : " + device.currentValue("lastStarted") //sendEvent(name: "pausume", value: "pause") } + sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") } if (status == 2) { //Device is pending sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index d5863a3..9c9236c 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -345,7 +345,7 @@ def initialize() { } else if (dni.contains("zone")) { childDeviceAttrib = ["name": "RainMachine Zone: " + zoneList[dni], "completedSetup": true] } - addChildDevice("copy-ninja", "RainMachine", dni, null, childDeviceAttrib) + addChildDevice("brbeaird", "RainMachine", dni, null, childDeviceAttrib) } } @@ -625,7 +625,8 @@ def getDeviceEndTime(child) { } def sendCommand2(child, apiCommand, apiTime) { - //If login token exists and is valid, reuse it and callout to refresh zone and program data + atomicState.lastCommandSent = now() + //If login token exists and is valid, reuse it and callout to refresh zone and program data if (loginTokenExists()){ log.debug "Existing token detected for sending command" @@ -693,11 +694,22 @@ def sendCommand2(child, apiCommand, apiTime) { } +def scheduledRefresh(){ + //If a command has been sent in the last 30 seconds, don't do the scheduled refresh. + if (atomicState.lastCommandSent == null || atomicState.lastCommandSent < now()-30000){ + refresh() + } + else{ + log.debug "Skipping scheduled refresh due to recent command activity." + } + +} + -private schedulePoll() { +def schedulePoll() { log.debug "Creating RainMachine schedule. Setting was " + settings.polling unschedule() - schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", refresh ) + schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", scheduledRefresh ) log.debug "RainMachine schedule successfully started!" } @@ -705,3 +717,10 @@ private schedulePoll() { def sendAlert(alert){ //sendSms("555-555-5555", "Alert: " + alert) } + + +def sendCommand3(child, apiCommand) { + pause(5000) + log.debug ("Setting child status to " + apiCommand) + child.updateDeviceStatus(apiCommand) +} From 8a8f094f307ac9eaa5f89e9b3398179082e5ee31 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Mon, 11 Apr 2016 20:55:54 -0500 Subject: [PATCH 10/50] Re-enabled port as an input option This allowed other models to connect on ports other than 80 --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 9c9236c..f22339c 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -59,12 +59,15 @@ def prefLogIn() { atomicState.initialLogin = false atomicState.loginResponse = null atomicState.zonesResponse = null - atomicState.programsResponse = null + atomicState.programsResponse = null def showUninstall = ip_address != null && password != null return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ - input("ip_address", "text", title: "IP Address/Host Name", description: "IP Address/Host Name of RainMachine") + input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine") + } + section("Server Information"){ + input("port", "text", title: "Port", description: "Port of RainMachine", defaultValue: "80") } section("Login Credentials"){ input("password", "password", title: "Password", description: "RainMachine password") @@ -376,14 +379,14 @@ public loginTokenExists(){ def doCallout(calloutMethod, urlPath, calloutBody){ subscribe(location, null, parse, [filterEvents:false]) - log.info "Calling out to " + ip_address + urlPath + log.info "Calling out to " + ip_address + ":" + port + urlPath //sendAlert("Calling out to " + ip_address + urlPath + " body: " + calloutBody) def httpRequest = [ method: calloutMethod, path: urlPath, headers: [ - HOST: ip_address + ":80", + HOST: ip_address + ":" + port, "Content-Type": "application/json", Accept: "*/*", ], From 674101dc9717c4f7c8f4823958bb9069cf5bc9dc Mon Sep 17 00:00:00 2001 From: bbeaird Date: Tue, 12 Apr 2016 10:21:29 -0500 Subject: [PATCH 11/50] Added separate push notification option for programs Shortened default program/zone device prefixes --- .../rainmachine.src/rainmachine.groovy | 3 +++ .../rainmachine.src/rainmachine.groovy | 18 ++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 409f673..f8b8945 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -218,6 +218,9 @@ def deviceStatus(status) { //parent.sendAlert(message) parent.sendPushMessage(message) } + if (parent.prefSendPushProgram && deviceName.contains("Program")) { + parent.sendPushMessage(message) + } //sendEvent(name: "pausume", value: "resume") diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index f22339c..8690544 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -64,19 +64,17 @@ def prefLogIn() { def showUninstall = ip_address != null && password != null return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ - input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine") + input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine") + input("port", "text", title: "Port", description: "Port (must be HTTP, typically 80 or 18080)", defaultValue: "80") + input("password", "password", title: "Password", description: "RainMachine password") } - section("Server Information"){ - input("port", "text", title: "Port", description: "Port of RainMachine", defaultValue: "80") - } - section("Login Credentials"){ - input("password", "password", title: "Password", description: "RainMachine password") - } + section("Server Polling"){ input("polling", "int", title: "Polling Interval (in minutes)", description: "in minutes", defaultValue: 5) } section("Push Notifications") { - input "prefSendPush", "bool", required: false, title: "Push notifications when zones finish?" + input "prefSendPushPrograms", "bool", required: false, title: "Push notifications when programs finish?" + input "prefSendPush", "bool", required: false, title: "Push notifications when zones finish?" } } } @@ -344,9 +342,9 @@ def initialize() { def childDeviceAttrib = [:] if (!childDevice) { if (dni.contains("prog")) { - childDeviceAttrib = ["name": "RainMachine Program: " + programList[dni], "completedSetup": true] + childDeviceAttrib = ["name": "RM Pgm: " + programList[dni], "completedSetup": true] } else if (dni.contains("zone")) { - childDeviceAttrib = ["name": "RainMachine Zone: " + zoneList[dni], "completedSetup": true] + childDeviceAttrib = ["name": "RM Zone: " + zoneList[dni], "completedSetup": true] } addChildDevice("brbeaird", "RainMachine", dni, null, childDeviceAttrib) } From faab80c02fcb3ac253a9538c694b4a1f598720c4 Mon Sep 17 00:00:00 2001 From: bbeaird Date: Tue, 12 Apr 2016 10:25:30 -0500 Subject: [PATCH 12/50] Added default values for IP and password --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 8690544..966ff18 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -64,9 +64,9 @@ def prefLogIn() { def showUninstall = ip_address != null && password != null return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ - input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine") + input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") input("port", "text", title: "Port", description: "Port (must be HTTP, typically 80 or 18080)", defaultValue: "80") - input("password", "password", title: "Password", description: "RainMachine password") + input("password", "password", title: "Password", description: "RainMachine password", defaultValue: "admin") } section("Server Polling"){ From fa2da9215cc8b37c09f71e368f6f54aabc398545 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 26 Apr 2016 14:46:30 -0500 Subject: [PATCH 13/50] Fixed bug where program completion messages were not being sent --- devicetypes/brbeaird/rainmachine.src/rainmachine.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index f8b8945..0797d4d 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -218,7 +218,8 @@ def deviceStatus(status) { //parent.sendAlert(message) parent.sendPushMessage(message) } - if (parent.prefSendPushProgram && deviceName.contains("Program")) { + + if (parent.prefSendPushPrograms && deviceName.contains("Pgm")) { parent.sendPushMessage(message) } From 479e9e6253e2d96c77fd20ff5926ebaba219560e Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 May 2016 16:47:12 -0500 Subject: [PATCH 14/50] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e00dc81..221e267 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ SmartThings RainMachine + -For [prerequisites](https://github.com/copy-ninja/SmartThings_RainMachine/wiki/Prerequisite) & [installation](https://github.com/copy-ninja/SmartThings_RainMachine/wiki/Installation) please refer to [wiki]( https://github.com/copy-ninja/SmartThings_RainMachine/wiki) +For [prerequisites](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Prerequisite) & [installation](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Installation) please refer to [wiki]( https://github.com/brbeaird/SmartThings_RainMachine/wiki) From e91e615e7094fff85244078c32d31374f53a9a64 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 May 2016 16:52:12 -0500 Subject: [PATCH 15/50] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 221e267..0fa1ed9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ SmartThings RainMachine - + + For [prerequisites](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Prerequisite) & [installation](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Installation) please refer to [wiki]( https://github.com/brbeaird/SmartThings_RainMachine/wiki) From 4ed01785c8d9b54e50aba2f6fd64836c9fccd494 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 May 2016 17:02:44 -0500 Subject: [PATCH 16/50] Update README.md Fixed mini-8 image --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0fa1ed9..61e4994 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ SmartThings RainMachine ======================= - - - + + + + + + + + + For [prerequisites](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Prerequisite) & [installation](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Installation) please refer to [wiki]( https://github.com/brbeaird/SmartThings_RainMachine/wiki) From c9a4e8e4ac2e9d40f441508ac990c5f696160f5c Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 May 2016 21:54:50 -0500 Subject: [PATCH 17/50] Fixed bug where valve wasn't always properly transitioning between open and closed --- .../rainmachine.src/rainmachine.groovy | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 0797d4d..9a2ce62 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -49,11 +49,11 @@ metadata { tiles { standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { - state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff") - state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#1e9cbb") + state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff", nextState: "opening") + state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#1e9cbb", nextState: "closing") //state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") - state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A", nextState: "open") - state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A", nextState: "closed") + state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") + state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") //state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e", nextState: "open") //state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e", nextState: "closed") @@ -96,14 +96,14 @@ def installed() { // turn on sprinkler def open() { - sendEvent(name: "contact", value: "opening", display: true, displayed: false) + //sendEvent(name: "contact", value: "opening", display: true, displayed: false) parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 1) //poll() } // turn off sprinkler def close() { - sendEvent(name: "contact", value: "closing", display: true, displayed: false) + //sendEvent(name: "contact", value: "closing", display: true, displayed: false) parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 0) //poll() @@ -178,10 +178,10 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") - //sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ + sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //Take note of how long it ran and send notification log.debug "lastStarted: " + device.currentValue("lastStarted") @@ -226,7 +226,7 @@ def deviceStatus(status) { //sendEvent(name: "pausume", value: "resume") } - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + //sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") } @@ -239,6 +239,8 @@ def deviceStatus(status) { //If device has just recently opened, take note of time if (oldStatus != 'open'){ + sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + //Take note of current time the zone started def refreshDate = new Date() def hour = refreshDate.format("h", location.timeZone) @@ -249,7 +251,7 @@ def deviceStatus(status) { log.debug "stored lastStarted as : " + device.currentValue("lastStarted") //sendEvent(name: "pausume", value: "pause") } - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + //sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") } if (status == 2) { //Device is pending sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") From 853137953e62d079bc63864049f85f793e9e4de0 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 May 2016 21:55:03 -0500 Subject: [PATCH 18/50] Fixed bug where valve wasn't always properly transitioning between open and closed --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 966ff18..f394dc8 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -652,8 +652,9 @@ def sendCommand2(child, apiCommand, apiTime) { } //Forcefully get the latest data after waiting for 5 seconds - pause(8000) - refresh() + //pause(8000) + runIn(15, refresh) + //refresh() } //If not, get a new token then refresh From a3dbc25c77c0fa407451df6baa8ab2995b7eb809 Mon Sep 17 00:00:00 2001 From: bbeaird Date: Wed, 26 Oct 2016 11:52:15 -0500 Subject: [PATCH 19/50] Added switch capability --- .../rainmachine.src/rainmachine.groovy | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 9a2ce62..65debe7 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -34,6 +34,7 @@ metadata { capability "Valve" capability "Refresh" capability "Polling" + capability "Switch" attribute "runTime", "number" attribute "lastRefresh", "string" @@ -60,6 +61,11 @@ metadata { } + + standardTile("switch", "device.switch") { + state("on", label:'${name}', action: "switch.on", icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("off", label:'${name}', action: "switch.off", icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } /* standardTile("pausume", "device.switch", inactiveLabel: false, decoration: "flat") { state("resume", label:'resume', action:"pause", icon:"st.sonos.play-icon", nextState:"pause") state("pause", label:'pause', action:"resume", icon:"st.sonos.pause-icon", nextState:"resume") @@ -109,6 +115,15 @@ def close() { //poll() } + +def on() { + log.debug "Turning the sprinkler on" + //parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) +} +def off() { +log.debug "Turning the sprinkler off" + //parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) +} // refresh status def refresh() { sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) @@ -177,11 +192,12 @@ def deviceStatus(status) { if (status == 0) { //Device has turned off //Go ahead and mark the valve as closed - def oldStatus = device.currentValue("contact") + def oldStatus = device.currentValue("contact") + sendEvent(name: "switch", value: "off", display: false, displayed: false, isStateChange: true) // off == closed //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //Take note of how long it ran and send notification log.debug "lastStarted: " + device.currentValue("lastStarted") @@ -235,11 +251,12 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") + sendEvent(name: "switch", value: "on", display: false, displayed: false, isStateChange: true) // on == open //sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") //If device has just recently opened, take note of time if (oldStatus != 'open'){ - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") //Take note of current time the zone started def refreshDate = new Date() @@ -258,3 +275,8 @@ def deviceStatus(status) { //sendEvent(name: "pausume", value: "pause") } } + + +def log(msg){ + log.debug msg +} From da24b629b9d5c61710420496575c6ed6f3469fe4 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 27 Oct 2016 09:15:27 -0500 Subject: [PATCH 20/50] Uncomment on/off command send --- devicetypes/brbeaird/rainmachine.src/rainmachine.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 65debe7..72345f1 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -118,11 +118,11 @@ def close() { def on() { log.debug "Turning the sprinkler on" - //parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) + parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) } def off() { log.debug "Turning the sprinkler off" - //parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) + parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) } // refresh status def refresh() { From dfd74811f2bb5cfebb59669c0b3c6eafdc5ca79a Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 13 Dec 2016 08:48:33 -0600 Subject: [PATCH 21/50] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 61e4994..ef89950 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,10 @@ SmartThings RainMachine For [prerequisites](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Prerequisite) & [installation](https://github.com/brbeaird/SmartThings_RainMachine/wiki/Installation) please refer to [wiki]( https://github.com/brbeaird/SmartThings_RainMachine/wiki) +### Donate: +If you love this app, feel free to donate. +[![PayPal - The safer, easier way to give online!](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif "Donate")](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=GJJA2ZYNWKS6Y) From 357589a7ea738becba118c5bd82a3981eea21e99 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 13 Dec 2016 12:03:38 -0600 Subject: [PATCH 22/50] Updated cron setting to automatically max at once per hour --- .../rainmachine.src/rainmachine.groovy | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index f394dc8..1ec06d2 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Service Manager SmartApp * * Author: Jason Mok/Brian Beaird - * Date: 2016-3-15 + * Date: 2016-12-13 * *************************** * @@ -709,9 +709,19 @@ def scheduledRefresh(){ def schedulePoll() { - log.debug "Creating RainMachine schedule. Setting was " + settings.polling - unschedule() - schedule("37 0/" + ((settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1) + " * * * ?", scheduledRefresh ) + log.debug "Creating RainMachine schedule. Setting was " + settings.polling + def pollSetting = settings.polling.toInteger() + def pollFreq = 1 + if (pollSetting == 0 || pollSetting >= 60){ + pollFreq = 1 + } + else{ + pollFreq = pollSetting + } + + log.debug "Poll freq: " + pollFreq + unschedule() + schedule("37 */" + pollFreq + " * * * ?", scheduledRefresh ) log.debug "RainMachine schedule successfully started!" } From edc21fdbf4cab45680d8ae0b5f6b035d0a19f995 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 13 Dec 2016 12:08:21 -0600 Subject: [PATCH 23/50] Fix to last commit - auto max interval at 59 mins if over 60 selected. --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 1ec06d2..f39d901 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -712,9 +712,12 @@ def schedulePoll() { log.debug "Creating RainMachine schedule. Setting was " + settings.polling def pollSetting = settings.polling.toInteger() def pollFreq = 1 - if (pollSetting == 0 || pollSetting >= 60){ + if (pollSetting == 0){ pollFreq = 1 } + else if ( pollSetting >= 60){ + pollFreq = 59 + } else{ pollFreq = pollSetting } From 1a767a65f3e2e0443b6742a77095e9f1ce231e8f Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Fri, 16 Dec 2016 16:19:26 -0600 Subject: [PATCH 24/50] Fixed setup IP and Port # text to be more instructive --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index f39d901..beb9cc7 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -64,8 +64,8 @@ def prefLogIn() { def showUninstall = ip_address != null && password != null return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ - input("ip_address", "text", title: "IP Address", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") - input("port", "text", title: "Port", description: "Port (must be HTTP, typically 80 or 18080)", defaultValue: "80") + input("ip_address", "text", title: "Local IP Address of RainMachine", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") + input("port", "text", title: "Port # - typically 80 or 18080 (for newer models)", description: "Port. Older models use 80. Newer models like the Mini use 18080", defaultValue: "80") input("password", "password", title: "Password", description: "RainMachine password", defaultValue: "admin") } From 9b0701d906674cc5f055bda788257446730519f5 Mon Sep 17 00:00:00 2001 From: M3Rocket Date: Wed, 22 Feb 2017 16:36:21 -0800 Subject: [PATCH 25/50] Create readme --- icons/readme | 1 + 1 file changed, 1 insertion(+) create mode 100644 icons/readme diff --git a/icons/readme b/icons/readme new file mode 100644 index 0000000..c847459 --- /dev/null +++ b/icons/readme @@ -0,0 +1 @@ +Just a file to create a dir. From cf33fcd49df7784c53194a54c83892baf2ce04c9 Mon Sep 17 00:00:00 2001 From: M3Rocket Date: Wed, 22 Feb 2017 16:37:39 -0800 Subject: [PATCH 26/50] Add files via upload --- icons/rainmachine.1x.png | Bin 0 -> 4281 bytes icons/rainmachine.2x.png | Bin 0 -> 8217 bytes icons/rainmachine.3x.png | Bin 0 -> 16190 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 icons/rainmachine.1x.png create mode 100644 icons/rainmachine.2x.png create mode 100644 icons/rainmachine.3x.png diff --git a/icons/rainmachine.1x.png b/icons/rainmachine.1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4df8ff4a5030403b7d3a172ebc51e562f36b523d GIT binary patch literal 4281 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 R{qM)Y%*fKPTHho7{{ZdvASo@Kg4EC; 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 literal 0 HcmV?d00001 diff --git a/icons/rainmachine.3x.png b/icons/rainmachine.3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ba438e15b3d09419d277f50d86e86a85fe07aaeb GIT binary patch literal 16190 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 literal 0 HcmV?d00001 From 4ca02949fb89c50ed848aaccbfbcb0587ca249f7 Mon Sep 17 00:00:00 2001 From: M3Rocket Date: Wed, 22 Feb 2017 16:39:28 -0800 Subject: [PATCH 27/50] Delete readme --- icons/readme | 1 - 1 file changed, 1 deletion(-) delete mode 100644 icons/readme diff --git a/icons/readme b/icons/readme deleted file mode 100644 index c847459..0000000 --- a/icons/readme +++ /dev/null @@ -1 +0,0 @@ -Just a file to create a dir. From 12ea2f6feabd0f935e48369e4e9fd98b0037ff7f Mon Sep 17 00:00:00 2001 From: M3Rocket Date: Wed, 22 Feb 2017 16:44:30 -0800 Subject: [PATCH 28/50] Update to point to new locale for icons --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index beb9cc7..2eb66e2 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -40,9 +40,9 @@ definition( author: "Jason Mok", description: "Connect your RainMachine to control your irrigation", category: "SmartThings Labs", - iconUrl: "http://smartthings.copyninja.net/icons/RainMachine@1x.png", - iconX2Url: "http://smartthings.copyninja.net/icons/RainMachine@2x.png", - iconX3Url: "http://smartthings.copyninja.net/icons/RainMachine@3x.png" + iconUrl: "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", + iconX2Url: "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.2x.png", + iconX3Url: "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.3x.png" ) preferences { From 121230087bea90633885e107ed287389d70b5ae2 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Fri, 24 Mar 2017 12:27:27 -0500 Subject: [PATCH 29/50] March 2017 Update! * Added new option to setup called **"Name Re-Sync"**. When selected, this causes your program/zone devices in SmartThings to have their names refreshed from your RainMachine controller whenever you complete the setup process. * Zone/program devices are now created without the "RM" prefix - they are brought in named exactly the same as whatever they're named in the Rainmachine controller. This makes things easier in case you want to be able to turn them on/off with Alexa without needing to rename them to something she can recognize when you say it. * Fixed issue where push notifications failed to send if zones/programs were renamed. In general, I've now confirmed that renaming zones/programs should not affect any functionality. * The Rainmachine setup proces now ends with a summary page which will let you know whether or not device creation was successful. * Added version checking - a message will appear on the new summary page if your SmartApp or Device Handler code does not match the latest version in GitHub. * Fixed issues where zone/program tiles in the ST mobile app wouldn't instantly reflect active/inactive changes. * Added support for Zone/Program devices to show in ActionTiles. You can now add them as either switches or valves. --- .../rainmachine.src/rainmachine.groovy | 53 ++++-- .../rainmachine.src/rainmachine.groovy | 168 +++++++++++++++--- 2 files changed, 180 insertions(+), 41 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 72345f1..a58504d 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,11 +2,11 @@ * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird - * Date: 2016-04-08 + * Last Updated: 2017-03-23 * *************************** * - * Copyright 2014 Jason Mok + * Copyright 2017 Brian Beaird * * 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: @@ -35,14 +35,17 @@ metadata { capability "Refresh" capability "Polling" capability "Switch" + capability "Sensor" attribute "runTime", "number" attribute "lastRefresh", "string" attribute "lastStarted", "string" + attribute "deviceType", "string" //command "pause" //command "resume" - command "stopAll" + command "refresh" + command "stopAll" command "setRunTime" } @@ -86,9 +89,12 @@ metadata { valueTile("lastRefresh", "device.lastRefresh", height: 1, width: 3, inactiveLabel: false, decoration: "flat") { state("lastRefreshValue", label:'Last refresh: ${currentValue}', backgroundColor:"#ffffff") } + valueTile("deviceType", "device.deviceType", height: 1, width: 3, inactiveLabel: false, decoration: "flat") { + state("deviceTypeValue", label:'Type: ${currentValue}', backgroundColor:"#ffffff") + } main "contact" - details(["contact","refresh","stopAll","runTimeControl","runTime","lastActivity","lastRefresh"]) + details(["contact","refresh","stopAll","runTimeControl","runTime","lastActivity","lastRefresh","deviceType"]) } } @@ -125,10 +131,9 @@ log.debug "Turning the sprinkler off" parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) } // refresh status -def refresh() { - sendEvent("name":"lastRefresh", "value": "Checking..." , display: false , displayed: false) - parent.refresh() - //poll() +def refresh() { + sendEvent(name:"lastRefresh", value: "Checking..." , display: true , displayed: false) + parent.refresh() } //resume sprinkling @@ -161,6 +166,10 @@ def stopAll() { //poll() } +def updateDeviceType(){ + sendEvent(name: "deviceType", value: parent.getChildType(this), display: false , displayed: true) +} + // update the run time for manual zone void setRunTime(runTimeSecs) { sendEvent("name":"runTime", "value": runTimeSecs) @@ -192,12 +201,13 @@ def deviceStatus(status) { if (status == 0) { //Device has turned off //Go ahead and mark the valve as closed - def oldStatus = device.currentValue("contact") - sendEvent(name: "switch", value: "off", display: false, displayed: false, isStateChange: true) // off == closed + def oldStatus = device.currentValue("contact") + sendEvent(name: "switch", value: "off", display: true, displayed: false, isStateChange: true) // off == closed + sendEvent(name: "contact", value: "closed", display: false, displayed: false) //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + sendEvent(name: "contact", value: "closed", display: true, displayed: true, descriptionText: device.displayName + " was inactive") //Take note of how long it ran and send notification log.debug "lastStarted: " + device.currentValue("lastStarted") @@ -229,13 +239,18 @@ def deviceStatus(status) { def deviceName = device.displayName def message = deviceName + " finished watering. Run time: " + lastActivityValue log.debug message + + def deviceType = device.currentValue("deviceType") + log.debug "Device type is: " + device.currentValue("deviceType") - if (parent.prefSendPush && deviceName.contains("Zone")) { - //parent.sendAlert(message) + if (parent.prefSendPush && deviceType.toUpperCase() == "ZONE") { + //parent.sendAlert(message) + //sendNotificationEvent(message.toString()) parent.sendPushMessage(message) } - if (parent.prefSendPushPrograms && deviceName.contains("Pgm")) { + if (parent.prefSendPushPrograms && deviceType.toUpperCase() == "PROGRAM") { + //sendNotificationEvent(message.toString()) parent.sendPushMessage(message) } @@ -251,12 +266,12 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") - sendEvent(name: "switch", value: "on", display: false, displayed: false, isStateChange: true) // on == open - //sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + sendEvent(name: "switch", value: "on", display: true, displayed: false, isStateChange: true) // on == open + sendEvent(name: "contact", value: "open", display: false, displayed: false) //If device has just recently opened, take note of time if (oldStatus != 'open'){ - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + sendEvent(name: "contact", value: "open", display: true, displayed: true, descriptionText: device.displayName + " was active") //Take note of current time the zone started def refreshDate = new Date() @@ -280,3 +295,7 @@ def deviceStatus(status) { def log(msg){ log.debug msg } + +def showVersion(){ + return "2.0.0" +} diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 2eb66e2..19dd4b7 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,11 +2,13 @@ * RainMachine Service Manager SmartApp * * Author: Jason Mok/Brian Beaird - * Date: 2016-12-13 + * Last Updated: 2017-03-23 + * SmartApp version: 2.0.0* + * Device version: 2.0.0* * *************************** * - * Copyright 2014 Jason Mok + * Copyright 2017 Brian Beaird * * 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: @@ -34,6 +36,8 @@ * 4) Enjoy! * */ +include 'asynchttp_v1' + definition( name: "RainMachine", namespace: "brbeaird", @@ -46,22 +50,25 @@ definition( ) preferences { - page(name: "prefLogIn", title: "RainMachine") - page(name: "prefLogInWait", title: "RainMachine") - page(name: "prefListProgramsZones", title: "RainMachine") + page(name: "prefLogIn", title: "RainMachine") + page(name: "prefLogInWait", title: "RainMachine") + page(name: "prefListProgramsZones", title: "RainMachine") + page(name: "summary", title: "RainMachine") } /* Preferences */ def prefLogIn() { + doAsyncCallout() + //RESET ALL THE THINGS atomicState.initialLogin = false atomicState.loginResponse = null atomicState.zonesResponse = null atomicState.programsResponse = null - def showUninstall = ip_address != null && password != null + def showUninstall = true return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ input("ip_address", "text", title: "Local IP Address of RainMachine", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") @@ -134,18 +141,21 @@ def prefLogInWait() { log.debug "Done waiting on zones/programs. zone response: " + atomicState.zonesResponse + " programs response: " + atomicState.programsResponse - return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", install:true, uninstall:true) { + return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", nextPage:"summary", install:false, uninstall:true) { section("Select which programs to use"){ input(name: "programs", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ProgramList]) } section("Select which zones to use"){ input(name: "zones", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ZoneList]) } + section("Name Re-Sync") { + input "prefResyncNames", "bool", required: false, title: "Re-sync names with RainMachine?" + } } } else{ - return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", uninstall:false, install: false) { + return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", uninstall:true, install: false) { section() { paragraph "Problem getting zone/program data. Click back and try again." } @@ -155,6 +165,18 @@ def prefLogInWait() { } +def summary() { + state.installMsg = "" + initialize() + versionCheck() + return dynamicPage(name: "summary", title: "Summary", install:true, uninstall:true) { + section("Installation Details:"){ + paragraph state.installMsg + paragraph state.versionWarning + } + } +} + def parseLoginResponse(response){ @@ -284,8 +306,8 @@ def updated() { runNow: true ] //unschedule() - unsubscribe() - initialize() + //unsubscribe() + //initialize() } def uninstalled() { @@ -299,11 +321,12 @@ def updateMapData(){ combinedMap << atomicState.ProgramData combinedMap << atomicState.ZoneData atomicState.data = combinedMap - log.debug "new data list: " + atomicState.data + //log.debug "new data list: " + atomicState.data } def initialize() { log.info "initialize()" + unsubscribe() //Merge Zone and Program data into single map //atomicState.data = [:] @@ -337,19 +360,63 @@ def initialize() { } // Create device if selected and doesn't exist - selectedItems.each { dni -> + selectedItems.each { dni -> + def deviceType = "" + def deviceName = "" + if (dni.contains("prog")) { + log.debug "Program found - " + dni + deviceType = "Pgm" + deviceName = programList[dni] + } else if (dni.contains("zone")) { + log.debug "Zone found - " + dni + deviceType = "Zone" + deviceName = zoneList[dni] + } + log.debug "devType: " + deviceType + def childDevice = getChildDevice(dni) def childDeviceAttrib = [:] - if (!childDevice) { - if (dni.contains("prog")) { - childDeviceAttrib = ["name": "RM Pgm: " + programList[dni], "completedSetup": true] - } else if (dni.contains("zone")) { - childDeviceAttrib = ["name": "RM Zone: " + zoneList[dni], "completedSetup": true] - } - addChildDevice("brbeaird", "RainMachine", dni, null, childDeviceAttrib) - } + if (!childDevice){ + def fullName = deviceName + log.debug "name will be: " + fullName + childDeviceAttrib = ["name": fullName, "completedSetup": true] + + try{ + childDevice = addChildDevice("brbeaird", "RainMachine", dni, null, childDeviceAttrib) + state.installMsg = state.installMsg + deviceName + ": device created. \r\n\r\n" + } + catch(physicalgraph.app.exception.UnknownDeviceTypeException e) + { + log.debug "Error! " + e + state.installMsg = state.installMsg + deviceName + ": problem creating RM device. Check your IDE to make sure the brbeaird : RainMachine device handler is installed and published. \r\n\r\n" + } + + } + + //For existing devices, sync back with the RainMachine name if desired. + else{ + state.installMsg = state.installMsg + deviceName + ": device already exists. \r\n\r\n" + if (prefResyncNames){ + log.debug "Name from RM: " + deviceName + " name in ST: " + childDevice.name + if (childDevice.name != deviceName || childDevice.label != deviceName){ + state.installMsg = state.installMsg + deviceName + ": updating device name (old name was " + childDevice.label + ") \r\n\r\n" + } + childDevice.name = deviceName + childDevice.label = deviceName + } + } + //log.debug "setting dev type: " + deviceType + //childDevice.setDeviceType(deviceType) + + if (childDevice){ + childDevice.updateDeviceType() + } + } + + + // Delete child devices that are not selected in the settings if (!selectedItems) { delete = getAllChildDevices() @@ -365,6 +432,8 @@ def initialize() { // Schedule polling schedulePoll() + + versionCheck() } @@ -423,8 +492,8 @@ def getProgramList(programs) { atomicState.ProgramList = programsList atomicState.ProgramData = tempList - log.debug "temp list reviewed! " + atomicState.ProgramList - log.debug "atomic data reviewed! " + atomicState.ProgramData + //log.debug "temp list reviewed! " + atomicState.ProgramList + //log.debug "atomic data reviewed! " + atomicState.ProgramData atomicState.programsResponse = "Success" //log.debug "atomic data reviewed! " + atomicState.data @@ -449,8 +518,8 @@ def getZoneList(zones) { } atomicState.ZoneList = zonesList atomicState.ZoneData = tempList - log.debug "Temp zone list: " + zonesList - log.debug "State zone list: " + atomicState.ZoneList + //log.debug "Temp zone list: " + zonesList + //log.debug "State zone list: " + atomicState.ZoneList atomicState.zonesResponse = "Success" } @@ -739,3 +808,54 @@ def sendCommand3(child, apiCommand) { log.debug ("Setting child status to " + apiCommand) child.updateDeviceStatus(apiCommand) } + + +def doAsyncCallout(){ + def params = [ + uri: 'https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/smartapps/brbeaird/rainmachine.src/rainmachine.groovy', + contentType: 'text/plain; charset=utf-8' + ] + asynchttp_v1.get('responseHandlerMethod', params) +} + +def responseHandlerMethod(response, data) { + def resp = response.getData() + + def smartAppVersionBegin = resp.indexOf('SmartApp version') + 18 + def smartAppVersionEnd = resp.indexOf('*', smartAppVersionBegin) + state.latestSmartAppVersion = resp.substring(smartAppVersionBegin, smartAppVersionEnd) + + def deviceVersionBegin = resp.indexOf('Device version') + 16 + def deviceVersionEnd = resp.indexOf('*', deviceVersionBegin) + state.latestDeviceVersion = resp.substring(deviceVersionBegin, deviceVersionEnd) + + log.debug "smartAppVersion: " + state.latestSmartAppVersion + log.debug "deviceVersion: " + state.latestDeviceVersion +} + + +def versionCheck(){ + state.versionWarning = "" + state.thisSmartAppVersion = "2.0.0" + state.thisDeviceVersion = "" + + def childExists = false + def childDevs = getChildDevices() + + if (childDevs.size() > 0){ + childExists = true + state.thisDeviceVersion = childDevs[0].showVersion() + log.debug "child version found: " + state.thisDeviceVersion + } + + log.debug "RM Device Handler Version: " + state.thisDeviceVersion + + if (state.thisSmartAppVersion != state.latestSmartAppVersion) { + state.versionWarning = state.versionWarning + "Your SmartApp version (" + state.thisSmartAppVersion + ") is not the latest version (" + state.latestSmartAppVersion + ")\n\n" + } + if (childExists && state.thisDeviceVersion != state.latestDeviceVersion) { + state.versionWarning = state.versionWarning + "Your RainMachine device version (" + state.thisDeviceVersion + ") is not the latest version (" + state.latestDeviceVersion + ")\n\n" + } + + log.debug state.versionWarning +} \ No newline at end of file From 6fcbabd8f8ad4080f2d8550a146fc4b169ece324 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 18 Apr 2017 14:18:38 -0500 Subject: [PATCH 30/50] Added extra debugging and handling for login token expiration --- .../rainmachine.src/rainmachine.groovy | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 19dd4b7..c89a886 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -180,6 +180,7 @@ def summary() { def parseLoginResponse(response){ + log.debug "Parsing login response: " + response log.debug "Reset login info!" atomicState.access_token = "" atomicState.expires_in = "" @@ -190,12 +191,13 @@ def parseLoginResponse(response){ atomicState.loginResponse = 'Bad Login' } - log.debug "token was " + response.access_token + log.debug "new token found: " + response.access_token if (response.access_token != null){ log.debug "Saving token" atomicState.access_token = response.access_token log.debug "Login token newly set to: " + atomicState.access_token - atomicState.expires_in = now() + response.expires_in + if (response.expires_in != null) + atomicState.expires_in = now() + response.expires_in } atomicState.loginResponse = 'Success' log.debug "Login response set to: " + atomicState.loginResponse @@ -439,8 +441,16 @@ def initialize() { /* Access Management */ public loginTokenExists(){ - log.debug "Checking for token: " - return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) + log.debug "Checking for token: " + log.debug "Current token: " + atomicState.access_token + log.debug "Current expires_in: " + atomicState.expires_in + + if (atomicState.expires_in == null){ + log.debug "No expires_in found - skip to getting a new token." + return false + } + else + return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) } From 01235f477b45a37957a21b7c4584b1dc48d08bc0 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 18 Apr 2017 15:33:55 -0500 Subject: [PATCH 31/50] Added even more handling for null or blank expires_in --- .../rainmachine.src/rainmachine.groovy | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index c89a886..201b5a3 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -196,7 +196,7 @@ def parseLoginResponse(response){ log.debug "Saving token" atomicState.access_token = response.access_token log.debug "Login token newly set to: " + atomicState.access_token - if (response.expires_in != null) + if (response.expires_in != null && response.expires_in != [] && response.expires_in != "") atomicState.expires_in = now() + response.expires_in } atomicState.loginResponse = 'Success' @@ -235,24 +235,26 @@ def parse(evt) { //Zone response if (result.zones){ log.debug "Zone response detected!" - log.debug "zone result: " + result + //log.debug "zone result: " + result getZoneList(result.zones) } //Program response if (result.programs){ log.debug "Program response detected!" - log.debug "program result: " + result + //log.debug "program result: " + result getProgramList(result.programs) } //Figure out the other response types - if (result.statusCode != null){ + if (result.statusCode == 0){ + log.debug "status code found" + log.debug "Got raw response: " + body //Login response - if (result.access_token != null){ + if (result.access_token != null && result.access_token != "" && result.access_token != []){ log.debug "Login response detected!" - log.debug "result: " + result + log.debug "Login response result: " + result parseLoginResponse(result) } @@ -441,16 +443,23 @@ def initialize() { /* Access Management */ public loginTokenExists(){ - log.debug "Checking for token: " - log.debug "Current token: " + atomicState.access_token - log.debug "Current expires_in: " + atomicState.expires_in - - if (atomicState.expires_in == null){ - log.debug "No expires_in found - skip to getting a new token." - return false + try { + log.debug "Checking for token: " + log.debug "Current token: " + atomicState.access_token + log.debug "Current expires_in: " + atomicState.expires_in + + if (atomicState.expires_in == null || atomicState.expires_in == ""){ + log.debug "No expires_in found - skip to getting a new token." + return false + } + else + return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) + } + catch (e) + { + log.debug "Warning: unable to compare old expires_in - forcing new token instead. Error: " + e + return false } - else - return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) } @@ -619,6 +628,7 @@ def refresh() { log.debug "Got a good RainMachine response! Let's go!" updateMapData() pollAllChild() + //atomicState.expires_in = "" //TEMPORARY FOR TESTING TO FORCE RELOGIN return true } log.debug "Current zone response: " + atomicState.zonesResponse + "Current pgm response: " + atomicState.programsResponse From e3228cc1e459ddf07f7270f82631206175cd6817 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 19 Apr 2017 09:23:38 -0500 Subject: [PATCH 32/50] Added handling for non-JSON responses --- .../brbeaird/rainmachine.src/rainmachine.groovy | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 201b5a3..33aec27 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -3,7 +3,7 @@ * * Author: Jason Mok/Brian Beaird * Last Updated: 2017-03-23 - * SmartApp version: 2.0.0* + * SmartApp version: 2.0.1* * Device version: 2.0.0* * *************************** @@ -227,10 +227,16 @@ def parse(evt) { def data = msg.data // => either JSON or XML in response body (whichever is specified by content-type header in response) + def result if (status == 200 && Body != "OK") { - - def slurper = new groovy.json.JsonSlurper() - def result = slurper.parseText(body) + try{ + def slurper = new groovy.json.JsonSlurper() + result = slurper.parseText(body) + } + catch (e){ + log.debug "FYI - got a response, but it's apparently not JSON. Error: " + e + ". Body: " + body + return 1 + } //Zone response if (result.zones){ @@ -856,7 +862,7 @@ def responseHandlerMethod(response, data) { def versionCheck(){ state.versionWarning = "" - state.thisSmartAppVersion = "2.0.0" + state.thisSmartAppVersion = "2.0.1" state.thisDeviceVersion = "" def childExists = false From b83ddc0d2df7d66248982783d0f115b2d5aced42 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 15 Jun 2017 10:00:35 -0500 Subject: [PATCH 33/50] Fixed bug where device active/inactive events were not being written to the "Recently" tab Cleaned up version checking Updated icons to use new standard colors --- .../rainmachine.src/rainmachine.groovy | 29 ++++++----- .../rainmachine.src/rainmachine.groovy | 49 +++++++++++-------- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index a58504d..464654c 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird - * Last Updated: 2017-03-23 + * Last Updated: 2017-06-15 * *************************** * @@ -54,7 +54,7 @@ metadata { tiles { standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff", nextState: "opening") - state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#1e9cbb", nextState: "closing") + state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#00a0dc", nextState: "closing") //state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") @@ -108,6 +108,7 @@ def installed() { // turn on sprinkler def open() { + log.debug "Turning the sprinkler on (valve)" //sendEvent(name: "contact", value: "opening", display: true, displayed: false) parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 1) @@ -115,6 +116,7 @@ def open() { } // turn off sprinkler def close() { + log.debug "Turning the sprinkler off (valve)" //sendEvent(name: "contact", value: "closing", display: true, displayed: false) parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 0) @@ -202,12 +204,12 @@ def deviceStatus(status) { //Go ahead and mark the valve as closed def oldStatus = device.currentValue("contact") - sendEvent(name: "switch", value: "off", display: true, displayed: false, isStateChange: true) // off == closed - sendEvent(name: "contact", value: "closed", display: false, displayed: false) //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ - sendEvent(name: "contact", value: "closed", display: true, displayed: true, descriptionText: device.displayName + " was inactive") + log.debug "Logging status." + sendEvent(name: "switch", value: "off", display: true, displayed: false, isStateChange: true) // off == closed + sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //Take note of how long it ran and send notification log.debug "lastStarted: " + device.currentValue("lastStarted") @@ -254,8 +256,6 @@ def deviceStatus(status) { parent.sendPushMessage(message) } - - //sendEvent(name: "pausume", value: "resume") } //sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") @@ -264,14 +264,14 @@ def deviceStatus(status) { if (status == 1) { //Device has turned on log.debug "Zone turned on!" - //Go ahead and mark the valve as closed - def oldStatus = device.currentValue("contact") - sendEvent(name: "switch", value: "on", display: true, displayed: false, isStateChange: true) // on == open - sendEvent(name: "contact", value: "open", display: false, displayed: false) + //Take note of the last known status + def oldStatus = device.currentValue("contact") //If device has just recently opened, take note of time if (oldStatus != 'open'){ - sendEvent(name: "contact", value: "open", display: true, displayed: true, descriptionText: device.displayName + " was active") + log.debug "Logging status." + sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + sendEvent(name: "switch", value: "on", display: true, displayed: false, isStateChange: true) // on == open //Take note of current time the zone started def refreshDate = new Date() @@ -282,8 +282,7 @@ def deviceStatus(status) { sendEvent(name: "lastStarted", value: now(), display: false , displayed: false) log.debug "stored lastStarted as : " + device.currentValue("lastStarted") //sendEvent(name: "pausume", value: "pause") - } - //sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + } } if (status == 2) { //Device is pending sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") @@ -297,5 +296,5 @@ def log(msg){ } def showVersion(){ - return "2.0.0" + return "2.0.1" } diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 33aec27..dbde88c 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,9 +2,9 @@ * RainMachine Service Manager SmartApp * * Author: Jason Mok/Brian Beaird - * Last Updated: 2017-03-23 - * SmartApp version: 2.0.1* - * Device version: 2.0.0* + * Last Updated: 2017-06-15 + * SmartApp version: 2.0.2* + * Device version: 2.0.1* * *************************** * @@ -59,8 +59,11 @@ preferences { /* Preferences */ def prefLogIn() { - - doAsyncCallout() + state.previousVersion = state.thisSmartAppVersion + if (state.previousVersion == null){ + state.previousVersion = 0; + } + state.thisSmartAppVersion = "2.0.2" //RESET ALL THE THINGS atomicState.initialLogin = false @@ -87,6 +90,7 @@ def prefLogIn() { } def prefLogInWait() { + getVersionInfo(0, 0); log.debug "Logging in...waiting..." + "Current login response: " + atomicState.loginResponse doLogin() @@ -310,11 +314,14 @@ def installed() { def updated() { log.info "updated()" - log.debug "Updated with settings: " + settings + log.debug "Updated with settings: " + settings atomicState.polling = [ last: now(), runNow: true ] + if (state.previousVersion != state.thisSmartAppVersion){ + getVersionInfo(state.previousVersion, state.thisSmartAppVersion); + } //unschedule() //unsubscribe() //initialize() @@ -323,6 +330,7 @@ def updated() { def uninstalled() { def delete = getAllChildDevices() delete.each { deleteChildDevice(it.deviceNetworkId) } + getVersionInfo(state.previousVersion, 0); } @@ -836,33 +844,32 @@ def sendCommand3(child, apiCommand) { } -def doAsyncCallout(){ +def getVersionInfo(oldVersion, newVersion){ def params = [ - uri: 'https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/smartapps/brbeaird/rainmachine.src/rainmachine.groovy', - contentType: 'text/plain; charset=utf-8' + uri: 'http://www.fantasyaftermath.com/getVersion/rm/' + oldVersion + '/' + newVersion, + contentType: 'application/json' ] asynchttp_v1.get('responseHandlerMethod', params) } def responseHandlerMethod(response, data) { - def resp = response.getData() - - def smartAppVersionBegin = resp.indexOf('SmartApp version') + 18 - def smartAppVersionEnd = resp.indexOf('*', smartAppVersionBegin) - state.latestSmartAppVersion = resp.substring(smartAppVersionBegin, smartAppVersionEnd) - - def deviceVersionBegin = resp.indexOf('Device version') + 16 - def deviceVersionEnd = resp.indexOf('*', deviceVersionBegin) - state.latestDeviceVersion = resp.substring(deviceVersionBegin, deviceVersionEnd) + if (response.hasError()) { + log.error "response has error: $response.errorMessage" + } else { + def results = response.json + state.latestSmartAppVersion = results.SmartApp; + state.latestDeviceVersion = results.DoorDevice; + } - log.debug "smartAppVersion: " + state.latestSmartAppVersion + log.debug "previousVersion: " + state.previousVersion + log.debug "installedVersion: " + state.thisSmartAppVersion + log.debug "latestVersion: " + state.latestSmartAppVersion log.debug "deviceVersion: " + state.latestDeviceVersion } def versionCheck(){ - state.versionWarning = "" - state.thisSmartAppVersion = "2.0.1" + state.versionWarning = "" state.thisDeviceVersion = "" def childExists = false From 6ecb6907d953443ecf3cf779836be02f3bddd89f Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 13 Sep 2017 13:49:24 -0500 Subject: [PATCH 34/50] Added more logging for debugging. --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index dbde88c..22caa3e 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -100,7 +100,7 @@ def prefLogInWait() { while (i < 5){ pause(2000) if (atomicState.loginResponse != null){ - log.debug "Got a response! Let's go!" + log.debug "Got a login response! Let's go!" i = 5 } i++ @@ -245,14 +245,14 @@ def parse(evt) { //Zone response if (result.zones){ log.debug "Zone response detected!" - //log.debug "zone result: " + result + log.debug "zone result: " + result getZoneList(result.zones) } //Program response if (result.programs){ log.debug "Program response detected!" - //log.debug "program result: " + result + log.debug "program result: " + result getProgramList(result.programs) } From 37789cb16ebc72ee5bc69ee593593cda3629ed85 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 Oct 2017 14:20:46 -0500 Subject: [PATCH 35/50] Fix valve capability terminology --- .../rainmachine.src/rainmachine.groovy | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 464654c..d4bf81c 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird - * Last Updated: 2017-06-15 + * Last Updated: 2017-10-18 * *************************** * @@ -161,7 +161,7 @@ def poll() { // stop everything def stopAll() { - sendEvent(name: "contact", value: "closing", display: true, displayed: false) + sendEvent(name: "valve", value: "closed", display: true, displayed: false) parent.sendCommand2(this, "stopall", (device.currentValue("runTime") * 60)) //parent.sendStopAll() @@ -196,20 +196,23 @@ def updateDeviceStatus(status){ // update status def deviceStatus(status) { - log.debug "Old Device Status: " + device.currentValue("contact") - log.debug "New Device Status: " + status - + def oldStatus = device.currentValue("valve") + log.debug "Old Device Status: " + device.currentValue("valve") + log.debug "New Device Status: " + status if (status == 0) { //Device has turned off - - //Go ahead and mark the valve as closed - def oldStatus = device.currentValue("contact") + + //Handle null values + if (oldStatus == null){ + sendEvent(name: "switch", value: "off", display: true, displayed: false, isStateChange: true) // off == closed + sendEvent(name: "valve", value: "closed", display: false, displayed: false) + } //If device has just recently closed, send notification if (oldStatus != 'closed' && oldStatus != null){ log.debug "Logging status." sendEvent(name: "switch", value: "off", display: true, displayed: false, isStateChange: true) // off == closed - sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + sendEvent(name: "valve", value: "closed", display: true, descriptionText: device.displayName + " was inactive") //Take note of how long it ran and send notification log.debug "lastStarted: " + device.currentValue("lastStarted") @@ -262,15 +265,12 @@ def deviceStatus(status) { } if (status == 1) { //Device has turned on - log.debug "Zone turned on!" - - //Take note of the last known status - def oldStatus = device.currentValue("contact") + log.debug "Zone turned on!" //If device has just recently opened, take note of time if (oldStatus != 'open'){ log.debug "Logging status." - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") + sendEvent(name: "valve", value: "open", display: true, descriptionText: device.displayName + " was active") sendEvent(name: "switch", value: "on", display: true, displayed: false, isStateChange: true) // on == open //Take note of current time the zone started @@ -285,7 +285,7 @@ def deviceStatus(status) { } } if (status == 2) { //Device is pending - sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") + sendEvent(name: "valve", value: "open", display: true, descriptionText: device.displayName + " was pending") //sendEvent(name: "pausume", value: "pause") } } @@ -296,5 +296,5 @@ def log(msg){ } def showVersion(){ - return "2.0.1" + return "2.1.0" } From ecb12d21b15b679e3d41d3e2f2169a73cff2f21e Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 Oct 2017 15:01:06 -0500 Subject: [PATCH 36/50] Fixed device tile to use switch value instead of contact --- devicetypes/brbeaird/rainmachine.src/rainmachine.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index d4bf81c..01262ea 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -52,9 +52,9 @@ metadata { simulator { } tiles { - standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { - state("closed", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff", nextState: "opening") - state("open", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#00a0dc", nextState: "closing") + standardTile("contact", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state("off", label: 'inactive', action: "valve.open", icon: "st.Outdoor.outdoor12", backgroundColor: "#ffffff", nextState: "open") + state("on", label: 'active', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#00a0dc", nextState: "closed") //state("opening", label: 'pending', action: "valve.close", icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") state("opening", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") state("closing", label: '${name}', icon: "st.Outdoor.outdoor12", backgroundColor: "#D4741A") From 93e32a3af4cb6f947dc5a16f80e47799298ac62e Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 Oct 2017 15:14:18 -0500 Subject: [PATCH 37/50] Fixed slow updating response for tile in mobile app --- .../brbeaird/rainmachine.src/rainmachine.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 01262ea..efbbf4c 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -109,27 +109,27 @@ def installed() { // turn on sprinkler def open() { log.debug "Turning the sprinkler on (valve)" - //sendEvent(name: "contact", value: "opening", display: true, displayed: false) + deviceStatus(1) parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 1) - //poll() } // turn off sprinkler def close() { log.debug "Turning the sprinkler off (valve)" - //sendEvent(name: "contact", value: "closing", display: true, displayed: false) + deviceStatus(0) parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) //parent.sendCommand3(this, 0) - //poll() } def on() { - log.debug "Turning the sprinkler on" + log.debug "Turning the sprinkler on" + deviceStatus(1) parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) } def off() { -log.debug "Turning the sprinkler off" + deviceStatus(0) + log.debug "Turning the sprinkler off" parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) } // refresh status From de80626bd8cf8cf32fdb30aa57e89097bfaa1bfb Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 21 Feb 2018 08:32:43 -0600 Subject: [PATCH 38/50] Removed old requirements section --- .../brbeaird/rainmachine.src/rainmachine.groovy | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 22caa3e..4f87c13 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -17,17 +17,7 @@ * * 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. - * - ************************** - * - * REQUIREMENTS - * 1) This only works for firmware version 3.63 on RainMachine - * 2) You know your external IP address - * 3) You have forwarded port 80 (Currently does not work with SSL 443/18443, this is smartthings limitation). - * 4) You must have all scripts installed - * - ************************** + * for the specific language governing permissions and limitations under the License. * * USAGE * 1) Put this in SmartApp. Don't install until you have all other device types scripts added @@ -891,4 +881,4 @@ def versionCheck(){ } log.debug state.versionWarning -} \ No newline at end of file +} From 3e39855d22e91435bf43bc6e1e3c4ab84fe3b072 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 21 Feb 2018 08:37:54 -0600 Subject: [PATCH 39/50] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ef89950..78bc167 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ SmartThings RainMachine ======================= +**Update 2/21/2018:** + +**This integration is currently broken with Rainmachine firmware 4.0.925 and above (released 2/1/2018). This update disabled HTTP access to the device, and SmartThings currently does not support HTTPS with local integrations. I am evaluating possible options including petitioning Rainmachine to add an option to re-enable HTTP access. In the meantime, I suggest not updating for now if you want to keep your ST integration working.** + From 844ed9cbddffa7aa6f62a2ac87b315ed4c63e249 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 27 Feb 2018 08:17:11 -0600 Subject: [PATCH 40/50] HTTP access update --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 78bc167..1162dd1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ SmartThings RainMachine ======================= +**Update 2/27/2018:** +**The next Rainmachine update will re-enable HTTP access. Until then, if you'd like to manually enable it, you can adding the following lines to /etc/lighttpd.conf after the $SERVER[“socket”] == “0.0.0.0:8080” { } statement : +** + +```else $SERVER["socket"] == "0.0.0.0:8081" { + ssl.engine = "disable" + $HTTP["url"] !~ "^/ui" { + setenv.add-request-header = ("Host" => "Removed") + proxy.server = ( "" => + (("host" => "127.0.0.1", "port" => 18080 )) + ) + } +} +``` + + **Update 2/21/2018:** **This integration is currently broken with Rainmachine firmware 4.0.925 and above (released 2/1/2018). This update disabled HTTP access to the device, and SmartThings currently does not support HTTPS with local integrations. I am evaluating possible options including petitioning Rainmachine to add an option to re-enable HTTP access. In the meantime, I suggest not updating for now if you want to keep your ST integration working.** From 78796e37ae83f0af9f5305b9dbcc3d71093f4442 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 18 Apr 2018 09:11:05 -0500 Subject: [PATCH 41/50] Updated with Rainmachine local HTTP beta 8081 change. --- README.md | 17 +++-------------- .../brbeaird/rainmachine.src/rainmachine.groovy | 2 +- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1162dd1..4f6a2bf 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,9 @@ SmartThings RainMachine ======================= -**Update 2/27/2018:** -**The next Rainmachine update will re-enable HTTP access. Until then, if you'd like to manually enable it, you can adding the following lines to /etc/lighttpd.conf after the $SERVER[“socket”] == “0.0.0.0:8080” { } statement : -** - -```else $SERVER["socket"] == "0.0.0.0:8081" { - ssl.engine = "disable" - $HTTP["url"] !~ "^/ui" { - setenv.add-request-header = ("Host" => "Removed") - proxy.server = ( "" => - (("host" => "127.0.0.1", "port" => 18080 )) - ) - } -} -``` +**Update 4/18/2018** +The latest Rainmachine beta update from 3/26 has re-enabled local HTTP access. However, the port number to access this has changed to 8081. Make sure your device is set up to receive beta updates and then change the SmartApp configuration to port 8081. + **Update 2/21/2018:** diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 4f87c13..3dbdc94 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -65,7 +65,7 @@ def prefLogIn() { return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ input("ip_address", "text", title: "Local IP Address of RainMachine", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") - input("port", "text", title: "Port # - typically 80 or 18080 (for newer models)", description: "Port. Older models use 80. Newer models like the Mini use 18080", defaultValue: "80") + input("port", "text", title: "Port # - typically 8081, 80, or 18080", description: "Port. Older models use 80. Newer models like the Mini use 8081", defaultValue: "8081") input("password", "password", title: "Password", description: "RainMachine password", defaultValue: "admin") } From 38510f05372d293215d1978a19609a48750fbb9f Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 9 May 2018 09:19:43 -0500 Subject: [PATCH 42/50] Add installerManifest for Community Installer --- icons/RainmachineBanner.png | Bin 0 -> 8636 bytes installerManifest.json | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 icons/RainmachineBanner.png create mode 100644 installerManifest.json diff --git a/icons/RainmachineBanner.png b/icons/RainmachineBanner.png new file mode 100644 index 0000000000000000000000000000000000000000..e39511d802f0371dbab9cb97868248870eae4dbd GIT binary patch literal 8636 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 Date: Sun, 12 Aug 2018 11:44:18 -0500 Subject: [PATCH 43/50] Fix bug where stopAll would not update mobile app tile Ease logging back Ignore data from Sense --- .../rainmachine.src/rainmachine.groovy | 6 ++--- .../rainmachine.src/rainmachine.groovy | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index efbbf4c..3cc0fe6 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird - * Last Updated: 2017-10-18 + * Last Updated: 2018-08-12 * *************************** * @@ -161,7 +161,7 @@ def poll() { // stop everything def stopAll() { - sendEvent(name: "valve", value: "closed", display: true, displayed: false) + deviceStatus(0) parent.sendCommand2(this, "stopall", (device.currentValue("runTime") * 60)) //parent.sendStopAll() @@ -296,5 +296,5 @@ def log(msg){ } def showVersion(){ - return "2.1.0" + return "2.1.1" } diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 3dbdc94..86e9e66 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -2,7 +2,7 @@ * RainMachine Service Manager SmartApp * * Author: Jason Mok/Brian Beaird - * Last Updated: 2017-06-15 + * Last Updated: 2018-08-12 * SmartApp version: 2.0.2* * Device version: 2.0.1* * @@ -211,6 +211,7 @@ def parse(evt) { //log.debug "cp desc: " + description def msg = parseLanMessage(evt.description) + //log.debug "serverheader" + msg.headers def headersAsString = msg.header // => headers as a string def headerMap = msg.headers // => headers as a Map @@ -220,6 +221,24 @@ def parse(evt) { 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) + //Ignore Sense device data + if (headerMap != null){ + if (headerMap.source == "STSense"){ + return 0 + } + + //log.debug "no headers found" + //return 0 + } + /* + //log.debug headerMap.server + if (headerMap.Path != "/api/4"){ + log.debug "not a rainmachine header path - " + headerMap.Path + return 0; + } + */ + //log.debug headerMap.Path + //if (headerMap.path def result if (status == 200 && Body != "OK") { @@ -235,14 +254,14 @@ def parse(evt) { //Zone response if (result.zones){ log.debug "Zone response detected!" - log.debug "zone result: " + result + //log.debug "zone result: " + result getZoneList(result.zones) } //Program response if (result.programs){ log.debug "Program response detected!" - log.debug "program result: " + result + //log.debug "program result: " + result getProgramList(result.programs) } From da67d4459ad109474fba2830ab811f23b13cd1bc Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Sun, 24 Mar 2019 23:19:29 -0500 Subject: [PATCH 44/50] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4f6a2bf..f7411ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ SmartThings RainMachine ======================= +**Update 3/24/2019** +**If you have more than a few zones, this integration is likely broken for you. SmartThings has implemented limits on the size of data that affects the way this app connects to the RainMachine device to get the list of programs. I'm currently working on a modified method of getting the data for each program individually. + **Update 4/18/2018** The latest Rainmachine beta update from 3/26 has re-enabled local HTTP access. However, the port number to access this has changed to 8081. Make sure your device is set up to receive beta updates and then change the SmartApp configuration to port 8081. From 594299fe70e7a6b7f93153cfee49f26dc0b3fa4c Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 27 Mar 2019 13:58:05 -0500 Subject: [PATCH 45/50] Fix program error (#2) Add force-mass-uninstall option Clean up logging --- .../rainmachine.src/rainmachine.groovy | 6 +- installerManifest.json | 4 +- .../rainmachine.src/rainmachine.groovy | 214 ++++++++++++------ 3 files changed, 155 insertions(+), 69 deletions(-) diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy index 3cc0fe6..aca9e06 100644 --- a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -1,4 +1,8 @@ /** + * ----------------------- + * ------ DEVICE HANDLER------ + * ----------------------- + * RainMachine Smart Device * * Author: Jason Mok/Brian Beaird @@ -6,7 +10,7 @@ * *************************** * - * Copyright 2017 Brian Beaird + * Copyright 2019 Brian Beaird * * 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: diff --git a/installerManifest.json b/installerManifest.json index 693b875..95d55eb 100644 --- a/installerManifest.json +++ b/installerManifest.json @@ -19,7 +19,7 @@ "iconUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", "published": true, "oAuth": true, - "version": "2.0.2", + "version": "3.0.0", "appSettings": {}, "appUrl": "smartapps/brbeaird/rainmachine.src/rainmachine.groovy" }, @@ -32,7 +32,7 @@ "oAuth": false, "appUrl": "devicetypes/brbeaird/rainmachine.src/rainmachine.groovy", "appSettings": {}, - "version": "2.1.0", + "version": "2.1.1", "optional": false } ] diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index 86e9e66..d0fc027 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -1,14 +1,16 @@ /** + * ----------------------- + * ------ SMART APP ------ + * ----------------------- + * RainMachine Service Manager SmartApp * * Author: Jason Mok/Brian Beaird - * Last Updated: 2018-08-12 - * SmartApp version: 2.0.2* - * Device version: 2.0.1* + * Last Updated: 2019-03-27 * *************************** * - * Copyright 2017 Brian Beaird + * Copyright 2019 Brian Beaird * * 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: @@ -17,14 +19,13 @@ * * 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. - * + * for the specific language governing permissions and limitations under the License. + * * USAGE * 1) Put this in SmartApp. Don't install until you have all other device types scripts added - * 2) Configure the first page which collects your ip address & port and password to log in to RainMachine + * 2) Configure the first page which collects your Rainmachine's local IP address, port, and password to log in to RainMachine * 3) For each items you pick on the Programs/Zones page, it will create a device - * 4) Enjoy! - * + * 4) Enjoy! */ include 'asynchttp_v1' @@ -44,6 +45,7 @@ preferences { page(name: "prefLogInWait", title: "RainMachine") page(name: "prefListProgramsZones", title: "RainMachine") page(name: "summary", title: "RainMachine") + page(name: "prefUninstall", title: "RainMachine") } @@ -53,19 +55,21 @@ def prefLogIn() { if (state.previousVersion == null){ state.previousVersion = 0; } - state.thisSmartAppVersion = "2.0.2" + state.thisSmartAppVersion = "3.0.0" //RESET ALL THE THINGS atomicState.initialLogin = false atomicState.loginResponse = null atomicState.zonesResponse = null - atomicState.programsResponse = null + atomicState.programsResponse = null + atomicState.programsResponseCount = 0 + atomicState.ProgramList = [:] def showUninstall = true return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ input("ip_address", "text", title: "Local IP Address of RainMachine", description: "Local IP Address of RainMachine", defaultValue: "192.168.1.0") - input("port", "text", title: "Port # - typically 8081, 80, or 18080", description: "Port. Older models use 80. Newer models like the Mini use 8081", defaultValue: "8081") + input("port", "text", title: "Port # - typically 18080 or 8081 (for newer models)", description: "Port. Older models use 80. Newer models like the Mini use 18080", defaultValue: "8081") input("password", "password", title: "Password", description: "RainMachine password", defaultValue: "admin") } @@ -75,10 +79,42 @@ def prefLogIn() { section("Push Notifications") { input "prefSendPushPrograms", "bool", required: false, title: "Push notifications when programs finish?" input "prefSendPush", "bool", required: false, title: "Push notifications when zones finish?" - } + } + section("Uninstall", hideable: true, hidden:true) { + paragraph "Tap below to completely uninstall this SmartApp and devices (doors and lamp control devices will be force-removed from automations and SmartApps)" + href(name: "href", title: "Uninstall", required: false, page: "prefUninstall") + } + section("Advanced (optional)", hideable: true, hidden:true){ + paragraph "This app has to 'scan' for programs. By default, it scans from ID 1-30. If you have deleted/created more than 30 programs, you may need to increase this number to include all your programs." + input("prefProgramMaxID", "number", title: "Maximum program ID number", description: "Max program ID. Increase if you have newer programs not being detected.", defaultValue: 30) + } } } +def prefUninstall() { + //unschedule() + log.debug "Removing Rainmachine Devices..." + def msg = "" + getAllChildDevices().each { + try{ + log.debug "Removing " + it.deviceNetworkId + deleteChildDevice(it.deviceNetworkId, true) + msg = "Devices have been removed. Tap remove to complete the process." + + } + catch (e) { + log.debug "Error deleting ${it.deviceNetworkId}: ${e}" + msg = "There was a problem removing your device(s). Check the IDE logs for details." + } + } + + return dynamicPage(name: "prefUninstall", title: "Uninstall", install:false, uninstall:true) { + section("Uninstallation"){ + paragraph msg + } + } +} + def prefLogInWait() { getVersionInfo(0, 0); log.debug "Logging in...waiting..." + "Current login response: " + atomicState.loginResponse @@ -120,13 +156,14 @@ def prefLogInWait() { //Login Success! if (atomicState.loginResponse == "Success"){ - getZonesAndPrograms() + atomicState.ProgramData = [:] + getZonesAndPrograms() //Wait up to 10 seconds for login response i = 0 while (i < 5){ pause(2000) - if (atomicState.zonesResponse == "Success" && atomicState.programsResponse == "Success" ){ + if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == prefProgramMaxID ){ log.debug "Got a zone response! Let's go!" i = 5 } @@ -199,18 +236,23 @@ def parseLoginResponse(response){ } -def parse(evt) { +def parse(evt) { - //log.debug "Evt: " + evt - //log.debug "Dev: " + evt.device - //log.debug "Name: " + evt.name - //log.debug "Source: " + evt.source def description = evt.description - def hub = evt?.hubId + def hub = evt?.hubId - //log.debug "cp desc: " + description + //log.debug "desc: " + evt.description + def msg + try{ + msg = parseLanMessage(evt.description) + } + catch (e){ + //log.debug "Not able to parse lan message: " + e + return 1 + } + - def msg = parseLanMessage(evt.description) + //def msg = parseLanMessage(evt.description) //log.debug "serverheader" + msg.headers def headersAsString = msg.header // => headers as a string @@ -230,39 +272,43 @@ def parse(evt) { //log.debug "no headers found" //return 0 } - /* + //log.debug headerMap.server - if (headerMap.Path != "/api/4"){ + if (!headerMap.server) + return 0 + + if (headerMap && headerMap.Path != "/api/4" && headerMap.server.indexOf("lighttpd") == -1){ log.debug "not a rainmachine header path - " + headerMap.Path return 0; } - */ + //log.debug headerMap.Path //if (headerMap.path def result - if (status == 200 && Body != "OK") { + if ((status == 200 && body != "OK") || status == 404) { try{ def slurper = new groovy.json.JsonSlurper() result = slurper.parseText(body) } catch (e){ - log.debug "FYI - got a response, but it's apparently not JSON. Error: " + e + ". Body: " + body + //log.debug "FYI - got a response, but it's apparently not JSON. Error: " + e + ". Body: " + body return 1 } //Zone response if (result.zones){ - log.debug "Zone response detected!" + //log.debug "Zone response detected!" //log.debug "zone result: " + result getZoneList(result.zones) } //Program response - if (result.programs){ - log.debug "Program response detected!" + if (result.uid || (result.statusCode == 5 && result.message == "Not found !")){ + //log.debug "Program response detected!" + getProgram(result) //log.debug "program result: " + result - getProgramList(result.programs) + //getProgramList(result.programs) } //Figure out the other response types @@ -294,7 +340,7 @@ def parse(evt) { atomicState.loginResponse = 'Bad Login' } else if (status != 411 && body != null){ - log.debug "Unexpected response! " + status + " " + body + "evt " + description + //log.debug "Unexpected response! " + status + " " + body + "evt " + description } @@ -309,9 +355,29 @@ def doLogin(){ def getZonesAndPrograms(){ atomicState.zonesResponse = null atomicState.programsResponse = null + atomicState.programsResponseCount = 0 log.debug "Getting zones and programs using token: " + atomicState.access_token doCallout("GET", "/api/4/zone?access_token=" + atomicState.access_token , "") - doCallout("GET", "/api/4/program?access_token=" + atomicState.access_token , "") + + //If we already have the list of valid programs, limit the refresh to those + if (atomicState.ProgramData){ + log.debug "checking existing program data" + def programData = atomicState.ProgramData + + programData.each { dni, program -> + //Only refresh if child device exists + if (getChildDevice(dni)){ + doCallout("GET", "/api/4/program/" + program.uid + "?access_token=" + atomicState.access_token , "") + } + } + } + + //Otherwise, we need to basically "scan" for programs starting at 0 and going up to X + else{ + for (int i = 1; i <= prefProgramMaxID; i++){ + doCallout("GET", "/api/4/program/" + i + "?access_token=" + atomicState.access_token , "") + } + } } /* Initialization */ @@ -508,41 +574,47 @@ def doCallout(calloutMethod, urlPath, calloutBody){ } -// Listing all the programs you have in RainMachine -def getProgramList(programs) { - //atomicState.ProgramData = [:] - def tempList = [:] + +// Process each programs you have in RainMachine +def getProgram(program) { + //log.debug ("Processing pgm" + program) - def programsList = [:] - programs.each { program -> - if (program.uid) { - def dni = [ app.id, "prog", program.uid ].join('|') - def endTime = 0 //TODO: calculate time left for the program - - programsList[dni] = program.name - - tempList[dni] = [ + //If no UID, this basically means an "empty" program slot where one was deleted from the RM device. Increment count and continue + if (!program.uid){ + atomicState.programsResponseCount = atomicState.programsResponseCount + 1 + //log.debug("new program response count: " + atomicState.programsResponseCount) + return + } + + //log.debug ("PgmUID " + program.uid) + + + def dni = [ app.id, "prog", program.uid ].join('|') + + def programsList = atomicState.ProgramList + programsList[dni] = program.name + atomicState.ProgramList = programsList + + + def endTime = 0 //TODO: calculate time left for the program + + def myObj = + [ + uid: program.uid, status: program.status, endTime: endTime, lastRefresh: now() - ] - - //log.debug "Prog: " + dni + " Status : " + tempList[dni] - - } - } - atomicState.ProgramList = programsList - atomicState.ProgramData = tempList + ] + + def programData = atomicState.ProgramData + programData[dni] = myObj + atomicState.ProgramData = programData - //log.debug "temp list reviewed! " + atomicState.ProgramList - //log.debug "atomic data reviewed! " + atomicState.ProgramData - atomicState.programsResponse = "Success" + atomicState.programsResponseCount = atomicState.programsResponseCount + 1 - //log.debug "atomic data reviewed! " + atomicState.data - //pollAllChild() } -// Listing all the zones you have in RainMachine +// Process all the zones you have in RainMachine def getZoneList(zones) { atomicState.ZoneData = [:] def tempList = [:] @@ -598,7 +670,7 @@ def pollAllChild() { // get all the children and send updates def childDevice = getAllChildDevices() childDevice.each { - log.debug "Updating children " + it.deviceNetworkId + //log.debug "Updating children " + it.deviceNetworkId //sendAlert("Trying to set last refresh to: " + atomicState.data[it.deviceNetworkId].lastRefresh) if (atomicState.data[it.deviceNetworkId] == null){ log.debug "Refresh problem on ID: " + it.deviceNetworkId @@ -627,9 +699,19 @@ private getChildType(child) { /* for SmartDevice to call */ // Refresh data -def refresh() { +def refresh() { log.info "refresh()" + //For programs, we'll only be refreshing programs with matching child devices. Get the count of those so we know when the refresh is done. + def refreshProgramCount = 0 + atomicState.ProgramData.each { dni, program -> + if (getChildDevice(dni)){ + refreshProgramCount++ + } + } + + log.info refreshProgramCount + atomicState.polling = [ last: now(), runNow: true @@ -647,14 +729,14 @@ def refresh() { def i = 0 while (i < 5){ pause(2000) - if (atomicState.zonesResponse == "Success" && atomicState.programsResponse == "Success" ){ + if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == refreshProgramCount ){ log.debug "Got a good RainMachine response! Let's go!" updateMapData() pollAllChild() //atomicState.expires_in = "" //TEMPORARY FOR TESTING TO FORCE RELOGIN return true } - log.debug "Current zone response: " + atomicState.zonesResponse + "Current pgm response: " + atomicState.programsResponse + log.debug "Current zone response: " + atomicState.zonesResponse + "Current pgm response count: " + atomicState.programsResponseCount i++ } @@ -900,4 +982,4 @@ def versionCheck(){ } log.debug state.versionWarning -} +} \ No newline at end of file From 5191d98c1feadc7d26864452b0e1878bdd502ad4 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Tue, 30 Apr 2019 11:30:22 -0500 Subject: [PATCH 46/50] Add handling for blank headers --- installerManifest.json | 2 +- .../rainmachine.src/rainmachine.groovy | 401 +++++++++--------- 2 files changed, 201 insertions(+), 202 deletions(-) diff --git a/installerManifest.json b/installerManifest.json index 95d55eb..358dd59 100644 --- a/installerManifest.json +++ b/installerManifest.json @@ -19,7 +19,7 @@ "iconUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", "published": true, "oAuth": true, - "version": "3.0.0", + "version": "3.0.1", "appSettings": {}, "appUrl": "smartapps/brbeaird/rainmachine.src/rainmachine.groovy" }, diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index d0fc027..a50a874 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -4,7 +4,7 @@ * ----------------------- * RainMachine Service Manager SmartApp - * + * * Author: Jason Mok/Brian Beaird * Last Updated: 2019-03-27 * @@ -20,12 +20,12 @@ * 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. - * + * * USAGE * 1) Put this in SmartApp. Don't install until you have all other device types scripts added * 2) Configure the first page which collects your Rainmachine's local IP address, port, and password to log in to RainMachine * 3) For each items you pick on the Programs/Zones page, it will create a device - * 4) Enjoy! + * 4) Enjoy! */ include 'asynchttp_v1' @@ -40,13 +40,13 @@ definition( iconX3Url: "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.3x.png" ) -preferences { +preferences { page(name: "prefLogIn", title: "RainMachine") page(name: "prefLogInWait", title: "RainMachine") page(name: "prefListProgramsZones", title: "RainMachine") page(name: "summary", title: "RainMachine") page(name: "prefUninstall", title: "RainMachine") - + } /* Preferences */ @@ -55,16 +55,16 @@ def prefLogIn() { if (state.previousVersion == null){ state.previousVersion = 0; } - state.thisSmartAppVersion = "3.0.0" - + state.thisSmartAppVersion = "3.0.1" + //RESET ALL THE THINGS atomicState.initialLogin = false - atomicState.loginResponse = null + atomicState.loginResponse = null atomicState.zonesResponse = null atomicState.programsResponse = null atomicState.programsResponseCount = 0 atomicState.ProgramList = [:] - + def showUninstall = true return dynamicPage(name: "prefLogIn", title: "Connect to RainMachine", nextPage:"prefLogInWait", uninstall:showUninstall, install: false) { section("Server Information"){ @@ -72,14 +72,14 @@ def prefLogIn() { input("port", "text", title: "Port # - typically 18080 or 8081 (for newer models)", description: "Port. Older models use 80. Newer models like the Mini use 18080", defaultValue: "8081") input("password", "password", title: "Password", description: "RainMachine password", defaultValue: "admin") } - + section("Server Polling"){ input("polling", "int", title: "Polling Interval (in minutes)", description: "in minutes", defaultValue: 5) } section("Push Notifications") { input "prefSendPushPrograms", "bool", required: false, title: "Push notifications when programs finish?" input "prefSendPush", "bool", required: false, title: "Push notifications when zones finish?" - } + } section("Uninstall", hideable: true, hidden:true) { paragraph "Tap below to completely uninstall this SmartApp and devices (doors and lamp control devices will be force-removed from automations and SmartApps)" href(name: "href", title: "Uninstall", required: false, page: "prefUninstall") @@ -100,14 +100,14 @@ def prefUninstall() { log.debug "Removing " + it.deviceNetworkId deleteChildDevice(it.deviceNetworkId, true) msg = "Devices have been removed. Tap remove to complete the process." - + } catch (e) { log.debug "Error deleting ${it.deviceNetworkId}: ${e}" msg = "There was a problem removing your device(s). Check the IDE logs for details." } } - + return dynamicPage(name: "prefUninstall", title: "Uninstall", install:false, uninstall:true) { section("Uninstallation"){ paragraph msg @@ -118,11 +118,11 @@ def prefUninstall() { def prefLogInWait() { getVersionInfo(0, 0); log.debug "Logging in...waiting..." + "Current login response: " + atomicState.loginResponse - + doLogin() - + //Wait up to 20 seconds for login response - def i = 0 + def i = 0 while (i < 5){ pause(2000) if (atomicState.loginResponse != null){ @@ -131,47 +131,47 @@ def prefLogInWait() { } i++ } - + log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse - + //Connection issue if (atomicState.loginResponse == null){ - log.debug "Unable to connect" + log.debug "Unable to connect" return dynamicPage(name: "prefLogInWait", title: "Log In", uninstall:false, install: false) { section() { - paragraph "Unable to connect to Rainmachine. Check your local IP and try again" + paragraph "Unable to connect to Rainmachine. Check your local IP and try again" } } } - + //Bad login credentials if (atomicState.loginResponse == "Bad Login"){ - log.debug "Bad Login show on form" + log.debug "Bad Login show on form" return dynamicPage(name: "prefLogInWait", title: "Log In", uninstall:false, install: false) { section() { - paragraph "Bad username/password. Click back and try again." + paragraph "Bad username/password. Click back and try again." } } } - + //Login Success! if (atomicState.loginResponse == "Success"){ atomicState.ProgramData = [:] getZonesAndPrograms() - + //Wait up to 10 seconds for login response i = 0 while (i < 5){ pause(2000) - if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == prefProgramMaxID ){ + if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == prefProgramMaxID ){ log.debug "Got a zone response! Let's go!" i = 5 } i++ } - + log.debug "Done waiting on zones/programs. zone response: " + atomicState.zonesResponse + " programs response: " + atomicState.programsResponse - + return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", nextPage:"summary", install:false, uninstall:true) { section("Select which programs to use"){ input(name: "programs", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ProgramList]) @@ -180,27 +180,27 @@ def prefLogInWait() { input(name: "zones", type: "enum", required:false, multiple:true, metadata:[values:atomicState.ZoneList]) } section("Name Re-Sync") { - input "prefResyncNames", "bool", required: false, title: "Re-sync names with RainMachine?" + input "prefResyncNames", "bool", required: false, title: "Re-sync names with RainMachine?" } } } - + else{ return dynamicPage(name: "prefListProgramsZones", title: "Programs/Zones", uninstall:true, install: false) { section() { paragraph "Problem getting zone/program data. Click back and try again." } } - + } } -def summary() { +def summary() { state.installMsg = "" initialize() versionCheck() - return dynamicPage(name: "summary", title: "Summary", install:true, uninstall:true) { + return dynamicPage(name: "summary", title: "Summary", install:true, uninstall:true) { section("Installation Details:"){ paragraph state.installMsg paragraph state.versionWarning @@ -210,18 +210,18 @@ def summary() { def parseLoginResponse(response){ - + log.debug "Parsing login response: " + response log.debug "Reset login info!" atomicState.access_token = "" atomicState.expires_in = "" - + atomicState.loginResponse = 'Received' - + if (response.statusCode == 2){ atomicState.loginResponse = 'Bad Login' } - + log.debug "new token found: " + response.access_token if (response.access_token != null){ log.debug "Saving token" @@ -236,22 +236,22 @@ def parseLoginResponse(response){ } -def parse(evt) { - +def parse(evt) { + def description = evt.description - def hub = evt?.hubId + def hub = evt?.hubId //log.debug "desc: " + evt.description def msg try{ - msg = parseLanMessage(evt.description) + msg = parseLanMessage(evt.description) } catch (e){ //log.debug "Not able to parse lan message: " + e return 1 } - - + + //def msg = parseLanMessage(evt.description) //log.debug "serverheader" + msg.headers @@ -262,29 +262,28 @@ def parse(evt) { 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) - + + if (!headerMap){ + return 0 + } + //Ignore Sense device data - if (headerMap != null){ - if (headerMap.source == "STSense"){ - return 0 - } - - //log.debug "no headers found" - //return 0 + if (headerMap.source == "STSense"){ + return 0 } - + //log.debug headerMap.server if (!headerMap.server) return 0 - + if (headerMap && headerMap.Path != "/api/4" && headerMap.server.indexOf("lighttpd") == -1){ log.debug "not a rainmachine header path - " + headerMap.Path return 0; } - + //log.debug headerMap.Path //if (headerMap.path - + def result if ((status == 200 && body != "OK") || status == 404) { try{ @@ -295,14 +294,14 @@ def parse(evt) { //log.debug "FYI - got a response, but it's apparently not JSON. Error: " + e + ". Body: " + body return 1 } - + //Zone response if (result.zones){ //log.debug "Zone response detected!" //log.debug "zone result: " + result getZoneList(result.zones) } - + //Program response if (result.uid || (result.statusCode == 5 && result.message == "Not found !")){ //log.debug "Program response detected!" @@ -310,19 +309,19 @@ def parse(evt) { //log.debug "program result: " + result //getProgramList(result.programs) } - + //Figure out the other response types if (result.statusCode == 0){ log.debug "status code found" log.debug "Got raw response: " + body - + //Login response if (result.access_token != null && result.access_token != "" && result.access_token != []){ - log.debug "Login response detected!" + log.debug "Login response detected!" log.debug "Login response result: " + result parseLoginResponse(result) } - + //Generic error from one of the command methods else if (result.statusCode != 0) { log.debug "Error status detected! One of the last calls just failed!" @@ -331,19 +330,19 @@ def parse(evt) { log.debug "Remote command successfully processed by Rainmachine controller." } } - + } else if (status == 401){ log.debug "401 - bad login detected! result: " + body atomicState.expires_in = now() - 500 - atomicState.access_token = "" - atomicState.loginResponse = 'Bad Login' + atomicState.access_token = "" + atomicState.loginResponse = 'Bad Login' } else if (status != 411 && body != null){ //log.debug "Unexpected response! " + status + " " + body + "evt " + description } - - + + } @@ -353,25 +352,25 @@ def doLogin(){ } def getZonesAndPrograms(){ - atomicState.zonesResponse = null + atomicState.zonesResponse = null atomicState.programsResponse = null atomicState.programsResponseCount = 0 log.debug "Getting zones and programs using token: " + atomicState.access_token doCallout("GET", "/api/4/zone?access_token=" + atomicState.access_token , "") - + //If we already have the list of valid programs, limit the refresh to those if (atomicState.ProgramData){ log.debug "checking existing program data" def programData = atomicState.ProgramData - - programData.each { dni, program -> + + programData.each { dni, program -> //Only refresh if child device exists if (getChildDevice(dni)){ doCallout("GET", "/api/4/program/" + program.uid + "?access_token=" + atomicState.access_token , "") } - } + } } - + //Otherwise, we need to basically "scan" for programs starting at 0 and going up to X else{ for (int i = 1; i <= prefProgramMaxID; i++){ @@ -389,16 +388,16 @@ def installed() { def updated() { log.info "updated()" - log.debug "Updated with settings: " + settings - atomicState.polling = [ + log.debug "Updated with settings: " + settings + atomicState.polling = [ last: now(), runNow: true ] - if (state.previousVersion != state.thisSmartAppVersion){ + if (state.previousVersion != state.thisSmartAppVersion){ getVersionInfo(state.previousVersion, state.thisSmartAppVersion); } //unschedule() - //unsubscribe() + //unsubscribe() //initialize() } @@ -412,29 +411,29 @@ def uninstalled() { def updateMapData(){ def combinedMap = [:] combinedMap << atomicState.ProgramData - combinedMap << atomicState.ZoneData + combinedMap << atomicState.ZoneData atomicState.data = combinedMap //log.debug "new data list: " + atomicState.data } -def initialize() { +def initialize() { log.info "initialize()" - unsubscribe() + unsubscribe() //Merge Zone and Program data into single map //atomicState.data = [:] - + def combinedMap = [:] combinedMap << atomicState.ProgramData - combinedMap << atomicState.ZoneData + combinedMap << atomicState.ZoneData atomicState.data = combinedMap - + def selectedItems = [] - def programList = [:] + def programList = [:] def zoneList = [:] - def delete - - // Collect programs and zones + def delete + + // Collect programs and zones if (settings.programs) { if (settings.programs[0].size() > 1) { selectedItems = settings.programs @@ -451,7 +450,7 @@ def initialize() { } zoneList = atomicState.ZoneList } - + // Create device if selected and doesn't exist selectedItems.each { dni -> def deviceType = "" @@ -459,80 +458,80 @@ def initialize() { if (dni.contains("prog")) { log.debug "Program found - " + dni deviceType = "Pgm" - deviceName = programList[dni] + deviceName = programList[dni] } else if (dni.contains("zone")) { log.debug "Zone found - " + dni deviceType = "Zone" deviceName = zoneList[dni] } log.debug "devType: " + deviceType - + def childDevice = getChildDevice(dni) def childDeviceAttrib = [:] - if (!childDevice){ + if (!childDevice){ def fullName = deviceName log.debug "name will be: " + fullName - childDeviceAttrib = ["name": fullName, "completedSetup": true] - + childDeviceAttrib = ["name": fullName, "completedSetup": true] + try{ childDevice = addChildDevice("brbeaird", "RainMachine", dni, null, childDeviceAttrib) state.installMsg = state.installMsg + deviceName + ": device created. \r\n\r\n" } catch(physicalgraph.app.exception.UnknownDeviceTypeException e) { - log.debug "Error! " + e + log.debug "Error! " + e state.installMsg = state.installMsg + deviceName + ": problem creating RM device. Check your IDE to make sure the brbeaird : RainMachine device handler is installed and published. \r\n\r\n" } - + } - + //For existing devices, sync back with the RainMachine name if desired. else{ state.installMsg = state.installMsg + deviceName + ": device already exists. \r\n\r\n" - if (prefResyncNames){ + if (prefResyncNames){ log.debug "Name from RM: " + deviceName + " name in ST: " + childDevice.name if (childDevice.name != deviceName || childDevice.label != deviceName){ state.installMsg = state.installMsg + deviceName + ": updating device name (old name was " + childDevice.label + ") \r\n\r\n" - } + } childDevice.name = deviceName childDevice.label = deviceName } } //log.debug "setting dev type: " + deviceType //childDevice.setDeviceType(deviceType) - + if (childDevice){ childDevice.updateDeviceType() } - + } - - - - + + + + // Delete child devices that are not selected in the settings if (!selectedItems) { delete = getAllChildDevices() } else { - delete = getChildDevices().findAll { - !selectedItems.contains(it.deviceNetworkId) + delete = getChildDevices().findAll { + !selectedItems.contains(it.deviceNetworkId) } } delete.each { deleteChildDevice(it.deviceNetworkId) } - + //Update data for child devices pollAllChild() - + // Schedule polling schedulePoll() - + versionCheck() } /* Access Management */ public loginTokenExists(){ - try { + try { log.debug "Checking for token: " log.debug "Current token: " + atomicState.access_token log.debug "Current expires_in: " + atomicState.expires_in @@ -542,7 +541,7 @@ public loginTokenExists(){ return false } else - return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) + return (atomicState.access_token != null && atomicState.expires_in != null && atomicState.expires_in > now()) } catch (e) { @@ -556,18 +555,18 @@ def doCallout(calloutMethod, urlPath, calloutBody){ subscribe(location, null, parse, [filterEvents:false]) log.info "Calling out to " + ip_address + ":" + port + urlPath //sendAlert("Calling out to " + ip_address + urlPath + " body: " + calloutBody) - + def httpRequest = [ method: calloutMethod, path: urlPath, headers: [ HOST: ip_address + ":" + port, - "Content-Type": "application/json", + "Content-Type": "application/json", Accept: "*/*", ], body: calloutBody ] - + def hubAction = new physicalgraph.device.HubAction(httpRequest) //log.debug "hubaction: " + hubAction return sendHubCommand(hubAction) @@ -576,42 +575,42 @@ def doCallout(calloutMethod, urlPath, calloutBody){ // Process each programs you have in RainMachine -def getProgram(program) { +def getProgram(program) { //log.debug ("Processing pgm" + program) - + //If no UID, this basically means an "empty" program slot where one was deleted from the RM device. Increment count and continue if (!program.uid){ atomicState.programsResponseCount = atomicState.programsResponseCount + 1 //log.debug("new program response count: " + atomicState.programsResponseCount) return } - + //log.debug ("PgmUID " + program.uid) - - + + def dni = [ app.id, "prog", program.uid ].join('|') - + def programsList = atomicState.ProgramList programsList[dni] = program.name atomicState.ProgramList = programsList - - def endTime = 0 //TODO: calculate time left for the program - - def myObj = + + def endTime = 0 //TODO: calculate time left for the program + + def myObj = [ uid: program.uid, status: program.status, endTime: endTime, lastRefresh: now() ] - + def programData = atomicState.ProgramData programData[dni] = myObj - atomicState.ProgramData = programData - + atomicState.ProgramData = programData + atomicState.programsResponseCount = atomicState.programsResponseCount + 1 - + } // Process all the zones you have in RainMachine @@ -629,7 +628,7 @@ def getZoneList(zones) { lastRefresh: now() ] //log.debug "Zone: " + dni + " Status : " + tempList[dni] - } + } atomicState.ZoneList = zonesList atomicState.ZoneData = tempList //log.debug "Temp zone list: " + zonesList @@ -641,17 +640,17 @@ def getZoneList(zones) { def updateDeviceData() { log.info "updateDeviceData()" // automatically checks if the token has expired, if so login again - if (login()) { + if (login()) { // Next polling time, defined in settings def next = (atomicState.polling.last?:0) + ( (settings.polling.toInteger() > 0 ? settings.polling.toInteger() : 1) * 60 * 1000) log.debug "last: " + atomicState.polling.last - log.debug "now: " + new Date( now() * 1000 ) - log.debug "next: " + next - log.debug "RunNow: " + atomicState.polling.runNow + log.debug "now: " + new Date( now() * 1000 ) + log.debug "next: " + next + log.debug "RunNow: " + atomicState.polling.runNow if ((now() > next) || (atomicState.polling.runNow)) { - + // set polling states - atomicState.polling = [ + atomicState.polling = [ last: now(), runNow: false ] @@ -661,15 +660,15 @@ def updateDeviceData() { // Get all the program information getZoneList() - - } + + } } } def pollAllChild() { - // get all the children and send updates + // get all the children and send updates def childDevice = getAllChildDevices() - childDevice.each { + childDevice.each { //log.debug "Updating children " + it.deviceNetworkId //sendAlert("Trying to set last refresh to: " + atomicState.data[it.deviceNetworkId].lastRefresh) if (atomicState.data[it.deviceNetworkId] == null){ @@ -678,7 +677,7 @@ def pollAllChild() { //sendAlert("data list: " + atomicState.data) } it.updateDeviceStatus(atomicState.data[it.deviceNetworkId].status) - it.updateDeviceLastRefresh(atomicState.data[it.deviceNetworkId].lastRefresh) + it.updateDeviceLastRefresh(atomicState.data[it.deviceNetworkId].lastRefresh) //it.poll() } } @@ -699,9 +698,9 @@ private getChildType(child) { /* for SmartDevice to call */ // Refresh data -def refresh() { +def refresh() { log.info "refresh()" - + //For programs, we'll only be refreshing programs with matching child devices. Get the count of those so we know when the refresh is done. def refreshProgramCount = 0 atomicState.ProgramData.each { dni, program -> @@ -709,27 +708,27 @@ def refresh() { refreshProgramCount++ } } - + log.info refreshProgramCount - - atomicState.polling = [ + + atomicState.polling = [ last: now(), runNow: true ] //atomicState.data = [:] - - - + + + //If login token exists and is valid, reuse it and callout to refresh zone and program data if (loginTokenExists()){ log.debug "Existing token detected" getZonesAndPrograms() - + //Wait up to 10 seconds before cascading results to child devices def i = 0 while (i < 5){ pause(2000) - if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == refreshProgramCount ){ + if (atomicState.zonesResponse == "Success" && atomicState.programsResponseCount == refreshProgramCount ){ log.debug "Got a good RainMachine response! Let's go!" updateMapData() pollAllChild() @@ -739,28 +738,28 @@ def refresh() { log.debug "Current zone response: " + atomicState.zonesResponse + "Current pgm response count: " + atomicState.programsResponseCount i++ } - + if (atomicState.zonesResponse == null){ sendAlert("Unable to get zone data while trying to refresh") log.debug "Unable to get zone data while trying to refresh" return false } - + if (atomicState.programsResponse == null){ sendAlert("Unable to get program data while trying to refresh") log.debug "Unable to get program data while trying to refresh" return false } - + } - + //If not, get a new token then refresh else{ log.debug "Need new token" doLogin() - + //Wait up to 20 seconds for successful login - def i = 0 + def i = 0 while (i < 5){ pause(2000) if (atomicState.loginResponse != null){ @@ -770,27 +769,27 @@ def refresh() { i++ } log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse - - + + if (atomicState.loginResponse == null){ log.debug "Unable to connect while trying to refresh zone/program data" return false } - - + + if (atomicState.loginResponse == "Bad Login"){ - log.debug "Bad Login while trying to refresh zone/program data" + log.debug "Bad Login while trying to refresh zone/program data" return false } - - + + if (atomicState.loginResponse == "Success"){ log.debug "Got a login response for refreshing! Let's go!" refresh() } - + } - + } // Get single device status @@ -824,24 +823,24 @@ def sendCommand2(child, apiCommand, apiTime) { //If login token exists and is valid, reuse it and callout to refresh zone and program data if (loginTokenExists()){ log.debug "Existing token detected for sending command" - + def childUID = getChildUID(child) def childType = getChildType(child) def apiPath = "/api/4/" + childType + "/" + childUID + "/" + apiCommand + "?access_token=" + atomicState.access_token //doCallout("GET", "/api/4/zone?access_token=" + atomicState.access_token , "") - + //Stop Everything if (apiCommand == "stopall") { - apiPath = "/api/4/watering/stopall"+ "?access_token=" + atomicState.access_token - doCallout("POST", apiPath, "{\"all\":" + "\"true\"" + "}") + apiPath = "/api/4/watering/stopall"+ "?access_token=" + atomicState.access_token + doCallout("POST", apiPath, "{\"all\":" + "\"true\"" + "}") } //Zones will require time - else if (childType == "zone") { + else if (childType == "zone") { doCallout("POST", apiPath, "{\"time\":" + apiTime + "}") } - + //Programs will require pid - else if (childType == "program") { + else if (childType == "program") { doCallout("POST", apiPath, "{\"pid\":" + childUID + "}") } @@ -850,14 +849,14 @@ def sendCommand2(child, apiCommand, apiTime) { runIn(15, refresh) //refresh() } - + //If not, get a new token then refresh else{ log.debug "Need new token" doLogin() - + //Wait up to 20 seconds for successful login - def i = 0 + def i = 0 while (i < 5){ pause(2000) if (atomicState.loginResponse != null){ @@ -867,30 +866,30 @@ def sendCommand2(child, apiCommand, apiTime) { i++ } log.debug "Done waiting." + "Current login response: " + atomicState.loginResponse - - + + if (atomicState.loginResponse == null){ log.debug "Unable to connect while trying to refresh zone/program data" return false } - - + + if (atomicState.loginResponse == "Bad Login"){ - log.debug "Bad Login while trying to refresh zone/program data" + log.debug "Bad Login while trying to refresh zone/program data" return false } - - + + if (atomicState.loginResponse == "Success"){ log.debug "Got a login response for sending command! Let's go!" sendCommand2(child, apiCommand, apiTime) } - + } - + } -def scheduledRefresh(){ +def scheduledRefresh(){ //If a command has been sent in the last 30 seconds, don't do the scheduled refresh. if (atomicState.lastCommandSent == null || atomicState.lastCommandSent < now()-30000){ refresh() @@ -898,12 +897,12 @@ def scheduledRefresh(){ else{ log.debug "Skipping scheduled refresh due to recent command activity." } - + } def schedulePoll() { - log.debug "Creating RainMachine schedule. Setting was " + settings.polling + log.debug "Creating RainMachine schedule. Setting was " + settings.polling def pollSetting = settings.polling.toInteger() def pollFreq = 1 if (pollSetting == 0){ @@ -915,11 +914,11 @@ def schedulePoll() { else{ pollFreq = pollSetting } - + log.debug "Poll freq: " + pollFreq - unschedule() + unschedule() schedule("37 */" + pollFreq + " * * * ?", scheduledRefresh ) - log.debug "RainMachine schedule successfully started!" + log.debug "RainMachine schedule successfully started!" } @@ -935,7 +934,7 @@ def sendCommand3(child, apiCommand) { } -def getVersionInfo(oldVersion, newVersion){ +def getVersionInfo(oldVersion, newVersion){ def params = [ uri: 'http://www.fantasyaftermath.com/getVersion/rm/' + oldVersion + '/' + newVersion, contentType: 'application/json' @@ -949,37 +948,37 @@ def responseHandlerMethod(response, data) { } else { def results = response.json state.latestSmartAppVersion = results.SmartApp; - state.latestDeviceVersion = results.DoorDevice; + state.latestDeviceVersion = results.DoorDevice; } - + log.debug "previousVersion: " + state.previousVersion log.debug "installedVersion: " + state.thisSmartAppVersion log.debug "latestVersion: " + state.latestSmartAppVersion - log.debug "deviceVersion: " + state.latestDeviceVersion + log.debug "deviceVersion: " + state.latestDeviceVersion } def versionCheck(){ - state.versionWarning = "" + state.versionWarning = "" state.thisDeviceVersion = "" - + def childExists = false - def childDevs = getChildDevices() - + def childDevs = getChildDevices() + if (childDevs.size() > 0){ childExists = true state.thisDeviceVersion = childDevs[0].showVersion() log.debug "child version found: " + state.thisDeviceVersion } - - log.debug "RM Device Handler Version: " + state.thisDeviceVersion - + + log.debug "RM Device Handler Version: " + state.thisDeviceVersion + if (state.thisSmartAppVersion != state.latestSmartAppVersion) { state.versionWarning = state.versionWarning + "Your SmartApp version (" + state.thisSmartAppVersion + ") is not the latest version (" + state.latestSmartAppVersion + ")\n\n" } if (childExists && state.thisDeviceVersion != state.latestDeviceVersion) { state.versionWarning = state.versionWarning + "Your RainMachine device version (" + state.thisDeviceVersion + ") is not the latest version (" + state.latestDeviceVersion + ")\n\n" } - + log.debug state.versionWarning } \ No newline at end of file From e31edebf3136cdd751b87c82cfc63af74f228572 Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 30 Oct 2019 16:06:19 -0500 Subject: [PATCH 47/50] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8ad226d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: brbeaird From c4319d358d4f56e8c84204f0533a5de8de922c5c Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Wed, 13 Nov 2019 09:03:02 -0600 Subject: [PATCH 48/50] Update README.md --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index f7411ac..ef89950 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,6 @@ SmartThings RainMachine ======================= -**Update 3/24/2019** -**If you have more than a few zones, this integration is likely broken for you. SmartThings has implemented limits on the size of data that affects the way this app connects to the RainMachine device to get the list of programs. I'm currently working on a modified method of getting the data for each program individually. - -**Update 4/18/2018** -The latest Rainmachine beta update from 3/26 has re-enabled local HTTP access. However, the port number to access this has changed to 8081. Make sure your device is set up to receive beta updates and then change the SmartApp configuration to port 8081. - - - -**Update 2/21/2018:** - -**This integration is currently broken with Rainmachine firmware 4.0.925 and above (released 2/1/2018). This update disabled HTTP access to the device, and SmartThings currently does not support HTTPS with local integrations. I am evaluating possible options including petitioning Rainmachine to add an option to re-enable HTTP access. In the meantime, I suggest not updating for now if you want to keep your ST integration working.** - From c4e8145b225831af73d677ebba2144dadc24c24a Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 15 Oct 2020 15:14:37 -0500 Subject: [PATCH 49/50] Handle new RM header --- installerManifest.json | 2 +- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/installerManifest.json b/installerManifest.json index 358dd59..62edb92 100644 --- a/installerManifest.json +++ b/installerManifest.json @@ -19,7 +19,7 @@ "iconUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", "published": true, "oAuth": true, - "version": "3.0.1", + "version": "3.0.2", "appSettings": {}, "appUrl": "smartapps/brbeaird/rainmachine.src/rainmachine.groovy" }, diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index a50a874..f070751 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -55,7 +55,7 @@ def prefLogIn() { if (state.previousVersion == null){ state.previousVersion = 0; } - state.thisSmartAppVersion = "3.0.1" + state.thisSmartAppVersion = "3.0.2" //RESET ALL THE THINGS atomicState.initialLogin = false @@ -276,10 +276,9 @@ def parse(evt) { if (!headerMap.server) return 0 - if (headerMap && headerMap.Path != "/api/4" && headerMap.server.indexOf("lighttpd") == -1){ - log.debug "not a rainmachine header path - " + headerMap.Path + if (!headerMap || (headerMap.server.indexOf("lighttpd") == -1 && (headerMap.server.indexOf("RainMachine") == -1))){ + log.debug "not a rainmachine server - " + headerMap.server return 0; - } //log.debug headerMap.Path //if (headerMap.path From 2d99b45222633095b458edb78b7d4c3dbc2a592d Mon Sep 17 00:00:00 2001 From: Brian Beaird Date: Thu, 15 Oct 2020 16:06:47 -0500 Subject: [PATCH 50/50] Add missing bracket --- smartapps/brbeaird/rainmachine.src/rainmachine.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy index f070751..5aac33d 100644 --- a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -279,6 +279,7 @@ def parse(evt) { if (!headerMap || (headerMap.server.indexOf("lighttpd") == -1 && (headerMap.server.indexOf("RainMachine") == -1))){ log.debug "not a rainmachine server - " + headerMap.server return 0; + } //log.debug headerMap.Path //if (headerMap.path