diff --git a/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy b/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy new file mode 100644 index 0000000..de1850c --- /dev/null +++ b/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy @@ -0,0 +1,107 @@ +/** +* BeaconThing +* +* Copyright 2015 obycode +* +* 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. +* +*/ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "BeaconThing", namespace: "com.obycode", author: "obycode") { + capability "Beacon" + capability "Presence Sensor" + capability "Sensor" + + attribute "inRange", "json_object" + attribute "inRangeFriendly", "string" + + command "setPresence", ["string"] + command "arrived", ["string"] + command "left", ["string"] + } + + simulator { + status "present": "presence: 1" + status "not present": "presence: 0" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0") + state("not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff") + } + valueTile("inRange", "device.inRangeFriendly", inactiveLabel: true, height:1, width:3, decoration: "flat") { + state "default", label:'${currentValue}', backgroundColor:"#ffffff" + } + main "presence" + details (["presence","inRange"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def installed() { + sendEvent(name: "presence", value: "not present") + def emptyList = [] + def json = new groovy.json.JsonBuilder(emptyList) + sendEvent(name:"inRange", value:json.toString()) +} + +def setPresence(status) { + log.debug "Status is $status" + sendEvent(name:"presence", value:status) +} + +def arrived(id) { + log.debug "$id has arrived" + def theList = device.latestValue("inRange") + def inRangeList = new JsonSlurper().parseText(theList) + if (inRangeList.contains(id)) { + return + } + inRangeList += id + def json = new groovy.json.JsonBuilder(inRangeList) + log.debug "Now in range: ${json.toString()}" + sendEvent(name:"inRange", value:json.toString()) + + // Generate human friendly string for tile + def friendlyList = "Nearby: " + inRangeList.join(", ") + sendEvent(name:"inRangeFriendly", value:friendlyList) + + if (inRangeList.size() == 1) { + setPresence("present") + } +} + +def left(id) { + log.debug "$id has left" + def theList = device.latestValue("inRange") + def inRangeList = new JsonSlurper().parseText(theList) + inRangeList -= id + def json = new groovy.json.JsonBuilder(inRangeList) + log.debug "Now in range: ${json.toString()}" + sendEvent(name:"inRange", value:json.toString()) + + // Generate human friendly string for tile + def friendlyList = "Nearby: " + inRangeList.join(", ") + + if (inRangeList.empty) { + setPresence("not present") + friendlyList = "No one is nearby" + } + + sendEvent(name:"inRangeFriendly", value:friendlyList) +} diff --git a/devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy b/devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy new file mode 100644 index 0000000..b54321a --- /dev/null +++ b/devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy @@ -0,0 +1,128 @@ +/** + * Simple Sync + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ +metadata +{ + definition (name: "Simple Sync", namespace: "roomieremote-agent", author: "Roomie Remote, Inc.") + { + capability "Media Controller" + } + + // simulator metadata + simulator + { + } + + // UI tile definitions + tiles + { + standardTile("mainTile", "device.status", width: 1, height: 1, icon: "st.Entertainment.entertainment11") + { + state "default", label: "Simple Sync", icon: "st.Home.home2", backgroundColor: "#55A7FF" + } + + def detailTiles = ["mainTile"] + + main "mainTile" + details(detailTiles) + } +} + +def parse(String description) +{ + def results = [] + + try + { + def msg = parseLanMessage(description) + + if (msg.headers && msg.body) + { + switch (msg.headers["X-Roomie-Echo"]) + { + case "getAllActivities": + handleGetAllActivitiesResponse(msg) + break + } + } + } + catch (Throwable t) + { + sendEvent(name: "parseError", value: "$t", description: description) + throw t + } + + results +} + +def handleGetAllActivitiesResponse(response) +{ + def body = parseJson(response.body) + + if (body.status == "success") + { + def json = new groovy.json.JsonBuilder() + def root = json activities: body.data + def data = json.toString() + + sendEvent(name: "activities", value: data) + } +} + +def getAllActivities(evt) +{ + def host = getHostAddress(device.deviceNetworkId) + + def action = new physicalgraph.device.HubAction(method: "GET", + path: "/api/v1/activities", + headers: [HOST: host, "X-Roomie-Echo": "getAllActivities"]) + + action +} + +def startActivity(evt) +{ + def uuid = evt + def host = getHostAddress(device.deviceNetworkId) + def activity = new groovy.json.JsonSlurper().parseText(device.currentValue('activities') ?: "{ 'activities' : [] }").activities.find { it.uuid == uuid } + def toggle = activity["toggle"] + def jsonMap = ["activity_uuid": uuid] + + if (toggle != null) + { + jsonMap << ["toggle_state": toggle ? "on" : "off"] + } + + def json = new groovy.json.JsonBuilder(jsonMap) + def jsonBody = json.toString() + def headers = [HOST: host, "Content-Type": "application/json"] + + def action = new physicalgraph.device.HubAction(method: "POST", + path: "/api/v1/runactivity", + body: jsonBody, + headers: headers) + + action +} + +def getHostAddress(d) +{ + def parts = d.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index 23fd384..0c76dc4 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -1,7 +1,7 @@ /** * Cree Bulb * - * Copyright 2014 SmartThings + * Copyright 2016 SmartThings * * 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: @@ -15,29 +15,29 @@ */ metadata { - definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" + capability "Actuator" capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Switch Level" + capability "Refresh" + capability "Switch" + capability "Switch Level" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" - } + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" + } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } - // UI tile definitions + // UI tile definitions tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { @@ -62,18 +62,12 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) + def resultMap = zigbee.getEvent(description) if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + sendEvent(resultMap) } else { - log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "DID NOT PARSE MESSAGE for description : $description" log.debug zigbee.parseDescriptionAsMap(description) } } @@ -87,7 +81,7 @@ def on() { } def setLevel(value) { - zigbee.setLevel(value) + zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } def refresh() { diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index f2583bf..6110a27 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -48,8 +48,8 @@ metadata { } standardTile("motion", "device.motion") { - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") } standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { diff --git a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy index ea47b6d..c8a3c01 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -36,155 +36,71 @@ * Slider range from 0..100 * Change 9: 2015-03-06 (Juan Risso) * Setlevel -> value to integer (to prevent smartapp calling this function from not working). + * Change 10: 2016-03-06 (Vinay Rao/Tom Manley) + * changed 2/3rds of the file to clean up code and add zigbee library improvements * */ metadata { - definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" + capability "Actuator" capability "Configuration" capability "Refresh" - capability "Sensor" + capability "Sensor" capability "Switch" - capability "Switch Level" + capability "Switch Level" capability "Polling" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb" - } + } // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState:"turningOff" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action: "switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action: "switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - main(["switch"]) - details(["switch", "level", "levelSliderControl", "refresh"]) - } - - preferences { - - input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) - input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } + preferences { + input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) + input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) } } // Parse incoming device messages to generate events def parse(String description) { - log.trace description - - if (description?.startsWith("on/off:")) { - log.debug "The bulb was sent a command to do something just now..." - if (description[-1] == "1") { - def result = createEvent(name: "switch", value: "on") - log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}" - return result - } else if (description[-1] == "0") { - def result = createEvent(name: "switch", value: "off") - log.debug "Off command was sent : Parse returned ${result?.descriptionText}" - return result + def resultMap = zigbee.getEvent(description) + if (resultMap) { + if ((resultMap.name == "level" && state.trigger == "setLevel") || resultMap.name != "level") { //doing this to account for weird level reporting bug with GE Link Bulbs + sendEvent(resultMap) } } - - def msg = zigbee.parse(description) - - if (description?.startsWith("catchall:")) { - // log.trace msg - // log.trace "data: $msg.data" - - def x = description[-4..-1] - // log.debug x - - switch (x) - { - - case "0000": - - def result = createEvent(name: "switch", value: "off") - log.debug "${result?.descriptionText}" - return result - break - - case "1000": - - def result = createEvent(name: "switch", value: "off") - log.debug "${result?.descriptionText}" - return result - break - - case "0100": - - def result = createEvent(name: "switch", value: "on") - log.debug "${result?.descriptionText}" - return result - break - - case "1001": - - def result = createEvent(name: "switch", value: "on") - log.debug "${result?.descriptionText}" - return result - break - } + else { + log.debug "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) } - - if (description?.startsWith("read attr")) { - - // log.trace description[27..28] - // log.trace description[-2..-1] - - if (description[27..28] == "0A") { - - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - - } - - else { - - if (description[-2..-1] == "00" && state.trigger == "setLevel") { - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - } - - if (description[-2..-1] == state.lvl) { - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - } - - } - } - } def poll() { - - [ - "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" + def refreshCmds = [ + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 500" ] + return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() } def updated() { @@ -270,109 +186,63 @@ def updated() { } def on() { - state.lvl = "00" state.trigger = "on/off" - - // log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + zigbee.on() } def off() { - state.lvl = "00" state.trigger = "on/off" - - // log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" + zigbee.off() } def refresh() { - - [ - "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" + def refreshCmds = [ + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 500" ] - poll() + return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() } def setLevel(value) { - - def cmds = [] - value = value as Integer - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 ${state.rate}}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - value = (value * 255 / 100) - def level = hex(value); - state.trigger = "setLevel" - state.lvl = "${level}" - + def cmd + def delayForRefresh = 500 if (dimRate && (state?.rate != null)) { - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}" + def computedRate = convertRateValue(state.rate) + cmd = zigbee.setLevel(value, computedRate) + delayForRefresh += computedRate * 100 //converting tenth of second to milliseconds } else { - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 1500}" + cmd = zigbee.setLevel(value, 20) + delayForRefresh += 2000 } + cmd + ["delay $delayForRefresh"] + zigbee.levelRefresh() +} - log.debug cmds - cmds +int convertRateValue(rate) { + int convertedRate = 0 + switch (rate) + { + case "0000": + convertedRate = 0 + break + + case "1500": + convertedRate = 20 //0015 hex in int is 2.1 + break + + case "2500": + convertedRate = 35 //0025 hex in int is 3.7 + break + + case "3500": + convertedRate = 50 //0035 hex in int is 5.1 + break + } + convertedRate } def configure() { - - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", - ] - return configCmds + refresh() // send refresh cmds as part of config + log.debug "Configuring Reporting and Bindings." + return zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() } - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy index 1742821..48b3945 100644 --- a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy @@ -14,6 +14,8 @@ * */ +//DEPRECATED - Using the smartsense-motion-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH + metadata { definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { capability "Motion Sensor" @@ -25,10 +27,6 @@ metadata { command "enrollResponse" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" } simulator { @@ -233,7 +231,7 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (rawValue == 0) {} + if (rawValue == 0 || rawValue == 255) {} else { if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy index 45dafe2..1e0daf6 100644 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -13,6 +13,7 @@ * for the specific language governing permissions and limitations under the License. * */ +//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH metadata { definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") { @@ -23,8 +24,7 @@ capability "Refresh" capability "Temperature Measurement" command "enrollResponse" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321" + } simulator { @@ -225,7 +225,8 @@ def getTemperature(value) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index 8de222d..e879148 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -220,7 +220,8 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index 26d0646..146df12 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -196,7 +196,8 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { diff --git a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy index ed0b23c..229c90c 100644 --- a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy +++ b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy @@ -32,14 +32,15 @@ metadata { attributeState("default", label:'${currentValue}', unit:"dF") } tileAttribute("device.temperature", key: "VALUE_CONTROL") { - attributeState("default", action: "setTemperature") + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") } tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { attributeState("default", label:'${currentValue}%', unit:"%") } tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { attributeState("idle", backgroundColor:"#44b621") - attributeState("heating", backgroundColor:"#ffa81e") + attributeState("heating", backgroundColor:"#ea5462") attributeState("cooling", backgroundColor:"#269bd2") } tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 9561ed7..14cac15 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -44,7 +44,7 @@ metadata { attributeState "power", label:'${currentValue} W' } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index a149359..770639c 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -39,7 +39,7 @@ metadata { attributeState "level", action:"switch level.setLevel" } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" diff --git a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy index a478827..29a24fa 100644 --- a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy +++ b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy @@ -63,7 +63,7 @@ metadata { state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index e779bde..39e78d2 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -52,7 +52,7 @@ valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) { state "battery", label:'${currentValue}% battery', unit:"" } - standardTile("refresh", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) { + standardTile("refresh", "device.refresh", inactiveLabel:false, decoration:"flat", width:2, height:2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index 9cf9c67..ade4987 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -57,7 +57,7 @@ metadata { valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "colorTemperature", label: '${currentValue} K' } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index 7521aec..430b66c 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -40,7 +40,7 @@ metadata { attributeState "power", label:'${currentValue} W' } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index 1ff3c4f..d861b95 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -42,7 +42,7 @@ metadata { attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index 40e0fc5..b8ad984 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -54,7 +54,7 @@ metadata { } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } diff --git a/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy b/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy new file mode 100644 index 0000000..3254f32 --- /dev/null +++ b/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy @@ -0,0 +1,147 @@ +/** + * BeaconThing Manager + * + * Copyright 2015 obycode + * + * 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. + * + */ +definition( + name: "BeaconThings Manager", + namespace: "com.obycode", + author: "obycode", + description: "SmartApp to interact with the BeaconThings iOS app. Use this app to integrate iBeacons into your smart home.", + category: "Convenience", + iconUrl: "http://beaconthingsapp.com/images/Icon-60.png", + iconX2Url: "http://beaconthingsapp.com/images/Icon-60@2x.png", + iconX3Url: "http://beaconthingsapp.com/images/Icon-60@3x.png", + oauth: true) + + +preferences { + section("Allow BeaconThings to talk to your home") { + + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def initialize() { +} + +def uninstalled() { + removeChildDevices(getChildDevices()) +} + +mappings { + path("/beacons") { + action: [ + DELETE: "clearBeacons", + POST: "addBeacon" + ] + } + + path("/beacons/:id") { + action: [ + PUT: "updateBeacon", + DELETE: "deleteBeacon" + ] + } +} + +void clearBeacons() { + removeChildDevices(getChildDevices()) +} + +void addBeacon() { + def beacon = request.JSON?.beacon + if (beacon) { + def beaconId = "BeaconThings" + if (beacon.major) { + beaconId = "$beaconId-${beacon.major}" + if (beacon.minor) { + beaconId = "$beaconId-${beacon.minor}" + } + } + log.debug "adding beacon $beaconId" + def d = addChildDevice("com.obycode", "BeaconThing", beaconId, null, [label:beacon.name, name:"BeaconThing", completedSetup: true]) + log.debug "addChildDevice returned $d" + + if (beacon.present) { + d.arrive(beacon.present) + } + else if (beacon.presence) { + d.setPresence(beacon.presence) + } + } +} + +void updateBeacon() { + log.debug "updating beacon ${params.id}" + def beaconDevice = getChildDevice(params.id) + // def children = getChildDevices() + // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + if (!beaconDevice) { + log.debug "Beacon not found directly" + def children = getChildDevices() + beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + if (!beaconDevice) { + log.debug "Beacon not found in list either" + return + } + } + + // This could be just updating the presence + def presence = request.JSON?.presence + if (presence) { + log.debug "Setting ${beaconDevice.label} to $presence" + beaconDevice.setPresence(presence) + } + + // It could be someone arriving + def arrived = request.JSON?.arrived + if (arrived) { + log.debug "$arrived arrived at ${beaconDevice.label}" + beaconDevice.arrived(arrived) + } + + // It could be someone left + def left = request.JSON?.left + if (left) { + log.debug "$left left ${beaconDevice.label}" + beaconDevice.left(left) + } + + // or it could be updating the name + def beacon = request.JSON?.beacon + if (beacon) { + beaconDevice.label = beacon.name + } +} + +void deleteBeacon() { + log.debug "deleting beacon ${params.id}" + deleteChildDevice(params.id) + // def children = getChildDevices() + // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + // if (beaconDevice) { + // deleteChildDevice(beaconDevice.deviceNetworkId) + // } +} + +private removeChildDevices(delete) { + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} diff --git a/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy b/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy new file mode 100644 index 0000000..44edc2f --- /dev/null +++ b/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy @@ -0,0 +1,383 @@ +/** + * Simple Sync Connect + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ + +definition( + name: "Simple Sync Connect", + namespace: "roomieremote-raconnect", + author: "Roomie Remote, Inc.", + description: "Integrate SmartThings with your Simple Control activities via Simple Sync.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + +preferences() +{ + page(name: "mainPage", title: "Simple Sync Setup", content: "mainPage", refreshTimeout: 5) + page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5) + page(name:"manualAgentEntry") + page(name:"verifyManualEntry") +} + +def mainPage() +{ + if (canInstallLabs()) + { + return agentDiscovery() + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") + { + paragraph "$upgradeNeeded" + } + } + } +} + +def agentDiscovery(params=[:]) +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 5 + + if (!state.subscribe) + { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //ssdp request every fifth refresh + if ((refreshCount % 5) == 0) + { + discoverAgents() + } + + def agentsDiscovered = agentsDiscovered() + + return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Pair with Simple Sync") + { + input "selectedAgent", "enum", required:true, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered + href(name:"manualAgentEntry", + title:"Manually Configure Simple Sync", + required:false, + page:"manualAgentEntry") + } + } +} + +def manualAgentEntry() +{ + dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) { + section("Manually Configure Simple Sync") + { + paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here." + input(name: "manualIPAddress", type: "text", title: "IP Address", required: true) + } + } +} + +def verifyManualEntry() +{ + def hexIP = convertIPToHexString(manualIPAddress) + def hexPort = convertToHexString(47147) + def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3" + def hubId = "" + + for (hub in location.hubs) + { + if (hub.localIP != null) + { + hubId = hub.id + break + } + } + + def manualAgent = [deviceType: "04", + mac: "unknown", + ip: hexIP, + port: hexPort, + ssdpPath: "/upnp/Roomie.xml", + ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1", + hub: hubId, + verified: true, + name: "Simple Sync $manualIPAddress"] + + state.agents[uuid] = manualAgent + + addOrUpdateAgent(state.agents[uuid]) + + dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) { + section("") + { + paragraph("Tap Done to complete the installation process.") + } + } +} + +def discoverAgents() +{ + def urn = getURN() + + sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN)) +} + +def agentsDiscovered() +{ + def gAgents = getAgents() + def agents = gAgents.findAll { it?.value?.verified == true } + def map = [:] + agents.each + { + map["${it.value.uuid}"] = it.value.name + } + map +} + +def getAgents() +{ + if (!state.agents) + { + state.agents = [:] + } + + state.agents +} + +def installed() +{ + initialize() +} + +def updated() +{ + initialize() +} + +def initialize() +{ + if (state.subscribe) + { + unsubscribe() + state.subscribe = false + } + + if (selectedAgent) + { + addOrUpdateAgent(state.agents[selectedAgent]) + } +} + +def addOrUpdateAgent(agent) +{ + def children = getChildDevices() + def dni = agent.ip + ":" + agent.port + def found = false + + children.each + { + if ((it.getDeviceDataByName("mac") == agent.mac)) + { + found = true + + if (it.getDeviceNetworkId() != dni) + { + it.setDeviceNetworkId(dni) + } + } + else if (it.getDeviceNetworkId() == dni) + { + found = true + } + } + + if (!found) + { + addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"]) + } +} + +def locationHandler(evt) +{ + def description = evt?.description + def urn = getURN() + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + + parsedEvent?.putAt("hub", hub) + + //SSDP DISCOVERY EVENTS + if (parsedEvent?.ssdpTerm?.contains(urn)) + { + def agent = parsedEvent + def ip = convertHexToIP(agent.ip) + def agents = getAgents() + + agent.verified = true + agent.name = "Simple Sync $ip" + + if (!agents[agent.uuid]) + { + state.agents[agent.uuid] = agent + } + } +} + +private def parseEventMessage(String description) +{ + def event = [:] + def parts = description.split(',') + + parts.each + { part -> + part = part.trim() + if (part.startsWith('devicetype:')) + { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) + { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) + { + event.ssdpUSN = valueString + + def uuid = getUUIDFromUSN(valueString) + + if (uuid) + { + event.uuid = uuid + } + } + } + else if (part.startsWith('ssdpTerm:')) + { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) + { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) + { + part -= "headers:" + def valueString = part.trim() + if (valueString) + { + event.headers = valueString + } + } + else if (part.startsWith('body')) + { + part -= "body:" + def valueString = part.trim() + if (valueString) + { + event.body = valueString + } + } + } + + event +} + +def getURN() +{ + return "urn:roomieremote-com:device:roomie:1" +} + +def getUUIDFromUSN(usn) +{ + def parts = usn.split(":") + + for (int i = 0; i < parts.size(); ++i) + { + if (parts[i] == "uuid") + { + return parts[i + 1] + } + } +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} + +def String convertToHexString(n) +{ + String hex = String.format("%X", n.toInteger()) +} + +def String convertIPToHexString(ipString) +{ + String hex = ipString.tokenize(".").collect { + String.format("%02X", it.toInteger()) + }.join() +} + +def Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +def Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +def List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file diff --git a/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy b/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy new file mode 100644 index 0000000..3fd4d08 --- /dev/null +++ b/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy @@ -0,0 +1,296 @@ +/** + * Simple Sync Trigger + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ +definition( + name: "Simple Sync Trigger", + namespace: "roomieremote-ratrigger", + author: "Roomie Remote, Inc.", + description: "Trigger Simple Control activities when certain actions take place in your home.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + + +preferences { + page(name: "agentSelection", title: "Select your Simple Sync") + page(name: "refreshActivities", title: "Updating list of Simple Sync activities") + page(name: "control", title: "Run a Simple Control activity when something happens") + page(name: "timeIntervalInput", title: "Only during a certain time", install: true, uninstall: true) { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def agentSelection() +{ + if (agent) + { + state.refreshCount = 0 + } + + dynamicPage(name: "agentSelection", title: "Select your Simple Sync", nextPage: "control", install: false, uninstall: true) { + section { + input "agent", "capability.mediaController", title: "Simple Sync", required: true, multiple: false + } + } +} + +def control() +{ + def activities = agent.latestValue('activities') + + if (!activities || !state.refreshCount) + { + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 4 + + // Request activities every 5th attempt + if((refreshCount % 5) == 0) + { + agent.getAllActivities() + } + + dynamicPage(name: "control", title: "Updating list of Simple Control activities", nextPage: "", refreshInterval: refreshInterval, install: false, uninstall: true) { + section("") { + paragraph "Retrieving activities from Simple Sync" + } + } + } + else + { + dynamicPage(name: "control", title: "Run a Simple Control activity when something happens", nextPage: "timeIntervalInput", install: false, uninstall: true) { + def anythingSet = anythingSet() + if (anythingSet) { + section("When..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Detected", required: false, multiple: true + ifSet "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Run this activity"){ + input "activity", "enum", title: "Activity?", required: true, options: new groovy.json.JsonSlurper().parseText(activities ?: "[]").activities?.collect { ["${it.uuid}": it.name] } + } + + section("More options", hideable: true, hidden: true) { + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } + } +} + +private anythingSet() { + for (name in ["motion","motionInactive","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + subscribeToEvents() +} + +def updated() { + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(motionInactive, "motion.inactive", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + startActivity(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + startActivity(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + startActivity(evt) +} + +private startActivity(evt) { + agent.startActivity(activity) + + if (frequency) { + state.lastActionTimeStamp = now() + } +} + +private frequencyKey(evt) { + //evt.deviceId ?: evt.value + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} \ No newline at end of file diff --git a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy new file mode 100644 index 0000000..c2b3fdd --- /dev/null +++ b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy @@ -0,0 +1,676 @@ +/** + * Simple Control + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ + +definition( + name: "Simple Control", + namespace: "roomieremote-roomieconnect", + author: "Roomie Remote, Inc.", + description: "Integrate SmartThings with your Simple Control activities.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + +preferences() +{ + section("Allow Simple Control to Monitor and Control These Things...") + { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + } + + page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5) + page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5) + page(name:"manualAgentEntry") + page(name:"verifyManualEntry") +} + +mappings { + path("/devices") { + action: [ + GET: "getDevices", + POST: "handleDevicesWithIDs" + ] + } + path("/device/:id") { + action: [ + GET: "getDevice", + POST: "updateDevice" + ] + } + path("/subscriptions") { + action: [ + GET: "listSubscriptions", + POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."} + DELETE: "removeAllSubscriptions" + ] + } + path("/subscriptions/:id") { + action: [ + DELETE: "removeSubscription" + ] + } +} + +private getAllDevices() +{ + //log.debug("getAllDevices()") + ([] + switches + locks + thermostats + imageCaptures + relaySwitches + doorControls + colorControls + musicPlayers + speechSynthesizers + switchLevels + indicators + mediaControllers + tones + tvs + alarms + valves + motionSensors + presenceSensors + beacons + pushButtons + smokeDetectors + coDetectors + contactSensors + accelerationSensors + energyMeters + powerMeters + lightSensors + humiditySensors + temperatureSensors + speechRecognizers + stepSensors + touchSensors)?.findAll()?.unique { it.id } +} + +def getDevices() +{ + //log.debug("getDevices, params: ${params}") + allDevices.collect { + //log.debug("device: ${it}") + deviceItem(it) + } +} + +def getDevice() +{ + //log.debug("getDevice, params: ${params}") + def device = allDevices.find { it.id == params.id } + if (!device) + { + render status: 404, data: '{"msg": "Device not found"}' + } + else + { + deviceItem(device) + } +} + +def handleDevicesWithIDs() +{ + //log.debug("handleDevicesWithIDs, params: ${params}") + def data = request.JSON + def ids = data?.ids?.findAll()?.unique() + //log.debug("ids: ${ids}") + def command = data?.command + def arguments = data?.arguments + if (command) + { + def success = false + //log.debug("command ${command}, arguments ${arguments}") + for (devId in ids) + { + def device = allDevices.find { it.id == devId } + if (device) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + success = true + } else { + //log.debug("device not found ${devId}") + } + } + + if (success) + { + render status: 200, data: "{}" + } + else + { + render status: 404, data: '{"msg": "Device not found"}' + } + } + else + { + ids.collect { + def currentId = it + def device = allDevices.find { it.id == currentId } + if (device) + { + deviceItem(device) + } + } + } +} + +private deviceItem(device) { + [ + id: device.id, + label: device.displayName, + currentState: device.currentStates, + capabilities: device.capabilities?.collect {[ + name: it.name + ]}, + attributes: device.supportedAttributes?.collect {[ + name: it.name, + dataType: it.dataType, + values: it.values + ]}, + commands: device.supportedCommands?.collect {[ + name: it.name, + arguments: it.arguments + ]}, + type: [ + name: device.typeName, + author: device.typeAuthor + ] + ] +} + +def updateDevice() +{ + //log.debug("updateDevice, params: ${params}") + def data = request.JSON + def command = data?.command + def arguments = data?.arguments + + //log.debug("updateDevice, params: ${params}, request: ${data}") + if (!command) { + render status: 400, data: '{"msg": "command is required"}' + } else { + def device = allDevices.find { it.id == params.id } + if (device) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 404, data: '{"msg": "Device not found"}' + } + } +} + +def listSubscriptions() +{ + //log.debug "listSubscriptions()" + app.subscriptions?.findAll { it.deviceId }?.collect { + def deviceInfo = state[it.deviceId] + def response = [ + id: it.id, + deviceId: it.deviceId, + attributeName: it.data, + handler: it.handler + ] + //if (!selectedAgent) { + response.callbackUrl = deviceInfo?.callbackUrl + //} + response + } ?: [] +} + +def addSubscription() { + def data = request.JSON + def attribute = data.attributeName + def callbackUrl = data.callbackUrl + + //log.debug "addSubscription, params: ${params}, request: ${data}" + if (!attribute) { + render status: 400, data: '{"msg": "attributeName is required"}' + } else { + def device = allDevices.find { it.id == data.deviceId } + if (device) { + //if (!selectedAgent) { + //log.debug "Adding callbackUrl: $callbackUrl" + state[device.id] = [callbackUrl: callbackUrl] + //} + //log.debug "Adding subscription" + def subscription = subscribe(device, attribute, deviceHandler) + if (!subscription || !subscription.eventSubscription) { + //log.debug("subscriptions: ${app.subscriptions}") + //for (sub in app.subscriptions) + //{ + //log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}") + //log.debug(sub.properties.collect{it}.join('\n')) + //} + subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } + } + + def response = [ + id: subscription.id, + deviceId: subscription.device?.id, + attributeName: subscription.data, + handler: subscription.handler + ] + //if (!selectedAgent) { + response.callbackUrl = callbackUrl + //} + response + } else { + render status: 400, data: '{"msg": "Device not found"}' + } + } +} + +def removeSubscription() +{ + def subscription = app.subscriptions?.find { it.id == params.id } + def device = subscription?.device + + //log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" + if (device) { + //log.debug "Removing subscription for device: ${device.id}" + state.remove(device.id) + unsubscribe(device) + } + render status: 204, data: "{}" +} + +def removeAllSubscriptions() +{ + for (sub in app.subscriptions) + { + //log.debug("Subscription: ${sub}") + //log.debug(sub.properties.collect{it}.join('\n')) + def handler = sub.handler + def device = sub.device + + if (device && handler == 'deviceHandler') + { + //log.debug(device.properties.collect{it}.join('\n')) + //log.debug("Removing subscription for device: ${device}") + state.remove(device.id) + unsubscribe(device) + } + } +} + +def deviceHandler(evt) { + def deviceInfo = state[evt.deviceId] + //if (selectedAgent) { + // sendToRoomie(evt, agentCallbackUrl) + //} else if (deviceInfo) { + if (deviceInfo) + { + if (deviceInfo.callbackUrl) { + sendToRoomie(evt, deviceInfo.callbackUrl) + } else { + log.warn "No callbackUrl set for device: ${evt.deviceId}" + } + } else { + log.warn "No subscribed device found for device: ${evt.deviceId}" + } +} + +def sendToRoomie(evt, String callbackUrl) { + def callback = new URI(callbackUrl) + def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host + def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path + sendHubCommand(new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + "Host": host, + "Content-Type": "application/json" + ], + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + )) +} + +def mainPage() +{ + if (canInstallLabs()) + { + return agentDiscovery() + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") + { + paragraph "$upgradeNeeded" + } + } + } +} + +def agentDiscovery(params=[:]) +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 5 + + if (!state.subscribe) + { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //ssdp request every fifth refresh + if ((refreshCount % 5) == 0) + { + discoverAgents() + } + + def agentsDiscovered = agentsDiscovered() + + return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Pair with Simple Sync") + { + input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered + href(name:"manualAgentEntry", + title:"Manually Configure Simple Sync", + required:false, + page:"manualAgentEntry") + } + section("Allow Simple Control to Monitor and Control These Things...") + { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + } + } +} + +def manualAgentEntry() +{ + dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) { + section("Manually Configure Simple Sync") + { + paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here." + input(name: "manualIPAddress", type: "text", title: "IP Address", required: true) + } + } +} + +def verifyManualEntry() +{ + def hexIP = convertIPToHexString(manualIPAddress) + def hexPort = convertToHexString(47147) + def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3" + def hubId = "" + + for (hub in location.hubs) + { + if (hub.localIP != null) + { + hubId = hub.id + break + } + } + + def manualAgent = [deviceType: "04", + mac: "unknown", + ip: hexIP, + port: hexPort, + ssdpPath: "/upnp/Roomie.xml", + ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1", + hub: hubId, + verified: true, + name: "Simple Sync $manualIPAddress"] + + state.agents[uuid] = manualAgent + + addOrUpdateAgent(state.agents[uuid]) + + dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) { + section("") + { + paragraph("Tap Done to complete the installation process.") + } + } +} + +def discoverAgents() +{ + def urn = getURN() + + sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN)) +} + +def agentsDiscovered() +{ + def gAgents = getAgents() + def agents = gAgents.findAll { it?.value?.verified == true } + def map = [:] + agents.each + { + map["${it.value.uuid}"] = it.value.name + } + map +} + +def getAgents() +{ + if (!state.agents) + { + state.agents = [:] + } + + state.agents +} + +def installed() +{ + initialize() +} + +def updated() +{ + initialize() +} + +def initialize() +{ + if (state.subscribe) + { + unsubscribe() + state.subscribe = false + } + + if (selectedAgent) + { + addOrUpdateAgent(state.agents[selectedAgent]) + } +} + +def addOrUpdateAgent(agent) +{ + def children = getChildDevices() + def dni = agent.ip + ":" + agent.port + def found = false + + children.each + { + if ((it.getDeviceDataByName("mac") == agent.mac)) + { + found = true + + if (it.getDeviceNetworkId() != dni) + { + it.setDeviceNetworkId(dni) + } + } + else if (it.getDeviceNetworkId() == dni) + { + found = true + } + } + + if (!found) + { + addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"]) + } +} + +def locationHandler(evt) +{ + def description = evt?.description + def urn = getURN() + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + + parsedEvent?.putAt("hub", hub) + + //SSDP DISCOVERY EVENTS + if (parsedEvent?.ssdpTerm?.contains(urn)) + { + def agent = parsedEvent + def ip = convertHexToIP(agent.ip) + def agents = getAgents() + + agent.verified = true + agent.name = "Simple Sync $ip" + + if (!agents[agent.uuid]) + { + state.agents[agent.uuid] = agent + } + } +} + +private def parseEventMessage(String description) +{ + def event = [:] + def parts = description.split(',') + + parts.each + { part -> + part = part.trim() + if (part.startsWith('devicetype:')) + { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) + { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) + { + event.ssdpUSN = valueString + + def uuid = getUUIDFromUSN(valueString) + + if (uuid) + { + event.uuid = uuid + } + } + } + else if (part.startsWith('ssdpTerm:')) + { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) + { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) + { + part -= "headers:" + def valueString = part.trim() + if (valueString) + { + event.headers = valueString + } + } + else if (part.startsWith('body')) + { + part -= "body:" + def valueString = part.trim() + if (valueString) + { + event.body = valueString + } + } + } + + event +} + +def getURN() +{ + return "urn:roomieremote-com:device:roomie:1" +} + +def getUUIDFromUSN(usn) +{ + def parts = usn.split(":") + + for (int i = 0; i < parts.size(); ++i) + { + if (parts[i] == "uuid") + { + return parts[i + 1] + } + } +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} + +def String convertToHexString(n) +{ + String hex = String.format("%X", n.toInteger()) +} + +def String convertIPToHexString(ipString) +{ + String hex = ipString.tokenize(".").collect { + String.format("%02X", it.toInteger()) + }.join() +} + +def Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +def Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +def List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} + + + + diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index ea32f56..d66e7c0 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -545,10 +545,15 @@ def updateSensorData() { def occupancy = "" it.capability.each { if (it.type == "temperature") { - if (location.temperatureScale == "F") { - temperature = Math.round(it.value.toDouble() / 10) + if (it.value == "unknown") { + temperature = "--" } else { - temperature = convertFtoC(it.value.toDouble() / 10) + if (location.temperatureScale == "F") { + temperature = Math.round(it.value.toDouble() / 10) + } else { + temperature = convertFtoC(it.value.toDouble() / 10) + } + } } else if (it.type == "occupancy") { diff --git a/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy b/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy index 30a6521..bb72b8a 100644 --- a/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy +++ b/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy @@ -76,7 +76,7 @@ def mainPage() { } section{ input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [ - //"Custom Message", + "Custom Message", "Bell 1", "Bell 2", "Dogs Barking", @@ -89,7 +89,7 @@ def mainPage() { "Someone is arriving", "Piano", "Lightsaber"] - //input "message","text",title:"Play this message", required:false, multiple: false + input "message","text",title:"Play this message", required:false, multiple: false } section { input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true @@ -408,13 +408,15 @@ private loadText() { case "Lightsaber": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] break; - default: - /*if (message) { + case "Custom Message": + if (message) { state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) } else { state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") - }*/ + } + break; + default: state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] break; }