From 20f1a7688909923f3f33084708476416ba6438db Mon Sep 17 00:00:00 2001 From: juano2310 Date: Tue, 27 Oct 2015 15:37:38 -0400 Subject: [PATCH] Wemo refactor final (DVCSMP-1189) https://smartthings.atlassian.net/browse/DVCSMP-1189 Detect and mark device offline within 5 minutes. Show Device offline in device tile. Show Device offline in Recent Activity. Log the current IP address to Recent Activity. Log the changed IP address to Recent Activity. Support 'Turning on' and 'Turning off' (blindly changing the state of device to ON or OFF without confirming bulb responded correctly) Turn on / off through Wemo-App reflected timely in SmartThings App/Ecosystem. Manual turn on / off of device is reflected timely in SmartThings App/Ecosystem. Lower case createEvent Bug Fixes Bug fixes setOffline Minor cosmetic fixes --- .../wemo-light-switch.groovy | 75 +++-- .../wemo-motion.src/wemo-motion.groovy | 54 +++- .../wemo-switch.src/wemo-switch.groovy | 268 +++++++++--------- .../wemo-connect.src/wemo-connect.groovy | 186 ++++-------- 4 files changed, 275 insertions(+), 308 deletions(-) diff --git a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy index b5f9f5c..1bd78f3 100644 --- a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy +++ b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy @@ -25,6 +25,8 @@ metadata { capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -34,21 +36,36 @@ metadata { // simulator metadata simulator {} - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } - main "switch" - details (["switch", "refresh"]) - } + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000" + } + + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "refresh"]) + } } // parse events into attributes @@ -68,6 +85,7 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { + unschedule("setOffline") def body = new XmlSlurper().parseText(bodyString) if (body?.property?.TimeSyncRequest?.text()) { @@ -78,13 +96,14 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) } } @@ -101,14 +120,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -195,6 +206,8 @@ def subscribe(ip, port) { if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0 def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy index eb3ea10..9649db4 100644 --- a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy +++ b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy @@ -21,6 +21,8 @@ capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -31,17 +33,30 @@ } // UI tile definitions - tiles { + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "motion", canChangeIcon: true){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" + attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + attributeState "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + standardTile("motion", "device.motion", width: 2, height: 2) { state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") - } - standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000") } + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "motion" - details (["motion", "refresh"]) + details (["rich-control", "refresh"]) } } @@ -62,6 +77,7 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { + unschedule("setOffline") def body = new XmlSlurper().parseText(bodyString) if (body?.property?.TimeSyncRequest?.text()) { @@ -72,7 +88,7 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive" log.debug "Notify - BinaryState = ${value}" - result << createEvent(name: "motion", value: value) + result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } @@ -91,14 +107,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -125,6 +133,8 @@ def refresh() { //////////////////////////// def getStatus() { log.debug "Executing WeMo Motion 'getStatus'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -165,7 +175,9 @@ def subscribe(ip, port) { def existingPort = getDataValue("port") if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -226,3 +238,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy index b385ceb..cd9e0ec 100644 --- a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy +++ b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy @@ -10,120 +10,142 @@ * 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. * - * Wemo Switch + * Wemo Switch * - * Author: superuser - * Date: 2013-10-11 + * Author: Juan Risso (SmartThings) + * Date: 2015-10-11 */ metadata { - definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Switch" - capability "Polling" - capability "Refresh" - capability "Sensor" + definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" - command "subscribe" - command "resubscribe" - command "unsubscribe" - } + attribute "currentIP", "string" - // simulator metadata - simulator {} + command "subscribe" + command "resubscribe" + command "unsubscribe" + } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } + // simulator metadata + simulator {} - main "switch" - details (["switch", "refresh"]) - } + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000" + } + + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "refresh"]) + } } // parse events into attributes def parse(String description) { - log.debug "Parsing '${description}'" + log.debug "Parsing '${description}'" - def msg = parseLanMessage(description) - def headerString = msg.header + def msg = parseLanMessage(description) + def headerString = msg.header - if (headerString?.contains("SID: uuid:")) { - def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" - sid -= "SID: uuid:".trim() + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() - updateDataValue("subscriptionId", sid) - } + updateDataValue("subscriptionId", sid) + } - def result = [] - def bodyString = msg.body - if (bodyString) { - def body = new XmlSlurper().parseText(bodyString) - - if (body?.property?.TimeSyncRequest?.text()) { - log.trace "Got TimeSyncRequest" - result << timeSyncResponse() - } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { - log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" - } else if (body?.property?.BinaryState?.text()) { - def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } else if (body?.property?.TimeZoneNotification?.text()) { - log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" - } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { - def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } - } - - result + def result = [] + def bodyString = msg.body + if (bodyString) { + unschedule("setOffline") + def body = new XmlSlurper().parseText(bodyString) + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}" + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { + def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}" + log.info "Connection: ${device.currentValue("connection")}" + if (device.currentValue("currentIP") == "Offline") { + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + } + def dispaux2 = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2) + } + } + result } private getTime() { - // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. - ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() } private getCallBackAddress() { - device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) + Integer.parseInt(hex,16) } private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } private getHostAddress() { - def ip = getDataValue("ip") - def port = getDataValue("port") - - if (!ip || !port) { - def parts = device.deviceNetworkId.split(":") - if (parts.length == 2) { - ip = parts[0] - port = parts[1] - } else { - log.warn "Can't figure out ip and port for device: ${device.id}" - } - } - log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" - return convertHexToIP(ip) + ":" + convertHexToInt(port) + def ip = getDataValue("ip") + def port = getDataValue("port") + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) } - def on() { - log.debug "Executing 'on'" - sendEvent(name: "switch", value: "on") +log.debug "Executing 'on'" def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} @@ -133,17 +155,16 @@ Content-Length: 333 - + 1 - + """, physicalgraph.device.Protocol.LAN) } def off() { - log.debug "Executing 'off'" - sendEvent(name: "switch", value: "off") - def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +log.debug "Executing 'off'" +def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} Content-Type: text/xml @@ -152,36 +173,13 @@ Content-Length: 333 - + 0 - + """, physicalgraph.device.Protocol.LAN) } -/*def refresh() { - log.debug "Executing 'refresh'" -new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 -SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" -Content-Length: 277 -Content-Type: text/xml; charset="utf-8" -HOST: ${getHostAddress()} -User-Agent: CyberGarage-HTTP/1.0 - - - - - - - -""", physicalgraph.device.Protocol.LAN) -}*/ - -def refresh() { - log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" - [subscribe(), timeSyncResponse(), poll()] -} - def subscribe(hostAddress) { log.debug "Executing 'subscribe()'" def address = getCallBackAddress() @@ -200,27 +198,30 @@ def subscribe() { subscribe(getHostAddress()) } -def subscribe(ip, port) { - def existingIp = getDataValue("ip") - def existingPort = getDataValue("port") - if (ip && ip != existingIp) { - log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) - } - if (port && port != existingPort) { - log.debug "Updating port from $existingPort to $port" - updateDataValue("port", port) - } +def refresh() { + log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" + [subscribe(), timeSyncResponse(), poll()] +} +def subscribe(ip, port) { + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) + } subscribe("${ip}:${port}") } -//////////////////////////// def resubscribe() { -log.debug "Executing 'resubscribe()'" - -def sid = getDeviceDataByName("subscriptionId") - + log.debug "Executing 'resubscribe()'" + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -228,12 +229,11 @@ TIMEOUT: Second-5400 """, physicalgraph.device.Protocol.LAN) - } -//////////////////////////// + def unsubscribe() { -def sid = getDeviceDataByName("subscriptionId") + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -242,7 +242,7 @@ SID: uuid:${sid} """, physicalgraph.device.Protocol.LAN) } -//////////////////////////// + //TODO: Use UTC Timezone def timeSyncResponse() { log.debug "Executing 'timeSyncResponse()'" @@ -267,9 +267,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } +def setOffline() { + //sendEvent(name: "currentIP", value: "Offline", displayed: false) + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy index 34f20b1..e82e5c7 100644 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -61,10 +61,7 @@ def firstPage() log.debug "REFRESH COUNT :: ${refreshCount}" - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } + subscribe(location, null, locationHandler, [filterEvents:false]) //ssdp request every 25 seconds if((refreshCount % 5) == 0) { @@ -168,21 +165,30 @@ def getWemoLightSwitches() def installed() { log.debug "Installed with settings: ${settings}" initialize() - - runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds - runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds - runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes - - // SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes) - runIn(1740, "refresh", [overwrite: false]) } def updated() { log.debug "Updated with settings: ${settings}" initialize() +} - runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds - runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds +def initialize() { + unsubscribe() + unschedule() + subscribe(location, null, locationHandler, [filterEvents:false]) + + if (selectedSwitches) + addSwitches() + + if (selectedMotions) + addMotions() + + if (selectedLightSwitches) + addLightSwitches() + + runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds + runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds + runEvery5Minutes("refresh") } def resubscribe() { @@ -192,8 +198,7 @@ def resubscribe() { def refresh() { log.debug "refresh() called" - //reschedule the refreshes - runIn(1740, "refresh", [overwrite: false]) + doDeviceSync() refreshDevices() } @@ -236,7 +241,8 @@ def addSwitches() { "port": selectedSwitch.value.port ] ]) - + def ipvalue = convertHexToIP(selectedSwitch.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -266,8 +272,9 @@ def addMotions() { "port": selectedMotion.value.port ] ]) - - log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + def ipvalue = convertHexToIP(selectedMotion.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" } else { log.debug "found ${d.displayName} with id $dni already exists" } @@ -296,7 +303,8 @@ def addLightSwitches() { "port": selectedLightSwitch.value.port ] ]) - + def ipvalue = convertHexToIP(selectedLightSwitch.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -304,27 +312,6 @@ def addLightSwitches() { } } -def initialize() { - // remove location subscription afterwards - unsubscribe() - state.subscribe = false - - if (selectedSwitches) - { - addSwitches() - } - - if (selectedMotions) - { - addMotions() - } - - if (selectedLightSwitches) - { - addLightSwitches() - } -} - def locationHandler(evt) { def description = evt.description def hub = evt?.hubId @@ -333,53 +320,32 @@ def locationHandler(evt) { log.debug parsedEvent if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) { - def switches = getWemoSwitches() - - if (!(switches."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist + if (!(switches."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { log.debug "Device was already found in state..." - def d = switches."${parsedEvent.ssdpUSN.toString()}" boolean deviceChangedValues = false - + log.debug "$d.ip <==> $parsedEvent.ip" if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { d.ip = parsedEvent.ip d.port = parsedEvent.port deviceChangedValues = true log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + child.subscribe(parsedEvent.ip, parsedEvent.port) + child.poll() } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - } - } else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) { - def motions = getWemoMotions() - - if (!(motions."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist + if (!(motions."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { // just update the values log.debug "Device was already found in state..." def d = motions."${parsedEvent.ssdpUSN.toString()}" @@ -412,10 +378,7 @@ def locationHandler(evt) { if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) { //if it doesn't already exist lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { log.debug "Device was already found in state..." def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}" @@ -426,21 +389,11 @@ def locationHandler(evt) { d.port = parsedEvent.port deviceChangedValues = true log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}" + child.subscribe(parsedEvent.ip, parsedEvent.port) } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - } - } else if (parsedEvent.headers && parsedEvent.body) { String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase() @@ -580,73 +533,30 @@ private def parseDiscoveryMessage(String description) { } } } - device } def doDeviceSync(){ log.debug "Doing Device Sync!" - runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes - - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } - discoverAllWemoTypes() } -def pollChildren() { - def devices = getAllChildDevices() - devices.each { d -> - //only poll switches? - d.poll() - } +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } -def delayPoll() { - log.debug "Executing 'delayPoll'" - - runIn(5, "pollChildren") +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) } -/*def poll() { - log.debug "Executing 'poll'" - runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes - - def lastPoll = getLastPollTime() - def currentTime = now() - def lastPollDiff = currentTime - lastPoll - log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff" - setLastPollTime(currentTime) - - doDeviceSync() -} - - -def setLastPollTime(currentTime) { - state.lastpoll = currentTime -} - -def getLastPollTime() { - state.lastpoll ?: now() -} - -def now() { - new Date().getTime() -}*/ - -private Boolean canInstallLabs() -{ +private Boolean canInstallLabs() { return hasAllHubsOver("000.011.00603") } -private Boolean hasAllHubsOver(String desiredFirmware) -{ +private Boolean hasAllHubsOver(String desiredFirmware) { return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } } -private List getRealHubFirmwareVersions() -{ +private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } }