Compare commits

..

1 Commits

54 changed files with 970 additions and 3077 deletions

View File

@@ -1,296 +0,0 @@
/**
* obything Music Player
*
* Copyright 2015 obycode
*/
import groovy.json.JsonSlurper
metadata {
definition (name: "obything Music Player", namespace: "com.obycode", author: "obycode") {
capability "Music Player"
capability "Refresh"
capability "Switch"
// These strings are comma separated lists of names
attribute "playlists", "json_object"
attribute "speakers", "json_object"
attribute "playlistDescription", "string"
// playPlaylist(String uri, speakers=null, volume=null, resume=false, restore=false)
command "playPlaylist", ["string", "string", "number", "number", "number"]
// playTrack(String uri, speakers=null, volume=null, resume=false, restore=false, playlist=null)
command "playTrack", ["string", "string", "number", "number", "number", "string"]
command "update", ["string"]
}
simulator {
// TODO: define status and reply messages here
}
tiles {
tiles(scale: 2) {
multiAttributeTile(name:"richmusic", type:"lighting", width:6, height:4) {
tileAttribute("device.status", key: "PRIMARY_CONTROL") {
attributeState "paused", label: 'Paused', action:"music Player.play", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#D0D0D0"
attributeState "stopped", label: 'Stopped', action:"music Player.play", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#D0D0D0"
attributeState "playing", label:'Playing', action:"music Player.pause", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#4C4CFF"
}
tileAttribute("device.trackDescription", key: "SECONDARY_CONTROL") {
attributeState "default", label:'${currentValue}'
}
tileAttribute("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"music Player.setLevel", range:"(0..100)"
}
}
standardTile("nextTrack", "device.status", width: 2, height: 2, decoration: "flat") {
state "next", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn", backgroundColor:"#ffffff"
}
standardTile("playpause", "device.status", width: 2, height: 2, decoration: "flat") {
state "default", label:'', action:"music Player.play", icon:"st.sonos.play-btn", backgroundColor:"#ffffff"
state "playing", label:'', action:"music Player.pause", icon:"st.sonos.pause-btn", backgroundColor:"#ffffff"
state "paused", label:'', action:"music Player.play", icon:"st.sonos.play-btn", backgroundColor:"#ffffff"
}
standardTile("previousTrack", "device.status", width: 2, height: 2, decoration: "flat") {
state "previous", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn", backgroundColor:"#ffffff"
}
standardTile("refresh", "device.status", width: 2, height: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff"
}
standardTile("mute", "device.mute", width: 2, height: 2, decoration: "flat") {
state "unmuted", label:"Mute", action:"music Player.mute", icon:"st.custom.sonos.unmuted", backgroundColor:"#ffffff"
state "muted", label:"Unmute", action:"music Player.unmute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff"
}
controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 4) {
state "level", label:"Volume", action:"music Player.setLevel", backgroundColor:"#ffffff"
}
valueTile("currentPlaylist", "device.playlistDescription", height:2, width:6, decoration: "flat") {
state "default", label:'${currentValue}', backgroundColor:"#ffffff"
}
main "richmusic"
details(["richmusic",
"previousTrack","nextTrack","refresh",
"levelSliderControl","mute",
"currentPlaylist"
])
}
}
}
// parse events into attributes
def parse(String description) {
// log.debug "parse called with $description"
def map = stringToMap(description)
if (map.headers && map.body) { //got device info response
if (map.body) {
def bodyString = new String(map.body.decodeBase64())
def slurper = new JsonSlurper()
def result = slurper.parseText(bodyString)
parseMessage(result)
}
}
}
def parseMessage(message) {
log.debug "message is $message"
if (message.containsKey("volume")) {
log.debug "setting volume to ${message.volume}"
sendEvent(name: "level", value: message.volume)
}
if (message.containsKey("mute")) {
log.debug "setting mute to ${message.mute}"
sendEvent(name: "mute", value: message.mute)
}
if (message.containsKey("status")) {
log.debug "setting status to ${message.status}"
sendEvent(name: "status", value: message.status)
}
if (message.containsKey("trackData")) {
def json = new groovy.json.JsonBuilder(message.trackData)
log.debug "setting trackData to ${json.toString()}"
sendEvent(name: "trackData", value: json.toString())
}
if (message.containsKey("trackDescription")) {
log.debug "setting trackDescription info to ${message.trackDescription}"
sendEvent(name: "trackDescription", value: message.trackDescription)
}
if (message.containsKey("playlistData")) {
def json = new groovy.json.JsonBuilder(message.playlistData)
log.debug "setting playlistData to ${json.toString()}"
sendEvent(name: "playlistData", value: json.toString())
}
if (message.containsKey("playlistDescription")) {
log.debug "setting playlistDescription info to ${message.playlistDescription}"
sendEvent(name: "playlistDescription", value: message.playlistDescription)
}
if (message.containsKey("playlists")) {
def json = new groovy.json.JsonBuilder(message.playlists)
log.debug "setting playlists to ${json.toString()}"
sendEvent(name: "playlists",value: json.toString())
}
if (message.containsKey("speakers")) {
def json = new groovy.json.JsonBuilder(message.speakers)
log.debug "setting speakers to ${json.toString()}"
sendEvent(name: "speakers",value: json.toString())
}
}
// Called by service manager to send updates from device
def update(message) {
log.debug "update: $message"
parseMessage(message)
}
def installed() {
// Refresh to get current state
refresh()
}
// handle commands
def refresh() {
log.debug "Executing 'refresh'"
getInfo("command=refresh")
}
def on() {
log.debug "Executing 'on' (play)"
sendCommand("command=play")
}
def off() {
log.debug "Executing 'off' (pause)"
sendCommand("command=pause")
}
def play() {
log.debug "Executing 'play'"
sendCommand("command=play")
}
def pause() {
log.debug "Executing 'pause'"
sendCommand("command=pause")
}
def stop() {
log.debug "Executing 'stop'"
sendCommand("command=stop")
}
def nextTrack() {
log.debug "Executing 'nextTrack'"
sendCommand("command=next")
}
def setLevel(value) {
log.debug "Executing 'setLevel' to $value"
sendCommand("command=volume&level=$value")
}
// def playText(String msg) {
// log.debug "Executing 'playText'"
// sendCommand("say=$msg")
// }
//
def mute() {
log.debug "Executing 'mute'"
sendCommand("command=mute")
}
def previousTrack() {
log.debug "Executing 'previousTrack'"
sendCommand("command=previous")
}
def unmute() {
log.debug "Executing 'unmute'"
sendCommand("command=unmute")
}
def playPlaylist(String uri, speakers=null, volume=null) {
log.trace "playPlaylist($uri, $speakers, $volume, $resume, $restore)"
def command = "command=playlist&name=${uri}"
if (speakers) {
command += "&speakers=${speakers}"
}
if (volume) {
command += "&volume=${volume}"
}
sendCommand(command)
}
def playTrack(String uri, speakers=null, volume=null, resume=false, restore=false, playlist=null) {
log.trace "playTrack($uri, $speakers, $volume, $resume, $restore, $playlist)"
def command = "command=track&url=${uri}"
if (speakers) {
command += "&speakers=${speakers}"
}
if (volume) {
command += "&volume=${volume}"
}
if (resume) {
command += "&resume="
}
else if (restore) {
command += "&restore="
}
if (playlist) {
command += "&playlist=$playlist"
}
sendCommand(command)
}
// def speak(text) {
// def url = textToSpeech(text)
// sendCommand("playTrack&track=${url.uri}&resume")
// }
//
// def beep() {
// sendCommand("beep")
// }
// Private functions used internally
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 parts = device.deviceNetworkId.split(":")
def ip = convertHexToIP(parts[0])
def port = convertHexToInt(parts[1])
return ip + ":" + port
}
private sendCommand(command) {
def path = "/player&" + command
def result = new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [
HOST: getHostAddress()
],
)
result
}
private getInfo(command) {
def path = "/player&" + command
def result = new physicalgraph.device.HubAction(
method: "GET",
path: path,
headers: [
HOST: getHostAddress()
],
)
result
}

View File

