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 diff --git a/README.md b/README.md index e00dc81..ef89950 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ 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) +### 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) diff --git a/RainMachine.SmartApp.groovy b/RainMachine.SmartApp.groovy deleted file mode 100644 index c93c577..0000000 --- a/RainMachine.SmartApp.groovy +++ /dev/null @@ -1,427 +0,0 @@ -/** - * RainMachine Service Manager SmartApp - * - * Author: Jason Mok - * Date: 2014-12-20 - * - *************************** - * - * Copyright 2014 Jason Mok - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License - * for the specific language governing permissions and limitations under the License. - * - ************************** - * - * 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 - * - ************************** - * - * 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 - * 3) For each items you pick on the Programs/Zones page, it will create a device - * 4) Enjoy! - * - */ -definition( - name: "RainMachine", - namespace: "copy-ninja", - 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" -) - -preferences { - page(name: "prefLogIn", 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) { - 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") - } - 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()]) - } - } - } -} - -/* Initialization */ -def installed() { - log.info "installed()" - log.debug "Installed with settings: " + settings - unschedule() - forceLogin() - initialize() -} - -def updated() { - log.info "updated()" - log.debug "Updated with settings: " + settings - state.polling = [ - last: now(), - runNow: true - ] - unschedule() - unsubscribe() - login() - initialize() -} - -def uninstalled() { - def delete = getAllChildDevices() - delete.each { deleteChildDevice(it.deviceNetworkId) } -} - -def initialize() { - log.info "initialize()" - - // Get initial device status in state.data - refresh() - - def progZones = [] - def programList = [:] - def zoneList = [:] - def delete - - // Collect programs and zones - if (settings.programs) { - if (settings.programs[0].size() > 1) { - progZones = settings.programs - } else { - progZones.add(settings.programs) - } - programList = getProgramList() - } - if (settings.zones) { - if (settings.zones[0].size() > 1) { - settings.zones.each { dni -> progZones.add(dni)} - } else { - progZones.add(settings.zones) - } - zoneList = getZoneList() - } - - // Create device if selected and doesn't exist - progZones.each { dni -> - def childDevice = getChildDevice(dni) - def childDeviceAttrib = [:] - if (!childDevice) { - if (dni.contains("prog")) { - childDeviceAttrib = ["name": "RainMachine Program: " + programList[dni], "completedSetup": true] - } else if (dni.contains("zone")) { - childDeviceAttrib = ["name": "RainMachine Zone: " + zoneList[dni], "completedSetup": true] - } - addChildDevice("copy-ninja", "RainMachine", dni, null, childDeviceAttrib) - } - } - - // Delete child devices that are not selected in the settings - if (!progZones) { - delete = getAllChildDevices() - } else { - delete = getChildDevices().findAll { - !progZones.contains(it.deviceNetworkId) - } - } - delete.each { deleteChildDevice(it.deviceNetworkId) } - - // Schedule polling - schedule("0 0/" + (settings.polling.toInteger() > 0 )? settings.polling.toInteger() : 1 + " * * * ?", refresh ) -} - -/* 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 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 - } - } -} - -// 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 -} - -// 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 -} - -// Updates devices -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() - log.debug "next: " + next - log.debug "RunNow: " + state.polling.runNow - if ((now() > next) || (state.polling.runNow)) { - - // set polling states - state.polling = [ - last: now(), - runNow: false - ] - - // Get all the program information - getProgramList() - - // Get all the program information - getZoneList() - - } - } -} - -def pollAllChild() { - // get all the children and send updates - def childDevice = getAllChildDevices() - childDevice.each { - log.debug "Polling " + it.deviceNetworkId - it.poll() - } -} - -// Returns UID of a Zone or Program -private getChildUID(child) { - return child.device.deviceNetworkId.split("\\|")[2] -} - -// Returns Type of a Zone or Program -private getChildType(child) { - def childType = child.device.deviceNetworkId.split("\\|")[1] - if (childType == "prog") { return "program" } - 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 = [ - last: now(), - runNow: true - ] - state.data = [:] - - //Update Devices - updateDeviceData() - - pause(1000) - pollAllChild() -} - -// Get single device status -def getDeviceStatus(child) { - log.info "getDeviceStatus()" - //tries to get latest data if polling limitation allows - //updateDeviceData() - return state.data[child.device.deviceNetworkId].status -} - -// 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 - } -} - -// 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") { - state.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(2000) - 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 -} diff --git a/RainMachine.SmartDevice.groovy b/RainMachine.SmartDevice.groovy deleted file mode 100644 index 9fc5795..0000000 --- a/RainMachine.SmartDevice.groovy +++ /dev/null @@ -1,144 +0,0 @@ -/** - * RainMachine Smart Device - * - * Author: Jason Mok - * Date: 2014-12-20 - * - *************************** - * - * Copyright 2014 Jason Mok - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License - * for the specific language governing permissions and limitations under the License. - * - ************************** - * - * REQUIREMENTS: - * Refer to RainMachine Service Manager SmartApp - * - ************************** - * - * USAGE: - * Put this in Device Type. Don't install until you have all other device types scripts added - * Refer to RainMachine Service Manager SmartApp - * - */ -metadata { - definition (name: "RainMachine", namespace: "copy-ninja", author: "Jason Mok") { - capability "Valve" - capability "Refresh" - capability "Polling" - - attribute "runTime", "number" - - //command "pause" - //command "resume" - command "stopAll" - command "setRunTime" - } - - 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") - 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") - } - /* 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") - - } */ - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state("default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh") - } - standardTile("stopAll", "device.switch", inactiveLabel: false, decoration: "flat") { - state("default", label:'Stop All', action:"stopAll", icon:"st.secondary.off") - } - controlTile("runTimeControl", "device.runTime", "slider", height: 1, width: 2, inactiveLabel: false) { - state("setRunTime", action:"setRunTime", backgroundColor: "#1e9cbb") - } - valueTile("runTime", "device.runTime", inactiveLabel: false, decoration: "flat") { - state("runTimeValue", label:'${currentValue} mins', backgroundColor:"#ffffff") - } - - main "contact" - details(["contact","refresh","stopAll","runTimeControl","runTime"]) - } -} - -// installation, set default value -def installed() { - runTime = 5 - poll() -} - -def parse(String description) {} - -// turn on sprinkler -def open() { - parent.sendCommand(this, "start", (device.currentValue("runTime") * 60)) - poll() -} -// turn off sprinkler -def close() { - parent.sendCommand(this, "stop", (device.currentValue("runTime") * 60)) - poll() -} - -// refresh status -def refresh() { - parent.refresh() - poll() -} - -//resume sprinkling -def resume() { - poll() -} - -//pause sprinkling -def pause() { - poll() -} - -// update status -def poll() { - log.info "Polling.." - deviceStatus(parent.getDeviceStatus(this)) -} - -// stop everything -def stopAll() { - parent.sendStopAll() - poll() -} - -// update the run time for manual zone -void setRunTime(runTimeSecs) { - sendEvent("name":"runTime", "value": runTimeSecs) -} - -// 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") - } - if (status == 1) { - sendEvent(name: "contact", value: "open", display: true, descriptionText: device.displayName + " was active") - //sendEvent(name: "pausume", value: "pause") - } - if (status == 2) { - sendEvent(name: "contact", value: "opening", display: true, descriptionText: device.displayName + " was pending") - //sendEvent(name: "pausume", value: "pause") - } -} diff --git a/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy new file mode 100644 index 0000000..aca9e06 --- /dev/null +++ b/devicetypes/brbeaird/rainmachine.src/rainmachine.groovy @@ -0,0 +1,304 @@ +/** + * ----------------------- + * ------ DEVICE HANDLER------ + * ----------------------- + + * RainMachine Smart Device + * + * Author: Jason Mok/Brian Beaird + * Last Updated: 2018-08-12 + * + *************************** + * + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + ************************** + * + * REQUIREMENTS: + * Refer to RainMachine Service Manager SmartApp + * + ************************** + * + * USAGE: + * Put this in Device Type. Don't install until you have all other device types scripts added + * Refer to RainMachine Service Manager SmartApp + * + */ +metadata { + definition (name: "RainMachine", namespace: "brbeaird", author: "Jason Mok/Brian Beaird") { + capability "Valve" + 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 "refresh" + command "stopAll" + command "setRunTime" + } + + simulator { } + + tiles { + 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") + + //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("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") + + } */ + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state("default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh") + } + standardTile("stopAll", "device.switch", inactiveLabel: false, decoration: "flat") { + state("default", label:'Stop All', action:"stopAll", icon:"st.secondary.off") + } + controlTile("runTimeControl", "device.runTime", "slider", height: 1, width: 2, inactiveLabel: false) { + state("setRunTime", action:"setRunTime", backgroundColor: "#1e9cbb") + } + 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") + } + 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","deviceType"]) + } +} + +// installation, set default value +def installed() { + runTime = 5 + //poll() +} + +//def parse(String description) {} + +// turn on sprinkler +def open() { + log.debug "Turning the sprinkler on (valve)" + deviceStatus(1) + parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) + //parent.sendCommand3(this, 1) +} +// turn off sprinkler +def close() { + log.debug "Turning the sprinkler off (valve)" + deviceStatus(0) + parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) + //parent.sendCommand3(this, 0) +} + + +def on() { + log.debug "Turning the sprinkler on" + deviceStatus(1) + parent.sendCommand2(this, "start", (device.currentValue("runTime") * 60)) +} +def off() { + deviceStatus(0) + log.debug "Turning the sprinkler off" + parent.sendCommand2(this, "stop", (device.currentValue("runTime") * 60)) +} +// refresh status +def refresh() { + sendEvent(name:"lastRefresh", value: "Checking..." , display: true , displayed: false) + parent.refresh() +} + +//resume sprinkling +def resume() { + poll() +} + +//pause sprinkling +def pause() { + poll() +} + +// update status +def poll() { + log.info "Polling.." + //deviceStatus(parent.getDeviceStatus(this)) + //def lastRefresh = parent.getDeviceLastRefresh(this) + //log.debug "Last refresh: " + lastRefresh + //sendEvent("name":"lastRefresh", "value": lastRefresh) +} + + + +// stop everything +def stopAll() { + deviceStatus(0) + parent.sendCommand2(this, "stopall", (device.currentValue("runTime") * 60)) + + //parent.sendStopAll() + //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) +} + +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 +def deviceStatus(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 + + //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: "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") + 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 + + def deviceType = device.currentValue("deviceType") + log.debug "Device type is: " + device.currentValue("deviceType") + + if (parent.prefSendPush && deviceType.toUpperCase() == "ZONE") { + //parent.sendAlert(message) + //sendNotificationEvent(message.toString()) + parent.sendPushMessage(message) + } + + if (parent.prefSendPushPrograms && deviceType.toUpperCase() == "PROGRAM") { + //sendNotificationEvent(message.toString()) + parent.sendPushMessage(message) + } + + } + //sendEvent(name: "contact", value: "closed", display: true, descriptionText: device.displayName + " was inactive") + + + } + if (status == 1) { //Device has turned on + log.debug "Zone turned on!" + + //If device has just recently opened, take note of time + if (oldStatus != 'open'){ + log.debug "Logging status." + 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 + 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) { //Device is pending + sendEvent(name: "valve", value: "open", display: true, descriptionText: device.displayName + " was pending") + //sendEvent(name: "pausume", value: "pause") + } +} + + +def log(msg){ + log.debug msg +} + +def showVersion(){ + return "2.1.1" +} diff --git a/icons/RainmachineBanner.png b/icons/RainmachineBanner.png new file mode 100644 index 0000000..e39511d Binary files /dev/null and b/icons/RainmachineBanner.png differ diff --git a/icons/rainmachine.1x.png b/icons/rainmachine.1x.png new file mode 100644 index 0000000..4df8ff4 Binary files /dev/null and b/icons/rainmachine.1x.png differ diff --git a/icons/rainmachine.2x.png b/icons/rainmachine.2x.png new file mode 100644 index 0000000..e9f4e67 Binary files /dev/null and b/icons/rainmachine.2x.png differ diff --git a/icons/rainmachine.3x.png b/icons/rainmachine.3x.png new file mode 100644 index 0000000..ba438e1 Binary files /dev/null and b/icons/rainmachine.3x.png differ diff --git a/installerManifest.json b/installerManifest.json new file mode 100644 index 0000000..62edb92 --- /dev/null +++ b/installerManifest.json @@ -0,0 +1,39 @@ +{ + "namespace": "brbeaird", + "repoOwner": "brbeaird", + "repoName": "SmartThings_RainMachine", + "repoBranch": "master", + "name": "RainMachine", + "author": "Brian Beaird", + "description": "SmartThings integration with the RainMachine Irrigation Controller", + "category": "My Apps", + "bannerUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/RainmachineBanner.png", + "forumUrl": "https://community.smartthings.com/t/release-rainmachine/8343", + "docUrl": "https://github.com/brbeaird/SmartThings_RainMachine/blob/master/README.md", + "releaseType": "production", + "keywords": ["RainMachine", "irrigation", "irrigation controller", "water", "lawn", "water timer", "sprinkler", "grass", "Yard", "zone"], + "notes": "Nothing to show here (yet)", + "smartApps": { + "parent": { + "name": "Rainmachine", + "iconUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", + "published": true, + "oAuth": true, + "version": "3.0.2", + "appSettings": {}, + "appUrl": "smartapps/brbeaird/rainmachine.src/rainmachine.groovy" + }, + "children": [] + }, + "deviceHandlers": [{ + "name": "RainMachine", + "iconUrl": "https://raw.githubusercontent.com/brbeaird/SmartThings_RainMachine/master/icons/rainmachine.1x.png", + "published": true, + "oAuth": false, + "appUrl": "devicetypes/brbeaird/rainmachine.src/rainmachine.groovy", + "appSettings": {}, + "version": "2.1.1", + "optional": false + } + ] +} \ No newline at end of file diff --git a/smartapps/brbeaird/rainmachine.src/rainmachine.groovy b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy new file mode 100644 index 0000000..5aac33d --- /dev/null +++ b/smartapps/brbeaird/rainmachine.src/rainmachine.groovy @@ -0,0 +1,984 @@ +/** + * ----------------------- + * ------ SMART APP ------ + * ----------------------- + + * RainMachine Service Manager SmartApp + * + * Author: Jason Mok/Brian Beaird + * Last Updated: 2019-03-27 + * + *************************** + * + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * 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! + */ +include 'asynchttp_v1' + +definition( + name: "RainMachine", + namespace: "brbeaird", + author: "Jason Mok", + description: "Connect your RainMachine to control your irrigation", + category: "SmartThings Labs", + 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 { + 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 */ +def prefLogIn() { + state.previousVersion = state.thisSmartAppVersion + if (state.previousVersion == null){ + state.previousVersion = 0; + } + state.thisSmartAppVersion = "3.0.2" + + //RESET ALL THE THINGS + atomicState.initialLogin = false + 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"){ + 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 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") + } + 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 + + 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 login 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"){ + 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 ){ + 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]) + } + 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:true, install: false) { + section() { + paragraph "Problem getting zone/program data. Click back and try again." + } + } + + } + +} + +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){ + + 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" + atomicState.access_token = response.access_token + log.debug "Login token newly set to: " + atomicState.access_token + if (response.expires_in != null && response.expires_in != [] && response.expires_in != "") + 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) { + + def description = evt.description + def hub = evt?.hubId + + //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) + //log.debug "serverheader" + msg.headers + + 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 (!headerMap){ + return 0 + } + + //Ignore Sense device data + if (headerMap.source == "STSense"){ + return 0 + } + + //log.debug headerMap.server + if (!headerMap.server) + return 0 + + 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 + + def result + 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 + 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!" + getProgram(result) + //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 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 "401 - bad login detected! 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 + 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 -> + //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 */ +def installed() { + log.info "installed()" + log.debug "Installed with settings: " + settings + //unschedule() +} + +def updated() { + log.info "updated()" + 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() +} + +def uninstalled() { + def delete = getAllChildDevices() + delete.each { deleteChildDevice(it.deviceNetworkId) } + getVersionInfo(state.previousVersion, 0); +} + + +def updateMapData(){ + def combinedMap = [:] + combinedMap << atomicState.ProgramData + combinedMap << atomicState.ZoneData + atomicState.data = combinedMap + //log.debug "new data list: " + atomicState.data +} + +def initialize() { + log.info "initialize()" + unsubscribe() + + //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 + + // Collect programs and zones + if (settings.programs) { + if (settings.programs[0].size() > 1) { + selectedItems = settings.programs + } else { + selectedItems.add(settings.programs) + } + programList = atomicState.ProgramList + } + if (settings.zones) { + if (settings.zones[0].size() > 1) { + settings.zones.each { dni -> selectedItems.add(dni)} + } else { + selectedItems.add(settings.zones) + } + zoneList = atomicState.ZoneList + } + + // Create device if selected and doesn't exist + 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){ + 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() + } else { + 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 { + 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 + } +} + + +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", + Accept: "*/*", + ], + body: calloutBody + ] + + def hubAction = new physicalgraph.device.HubAction(httpRequest) + //log.debug "hubaction: " + hubAction + return sendHubCommand(hubAction) +} + + + +// Process each programs you have in RainMachine +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 = + [ + uid: program.uid, + status: program.status, + endTime: endTime, + lastRefresh: now() + ] + + def programData = atomicState.ProgramData + programData[dni] = myObj + atomicState.ProgramData = programData + + atomicState.programsResponseCount = atomicState.programsResponseCount + 1 + +} + +// Process all the zones you have in RainMachine +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 "Temp zone list: " + zonesList + //log.debug "State zone list: " + atomicState.ZoneList + atomicState.zonesResponse = "Success" +} + +// Updates devices +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 = (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 + if ((now() > next) || (atomicState.polling.runNow)) { + + // set polling states + atomicState.polling = [ + last: now(), + runNow: false + ] + + // Get all the program information + getProgramList() + + // Get all the program information + getZoneList() + + } + } +} + +def pollAllChild() { + // get all the children and send updates + def childDevice = getAllChildDevices() + 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){ + 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) + //it.poll() + } +} + +// Returns UID of a Zone or Program +private getChildUID(child) { + return child.device.deviceNetworkId.split("\\|")[2] +} + +// Returns Type of a Zone or Program +private getChildType(child) { + def childType = child.device.deviceNetworkId.split("\\|")[1] + if (childType == "prog") { return "program" } + if (childType == "zone") { return "zone" } +} + + + +/* for SmartDevice to call */ +// Refresh data +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 + ] + //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 ){ + 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 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 + 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 refreshing! Let's go!" + refresh() + } + + } + +} + +// Get single device status +def getDeviceStatus(child) { + log.info "getDeviceStatus()" + //tries to get latest data if polling limitation allows + //updateDeviceData() + 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 (atomicState.data[child.device.deviceNetworkId]) { + return atomicState.data[child.device.deviceNetworkId].endTime + } +} + +def sendCommand2(child, apiCommand, apiTime) { + 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" + + 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) + 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 + 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) + } + + } + +} + +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." + } + +} + + +def schedulePoll() { + log.debug "Creating RainMachine schedule. Setting was " + settings.polling + def pollSetting = settings.polling.toInteger() + def pollFreq = 1 + if (pollSetting == 0){ + pollFreq = 1 + } + else if ( pollSetting >= 60){ + pollFreq = 59 + } + else{ + pollFreq = pollSetting + } + + log.debug "Poll freq: " + pollFreq + unschedule() + schedule("37 */" + pollFreq + " * * * ?", scheduledRefresh ) + log.debug "RainMachine schedule successfully started!" +} + + +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) +} + + +def getVersionInfo(oldVersion, newVersion){ + def params = [ + uri: 'http://www.fantasyaftermath.com/getVersion/rm/' + oldVersion + '/' + newVersion, + contentType: 'application/json' + ] + asynchttp_v1.get('responseHandlerMethod', params) +} + +def responseHandlerMethod(response, data) { + 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 "previousVersion: " + state.previousVersion + log.debug "installedVersion: " + state.thisSmartAppVersion + log.debug "latestVersion: " + state.latestSmartAppVersion + log.debug "deviceVersion: " + state.latestDeviceVersion +} + + +def versionCheck(){ + state.versionWarning = "" + 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