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.
+
+[](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