@@ -1,9 +1,12 @@
// keen home smart vent /**
// http://www.keenhome.io * Keen Home Smart Vent
// SmartThings Device Handler v1.0.0 *
* Author: Keen Home
* Date: 2015-06-23
*/
metadata { metadata {
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") { definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
capability "Switch Level" capability "Switch Level"
capability "Switch" capability "Switch"
capability "Configuration" capability "Configuration"
@@ -18,7 +21,6 @@ metadata {
command "getBattery" command "getBattery"
command "getTemperature" command "getTemperature"
command "setZigBeeIdTile" command "setZigBeeIdTile"
command "clearObstruction"
fingerprint endpoint: "1", fingerprint endpoint: "1",
profileId: "0104", profileId: "0104",
@@ -40,10 +42,9 @@ metadata {
// UI tile definitions // UI tile definitions
tiles { tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#53a7c0" state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff" state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#ff0000" state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffff33"
} }
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
state "level", action:"switch level.setLevel" state "level", action:"switch level.setLevel"
@@ -205,12 +206,12 @@ private Map makeOnOffResult(rawValue) {
private Map makeLevelResult(rawValue) { private Map makeLevelResult(rawValue) {
def linkText = getLinkText(device) def linkText = getLinkText(device)
// log.debug "rawValue: ${rawValue}"
def value = Integer.parseInt(rawValue, 16) def value = Integer.parseInt(rawValue, 16)
def rangeMax = 254 def rangeMax = 254
// catch obstruction level
if (value == 255) { if (value == 255) {
log.debug "${linkText} is obstructed" log.debug "obstructed"
// Just return here. Once the vent is power cycled // Just return here. Once the vent is power cycled
// it will go back to the previous level before obstruction. // it will go back to the previous level before obstruction.
// Therefore, no need to update level on the display. // Therefore, no need to update level on the display.
@@ -219,9 +220,24 @@ private Map makeLevelResult(rawValue) {
value: "obstructed", value: "obstructed",
descriptionText: "${linkText} is obstructed. Please power cycle." descriptionText: "${linkText} is obstructed. Please power cycle."
] ]
} else if ( device.currentValue("switch") == "obstructed" &&
value == 254) {
// When the device is reset after an obstruction, the switch
// state will be obstructed and the value coming from the device
// will be 254. Since we're not using heating/cooling mode from
// the device type handler, we need to bump it down to the lower
// (cooling) range
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
value = rangeMax
} }
// else if (device.currentValue("switch") == "off") {
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
// }
// log.debug "pre-value: ${value}"
value = Math.floor(value / rangeMax * 100) value = Math.floor(value / rangeMax * 100)
// log.debug "post-value: ${value}"
return [ return [
name: "level", name: "level",
@@ -311,79 +327,35 @@ private def makeSerialResult(serial) {
value: serial, value: serial,
descriptionText: "${linkText} has serial ${serial}" ] descriptionText: "${linkText} has serial ${serial}" ]
} }
// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command
private def makeLevelCommand(level) {
def rangeMax = 254
def scaledLevel = Math.round(level * rangeMax / 100)
log.debug "scaled level for ${level}%: ${scaledLevel}"
// convert to hex string and pad to two digits
def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0')
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}"
}
/**** COMMAND METHODS ****/ /**** COMMAND METHODS ****/
// def mfgCode() {
// ["zcl mfg-code 0x115B", "delay 200"]
// }
def on() { def on() {
def linkText = getLinkText(device) log.debug "on()"
log.debug "open ${linkText}"
// only change the state if the vent is not obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot open because ${linkText} is obstructed")
return
}
sendEvent(makeOnOffResult(1)) sendEvent(makeOnOffResult(1))
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}" "st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
} }
def off() { def off() {
def linkText = getLinkText(device) log.debug "off()"
log.debug "close ${linkText}"
// only change the state if the vent is not obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot close because ${linkText} is obstructed")
return
}
sendEvent(makeOnOffResult(0)) sendEvent(makeOnOffResult(0))
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}" "st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
} }
def clearObstruction() { // does this work?
def linkText = getLinkText(device) def toggle() {
log.debug "attempting to clear ${linkText} obstruction" log.debug "toggle()"
sendEvent([ "st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
name: "switch",
value: "clearing",
descriptionText: "${linkText} is clearing obstruction"
])
// send a move command to ensure level attribute gets reset for old, buggy firmware
// then send a reset to factory defaults
// finally re-configure to ensure reports and binding is still properly set after the rtfd
[
makeLevelCommand(device.currentValue("level")), "delay 500",
"st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000"
] + configure()
} }
def setLevel(value) { def setLevel(value) {
log.debug "setting level: ${value}" log.debug "setting level: ${value}"
def linkText = getLinkText(device) def linkText = getLinkText(device)
// only change the level if the vent is not obstructed
def currentState = device.currentValue("switch")
if (currentState == "obstructed") {
log.error("cannot set level because ${linkText} is obstructed")
return
}
sendEvent(name: "level", value: value) sendEvent(name: "level", value: value)
if (value > 0) { if (value > 0) {
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level") sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
@@ -391,26 +363,29 @@ def setLevel(value) {
else { else {
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0") sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
} }
def rangeMax = 254
def computedLevel = Math.round(value * rangeMax / 100)
log.debug "computedLevel: ${computedLevel}"
makeLevelCommand(value) def level = new BigInteger(computedLevel.toString()).toString(16)
log.debug "level: ${level}"
if (level.size() < 2){
level = '0' + level
}
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
} }
def getOnOff() { def getOnOff() {
log.debug "getOnOff()" log.debug "getOnOff()"
// disallow on/off updates while vent is obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot update open/close status because ${getLinkText(device)} is obstructed")
return []
}
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"] ["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
} }
def getPressure() { def getPressure() {
log.debug "getPressure()" log.debug "getPressure()"
// using a Keen Home specific attribute in the pressure measurement cluster
[ [
"zcl mfg-code 0x115B", "delay 200", "zcl mfg-code 0x115B", "delay 200",
"zcl global read 0x0403 0x20", "delay 200", "zcl global read 0x0403 0x20", "delay 200",
@@ -420,13 +395,12 @@ def getPressure() {
def getLevel() { def getLevel() {
log.debug "getLevel()" log.debug "getLevel()"
// rattr = read attribute
// disallow level updates while vent is obstructed // 0x${} = device net id
if (device.currentValue("switch") == "obstructed") { // 1 = endpoint
log.error("cannot update level status because ${getLinkText(device)} is obstructed") // 8 = cluster id (level control, in this case)
return [] // 0 = attribute within cluster
} // sendEvent(name: "level", value: value)
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"] ["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
} }
@@ -451,59 +425,78 @@ def setZigBeeIdTile() {
name: "zigbeeId", name: "zigbeeId",
value: device.zigbeeId, value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]) descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
return [ return [
name: "zigbeeId", name: "zigbeeId",
value: device.zigbeeId, value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ] descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
} }
def refresh() { def refresh() {
getOnOff() + getOnOff() +
getLevel() + getLevel() +
getTemperature() + getTemperature() +
getPressure() + getPressure() +
getBattery() getBattery()
} }
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
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
def configure() { def configure() {
log.debug "CONFIGURE" log.debug "CONFIGURE"
log.debug "zigbeeId: ${device.hub.zigbeeId}"
// get ZigBee ID by hidden tile because that's the only way we can do it
setZigBeeIdTile() setZigBeeIdTile()
def configCmds = [ def configCmds = [
// bind reporting clusters to hub // binding commands
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500" "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
// configure report commands // configure report commands
// zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change] // [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
// report with these parameters is preconfigured in firmware, can be overridden here // mike 2015/06/22: preconfigured; see tech spec
// vent on/off state - type: boolean, change: 1 // vent on/off state - type: boolean, change: 1
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200", // "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// report with these parameters is preconfigured in firmware, can be overridden here // mike 2015/06/22: preconfigured; see tech spec
// vent level - type: int8u, change: 1 // vent level - type: int8u, change: 1
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200", // "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// report with these parameters is preconfigured in firmware, can be overridden here // mike 2015/06/22: temp and pressure reports are preconfigured, but
// we'd like to override their settings for our own purposes
// temperature - type: int16s, change: 0xA = 10 = 0.1C // temperature - type: int16s, change: 0xA = 10 = 0.1C
// "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200", "zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500", "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// report with these parameters is preconfigured in firmware, can be overridden here // mike 2015/06/22: use new custom pressure attribute
// keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa // pressure - type: int32u, change: 1 = 0.1Pa
// "zcl mfg-code 0x115B", "delay 200", "zcl mfg-code 0x115B", "delay 200",
// "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200", "zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500", "send 0x${device.deviceNetworkId} 1 1", "delay 1500"
// report with these parameters is preconfigured in firmware, can be overridden here // mike 2015/06/22: preconfigured; see tech spec
// battery - type: int8u, change: 1 // battery - type: int8u, change: 1
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200", // "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500",

View File

@@ -24,8 +24,6 @@ metadata {
capability "Battery" capability "Battery"
attribute "tamper", "enum", ["detected", "clear"] attribute "tamper", "enum", ["detected", "clear"]
attribute "batteryStatus", "string"
attribute "powerSupply", "enum", ["USB Cable", "Battery"]
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
} }
@@ -65,19 +63,6 @@ metadata {
status "wake up" : "command: 8407, payload: " status "wake up" : "command: 8407, payload: "
} }
preferences {
input description: "Please consult AEOTEC MULTISENSOR 6 operating manual for advanced setting options. You can skip this configuration to use default settings",
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input "motionDelayTime", "enum", title: "Motion Sensor Delay Time",
options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true
input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true
input "reportInterval", "enum", title: "Sensors Report Interval",
options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true
}
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
@@ -100,78 +85,53 @@ metadata {
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
state "humidity", label:'${currentValue}% humidity', unit:"" state "humidity", label:'${currentValue}% humidity', unit:""
} }
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
state "illuminance", label:'${currentValue} ${unit}', unit:"lux" state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
} }
valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) {
state "ultravioletIndex", label:'${currentValue} UV index', unit:""
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:"" state "battery", label:'${currentValue}% battery', unit:""
} }
valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { main(["motion", "temperature", "humidity", "illuminance"])
state "batteryStatus", label:'${currentValue}', unit:"" details(["motion", "temperature", "humidity", "illuminance", "battery"])
}
valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") {
state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff"
}
main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"])
details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus"])
} }
} }
def updated() { def updated()
log.debug "Updated with settings: ${settings}" {
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}" if (state.sec && !isConfigured()) {
// in case we miss the SCSR
if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered
response(configure()) response(configure())
} else if (device.latestValue("powerSupply") == "Battery") { //case2: battery powered
// setConfigured("false") is used by WakeUpNotification
setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference
} else { //case3: power source is not identified, ask user to properly pair the sensor again
log.warn "power source is not identified, check it sensor is powered by USB, if so > configure()"
def request = []
request << zwave.configurationV1.configurationGet(parameterNumber: 101)
response(commands(request))
} }
} }
def parse(String description) { def parse(String description)
log.debug "parse() >> description: $description" {
def result = null def result = null
if (description.startsWith("Err 106")) { if (description.startsWith("Err 106")) {
log.debug "parse() >> Err 106" state.sec = 0
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
} else if (description != "updated") { } else if (description != "updated") {
log.debug "parse() >> zwave.parse(description)"
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
if (cmd) { if (cmd) {
result = zwaveEvent(cmd) result = zwaveEvent(cmd)
} }
} }
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" log.debug "Parsed '${description}' to ${result.inspect()}"
return result return result
} }
//this notification will be sent only when device is battery powered def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { {
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
def cmds = []
if (!isConfigured()) { if (!isConfigured()) {
// we're still in the process of configuring a newly joined device
log.debug("late configure") log.debug("late configure")
result << response(configure()) result += response(configure())
} else { } else {
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
result << response(cmds)
} }
result result
} }
@@ -189,29 +149,10 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat
} }
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" response(configure())
state.sec = 1
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
state.sec = 1
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)]
result
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
} }
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def result = []
def map = [ name: "battery", unit: "%" ] def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) { if (cmd.batteryLevel == 0xFF) {
map.value = 1 map.value = 1
@@ -221,14 +162,11 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
map.value = cmd.batteryLevel map.value = cmd.batteryLevel
} }
state.lastbatt = now() state.lastbatt = now()
result << createEvent(map) createEvent(map)
if (device.latestValue("powerSupply") != "USB Cable"){
result << createEvent(name: "batteryStatus", value: "${map.value} % battery", displayed: false)
}
result
} }
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
{
def map = [:] def map = [:]
switch (cmd.sensorType) { switch (cmd.sensorType) {
case 1: case 1:
@@ -270,6 +208,7 @@ def motionEvent(value) {
} }
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
setConfigured()
motionEvent(cmd.sensorValue) motionEvent(cmd.sensorValue)
} }
@@ -286,112 +225,47 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
result << createEvent(name: "tamper", value: "clear", displayed: false) result << createEvent(name: "tamper", value: "clear", displayed: false)
break break
case 3: case 3:
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved")
break break
case 7: case 7:
result << motionEvent(1) result << motionEvent(1)
break break
} }
} else { } else {
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
result << createEvent(descriptionText: cmd.toString(), isStateChange: false) result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
} }
result result
} }
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
log.debug "ConfigurationReport: $cmd"
def result = []
def value
if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) {
value = "USB Cable"
if (!isConfigured()) {
log.debug("ConfigurationReport: configuring device")
result << response(configure())
}
result << createEvent(name: "batteryStatus", value: value, displayed: false)
result << createEvent(name: "powerSupply", value: value, displayed: false)
}else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) {
value = "Battery"
result << createEvent(name: "powerSupply", value: value, displayed: false)
} else if (cmd.parameterNumber == 101){
result << response(configure())
}
result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) { def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.debug "General zwaveEvent cmd: ${cmd}"
createEvent(descriptionText: cmd.toString(), isStateChange: false) createEvent(descriptionText: cmd.toString(), isStateChange: false)
} }
def configure() { def configure() {
// This sensor joins as a secure device if you double-click the button to include it // This sensor joins as a secure device if you double-click the button to include it
log.debug "${device.displayName} is configuring its settings" if (device.device.rawDescription =~ /98/ && !state.sec) {
def request = [] log.debug "Multi 6 not sending configure until secure"
return []
}
log.debug "Multi 6 configure()"
def request = [
// send no-motion report 20 seconds after motion stops
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20),
//1. set association groups for hub // report every 8 minutes (threshold reports don't work on battery power)
request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId) // report automatically on threshold change
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
//2. automatic report flags
// param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports
request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1
request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2
//3. no-motion report x seconds after motion stops (default 20 secs)
request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20)
//4. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum
request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1,
scaledConfigurationValue:
motionSensitivity == "normal" ? 64 :
motionSensitivity == "maximum" ? 127 :
motionSensitivity == "minimum" ? 0 : 64)
//5. report every x minutes (threshold reports don't work on battery power, default 8 mins)
request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) //association group 1
request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2
//6. report automatically on threshold change
request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1)
//7. query sensor data
request << zwave.batteryV1.batteryGet()
request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex
setConfigured("true")
zwave.batteryV1.batteryGet(),
zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C),
]
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
} }
private def getTimeOptionValueMap() { [ private setConfigured() {
"20 seconds" : 20, updateDataValue("configured", "true")
"40 seconds" : 40,
"1 minute" : 60,
"2 minutes" : 2*60,
"3 minutes" : 3*60,
"4 minutes" : 4*60,
"5 minutes" : 5*60,
"8 minutes" : 8*60,
"15 minutes" : 15*60,
"30 minutes" : 30*60,
"1 hours" : 1*60*60,
"6 hours" : 6*60*60,
"12 hours" : 12*60*60,
"18 hours" : 6*60*60,
"24 hours" : 24*60*60,
]}
private setConfigured(configure) {
updateDataValue("configured", configure)
} }
private isConfigured() { private isConfigured() {
@@ -407,6 +281,5 @@ private command(physicalgraph.zwave.Command cmd) {
} }
private commands(commands, delay=200) { private commands(commands, delay=200) {
log.info "sending commands: ${commands}"
delayBetween(commands.collect{ command(it) }, delay) delayBetween(commands.collect{ command(it) }, delay)
} }

View File

@@ -38,63 +38,168 @@ metadata {
} }
// UI tile definitions // UI tile definitions
tiles(scale: 2) { tiles {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
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" standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
tileAttribute ("device.level", key: "SLIDER_CONTROL") { controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
attributeState "level", action:"switch level.setLevel" state "level", action:"switch level.setLevel"
} }
} valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "level", label: 'Level ${currentValue}%'
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" }
}
main "switch"
details(["switch", "refresh"]) main(["switch"])
} details(["switch", "level", "levelSliderControl", "refresh"])
}
} }
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.trace description
if (description?.startsWith("catchall:")) {
def msg = zigbee.parse(description)
log.trace msg
log.trace "data: $msg.data"
def resultMap = zigbee.getKnownDescription(description) if(description?.endsWith("0100") ||description?.endsWith("1001"))
if (resultMap) { {
log.info resultMap def result = createEvent(name: "switch", value: "on")
if (resultMap.type == "update") { log.debug "Parse returned ${result?.descriptionText}"
log.info "$device updates: ${resultMap.value}" return result
} }
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() { if(description?.endsWith("0000") || description?.endsWith("1000"))
zigbee.off() {
def result = createEvent(name: "switch", value: "off")
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
if (description?.startsWith("read attr")) {
log.debug description[-2..-1]
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
sendEvent( name: "level", value: i )
}
} }
def on() { def on() {
zigbee.on() log.debug "on()"
} sendEvent(name: "switch", value: "on")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
}
def off() {
log.debug "off()"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
def setLevel(value) {
zigbee.setLevel(value)
} }
def refresh() { def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() // Schedule poll every 1 min
//schedule("0 */1 * * * ?", poll)
//poll()
[
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0"
]
}
def setLevel(value) {
log.trace "setLevel($value)"
def cmds = []
if (value == 0) {
sendEvent(name: "switch", value: "off")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}"
}
else if (device.latestValue("switch") == "off") {
sendEvent(name: "switch", value: "on")
}
sendEvent(name: "level", value: value)
def level = hex(value * 255/100)
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
//log.debug cmds
cmds
} }
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() 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} ${endpointId} 1", "delay 1000",
//Level Control Reporting
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
]
return configCmds + refresh() // send refresh cmds as part of config
}
def uninstalled() {
log.debug "uninstalled()"
response("zcl rftd")
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}
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
} }

View File

@@ -55,136 +55,141 @@ metadata {
} }
} }
standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
} }
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
} }
main(["switch"]) main(["switch"])
details(["switch", "level", "indicator", "refresh"]) details(["switch", "refresh", "indicator"])
} }
} }
def parse(String description) { def parse(String description) {
def result = null def item1 = [
if (description != "updated") { canBeCurrentState: false,
log.debug "parse() >> zwave.parse($description)" linkText: getLinkText(device),
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) isStateChange: false,
if (cmd) { displayed: false,
result = zwaveEvent(cmd) descriptionText: description,
} value: description
]
def result
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
if (cmd) {
result = createEvent(cmd, item1)
} }
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { else {
result = [result, response(zwave.basicV1.basicGet())] item1.displayed = displayed(description, item1.isStateChange)
log.debug "Was hailed: requesting state update" result = [item1]
} else {
log.debug "Parse returned ${result?.descriptionText}"
} }
return result log.debug "Parse returned ${result?.descriptionText}"
result
} }
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
dimmerEvents(cmd) def result = doCreateEvent(cmd, item1)
} for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
dimmerEvents(cmd)
}
private dimmerEvents(physicalgraph.zwave.Command cmd) {
def value = (cmd.value ? "on" : "off")
def result = [createEvent(name: "switch", value: value)]
if (cmd.value && cmd.value <= 100) {
result << createEvent(name: "level", value: cmd.value, unit: "%")
} }
return result result
} }
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
[]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
[response(zwave.basicV1.basicGet())]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
for (int i = 0; i < result.size(); i++) {
result[i].type = "digital"
}
result
}
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
def result = [item1]
item1.name = "switch"
item1.value = cmd.value ? "on" : "off"
item1.handlerName = item1.value
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
item1.canBeCurrentState = true
item1.isStateChange = isStateChange(device, item1.name, item1.value)
item1.displayed = item1.isStateChange
if (cmd.value >= 5) {
def item2 = new LinkedHashMap(item1)
item2.name = "level"
item2.value = cmd.value as String
item2.unit = "%"
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
item2.canBeCurrentState = true
item2.isStateChange = isStateChange(device, item2.name, item2.value)
item2.displayed = false
result << item2
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
log.debug "ConfigurationReport $cmd"
def value = "when off" def value = "when off"
if (cmd.configurationValue[0] == 1) {value = "when on"} if (cmd.configurationValue[0] == 1) {value = "when on"}
if (cmd.configurationValue[0] == 2) {value = "never"} if (cmd.configurationValue[0] == 2) {value = "never"}
createEvent([name: "indicatorStatus", value: value]) [name: "indicatorStatus", value: value, display: false]
} }
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { def createEvent(physicalgraph.zwave.Command cmd, Map map) {
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) // Handles any Z-Wave commands we aren't interested in
} log.debug "UNHANDLED COMMAND $cmd"
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
[:]
} }
def on() { def on() {
delayBetween([ log.info "on"
zwave.basicV1.basicSet(value: 0xFF).format(), delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
} }
def off() { def off() {
delayBetween([ delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
} }
def setLevel(value) { def setLevel(value) {
log.debug "setLevel >> value: $value"
def valueaux = value as Integer def valueaux = value as Integer
def level = Math.max(Math.min(valueaux, 99), 0) def level = Math.min(valueaux, 99)
if (level > 0) {
sendEvent(name: "switch", value: "on")
} else {
sendEvent(name: "switch", value: "off")
}
sendEvent(name: "level", value: level, unit: "%")
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
} }
def setLevel(value, duration) { def setLevel(value, duration) {
log.debug "setLevel >> value: $value, duration: $duration"
def valueaux = value as Integer def valueaux = value as Integer
def level = Math.max(Math.min(valueaux, 99), 0) def level = Math.min(valueaux, 99)
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000 zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay)
} }
def poll() { def poll() {
@@ -192,27 +197,21 @@ def poll() {
} }
def refresh() { def refresh() {
log.debug "refresh() is called" zwave.switchMultilevelV1.switchMultilevelGet().format()
def commands = []
commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
if (getDataValue("MSR") == null) {
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
delayBetween(commands,100)
} }
def indicatorWhenOn() { def indicatorWhenOn() {
sendEvent(name: "indicatorStatus", value: "when on") sendEvent(name: "indicatorStatus", value: "when on", display: false)
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
} }
def indicatorWhenOff() { def indicatorWhenOff() {
sendEvent(name: "indicatorStatus", value: "when off") sendEvent(name: "indicatorStatus", value: "when off", display: false)
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
} }
def indicatorNever() { def indicatorNever() {
sendEvent(name: "indicatorStatus", value: "never") sendEvent(name: "indicatorStatus", value: "never", display: false)
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
} }

View File

@@ -50,7 +50,7 @@ metadata {
capability "Switch Level" capability "Switch Level"
capability "Polling" 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" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
} }
// UI tile definitions // UI tile definitions

View File

@@ -26,6 +26,8 @@ metadata {
capability "Actuator" capability "Actuator"
capability "Sensor" capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857"
} }
// simulator metadata // simulator metadata

View File

@@ -26,6 +26,8 @@ metadata {
capability "Actuator" capability "Actuator"
capability "Sensor" capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856"
} }
// simulator metadata // simulator metadata

View File

@@ -1,81 +0,0 @@
/**
* Logitech Harmony Activity
*
* Copyright 2015 Juan Risso
*
* 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.
*
*/
metadata {
definition (name: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
capability "Switch"
capability "Actuator"
capability "Refresh"
command "huboff"
command "alloff"
command "refresh"
}
// simulator metadata
simulator {
}
// UI tile definitions
tiles {
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on"
state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off"
}
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon"
}
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off"
}
main "button"
details(["button", "refresh", "forceoff", "huboff", "alloff"])
}
}
def parse(String description) {
}
def on() {
sendEvent(name: "switch", value: "on")
log.trace parent.activity(device.deviceNetworkId,"start")
}
def off() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"end")
}
def huboff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"hub")
}
def alloff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity("all","end")
}
def refresh() {
log.debug "Executing 'refresh'"
log.trace parent.poll()
}

View File

@@ -5,8 +5,6 @@
that issue by using state variables that issue by using state variables
*/ */
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata { metadata {
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") { definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
@@ -24,6 +22,9 @@ metadata {
attribute "heartbeat", "string" attribute "heartbeat", "string"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White"
} }
// simulator metadata // simulator metadata

View File

@@ -11,9 +11,6 @@
* for the specific language governing permissions and limitations under the License. * for the specific language governing permissions and limitations under the License.
* *
*/ */
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata { metadata {
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") { definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
capability "Switch Level" capability "Switch Level"

View File

@@ -1,5 +1,5 @@
metadata { metadata {
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") { definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") {
capability "Color Control" capability "Color Control"
} }

View File

@@ -15,8 +15,6 @@
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart
* *
*/ */
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata { metadata {
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") { definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
@@ -27,6 +25,7 @@ metadata {
capability "Switch" capability "Switch"
capability "Switch Level" capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019"
} }
// simulator metadata // simulator metadata

View File

@@ -25,8 +25,6 @@ metadata {
capability "Refresh" capability "Refresh"
capability "Sensor" capability "Sensor"
attribute "currentIP", "string"
command "subscribe" command "subscribe"
command "resubscribe" command "resubscribe"
command "unsubscribe" command "unsubscribe"
@@ -36,36 +34,21 @@ metadata {
// simulator metadata // simulator metadata
simulator {} simulator {}
// UI tile definitions // UI tile definitions
tiles(scale: 2) { tiles {
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){ standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
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" standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
} state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { }
attributeState "currentIP", label: ''
}
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { main "switch"
state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" details (["switch", "refresh"])
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 // parse events into attributes
@@ -85,7 +68,6 @@ def parse(String description) {
def result = [] def result = []
def bodyString = msg.body def bodyString = msg.body
if (bodyString) { if (bodyString) {
unschedule("setOffline")
def body = new XmlSlurper().parseText(bodyString) def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) { if (body?.property?.TimeSyncRequest?.text()) {
@@ -96,14 +78,13 @@ def parse(String description) {
} else if (body?.property?.BinaryState?.text()) { } else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "Notify: BinaryState = ${value}" log.trace "Notify: BinaryState = ${value}"
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}") result << createEvent(name: "switch", value: value)
} else if (body?.property?.TimeZoneNotification?.text()) { } else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "GetBinaryResponse: BinaryState = ${value}" log.trace "GetBinaryResponse: BinaryState = ${value}"
def dispaux = device.currentValue("switch") != value result << createEvent(name: "switch", value: value)
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
} }
} }
@@ -120,6 +101,14 @@ 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)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() { private getHostAddress() {
def ip = getDataValue("ip") def ip = getDataValue("ip")
def port = getDataValue("port") def port = getDataValue("port")
@@ -206,8 +195,6 @@ def subscribe(ip, port) {
if (ip && ip != existingIp) { if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip" 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) { if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port" log.debug "Updating port from $existingPort to $port"
@@ -272,8 +259,6 @@ User-Agent: CyberGarage-HTTP/1.0
def poll() { def poll() {
log.debug "Executing 'poll'" log.debug "Executing 'poll'"
if (device.currentValue("currentIP") != "Offline")
runIn(10, setOffline)
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277 Content-Length: 277
@@ -289,15 +274,3 @@ User-Agent: CyberGarage-HTTP/1.0
</s:Body> </s:Body>
</s:Envelope>""", physicalgraph.device.Protocol.LAN) </s:Envelope>""", 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(".")
}

View File

@@ -21,8 +21,6 @@
capability "Refresh" capability "Refresh"
capability "Sensor" capability "Sensor"
attribute "currentIP", "string"
command "subscribe" command "subscribe"
command "resubscribe" command "resubscribe"
command "unsubscribe" command "unsubscribe"
@@ -33,30 +31,17 @@
} }
// UI tile definitions // UI tile definitions
tiles(scale: 2) { tiles {
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) { standardTile("motion", "device.motion", width: 2, height: 2) {
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000") }
standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
} }
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "motion" main "motion"
details (["rich-control", "refresh"]) details (["motion", "refresh"])
} }
} }
@@ -77,7 +62,6 @@ def parse(String description) {
def result = [] def result = []
def bodyString = msg.body def bodyString = msg.body
if (bodyString) { if (bodyString) {
unschedule("setOffline")
def body = new XmlSlurper().parseText(bodyString) def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) { if (body?.property?.TimeSyncRequest?.text()) {
@@ -88,7 +72,7 @@ def parse(String description) {
} else if (body?.property?.BinaryState?.text()) { } else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive" def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
log.debug "Notify - BinaryState = ${value}" log.debug "Notify - BinaryState = ${value}"
result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}") result << createEvent(name: "motion", value: value)
} else if (body?.property?.TimeZoneNotification?.text()) { } else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
} }
@@ -107,6 +91,14 @@ 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)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() { private getHostAddress() {
def ip = getDataValue("ip") def ip = getDataValue("ip")
def port = getDataValue("port") def port = getDataValue("port")
@@ -133,8 +125,6 @@ def refresh() {
//////////////////////////// ////////////////////////////
def getStatus() { def getStatus() {
log.debug "Executing WeMo Motion '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 new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277 Content-Length: 277
@@ -175,9 +165,7 @@ def subscribe(ip, port) {
def existingPort = getDataValue("port") def existingPort = getDataValue("port")
if (ip && ip != existingIp) { if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip" 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) { if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port" log.debug "Updating port from $existingPort to $port"
@@ -238,15 +226,3 @@ User-Agent: CyberGarage-HTTP/1.0
</s:Envelope> </s:Envelope>
""", physicalgraph.device.Protocol.LAN) """, 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(".")
}

View File

@@ -10,142 +10,120 @@
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License. * for the specific language governing permissions and limitations under the License.
* *
* Wemo Switch * Wemo Switch
* *
* Author: Juan Risso (SmartThings) * Author: superuser
* Date: 2015-10-11 * Date: 2013-10-11
*/ */
metadata { metadata {
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
capability "Actuator" capability "Actuator"
capability "Switch" capability "Switch"
capability "Polling" capability "Polling"
capability "Refresh" capability "Refresh"
capability "Sensor" capability "Sensor"
attribute "currentIP", "string" command "subscribe"
command "resubscribe"
command "unsubscribe"
}
command "subscribe" // simulator metadata
command "resubscribe" simulator {}
command "unsubscribe"
}
// simulator metadata // UI tile definitions
simulator {} 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"
}
// UI tile definitions main "switch"
tiles(scale: 2) { details (["switch", "refresh"])
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 // parse events into attributes
def parse(String description) { def parse(String description) {
log.debug "Parsing '${description}'" log.debug "Parsing '${description}'"
def msg = parseLanMessage(description) def msg = parseLanMessage(description)
def headerString = msg.header def headerString = msg.header
if (headerString?.contains("SID: uuid:")) { if (headerString?.contains("SID: uuid:")) {
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
sid -= "SID: uuid:".trim() sid -= "SID: uuid:".trim()
updateDataValue("subscriptionId", sid) updateDataValue("subscriptionId", sid)
} }
def result = [] def result = []
def bodyString = msg.body def bodyString = msg.body
if (bodyString) { if (bodyString) {
unschedule("setOffline") def body = new XmlSlurper().parseText(bodyString)
def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) { if (body?.property?.TimeSyncRequest?.text()) {
log.trace "Got TimeSyncRequest" log.trace "Got TimeSyncRequest"
result << timeSyncResponse() result << timeSyncResponse()
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
} else if (body?.property?.BinaryState?.text()) { } else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}" log.trace "Notify: BinaryState = ${value}"
def dispaux = device.currentValue("switch") != value result << createEvent(name: "switch", value: value)
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) } else if (body?.property?.TimeZoneNotification?.text()) {
} else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" log.trace "GetBinaryResponse: BinaryState = ${value}"
log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}" result << createEvent(name: "switch", value: value)
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}") result
}
def dispaux2 = device.currentValue("switch") != value
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2)
}
}
result
} }
private getTime() { private getTime() {
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
((new GregorianCalendar().time.time / 1000l).toInteger()).toString() ((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
} }
private getCallBackAddress() { private getCallBackAddress() {
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
} }
private Integer convertHexToInt(hex) { private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16) Integer.parseInt(hex,16)
} }
private String convertHexToIP(hex) { 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() { private getHostAddress() {
def ip = getDataValue("ip") def ip = getDataValue("ip")
def port = getDataValue("port") def port = getDataValue("port")
if (!ip || !port) {
def parts = device.deviceNetworkId.split(":") if (!ip || !port) {
if (parts.length == 2) { def parts = device.deviceNetworkId.split(":")
ip = parts[0] if (parts.length == 2) {
port = parts[1] ip = parts[0]
} else { port = parts[1]
log.warn "Can't figure out ip and port for device: ${device.id}" } 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) log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
return convertHexToIP(ip) + ":" + convertHexToInt(port)
} }
def on() { def on() {
log.debug "Executing 'on'" log.debug "Executing 'on'"
sendEvent(name: "switch", value: "on")
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
Host: ${getHostAddress()} Host: ${getHostAddress()}
@@ -155,16 +133,17 @@ Content-Length: 333
<?xml version="1.0"?> <?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body> <SOAP-ENV:Body>
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1"> <m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<BinaryState>1</BinaryState> <BinaryState>1</BinaryState>
</m:SetBinaryState> </m:SetBinaryState>
</SOAP-ENV:Body> </SOAP-ENV:Body>
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN) </SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
} }
def off() { def off() {
log.debug "Executing 'off'" log.debug "Executing 'off'"
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 sendEvent(name: "switch", value: "off")
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
Host: ${getHostAddress()} Host: ${getHostAddress()}
Content-Type: text/xml Content-Type: text/xml
@@ -173,13 +152,36 @@ Content-Length: 333
<?xml version="1.0"?> <?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body> <SOAP-ENV:Body>
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1"> <m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<BinaryState>0</BinaryState> <BinaryState>0</BinaryState>
</m:SetBinaryState> </m:SetBinaryState>
</SOAP-ENV:Body> </SOAP-ENV:Body>
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN) </SOAP-ENV:Envelope>""", 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
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
</u:GetBinaryState>
</s:Body>
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
}*/
def refresh() {
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
[subscribe(), timeSyncResponse(), poll()]
}
def subscribe(hostAddress) { def subscribe(hostAddress) {
log.debug "Executing 'subscribe()'" log.debug "Executing 'subscribe()'"
def address = getCallBackAddress() def address = getCallBackAddress()
@@ -198,30 +200,27 @@ def subscribe() {
subscribe(getHostAddress()) subscribe(getHostAddress())
} }
def refresh() {
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
[subscribe(), timeSyncResponse(), poll()]
}
def subscribe(ip, port) { def subscribe(ip, port) {
def existingIp = getDataValue("ip") def existingIp = getDataValue("ip")
def existingPort = getDataValue("port") def existingPort = getDataValue("port")
if (ip && ip != existingIp) { if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip" 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"
updateDataValue("port", port)
} }
if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port"
updateDataValue("port", port)
}
subscribe("${ip}:${port}") subscribe("${ip}:${port}")
} }
////////////////////////////
def resubscribe() { def resubscribe() {
log.debug "Executing 'resubscribe()'" log.debug "Executing 'resubscribe()'"
def sid = getDeviceDataByName("subscriptionId")
def sid = getDeviceDataByName("subscriptionId")
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
HOST: ${getHostAddress()} HOST: ${getHostAddress()}
SID: uuid:${sid} SID: uuid:${sid}
@@ -229,11 +228,12 @@ TIMEOUT: Second-5400
""", physicalgraph.device.Protocol.LAN) """, physicalgraph.device.Protocol.LAN)
} }
////////////////////////////
def unsubscribe() { def unsubscribe() {
def sid = getDeviceDataByName("subscriptionId") def sid = getDeviceDataByName("subscriptionId")
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
HOST: ${getHostAddress()} HOST: ${getHostAddress()}
SID: uuid:${sid} SID: uuid:${sid}
@@ -242,7 +242,7 @@ SID: uuid:${sid}
""", physicalgraph.device.Protocol.LAN) """, physicalgraph.device.Protocol.LAN)
} }
////////////////////////////
//TODO: Use UTC Timezone //TODO: Use UTC Timezone
def timeSyncResponse() { def timeSyncResponse() {
log.debug "Executing 'timeSyncResponse()'" log.debug "Executing 'timeSyncResponse()'"
@@ -267,15 +267,9 @@ User-Agent: CyberGarage-HTTP/1.0
""", physicalgraph.device.Protocol.LAN) """, 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() { def poll() {
log.debug "Executing 'poll'" log.debug "Executing 'poll'"
if (device.currentValue("currentIP") != "Offline")
runIn(10, setOffline)
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277 Content-Length: 277

View File

@@ -24,9 +24,6 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -23,9 +23,6 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -32,13 +32,7 @@
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt" manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever Lock" manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale YRL220 Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
} }
tiles(scale: 2) { tiles(scale: 2) {
@@ -91,24 +85,11 @@ def uninstalled() {
} }
def configure() { def configure() {
/*
def cmds = def cmds =
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}", zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
"${TYPE_ENUM8}", 0, 3600, "{01}") + "${TYPE_ENUM8}", 0, 3600, "{01}") +
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}", zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
"${TYPE_U8}", 600, 21600, "{01}") "${TYPE_U8}", 3600, 3600, "{01}")
*/
def zigbeeId = device.zigbeeId
def cmds =
[
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
]
log.info "configure() --- cmds: $cmds" log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config return cmds + refresh() // send refresh cmds as part of config
} }
@@ -138,15 +119,13 @@ def parse(String description) {
def lock() { def lock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}") def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
log.info "lock() -- cmds: $cmds" log.info "lock() -- cmds: $cmds"
//return cmds return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
} }
def unlock() { def unlock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}") def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
log.info "unlock() -- cmds: $cmds" log.info "unlock() -- cmds: $cmds"
//return cmds return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
} }
// Private methods // Private methods
@@ -160,10 +139,8 @@ private Map parseReportAttributeMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery" resultMap.name = "battery"
resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) // BatteryPercentageRemaining is specified in .5% increments
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting resultMap.value = Integer.parseInt(descMap.value, 16) / 2
resultMap.value = Integer.parseInt(descMap.value, 16)
}
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}" log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
} }
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {

View File

@@ -23,8 +23,6 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -1,134 +0,0 @@
/**
* Copyright 2015 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:
*
* 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.
*
* ZigBee White Color Temperature Bulb
*
* Author: SmartThings
* Date: 2015-09-22
*/
metadata {
definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Color Temperature"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
attribute "colorName", "string"
command "setGenericName"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
}
// UI tile definitions
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"
}
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
attributeState "colorName", label:'${currentValue}'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getKnownDescription(description)
if (finalResult) {
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
}
def setColorTemperature(value) {
setGenericName(value)
zigbee.setColorTemperature(value)
}
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
def setGenericName(value){
if (value != null) {
def genericName = "White"
if (value < 3300) {
genericName = "Soft White"
} else if (value < 4150) {
genericName = "Moonlight"
} else if (value <= 5000) {
genericName = "Cool White"
} else if (value >= 5000) {
genericName = "Daylight"
}
sendEvent(name: "colorName", value: genericName)
}
}

View File

@@ -0,0 +1,48 @@
/**
* Switch Too
*
* Copyright 2015 Bob Florian
*
* 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.
*
*/
metadata {
definition (name: "Switch Too", author: "Bob Florian") {
capability "Switch"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
// TODO: define your main and details tiles here
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle 'switch' attribute
}
// handle commands
def on() {
log.debug "Executing 'on'"
// TODO: handle 'on' command
}
def off() {
log.debug "Executing 'off'"
// TODO: handle 'off' command
}

View File

@@ -1,219 +0,0 @@
/**
* ObyThing Music Connect
*
* Copyright 2015 obycode
*
*/
definition(
name: "obything Connect",
namespace: "com.obycode",
author: "obycode",
description: "Smart home, smart Mac. With obything.",
category: "Fun & Social",
iconUrl: "http://obything.obycode.com/icons/icon60.png",
iconX2Url: "http://obything.obycode.com/icons/icon120.png",
iconX3Url: "http://obything.obycode.com/icons/icon120.png",
oauth: [displayName: "obything", displayLink: "http://obything.obycode.com"])
preferences {
section("Smart home, smart Mac. With obything.") {
app(name: "childApps", appName: "obything Notify with Sound", namespace: "com.obycode", title: "Notify with Sound", multiple: true)
app(name: "childApps", appName: "obything Music Control", namespace: "com.obycode", title: "Music Control", multiple: true)
app(name: "childApps", appName: "obything Trigger Playlist", namespace: "com.obycode", title: "Trigger Playlists", multiple: true)
app(name: "childApps", appName: "obything Weather Forecast", namespace: "com.obycode", title: "Weather Forecast", multiple: true)
}
}
mappings {
path("/setup") {
action: [
POST: "setup",
]
}
path("/:uuid/:kind") {
action: [
POST: "createChild",
PUT: "updateChild",
]
}
}
def installed() {
log.debug "Installed"
}
def updated() {
log.debug "Updated"
unsubscribe()
}
def uninstalled() {
logout()
}
def logout() {
revokeAccessToken()
}
// mapping handlers
// /setup POST
def setup() {
def ip = request.JSON?.ip
if (ip == null) {
return httpError(400, "IP not specified")
}
def port = request.JSON?.port
if (port == null) {
return httpError(400, "Port not specified")
}
def name = request.JSON?.name
if (name == null) {
return httpError(400, "Name not specified")
}
def uuid = request.JSON?.uuid
if (uuid == null) {
return httpError(400, "UUID not specified")
}
// If machines is not initialized yet, set it up
if (state.machines == null) {
state.machines = [:]
}
// If this machine has already been initialized, just update it
if (state.machines[uuid]) {
state.machines[uuid]["ip"] = ip
state.machines[uuid]["port"] = port
state.machines[uuid]["name"] = name
log.debug "Updated machine"
def dead = []
// Update the child devices
def newHexIP = convertIPtoHex(ip)
state.machines[uuid]["children"].keySet().each {
def ids = state.machines[uuid]["children"][it]
def child = getChildDevice(ids.dni)
if (!child) {
dead.add(it)
}
else {
// Only change the IP; the label could've been manually changed and I'm
// not sure how to handle the port changing (its not allowed now anyway).
def oldHexPort = child.deviceNetworkId.split(':')[1]
def newDNI = "$newHexIP:$oldHexPort"
child.deviceNetworkId = newDNI
state.machines[uuid]["children"][it]["dni"] = newDNI
}
}
dead.each {
state.machines[uuid]["children"].remove(it)
}
}
// Otherwise, just create a new machine
else {
def machine = [ip:ip, port:port, name:name, children:[:]]
state.machines[uuid] = machine
log.debug "Added new machine"
}
sendCommand(state.machines[uuid], "/ping")
}
private removeChildDevices(delete) {
delete.each {
deleteChildDevice(it.deviceNetworkId)
}
}
// /:uuid/:kind POST
def createChild() {
// Constants for the port offsets
final int iTunesService = 1
final int pandoraService = 2
final int spotifyService = 3
def machine = state.machines[params.uuid]
if (machine == null) {
return httpError(404, "Machine not found")
}
def childName = machine["name"]
def portNum = machine["port"].toInteger()
switch (params.kind) {
case "itunes":
childName = childName + " iTunes"
portNum += iTunesService
break
case "pandora":
childName = childName + " Pandora"
portNum += pandoraService
break
case "spotify":
childName = childName + " Spotify"
portNum += spotifyService
break
default:
return httpError(400, "Unknown or unspecified device type")
}
def hexIP = convertIPtoHex(machine["ip"])
def hexPort = convertPortToHex(portNum.toString())
def childId = "$hexIP:$hexPort"
// If this child already exists, re-associate with it
def existing = getChildDevice(childId)
if (existing) {
log.debug "Found existing device: $existing"
state.machines[params.uuid]["children"][params.kind] = [id:existing.id, dni:childId]
}
// otherwise, create it
else {
def d = addChildDevice("com.obycode", "obything Music Player", childId, location.hubs[0].id, [name:"obything Music Player", label:childName, completedSetup:true])
log.debug "Created child device: $d"
state.machines[params.uuid]["children"][params.kind] = [id:d.id, dni:d.deviceNetworkId]
}
return [dni:childId]
}
// /:uuid/:kind PUT
def updateChild() {
def machine = state.machines[params.uuid]
if (machine == null) {
return httpError(404, "Machine not found")
}
def child = machine["children"][params.kind]
if (child == null) {
return httpError(404, "Device not found")
}
def device = getChildDevice(child.dni)
if (device == null) {
return httpError(404, "Device not found")
}
device.update(request.JSON)
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02X', it.toInteger() ) }.join()
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format( '%04X', port.toInteger() )
return hexport
}
private void sendCommand(machine, path, command = null) {
def fullPath = path
if (command) {
fullPath = fullPath + "?" + command
}
sendHubCommand(new physicalgraph.device.HubAction("GET " + fullPath + """ HTTP/1.1\r\nHOST: """ + machine["ip"] + ":" + machine["port"] + """\r\n\r\n""", physicalgraph.device.Protocol.LAN))
}

View File

@@ -20,8 +20,7 @@ definition(
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.", description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png", iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png", iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png")
singleInstance: true)
preferences { preferences {

View File

@@ -1,306 +0,0 @@
/**
* Music Control
*
* Author: obything
*
* Date: 2015-11-09
*/
definition(
name: "obything Music Control",
namespace: "com.obycode",
author: "obycode, based on Music Control by SmartThings",
description: "Play or pause your music when certain actions take place in your home.",
category: "Convenience",
iconUrl: "http://obything.obycode.com/icons/obything-device.png",
iconX2Url: "http://obything.obycode.com/icons/obything-device.png"
)
preferences {
page(name: "mainPage", title: "Control your music when something happens", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("When..."){
ifSet "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", 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 "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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", 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("Perform this action"){
input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
"Play",
"Stop Playing",
"Toggle Play/Pause",
"Skip to Next Track",
"Play Previous Track"
]
}
section {
input "obything", "capability.musicPlayer", title: "obything Music player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
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"]
if (settings.modes) {
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","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","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() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
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(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", 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) {
takeAction(evt)
}
else {
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
}
}
else {
takeAction(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) {
takeAction(evt)
}
private takeAction(evt) {
log.debug "takeAction($actionType)"
def options = [:]
if (volume) {
obything.setLevel(volume as Integer)
options.delay = 1000
}
switch (actionType) {
case "Play":
obything.play()
break
case "Stop Playing":
obything.pause()
break
case "Toggle Play/Pause":
def currentStatus = obything.currentValue("status")
log.debug "Current status is $currentStatus"
if (currentStatus == "playing") {
obything.pause()
}
else {
obything.play()
}
break
case "Skip to Next Track":
obything.nextTrack()
break
case "Play Previous Track":
obything.previousTrack()
break
default:
log.error "Action type '$actionType' not defined"
}
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") : ""
}
// TODO - End Centralize

View File

@@ -1,410 +0,0 @@
/**
* obything Notify with Sound
*
* Author: obycode
* Date: 2015-08-30
*/
import groovy.json.JsonSlurper
definition(
name: "obything Notify with Sound",
namespace: "com.obycode",
author: "obycode, based on 'Sonos Notify with Sound' by SmartThings",
description: "Play a sound or custom message through your Mac with obything when the mode changes or other events occur.",
category: "Convenience",
iconUrl: "http://obything.obycode.com/icons/obything-device.png",
iconX2Url: "http://obything.obycode.com/icons/obything-device.png",
parent: "com.obycode:obything Connect"
)
preferences {
page(name: "mainPage", title: "Play a message over your speakers when something happens", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a playlist")
page(name: "chooseSpeakers", title: "Select speakers")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("Play message when"){
ifSet "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", 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
}
}
def hideable = anythingSet || app.installationState == "COMPLETE"
def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..."
section(sectionTitle, hideable: hideable, hidden: true){
ifUnset "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", 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", description: "Select mode(s)", required: false, multiple: true
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section{
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [
"Custom Message",
"Custom URL",
"Bell 1",
"Bell 2",
"Dogs Barking",
"Fire Alarm",
"The mail has arrived",
"A door opened",
"There is motion",
"Smartthings detected a flood",
"Smartthings detected smoke",
"Someone is arriving",
"Piano",
"Lightsaber"]
input "message","text",title:"Play this message", required:false, multiple: false
input "url","text",title:"Play a sound at this URL", required:false, multiple: false
}
section {
input "obything", "capability.musicPlayer", title: "On this obything iTunes device", required: true
}
section {
href "chooseSpeakers", title: "With these speakers", description: speakers ? speakers : "Tap to set", state: speakers ? "complete" : "incomplete"
}
section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
href "chooseTrack", title: "Or play this music or radio station", description: playlist ? playlist : "Tap to set", state: playlist ? "complete" : "incomplete"
input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
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"]
if (settings.modes) {
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)", required: false
}
}
}
def chooseTrack() {
dynamicPage(name: "chooseTrack") {
section{
input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions()
}
}
}
private playlistOptions() {
def playlistString = obything.currentValue("playlists")
log.debug "Playlists are $playlistString"
def jsonList = new JsonSlurper().parseText(playlistString)
jsonList.collect {
it.Name
}
}
def chooseSpeakers() {
dynamicPage(name: "chooseSpeakers") {
section{
input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions()
}
}
}
private speakerOptions() {
def speakersString = obything.currentValue("speakers")
log.debug "Speakers are $speakersString"
def slurper = new JsonSlurper()
slurper.parseText(speakersString)
}
private anythingSet() {
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","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() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
subscribeToEvents()
}
def 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(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", eventHandler)
subscribe(button1, "button.pushed", eventHandler)
if (triggerModes) {
subscribe(location, modeChangeHandler)
}
if (timeOfDay) {
runDaily(timeOfDay, scheduledTimeHandler)
}
loadText()
}
def eventHandler(evt) {
log.trace "eventHandler($evt?.name: $evt?.value)"
if (allOk) {
log.trace "allOk"
def lastTime = state[frequencyKey(evt)]
if (oncePerDayOk(lastTime)) {
if (frequency) {
if (lastTime == null || now() - lastTime >= frequency * 60000) {
takeAction(evt)
}
else {
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
}
}
else {
takeAction(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) {
takeAction(evt)
}
private takeAction(evt) {
log.trace "takeAction()"
def speakerString
if (speakers) {
speakerString = ""
speakers.each {
speakerString += "\"$it\","
}
// remove the last comma and encode
speakerString = encode(speakerString[0..-2])
}
if (playlist) {
obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying, playlist)
}
else {
obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying)
}
if (frequency || oncePerDay) {
state[frequencyKey(evt)] = now()
}
log.trace "Exiting takeAction()"
}
private frequencyKey(evt) {
"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 getTimeLabel()
{
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
// TODO - End Centralize
private loadText() {
switch ( actionType) {
case "Bell 1":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
break;
case "Bell 2":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"]
break;
case "Dogs Barking":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"]
break;
case "Fire Alarm":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"]
break;
case "The mail has arrived":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"]
break;
case "A door opened":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"]
break;
case "There is motion":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"]
break;
case "Smartthings detected a flood":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"]
break;
case "Smartthings detected smoke":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"]
break;
case "Someone is arriving":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"]
break;
case "Piano":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"]
break;
case "Lightsaber":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
break;
case "Custom Message":
if (message) {
log.debug "message is $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;
case "Custom URL":
if (url) {
state.sound = [uri: url, duration: "0"]
}
else {
state.sound = textToSpeech("You selected the custom URL option but did not enter a URL in the $app.label Smart App")
}
break;
default:
log.debug "Invalid selection."
break;
}
}

View File

@@ -1,313 +0,0 @@
/**
* obything Trigger Playlist
*
* Author: obycode
* Date: 2015-11-05
*/
import groovy.json.JsonSlurper
definition(
name: "obything Trigger Playlist",
namespace: "com.obycode",
author: "obycode, based on Sonos Mood Music by SmartThings",
description: "Plays a selected playlist on your Mac with obything",
category: "Convenience",
iconUrl: "http://obything.obycode.com/icons/obything-device.png",
iconX2Url: "http://obything.obycode.com/icons/obything-device.png"
)
preferences {
page(name: "mainPage", title: "Choose the trigger(s)", nextPage: "chooseTrackAndSpeakers", uninstall: true)
page(name: "chooseTrackAndSpeakers", title: "Choose the playlist and speaker(s)", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("Play music when..."){
ifSet "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
}
def hideable = anythingSet || app.installationState == "COMPLETE"
def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
section(sectionTitle, hideable: hideable, hidden: true){
ifUnset "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section {
input "obything", "capability.musicPlayer", title: "On this obything music player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume", description: "0-100%", required: false
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: ["Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
if (settings.modes) {
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
}
}
}
def chooseTrackAndSpeakers() {
dynamicPage(name: "chooseTrackAndSpeakers") {
section {
input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions()
input "speakers", "enum", title:"On these speakers", required:false, multiple: true, options: speakerOptions()
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
mode title: "Set for specific mode(s)", required: false
}
}
}
private playlistOptions() {
def playlistString = obything.currentValue("playlists")
log.debug "Playlists are $playlistString"
def jsonList = new JsonSlurper().parseText(playlistString)
log.debug("jsonList is $jsonList")
jsonList.collect {
it.name
}
}
def chooseSpeakers() {
dynamicPage(name: "chooseSpeakers") {
section{
input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions()
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
mode title: "Set for specific mode(s)", required: false
}
}
}
private speakerOptions() {
def speakersString = obything.currentValue("speakers")
log.debug "Speakers are $speakersString"
def slurper = new JsonSlurper()
slurper.parseText(speakersString)
}
private anythingSet() {
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","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() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
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(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", eventHandler)
subscribe(button1, "button.pushed", eventHandler)
if (triggerModes) {
subscribe(location, modeChangeHandler)
}
if (timeOfDay) {
runDaily(timeOfDay, scheduledTimeHandler)
}
}
def eventHandler(evt) {
log.debug "In eventHandler"
if (allOk) {
if (frequency) {
def lastTime = state[frequencyKey(evt)]
if (lastTime == null || now() - lastTime >= frequency * 60000) {
takeAction(evt)
}
}
else {
takeAction(evt)
}
}
}
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) {
takeAction(evt)
}
private takeAction(evt) {
log.info "Playing '$playlist'"
def speakerString
if (speakers) {
speakerString = ""
speakers.each {
speakerString += "\"$it\","
}
// remove the last comma and encode
speakerString = encode(speakerString[0..-2])
}
obything.playPlaylist(encode(playlist), speakerString, volume)
if (frequency || oncePerDay) {
state[frequencyKey(evt)] = now()
}
}
private frequencyKey(evt) {
"lastActionTimeStamp"
}
private encode(text) {
return URLEncoder.encode(text).replaceAll("\\+", "%20")
}
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 = 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") : ""
}

View File

@@ -1,405 +0,0 @@
/**
* obything Weather Forecast
*
* Author: obycode
* Date: 2014-10-13
*/
import groovy.json.JsonSlurper
definition(
name: "obything Weather Forecast",
namespace: "com.obycode",
author: "obycode, based on Sonos Weather Forecast by SmartThings",
description: "Play a weather report through your Mac or AirPlay speakers when the mode changes or other events occur",
category: "Convenience",
iconUrl: "http://obything.obycode.com/icons/obything-device.png",
iconX2Url: "http://obything.obycode.com/icons/obything-device.png"
)
preferences {
page(name: "mainPage", title: "Play the weather report on your AirPlay speakers", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a playlist")
page(name: "chooseSpeakers", title: "Select speakers")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("Play weather report when"){
ifSet "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", 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
}
}
def hideable = anythingSet || app.installationState == "COMPLETE"
def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..."
section(sectionTitle, hideable: hideable, hidden: true){
ifUnset "motion", "capability.motionSensor", title: "Motion Here", 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 "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", 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 {
input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true,
options: [
["0": "Current Conditions"],
["1": "Today's Forecast"],
["2": "Tonight's Forecast"],
["3": "Tomorrow's Forecast"],
]
)
}
section {
input "obything", "capability.musicPlayer", title: "On this obything iTunes device", required: true
}
section {
href "chooseSpeakers", title: "With these speakers", description: speakers ? speakers : "Tap to set", state: speakers ? "complete" : "incomplete"
}
section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
href "choosePlaylist", title: "Or play this playlist", description: playlist ? playlist : "Tap to set", state: playlist ? "complete" : "incomplete"
input "zipCode", "text", title: "Zip Code", required: false
input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
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"]
if (settings.modes) {
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)"
}
}
}
def chooseTrack() {
dynamicPage(name: "chooseTrack") {
section{
input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions()
}
}
}
def chooseSpeakers() {
dynamicPage(name: "chooseSpeakers") {
section{
input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions()
}
}
}
private speakerOptions() {
def speakersString = obything.currentValue("speakers")
log.debug "Speakers are $speakersString"
def slurper = new JsonSlurper()
slurper.parseText(speakersString)
}
private anythingSet() {
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) {
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() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
subscribeToEvents()
}
def 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(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", eventHandler)
subscribe(button1, "button.pushed", eventHandler)
if (triggerModes) {
subscribe(location,modeChangeHandler)
}
if (timeOfDay) {
runDaily(timeOfDay, scheduledTimeHandler)
}
}
def eventHandler(evt) {
log.trace "eventHandler($evt?.name: $evt?.value)"
if (allOk) {
log.trace "allOk"
def lastTime = state[frequencyKey(evt)]
if (oncePerDayOk(lastTime)) {
if (frequency) {
if (lastTime == null || now() - lastTime >= frequency * 60000) {
takeAction(evt)
}
else {
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
}
}
else {
takeAction(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) {
takeAction(evt)
}
private takeAction(evt) {
loadText()
def speakerString
if (speakers) {
speakerString = ""
speakers.each {
speakerString += "$it,"
}
// remove the last comma and encode
speakerString = encode(speakerString[0..-2])
}
if (playlist) {
obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying, playlist)
}
else {
obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying)
}
if (frequency || oncePerDay) {
state[frequencyKey(evt)] = now()
}
}
private playlistOptions() {
def playlistString = obything.currentValue("playlists")
log.debug "Playlists are $playlistString"
def jsonList = new JsonSlurper().parseText(playlistString)
jsonList.collect {
it.Name
}
}
private frequencyKey(evt) {
"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 getTimeLabel()
{
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
// TODO - End Centralize
private loadText() {
if (location.timeZone || zipCode) {
def weather = getWeatherFeature("forecast", zipCode)
def current = getWeatherFeature("conditions", zipCode)
def isMetric = location.temperatureScale == "C"
def delim = ""
def sb = new StringBuilder()
list(forecastOptions).sort().each {opt ->
if (opt == "0") {
if (isMetric) {
sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees."
}
else {
sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees."
}
delim = " "
}
else if (opt == "1") {
sb << delim
sb << "Today's forecast is "
if (isMetric) {
sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric
}
else {
sb << weather.forecast.txt_forecast.forecastday[0].fcttext
}
}
else if (opt == "2") {
sb << delim
sb << "Tonight will be "
if (isMetric) {
sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric
}
else {
sb << weather.forecast.txt_forecast.forecastday[1].fcttext
}
}
else if (opt == "3") {
sb << delim
sb << "Tomorrow will be "
if (isMetric) {
sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric
}
else {
sb << weather.forecast.txt_forecast.forecastday[2].fcttext
}
}
}
def msg = sb.toString()
msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release
log.debug "msg = ${msg}"
state.sound = textToSpeech(msg, true)
}
else {
state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.")
}
}
private list(String s) {
[s]
}
private list(l) {
l
}

View File

@@ -22,8 +22,7 @@ definition(
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
oauth: true, oauth: true
singleInstance: true
){ ){
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"

View File

@@ -13,11 +13,11 @@ definition(
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
oauth: true, oauth: true,
usePreferencesForAuthorization: false, usePreferencesForAuthorization: false
singleInstance: true
) { ) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
appSetting "serverUrl"
} }
preferences { preferences {
@@ -28,13 +28,16 @@ mappings {
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
path("/oauth/callback") { action: [ GET: "callback" ] } path("/oauth/callback") { action: [ GET: "callback" ] }
} }
def getServerUrl() { return "https://graph.api.smartthings.com" } def getSmartThingsClientId() {
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } return appSettings.clientId
def buildRedirectUrl(page) { return buildActionUrl(page) } }
def getSmartThingsClientSecret() {
return appSettings.clientSecret
}
def callback() { def callback() {
def redirectUrl = null def redirectUrl = null
@@ -60,8 +63,9 @@ def callback() {
// SmartThings code, which we ignore, as we don't need to exchange for an access token. // SmartThings code, which we ignore, as we don't need to exchange for an access token.
// Instead, go initiate the Jawbone OAuth flow. // Instead, go initiate the Jawbone OAuth flow.
log.debug "Executing callback redirect to auth page" log.debug "Executing callback redirect to auth page"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString() state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
} }
} else { } else {
@@ -80,11 +84,10 @@ def authPage() {
createAccessToken() createAccessToken()
} }
description = "Click to enter Jawbone Credentials" description = "Click to enter Jawbone Credentials"
def redirectUrl = buildRedirectUrl def redirectUrl = oauthInitUrl()
log.debug "RedirectURL = ${redirectUrl}" // log.debug "RedirectURL = ${redirectUrl}"
def donebutton= state.JawboneAccessToken != null return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) { section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
} }
} else { } else {
description = "Jawbone Credentials Already Entered." description = "Jawbone Credentials Already Entered."
@@ -96,14 +99,17 @@ def authPage() {
def oauthInitUrl() { def oauthInitUrl() {
log.debug "oauthInitUrl" log.debug "oauthInitUrl"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString() state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ] def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
} }
def receiveToken(redirectUrl = null) { def receiveToken(redirectUrl = null) {
log.debug "receiveToken" log.debug "receiveToken"
def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ] def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
def params = [ def params = [
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
] ]
@@ -225,10 +231,18 @@ String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
} }
def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
def buildRedirectUrl(page) {
// log.debug "buildRedirectUrl"
// /api/token/:st_token/smartapps/installations/:id/something
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
}
def validateCurrentToken() { def validateCurrentToken() {
log.debug "validateCurrentToken" log.debug "validateCurrentToken"
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
def requestBody = "secret=${appSettings.clientSecret}" def requestBody = "secret=${getSmartThingsClientSecret()}"
try { try {
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
@@ -242,7 +256,9 @@ def validateCurrentToken() {
if (e.statusCode == 401) { // token is expired if (e.statusCode == 401) { // token is expired
log.debug "Access token is expired" log.debug "Access token is expired"
if (state.refreshToken) { // if we have this we are okay if (state.refreshToken) { // if we have this we are okay
def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}" def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
def params = [ def params = [
uri: tokenUrl uri: tokenUrl
@@ -271,10 +287,9 @@ def validateCurrentToken() {
} }
def initialize() { def initialize() {
log.debug "Callback URL - Webhook" def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
def localServerUrl = getApiServerUrl()
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
log.debug "Callback URL: $webhook"
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
} }
@@ -312,6 +327,7 @@ def setup() {
} }
def installed() { def installed() {
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
@@ -324,6 +340,7 @@ def installed() {
} }
def updated() { def updated() {
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"

View File

@@ -0,0 +1,53 @@
/**
*
* Lights On When Door Open After Sundown
*
* Based on "Turn It On When It Opens" by SmartThings
*
* Author: Aaron Crocco
*/
preferences {
section("When the door opens..."){
input "contact1", "capability.contactSensor", title: "Where?"
}
section("Turn on these lights..."){
input "switches", "capability.switch", multiple: true
}
section("and change mode to...") {
input "HomeAfterDarkMode", "mode", title: "Mode?"
}
}
def installed()
{
subscribe(contact1, "contact.open", contactOpenHandler)
}
def updated()
{
unsubscribe()
subscribe(contact1, "contact.open", contactOpenHandler)
}
def contactOpenHandler(evt) {
log.debug "$evt.value: $evt, $settings"
//Check current time to see if it's after sundown.
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
def now = new Date()
def setTime = s.sunset
log.debug "Sunset is at $setTime. Current time is $now"
if (setTime.before(now)) { //Executes only if it's after sundown.
log.trace "Turning on switches: $switches"
switches.on()
log.trace "Changing house mode to $HomeAfterDarkMode"
setLocationMode(HomeAfterDarkMode)
sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.")
}
}

View File

@@ -26,8 +26,7 @@ definition(
iconUrl: "http://i.imgur.com/HU0ANBp.png", iconUrl: "http://i.imgur.com/HU0ANBp.png",
iconX2Url: "http://i.imgur.com/HU0ANBp.png", iconX2Url: "http://i.imgur.com/HU0ANBp.png",
iconX3Url: "http://i.imgur.com/HU0ANBp.png", iconX3Url: "http://i.imgur.com/HU0ANBp.png",
oauth: true, oauth: true)
singleInstance: true)
preferences { preferences {

View File

@@ -102,7 +102,7 @@ def contactHandler(evt) {
} }
def motionHandler(evt) { def motionHandler(evt) {
log.debug "motionHandler: $evt.name: $evt.value" log.debug "motionHandler: $evt.name: $evt.value (current state: " + state.myState + ")"
if (evt.value == "active") { if (evt.value == "active") {
if(state.myState == "ready" || state.myState == "active" || state.myState == "activating" ) { if(state.myState == "ready" || state.myState == "active" || state.myState == "activating" ) {

View File

@@ -14,8 +14,8 @@
* *
*/ */
definition( definition(
name: "Smart Auto Lock / Unlock", name: "Smart Lock / Unlock",
namespace: "smart-auto-lock-unlock", namespace: "",
author: "Arnaud", author: "Arnaud",
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.", description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
category: "Safety & Security", category: "Safety & Security",

View File

@@ -21,8 +21,7 @@
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
singleInstance: true
) )
preferences { preferences {

View File

@@ -26,8 +26,7 @@ definition(
description: "Connect your Ecobee thermostat to SmartThings.", description: "Connect your Ecobee thermostat to SmartThings.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
singleInstance: true
) { ) {
appSetting "clientId" appSetting "clientId"
appSetting "serverUrl" appSetting "serverUrl"

View File

@@ -23,8 +23,7 @@ definition(
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.", description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
category: "SmartThings Internal", category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png"
singleInstance: true
) )
preferences { preferences {

View File

@@ -23,8 +23,7 @@ definition(
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.", description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
singleInstance: true
) )
preferences { preferences {
@@ -144,7 +143,7 @@ def bulbDiscovery() {
if (numFound == 0) if (numFound == 0)
app.updateSetting("selectedBulbs", "") app.updateSetting("selectedBulbs", "")
if((bulbRefreshCount % 5) == 0) { if((bulbRefreshCount % 3) == 0) {
discoverHueBulbs() discoverHueBulbs()
} }
@@ -319,15 +318,11 @@ def addBulbs() {
def newHueBulb def newHueBulb
if (bulbs instanceof java.util.Map) { if (bulbs instanceof java.util.Map) {
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
if (newHueBulb != null) { if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) { d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) } else {
} else { d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) }
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else { } else {
//backwards compatable //backwards compatable
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
@@ -609,22 +604,23 @@ def parse(childDevice, description) {
} }
} }
def on(childDevice) { def on(childDevice, transition_deprecated = 0) {
log.debug "Executing 'on'" log.debug "Executing 'on'"
put("lights/${getId(childDevice)}/state", [on: true]) def percent = childDevice.device?.currentValue("level") as Integer
return "Bulb is On" def level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
return "level: $percent"
} }
def off(childDevice) { def off(childDevice, transition_deprecated = 0) {
log.debug "Executing 'off'" log.debug "Executing 'off'"
put("lights/${getId(childDevice)}/state", [on: false]) put("lights/${getId(childDevice)}/state", [on: false])
return "Bulb is Off" return "level: 0"
} }
def setLevel(childDevice, percent) { def setLevel(childDevice, percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
def level def level = Math.min(Math.round(percent * 255 / 100), 255)
if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0]) put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
} }
@@ -640,7 +636,7 @@ def setHue(childDevice, percent) {
put("lights/${getId(childDevice)}/state", [hue: level]) put("lights/${getId(childDevice)}/state", [hue: level])
} }
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
@@ -649,7 +645,7 @@ def setColor(childDevice, huesettings) {
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
if (huesettings.level != null) { if (huesettings.level != null) {
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
value.on = value.bri > 0 value.on = value.bri > 0
} }
@@ -727,6 +723,8 @@ private getBridgeIP() {
def serialNumber = selectedHue def serialNumber = selectedHue
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
if (!bridge) { if (!bridge) {
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
//in this case we will look up the bridge by comparing the incorrect mac addresses
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
} }
if (bridge?.ip && bridge?.port) { if (bridge?.ip && bridge?.port) {

View File

@@ -98,15 +98,6 @@ def installed() {
} }
def updated() { def updated() {
def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique()
def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device ->
!currentDeviceIds.contains(device.id)
}
subscriptionDevicesToRemove.each { device ->
log.debug "Removing $device.displayName subscription"
state.remove(device.id)
unsubscribe(device)
}
log.debug settings log.debug settings
} }

View File

@@ -22,8 +22,7 @@ definition(
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
oauth: [displayName: "Life360", displayLink: "Life360"], oauth: [displayName: "Life360", displayLink: "Life360"]
singleInstance: true
) { ) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"

View File

@@ -13,8 +13,7 @@ definition(
iconUrl: "https://cloud.lifx.com/images/lifx.png", iconUrl: "https://cloud.lifx.com/images/lifx.png",
iconX2Url: "https://cloud.lifx.com/images/lifx.png", iconX2Url: "https://cloud.lifx.com/images/lifx.png",
iconX3Url: "https://cloud.lifx.com/images/lifx.png", iconX3Url: "https://cloud.lifx.com/images/lifx.png",
oauth: true, oauth: true) {
singleInstance: true) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
} }

View File

@@ -43,11 +43,11 @@ definition(
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"]
singleInstance: true
){ ){
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
appSetting "callbackUrl"
} }
preferences(oauthPage: "deviceAuthorization") { preferences(oauthPage: "deviceAuthorization") {
@@ -89,8 +89,6 @@ mappings {
} }
def getServerUrl() { return "https://graph.api.smartthings.com" } def getServerUrl() { return "https://graph.api.smartthings.com" }
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
def authPage() { def authPage() {
def description = null def description = null
@@ -100,7 +98,7 @@ def authPage() {
createAccessToken() createAccessToken()
} }
description = "Click to enter Harmony Credentials" description = "Click to enter Harmony Credentials"
def redirectUrl = buildRedirectUrl def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}"
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
} }
@@ -122,8 +120,7 @@ def authPage() {
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
} }
// Virtual activity flag if (numFoundHub > 0 && numFoundAct > 0 && false)
if (numFoundHub > 0 && numFoundAct > 0 && true)
section("You can also add activities as virtual switches for other convenient integrations") { section("You can also add activities as virtual switches for other convenient integrations") {
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
} }
@@ -166,8 +163,8 @@ def callback() {
def init() { def init() {
log.debug "Requesting Code" log.debug "Requesting Code"
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ] def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
} }
def receiveToken(redirectUrl = null) { def receiveToken(redirectUrl = null) {
@@ -305,6 +302,7 @@ def buildRedirectUrl(page) {
} }
def installed() { def installed() {
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
@@ -315,7 +313,8 @@ def installed() {
def updated() { def updated() {
unsubscribe() unsubscribe()
unschedule() unschedule()
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
@@ -660,16 +659,12 @@ def updateDevice() {
} else { } else {
def device = allDevices.find { it.id == params.id } def device = allDevices.find { it.id == params.id }
if (device) { if (device) {
if (device.hasCommand("$command")) { if (arguments) {
if (arguments) { device."$command"(*arguments)
device."$command"(*arguments) } else {
} else { device."$command"()
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Command not supported by this Device"}'
} }
render status: 204, data: "{}"
} else { } else {
render status: 404, data: '{"msg": "Device not found"}' render status: 404, data: '{"msg": "Device not found"}'
} }

View File

@@ -22,8 +22,7 @@ definition(
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.", description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png"
singleInstance: true
) )
preferences { preferences {

View File

@@ -16,8 +16,8 @@
* *
*/ */
definition( definition(
name: "Send HAM Bridge Command When", name: "Send HAM Bridge Command When",
namespace: "smartthings", namespace: "soletc.com",
author: "Scottin Pollock", author: "Scottin Pollock",
description: "Sends a command to your HAM Bridge server when SmartThings are activated.", description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
category: "Convenience", category: "Convenience",
@@ -25,6 +25,7 @@ definition(
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png" iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
) )
preferences { preferences {
section("Choose one or more, when..."){ section("Choose one or more, when..."){
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true

View File

@@ -23,8 +23,7 @@ definition(
description: "Integrate your Tesla car with SmartThings.", description: "Integrate your Tesla car with SmartThings.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png"
singleInstance: true
) )
preferences { preferences {

View File

@@ -22,8 +22,7 @@ definition(
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.", description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png"
singleInstance: true
) )
preferences { preferences {
@@ -62,7 +61,10 @@ def firstPage()
log.debug "REFRESH COUNT :: ${refreshCount}" log.debug "REFRESH COUNT :: ${refreshCount}"
subscribe(location, null, locationHandler, [filterEvents:false]) if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
//ssdp request every 25 seconds //ssdp request every 25 seconds
if((refreshCount % 5) == 0) { if((refreshCount % 5) == 0) {
@@ -166,30 +168,21 @@ def getWemoLightSwitches()
def installed() { def installed() {
log.debug "Installed with settings: ${settings}" log.debug "Installed with settings: ${settings}"
initialize() 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() { def updated() {
log.debug "Updated with settings: ${settings}" log.debug "Updated with settings: ${settings}"
initialize() initialize()
}
def initialize() { runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds
unsubscribe() runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds
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() { def resubscribe() {
@@ -199,7 +192,8 @@ def resubscribe() {
def refresh() { def refresh() {
log.debug "refresh() called" log.debug "refresh() called"
doDeviceSync() //reschedule the refreshes
runIn(1740, "refresh", [overwrite: false])
refreshDevices() refreshDevices()
} }
@@ -242,8 +236,7 @@ def addSwitches() {
"port": selectedSwitch.value.port "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}" log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else { } else {
log.debug "found ${d.displayName} with id $dni already exists" log.debug "found ${d.displayName} with id $dni already exists"
@@ -273,9 +266,8 @@ def addMotions() {
"port": selectedMotion.value.port "port": selectedMotion.value.port
] ]
]) ])
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}"
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else { } else {
log.debug "found ${d.displayName} with id $dni already exists" log.debug "found ${d.displayName} with id $dni already exists"
} }
@@ -304,8 +296,7 @@ def addLightSwitches() {
"port": selectedLightSwitch.value.port "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" log.debug "created ${d.displayName} with id $dni"
} else { } else {
log.debug "found ${d.displayName} with id $dni already exists" log.debug "found ${d.displayName} with id $dni already exists"
@@ -313,6 +304,27 @@ 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 locationHandler(evt) {
def description = evt.description def description = evt.description
def hub = evt?.hubId def hub = evt?.hubId
@@ -321,32 +333,53 @@ def locationHandler(evt) {
log.debug parsedEvent log.debug parsedEvent
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) { if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
def switches = getWemoSwitches() 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] switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else { }
else
{ // just update the values
log.debug "Device was already found in state..." log.debug "Device was already found in state..."
def d = switches."${parsedEvent.ssdpUSN.toString()}" def d = switches."${parsedEvent.ssdpUSN.toString()}"
boolean deviceChangedValues = false boolean deviceChangedValues = false
log.debug "$d.ip <==> $parsedEvent.ip"
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
d.ip = parsedEvent.ip d.ip = parsedEvent.ip
d.port = parsedEvent.port d.port = parsedEvent.port
deviceChangedValues = true deviceChangedValues = true
log.debug "Device's port or ip changed..." 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")) { else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
def motions = getWemoMotions() 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] motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else { // just update the values }
else
{ // just update the values
log.debug "Device was already found in state..." log.debug "Device was already found in state..."
def d = motions."${parsedEvent.ssdpUSN.toString()}" def d = motions."${parsedEvent.ssdpUSN.toString()}"
@@ -379,7 +412,10 @@ def locationHandler(evt) {
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
{ //if it doesn't already exist { //if it doesn't already exist
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else { }
else
{ // just update the values
log.debug "Device was already found in state..." log.debug "Device was already found in state..."
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}" def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
@@ -390,11 +426,21 @@ def locationHandler(evt) {
d.port = parsedEvent.port d.port = parsedEvent.port
deviceChangedValues = true deviceChangedValues = true
log.debug "Device's port or ip changed..." 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) { else if (parsedEvent.headers && parsedEvent.body) {
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase() String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
@@ -534,30 +580,73 @@ private def parseDiscoveryMessage(String description) {
} }
} }
} }
device device
} }
def doDeviceSync(){ def doDeviceSync(){
log.debug "Doing Device Sync!" 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() discoverAllWemoTypes()
} }
private String convertHexToIP(hex) { def pollChildren() {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") def devices = getAllChildDevices()
devices.each { d ->
//only poll switches?
d.poll()
}
} }
private Integer convertHexToInt(hex) { def delayPoll() {
Integer.parseInt(hex,16) log.debug "Executing 'delayPoll'"
runIn(5, "pollChildren")
} }
private Boolean canInstallLabs() { /*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()
{
return hasAllHubsOver("000.011.00603") return hasAllHubsOver("000.011.00603")
} }
private Boolean hasAllHubsOver(String desiredFirmware) { private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
} }
private List getRealHubFirmwareVersions() { private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it } return location.hubs*.firmwareVersionString.findAll { it }
} }

View File

@@ -24,8 +24,7 @@ definition(
category: "Connections", category: "Connections",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
oauth: true, oauth: true
singleInstance: true
) { ) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"

View File

@@ -24,8 +24,7 @@ definition(
category: "SmartThings Internal", category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
oauth: true, oauth: true
singleInstance: true
) { ) {
appSetting "serverUrl" appSetting "serverUrl"
} }

View File

@@ -15,7 +15,7 @@
*/ */
definition( definition(
name: "Sprayer Controller 2", name: "Sprayer Controller 2",
namespace: "sprayercontroller", namespace: "",
author: "Cooper Lee", author: "Cooper Lee",
description: "Control Sprayers for a period of time a number of times per hour", description: "Control Sprayers for a period of time a number of times per hour",
category: "My Apps", category: "My Apps",

View File

@@ -50,7 +50,7 @@ preferences {
} }
section("Send Notifications?") { section("Send Notifications?") {
input("recipients", "contact", title: "Send notifications to") { input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Send an SMS to this number?", required:false input "phone", "phone", title: "Send an SMS to this number?"
} }
} }
@@ -266,9 +266,7 @@ def sendAway(msg) {
} }
else { else {
sendPush(msg) sendPush(msg)
if(phone){ sendSms(phone, msg)
sendSms(phone, msg)
}
} }
} }
@@ -282,9 +280,7 @@ def sendHome(msg) {
} }
else { else {
sendPush(msg) sendPush(msg)
if(phone){ sendSms(phone, msg)
sendSms(phone, msg)
}
} }
} }

View File

@@ -58,8 +58,7 @@ definition(
description: "Connect your Quirky to SmartThings.", description: "Connect your Quirky to SmartThings.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png"
singleInstance: true
) { ) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"

View File

@@ -25,8 +25,7 @@ definition(
description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png"
singleInstance: true
) )