Compare commits

..

1 Commits

Author SHA1 Message Date
Marcus Peters
9d08b97561 MSA-1461: I added a little bit of code that makes the Hue Connect Smartapp recognize the Living White Adapters. I just added the correct naming convention to achieve this. Line 363:
else if (hueType?.equalsIgnoreCase("Dimmable plug-in unit"))
		return "Hue Lux Bulb"

These adapters work exactly like the Lux bulbs and can use the same DeviceType. So basically its a dimmable plug working with Hue.
2016-09-01 14:23:37 -05:00
92 changed files with 1026 additions and 2036 deletions

View File

@@ -19,7 +19,7 @@ buildscript {
username smartThingsArtifactoryUserName username smartThingsArtifactoryUserName
password smartThingsArtifactoryPassword password smartThingsArtifactoryPassword
} }
url "https://artifactory.smartthings.com/libs-release-local" url "http://artifactory.smartthings.com/libs-release-local"
} }
} }
} }
@@ -27,37 +27,9 @@ buildscript {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
maven {
credentials {
username smartThingsArtifactoryUserName
password smartThingsArtifactoryPassword
}
url "https://artifactory.smartthings.com/libs-release-local"
}
}
sourceSets {
devicetypes {
groovy {
srcDirs = ['devicetypes']
}
}
smartapps {
groovy {
srcDirs = ['smartapps']
}
}
} }
dependencies { dependencies {
devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7'
devicetypesCompile 'smartthings:appengine-z-wave:0.1.2'
devicetypesCompile 'smartthings:appengine-zigbee:0.1.11'
smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7'
smartappsCompile 'smartthings:appengine-common:0.1.8'
smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
smartappsCompile 'org.grails:grails-web:2.3.11'
smartappsCompile 'org.json:json:20140107'
} }
slackSendMessage { slackSendMessage {

View File

@@ -5,9 +5,7 @@ machine:
dependencies: dependencies:
override: override:
- ./gradlew dependencies -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" - echo "Nothing to do."
post:
- ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD"
test: test:
override: override:

View File

@@ -15,7 +15,6 @@
*/ */
metadata { metadata {
definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") { definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") {
capability "Sensor"
capability "Relative Humidity Measurement" capability "Relative Humidity Measurement"
capability "Temperature Measurement" capability "Temperature Measurement"

View File

@@ -15,7 +15,6 @@
*/ */
metadata { metadata {
definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") { definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") {
capability "Sensor"
capability "Relative Humidity Measurement" capability "Relative Humidity Measurement"
capability "Temperature Measurement" capability "Temperature Measurement"

View File

@@ -15,7 +15,6 @@
*/ */
metadata { metadata {
definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") { definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") {
capability "Sensor"
capability "Relative Humidity Measurement" capability "Relative Humidity Measurement"
capability "Temperature Measurement" capability "Temperature Measurement"
} }

View File

@@ -15,8 +15,6 @@
*/ */
metadata { metadata {
definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") { definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") {
capability "Sensor"
attribute "rain", "number" attribute "rain", "number"
attribute "rainSumHour", "number" attribute "rainSumHour", "number"
attribute "rainSumDay", "number" attribute "rainSumDay", "number"

View File

@@ -33,8 +33,8 @@ metadata {
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
tileAttribute("device.motion", key:"PRIMARY_CONTROL") { tileAttribute("device.motion", key:"PRIMARY_CONTROL") {
attributeState("inactive", label:"no motion", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
attributeState("active", label:"motion", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
} }
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
@@ -127,10 +127,9 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR
def map = [ displayed: true ] def map = [ displayed: true ]
switch (cmd.sensorType) { switch (cmd.sensorType) {
case 1: case 1:
def cmdScale = cmd.scale == 1 ? "F" : "C" map.name = "temperature"
map.name = "temperature" map.unit = cmd.scale == 1 ? "F" : "C"
map.unit = getTemperatureScale() map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, map.unit, cmd.precision)
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
break break
case 3: case 3:
map.name = "illuminance" map.name = "illuminance"
@@ -279,4 +278,4 @@ private encap(physicalgraph.zwave.Command cmd) {
} else { } else {
crc16(cmd) crc16(cmd)
} }
} }

View File

@@ -87,27 +87,16 @@ def beep() {
up to this long from the time you send the message to the time you hear a sound. up to this long from the time you send the message to the time you hear a sound.
*/ */
// Used source endpoint of 0x02 because we are using smartthings manufacturer specific cluster.
[ [
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}"
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
] ]
} }

View File

@@ -105,21 +105,11 @@ def parseDescriptionAsMap(description) {
// Commands to device // Commands to device
def on() { def on() {
[ 'zcl on-off on'
'zcl on-off on',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def off() { def off() {
[ 'zcl on-off off'
'zcl on-off off',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def setLevel(value) { def setLevel(value) {

View File

@@ -23,8 +23,10 @@ Works with:
## Device Health ## Device Health
A Category C6 Connected Cree LED Bulb with maxReportTime of 5 mins. A Category C6 Connected Cree LED Bulb with maxReportTime of 10 min.
Check-in interval = 12 mins Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 2*10 = 20 min
## Troubleshooting ## Troubleshooting

View File

@@ -19,6 +19,7 @@ metadata {
capability "Actuator" capability "Actuator"
capability "Configuration" capability "Configuration"
capability "Polling"
capability "Refresh" capability "Refresh"
capability "Switch" capability "Switch"
capability "Switch Level" capability "Switch Level"
@@ -66,6 +67,12 @@ def parse(String description) {
def resultMap = zigbee.getEvent(description) def resultMap = zigbee.getEvent(description)
if (resultMap) { if (resultMap) {
sendEvent(resultMap) sendEvent(resultMap)
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
} }
else { else {
log.debug "DID NOT PARSE MESSAGE for description : $description" log.debug "DID NOT PARSE MESSAGE for description : $description"
@@ -89,23 +96,27 @@ def setLevel(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.levelRefresh()
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.levelRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
} }
def healthPoll() { def poll() {
log.debug "healthPoll()" zigbee.onOffRefresh() + zigbee.levelRefresh()
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh()
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
} }
def configure() { def configure() {
unschedule() log.debug "Configuring Reporting and Bindings."
runEvery5Minutes("healthPoll") sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
// Device-Watch allows 2 check-in misses from device zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
zigbee.onOffRefresh() + zigbee.levelRefresh()
} }

View File

@@ -31,13 +31,13 @@ metadata {
command "switchMode" command "switchMode"
command "switchFanMode" command "switchFanMode"
attribute "thermostatSetpoint", "number" attribute "thermostatSetpoint","number"
attribute "thermostatStatus", "string" attribute "thermostatStatus","string"
attribute "maxHeatingSetpoint", "number" attribute "maxHeatingSetpoint", "number"
attribute "minHeatingSetpoint", "number" attribute "minHeatingSetpoint", "number"
attribute "maxCoolingSetpoint", "number" attribute "maxCoolingSetpoint", "number"
attribute "minCoolingSetpoint", "number" attribute "minCoolingSetpoint", "number"
attribute "deviceTemperatureUnit", "string" attribute "deviceTemperatureUnit", "number"
} }
tiles { tiles {
@@ -655,60 +655,55 @@ void lowerSetpoint() {
void alterSetpoint(temp) { void alterSetpoint(temp) {
def mode = device.currentValue("thermostatMode") def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
if (mode == "off" || mode == "auto") { def targetHeatingSetpoint
log.warn "this mode: $mode does not allow alterSetpoint" def targetCoolingSetpoint
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
def targetHeatingSetpoint //step1: check thermostatMode, enforce limits before sending request to cloud
def targetCoolingSetpoint if (mode == "heat" || mode == "auxHeatOnly"){
if (temp.value > coolingSetpoint){
//step1: check thermostatMode, enforce limits before sending request to cloud targetHeatingSetpoint = temp.value
if (mode == "heat" || mode == "auxHeatOnly"){ targetCoolingSetpoint = temp.value
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = coolingSetpoint
}
} else if (mode == "cool") {
//enforce limits before sending request to cloud
if (temp.value < heatingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = heatingSetpoint
targetCoolingSetpoint = temp.value
}
}
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " +
"coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else { } else {
log.error "Error alterSetpoint()" targetHeatingSetpoint = temp.value
if (mode == "heat" || mode == "auxHeatOnly"){ targetCoolingSetpoint = coolingSetpoint
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false) }
} else if (mode == "cool") { } else if (mode == "cool") {
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) //enforce limits before sending request to cloud
} if (temp.value < heatingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = heatingSetpoint
targetCoolingSetpoint = temp.value
} }
generateStatusEvent()
} }
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " +
"coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else {
log.error "Error alterSetpoint()"
if (mode == "heat" || mode == "auxHeatOnly"){
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
}
}
generateStatusEvent()
} }
def generateStatusEvent() { def generateStatusEvent() {

View File

@@ -57,7 +57,7 @@ metadata {
} }
void installed() { void installed() {
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false)
} }
// parse events into attributes // parse events into attributes

View File

@@ -7,11 +7,9 @@
metadata { metadata {
// Automatically generated. Make future change here. // Automatically generated. Make future change here.
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") { definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
capability "Health Check"
attribute "networkAddress", "string" attribute "networkAddress", "string"
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network // Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
// Possible values "Online" or "Offline" // Possible values "Online" or "Offline"
attribute "status", "string" attribute "status", "string"
// Id is the number on the back of the hub, Hue uses last six digits of Mac address // Id is the number on the back of the hub, Hue uses last six digits of Mac address
// This is also used in the Hue application as ID // This is also used in the Hue application as ID
@@ -44,10 +42,6 @@ metadata {
} }
} }
void installed() {
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
}
// parse events into attributes // parse events into attributes
def parse(description) { def parse(description) {
log.debug "Parsing '${description}'" log.debug "Parsing '${description}'"
@@ -68,7 +62,7 @@ def parse(description) {
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value" log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
results << createEvent(name: "${map.name}", value: "${map.value}") results << createEvent(name: "${map.name}", value: "${map.value}")
} else { } else {
log.trace "Parsing description" log.trace "Parsing description"
def msg = parseLanMessage(description) def msg = parseLanMessage(description)
if (msg.body) { if (msg.body) {
def contentType = msg.headers["Content-Type"] def contentType = msg.headers["Content-Type"]
@@ -76,17 +70,18 @@ def parse(description) {
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body) def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
if (bulbs.state) { if (bulbs.state) {
log.info "Bridge response: $msg.body" log.info "Bridge response: $msg.body"
} else {
// Sending Bulbs List to parent"
if (parent.state.inBulbDiscovery)
log.info parent.bulbListHandler(device.hub.id, msg.body)
} }
} else if (contentType?.contains("xml")) { }
else if (contentType?.contains("xml")) {
log.debug "HUE BRIDGE ALREADY PRESENT" log.debug "HUE BRIDGE ALREADY PRESENT"
parent.hubVerification(device.hub.id, msg.body) parent.hubVerification(device.hub.id, msg.body)
} }
} }
} }
} }
results results
} }
def ping() {
log.debug "${parent.ping(this)}"
}

View File

@@ -66,7 +66,7 @@ metadata {
} }
void installed() { void installed() {
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false)
} }
// parse events into attributes // parse events into attributes
@@ -174,7 +174,7 @@ void setColorTemperature(value) {
void refresh() { void refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
parent?.manualRefresh() parent.manualRefresh()
} }
def verifyPercent(percent) { def verifyPercent(percent) {

View File

@@ -50,7 +50,7 @@ metadata {
} }
void installed() { void installed() {
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false)
} }
// parse events into attributes // parse events into attributes

View File

@@ -55,7 +55,7 @@ metadata {
} }
void installed() { void installed() {
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false)
} }
// parse events into attributes // parse events into attributes

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,37 +0,0 @@
# Nyce Door/Window Sensor (Open/Close Sensor)
Works with:
* [NYCE Door/Window Sensor NCZ-3011](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Battery](#battery-specification)
* [Troubleshooting](#troubleshooting)
## Capabilities
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Contact Sensor** - can detect contact (with possible values - open/closed)
* **Battery** - defines device uses a battery
* **Refresh** - _refresh()_ command for status updates
* **Health Check** - indicates ability to get device health notifications
## Device Health
A Category C2 Nyce Door/Window sensor that has 12min check-in interval
## Battery Specification
One 3V CR2032 battery required.
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range.
Pairing needs to be tried again by placing the sensor closer to the hub.
Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link:
* [Nyce Door/Window Sensor](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)

View File

@@ -19,26 +19,25 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata { metadata {
definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") { definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") {
capability "Battery" capability "Battery"
capability "Configuration" capability "Configuration"
capability "Contact Sensor" capability "Contact Sensor"
capability "Refresh" capability "Refresh"
capability "Health Check"
command "enrollResponse"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor" fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor" fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor" fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor" fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
} }
simulator { simulator {
} }
tiles { tiles {
standardTile("contact", "device.contact", width: 2, height: 2) { standardTile("contact", "device.contact", width: 2, height: 2) {
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
@@ -274,28 +273,23 @@ private List parseIasMessage(String description) {
return resultListMap return resultListMap
} }
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
}
def configure() { def configure() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
def enrollCmds = [ def configCmds = [
//battery reporting and heartbeat
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
// Writes CIE attribute on end device to direct reports to the hub's EUID // Writes CIE attribute on end device to direct reports to the hub's EUID
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500", "send 0x${device.deviceNetworkId} 1 1", "delay 500",
] ]
log.debug "configure: Write IAS CIE" log.debug "configure: Write IAS CIE"
// battery minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity return configCmds
return enrollCmds + zigbee.batteryConfig(30, 300) + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {
@@ -340,8 +334,7 @@ Integer convertHexToInt(hex) {
def refresh() { def refresh() {
log.debug "Refreshing Battery" log.debug "Refreshing Battery"
def refreshCmds = [ [
"st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200" "st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200"
] ]
return refreshCmds + enrollResponse()
} }

View File

@@ -21,6 +21,9 @@ metadata {
attribute "colorName", "string" attribute "colorName", "string"
command "setAdjustedColor" command "setAdjustedColor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
} }
// simulator metadata // simulator metadata
@@ -88,7 +91,7 @@ def parse(String description) {
if (descMap.cluster == "0300") { if (descMap.cluster == "0300") {
if(descMap.attrId == "0000"){ //Hue Attribute if(descMap.attrId == "0000"){ //Hue Attribute
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
log.debug "Hue value returned is $hueValue" log.debug "Hue value returned is $hueValue"
sendEvent(name: "hue", value: hueValue, displayed:false) sendEvent(name: "hue", value: hueValue, displayed:false)
} }
@@ -200,7 +203,7 @@ def setLevel(value) {
//input Hue Integer values; returns color name for saturation 100% //input Hue Integer values; returns color name for saturation 100%
private getColorName(hueValue){ private getColorName(hueValue){
if(hueValue>100 || hueValue<0) if(hueValue>360 || hueValue<0)
return return
hueValue = Math.round(hueValue / 100 * 360) hueValue = Math.round(hueValue / 100 * 360)

View File

@@ -1,4 +1,4 @@
/* /*
Osram Flex RGBW Light Strip Osram Flex RGBW Light Strip
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
@@ -8,7 +8,7 @@
metadata { metadata {
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") { definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
capability "Color Temperature" capability "Color Temperature"
capability "Actuator" capability "Actuator"
capability "Switch" capability "Switch"
@@ -18,7 +18,7 @@ metadata {
capability "Refresh" capability "Refresh"
capability "Sensor" capability "Sensor"
capability "Color Control" capability "Color Control"
attribute "colorName", "string" attribute "colorName", "string"
command "setAdjustedColor" command "setAdjustedColor"
@@ -49,7 +49,7 @@ metadata {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature" state "colorTemperature", action:"color temperature.setColorTemperature"
} }
@@ -118,7 +118,7 @@ def parse(String description) {
} }
} }
else if(descMap.attrId == "0000"){ //Hue Attribute else if(descMap.attrId == "0000"){ //Hue Attribute
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
log.debug "Hue value returned is $hueValue" log.debug "Hue value returned is $hueValue"
sendEvent(name: "hue", value: hueValue, displayed:false) sendEvent(name: "hue", value: hueValue, displayed:false)
} }
@@ -274,7 +274,7 @@ private getGenericName(value){
//input Hue Integer values; returns color name for saturation 100% //input Hue Integer values; returns color name for saturation 100%
private getColorName(hueValue){ private getColorName(hueValue){
if(hueValue>100 || hueValue<0) if(hueValue>360 || hueValue<0)
return return
hueValue = Math.round(hueValue / 100 * 360) hueValue = Math.round(hueValue / 100 * 360)
@@ -449,7 +449,7 @@ def setColor(value){
def level = hex(value.level * 255 / 100) def level = hex(value.level * 255 / 100)
cmd << zigbeeSetLevel(level) cmd << zigbeeSetLevel(level)
} }
if (value.switch == "off") { if (value.switch == "off") {
cmd << "delay 150" cmd << "delay 150"
cmd << off() cmd << off()

View File

@@ -133,7 +133,7 @@ def refresh() {
} }
def configure() { def configure() {
refresh() + onOffConfig() + levelConfig() + powerConfig() onOffConfig() + levelConfig() + powerConfig() + refresh()
} }

View File

@@ -47,21 +47,9 @@ def parse(String description) {
// Commands to device // Commands to device
def on() { def on() {
[ 'zcl on-off on'
'zcl on-off on',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def off() { def off() {
[ 'zcl on-off off'
'zcl on-off off',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }

View File

@@ -23,10 +23,10 @@ Works with:
## Device Health ## Device Health
A Category C1 smart power outlet with maxReportTime of 5 mins. A Category C1 smart power outlet with maxReportTime of 10 min.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*10 = 20 min
## Troubleshooting ## Troubleshooting

View File

@@ -16,7 +16,7 @@
metadata { metadata {
// Automatically generated. Make future change here. // Automatically generated. Make future change here.
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") {
capability "Actuator" capability "Actuator"
capability "Switch" capability "Switch"
capability "Power Meter" capability "Power Meter"
@@ -101,24 +101,17 @@ def parse(String description) {
else { else {
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true) sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
} }
} }
else { else {
def cluster = zigbee.parse(description) log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07){
if (cluster.data[0] == 0x00) {
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug "${cluster}"
}
} }
} }
@@ -133,7 +126,15 @@ def on() {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.onOffRefresh()
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
@@ -141,12 +142,8 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed zigbee.onOffConfig() + powerConfig() + refresh()
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh() + zigbee.onOffConfig(0, 300) + powerConfig()
} }
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) //power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)

View File

@@ -23,10 +23,10 @@ Works with:
## Device Health ## Device Health
A Category C2 moisture sensor with maxReportTime of 5 mins. A Category C2 moisture sensor with maxReportTime of 1 hr.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*60 = 120 min
## Battery Specification ## Battery Specification

View File

@@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata { metadata {
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Configuration" capability "Configuration"
capability "Battery" capability "Battery"
capability "Refresh" capability "Refresh"
@@ -101,6 +101,13 @@ def parse(String description) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
@@ -118,28 +125,14 @@ private Map parseCatchAllMessage(String description) {
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
// 0x07 - configure reporting resultMap = getBatteryResult(cluster.data.last())
if (cluster.command != 0x07) {
resultMap = getBatteryResult(cluster.data.last())
}
break break
case 0x0402: case 0x0402:
if (cluster.command == 0x07) { // temp is last 2 data values. reverse to swap endian
if (cluster.data[0] == 0x00){ String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster def value = getTemperature(temp)
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) resultMap = getTemperatureResult(value)
}
else {
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
}
break break
} }
} }
@@ -149,8 +142,10 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
@@ -192,9 +187,9 @@ private Map parseIasMessage(String description) {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return Math.round(celsius) return celsius
} else { } else {
return Math.round(celsiusToFahrenheit(celsius)) return celsiusToFahrenheit(celsius) as Integer
} }
} }
@@ -238,8 +233,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
@@ -290,7 +283,15 @@ private Map getMoistureResult(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
@@ -304,13 +305,23 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
// battery minReport 30 seconds, maxReportTime 6 hrs by default log.debug "Configuring Reporting, IAS CIE, and Bindings."
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -22,10 +22,10 @@ Works with:
## Device Health ## Device Health
A Category C2 motion sensor with maxReportTime of 5 mins. A Category C2 motion sensor with maxReportTime of 1 hr.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*60 = 120 min
## Battery Specification ## Battery Specification

View File

@@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata { metadata {
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Motion Sensor" capability "Motion Sensor"
capability "Configuration" capability "Configuration"
capability "Battery" capability "Battery"
@@ -105,6 +105,13 @@ def parse(String description) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
@@ -122,37 +129,19 @@ private Map parseCatchAllMessage(String description) {
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
// 0x07 - configure reporting resultMap = getBatteryResult(cluster.data.last())
if (cluster.command != 0x07) {
resultMap = getBatteryResult(cluster.data.last())
}
break break
case 0x0402: case 0x0402:
if (cluster.command == 0x07) { // temp is last 2 data values. reverse to swap endian
if (cluster.data[0] == 0x00) { String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster def value = getTemperature(temp)
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) resultMap = getTemperatureResult(value)
}
else {
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
}
break break
case 0x0406: case 0x0406:
// 0x07 - configure reporting log.debug 'motion'
if (cluster.command != 0x07) { resultMap.name = 'motion'
log.debug 'motion'
resultMap.name = 'motion'
}
break break
} }
} }
@@ -162,8 +151,10 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
@@ -210,9 +201,9 @@ private Map parseIasMessage(String description) {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return Math.round(celsius) return celsius
} else { } else {
return Math.round(celsiusToFahrenheit(celsius)) return celsiusToFahrenheit(celsius) as Integer
} }
} }
@@ -257,8 +248,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
@@ -305,7 +294,15 @@ private Map getMotionResult(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
@@ -319,13 +316,24 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
// battery minReport 30 seconds, maxReportTime 6 hrs by default log.debug "Configuring Reporting, IAS CIE, and Bindings."
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -206,8 +206,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }
@@ -255,9 +253,13 @@ def refresh() {
} }
def configure() { def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings." log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [ def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
@@ -266,7 +268,7 @@ def configure() {
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
] ]
return refresh() + configCmds // send refresh cmds as part of config return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -26,10 +26,10 @@ Works with:
## Device Health ## Device Health
A Category C2 multi sensor with maxReportTime of 5 mins. A Category C2 multi sensor with maxReportTime of 1 hr.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*60 = 120 min
## Battery Specification ## Battery Specification

View File

@@ -16,7 +16,7 @@
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata { metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Three Axis" capability "Three Axis"
capability "Battery" capability "Battery"
@@ -127,6 +127,13 @@ def parse(String description) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
@@ -147,33 +154,20 @@ private Map parseCatchAllMessage(String description) {
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
// 0x07 - configure reporting resultMap = getBatteryResult(cluster.data.last())
if (cluster.command != 0x07) {
resultMap = getBatteryResult(cluster.data.last())
}
break break
case 0xFC02: case 0xFC02:
log.debug 'ACCELERATION' log.debug 'ACCELERATION'
break break
case 0x0402: case 0x0402:
if (cluster.command == 0x07) { log.debug 'TEMP'
if(cluster.data[0] == 0x00) { // temp is last 2 data values. reverse to swap endian
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) def value = getTemperature(temp)
} resultMap = getTemperatureResult(value)
else { break
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
}
break
} }
} }
@@ -182,8 +176,10 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
@@ -272,9 +268,9 @@ def updated() {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return Math.round(celsius) return celsius
} else { } else {
return Math.round(celsiusToFahrenheit(celsius)) return celsiusToFahrenheit(celsius) as Integer
} }
} }
@@ -317,8 +313,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
@@ -382,7 +376,15 @@ private getAccelerationResult(numValue) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
@@ -412,22 +414,19 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
log.debug "Configuring Reporting" log.debug "Configuring Reporting"
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity def configCmds = enrollResponse() +
// battery minReport 30 seconds, maxReportTime 6 hrs by default zigbee.batteryConfig() +
def configCmds = zigbee.batteryConfig() + zigbee.temperatureConfig() +
zigbee.temperatureConfig(30, 300) +
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
return refresh() + configCmds return configCmds + refresh()
} }
private getEndpointId() { private getEndpointId() {

View File

@@ -206,8 +206,6 @@ def getTemperature(value) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }
@@ -277,8 +275,12 @@ def getTemperature(value) {
def configure() { def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false) sendEvent(name: "checkInterval", value: 7200, displayed: false)
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings." log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [ def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
@@ -291,7 +293,7 @@ def configure() {
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}", "zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
] ]
return refresh() + configCmds // send refresh cmds as part of config return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -24,10 +24,10 @@ Works with:
## Device Health ## Device Health
A Category C2 open/closed sensor with maxReportTime of 5 mins. A Category C2 open/closed sensor with maxReportTime of 1 hr.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*60 = 120 min
## Battery Specification ## Battery Specification

View File

@@ -16,7 +16,7 @@
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata { metadata {
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Battery" capability "Battery"
capability "Configuration" capability "Configuration"
capability "Contact Sensor" capability "Contact Sensor"
@@ -92,6 +92,13 @@ def parse(String description) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
@@ -109,28 +116,15 @@ private Map parseCatchAllMessage(String description) {
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
// 0x07 - configure reporting resultMap = getBatteryResult(cluster.data.last())
if (cluster.command != 0x07) {
resultMap = getBatteryResult(cluster.data.last())
}
break break
case 0x0402: case 0x0402:
if (cluster.command == 0x07){ log.debug 'TEMP'
if (cluster.data[0] == 0x00) { // temp is last 2 data values. reverse to swap endian
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) def value = getTemperature(temp)
} resultMap = getTemperatureResult(value)
else {
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
}
break break
} }
} }
@@ -140,8 +134,10 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
@@ -211,8 +207,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }
@@ -252,7 +246,15 @@ private Map getContactResult(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
@@ -266,15 +268,23 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings." log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
// battery minReport 30 seconds, maxReportTime 6 hrs by default "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config "send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -24,10 +24,10 @@ Works with:
## Device Health ## Device Health
A Category C2 SmartSense Temp/Humidity Sensor with maxReportTime of 5 mins. A Category C2 SmartSense Temp/Humidity Sensor with maxReportTime of 1 hr.
Check-in interval is double the value of maxReportTime. Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline. This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins Check-in interval = 2*60 = 120 min
## Battery Specification ## Battery Specification

View File

@@ -14,7 +14,7 @@
* *
*/ */
metadata { metadata {
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Configuration" capability "Configuration"
capability "Battery" capability "Battery"
capability "Refresh" capability "Refresh"
@@ -83,6 +83,13 @@ def parse(String description) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
log.debug "Parse returned $map" log.debug "Parse returned $map"
return map ? createEvent(map) : null return map ? createEvent(map) : null
} }
@@ -93,37 +100,20 @@ private Map parseCatchAllMessage(String description) {
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
// 0x07 - configure reporting resultMap = getBatteryResult(cluster.data.last())
if (cluster.command != 0x07) {
resultMap = getBatteryResult(cluster.data.last())
}
break break
case 0x0402: case 0x0402:
if (cluster.command == 0x07) { // temp is last 2 data values. reverse to swap endian
if (cluster.data[0] == 0x00){ String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster def value = getTemperature(temp)
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) resultMap = getTemperatureResult(value)
} break
else {
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
}
break
case 0xFC45: case 0xFC45:
// 0x07 - configure reporting String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('')
if (cluster.command != 0x07) { String display = Math.round(Integer.valueOf(pctStr, 16) / 100)
String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('') resultMap = getHumidityResult(display)
String display = Math.round(Integer.valueOf(pctStr, 16) / 100)
resultMap = getHumidityResult(display)
}
break break
} }
} }
@@ -133,8 +123,10 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
@@ -222,8 +214,6 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct) result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }
@@ -250,40 +240,59 @@ private Map getTemperatureResult(value) {
private Map getHumidityResult(value) { private Map getHumidityResult(value) {
log.debug 'Humidity' log.debug 'Humidity'
return value ? [name: 'humidity', value: value, unit: '%'] : [:] return [
name: 'humidity',
value: value,
unit: '%'
]
} }
/** /**
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() def refresh()
{ {
log.debug "refresh temperature, humidity, and battery" log.debug "refresh temperature, humidity, and battery"
return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware [
zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware
zigbee.readAttribute(0x0402, 0x0000) + "zcl mfg-code 0xC2DF", "delay 1000",
zigbee.readAttribute(0x0001, 0x0020) "zcl global read 0xFC45 0", "delay 1000",
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
]
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
def humidityConfigCmds = [ def configCmds = [
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}", "zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500" "send 0x${device.deviceNetworkId} 1 1", "delay 500"
] ]
return configCmds + refresh() // send refresh cmds as part of config
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default
return refresh() + humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
} }
private hex(value) { private hex(value) {

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,42 +0,0 @@
# Tyco Door Window Sensor
Works with:
* [Tyco Door Window Sensor](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Battery](#battery-specification)
## Capabilities
* **Battery** - defines device uses a battery
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Contact Sensor** - can detect contact (open/close)
* **Refresh** - _refresh()_ command for status updates
* **Temperature Measurement** - can measure the device temperature
* **Health Check** - indicates ability to get device health notifications
## Device Health
Contact sensor with maxReportTime of 5 mins.
Check-in interval is double the value of maxReportTime for Zigbee device.
This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 min
## Battery Specification
3V CR2032 battery is required.
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that either the sensor needs to be reseted or the sensor is out of range.
Reset needs to be done by inserting the battery in the sensor and then quickly pressing the adjacent black button 10 times. Pairing should be tried again now.
It may happen that sensor is out of range, then pairing needs to be tried again by placing the sensor closer to the hub.
Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links
for the different models:
* [Tyco Door Window Sensor (MCT-340)](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)

View File

@@ -22,7 +22,6 @@ metadata {
capability "Contact Sensor" capability "Contact Sensor"
capability "Refresh" capability "Refresh"
capability "Temperature Measurement" capability "Temperature Measurement"
capability "Health Check"
command "enrollResponse" command "enrollResponse"
@@ -230,42 +229,44 @@ private Map getContactResult(value) {
] ]
} }
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return zigbee.readAttribute(0x0402, 0x0000) // Read the Temperature Cluster
}
def refresh() def refresh()
{ {
log.debug "Refreshing Temperature and Battery" log.debug "Refreshing Temperature and Battery"
def refreshCmds = [ [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20" "st rattr 0x${device.deviceNetworkId} 1 1 0x20"
] ]
return refreshCmds + enrollResponse()
} }
def configure() { def configure() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings." log.debug "Configuring Reporting, IAS CIE, and Bindings."
def enrollCmds = [ def configCmds = [
"delay 1000", "delay 1000",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500", "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
//"raw 0x500 {01 23 00 00 00}", "delay 200", //"raw 0x500 {01 23 00 00 00}", "delay 200",
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500", //"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}",
"delay 500"
] ]
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -89,8 +89,14 @@ def parse(String description) {
} }
private Map parseIasButtonMessage(String description) { private Map parseIasButtonMessage(String description) {
def zs = zigbee.parseZoneStatus(description) int zoneInt = Integer.parseInt((description - "zone status 0x"), 16)
return zs.isAlarm2Set() ? getButtonResult("press") : getButtonResult("release") if (zoneInt & 0x02) {
resultMap = getButtonResult('press')
} else {
resultMap = getButtonResult('release')
}
return resultMap
} }
private Map getBatteryResult(rawValue) { private Map getBatteryResult(rawValue) {

View File

@@ -93,5 +93,5 @@ def refresh() {
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
refresh() zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
} }

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,36 +0,0 @@
# OSRAM Lightify LED On/Off/Dim
Works with:
* [OSRAM Lightify LED On/Off/Dim](https://shop.smartthings.com/#!/products/osram-led-smart-bulb-on-off-dim)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Battery](#battery-specification)
## Capabilities
* **Actuator** - represents that a Device has commands
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Refresh** - _refresh()_ command for status updates
* **Switch** - can detect state (possible values: on/off)
* **Switch Level** - represents current light level, usually 0-100 in percent
* **Health Check** - indicates ability to get device health notifications
## Device Health
A Category C1 Zigbee dimmer with maxReportTime of 5 mins.
Check-in interval is double the value of maxReportTime.
This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
Other troubleshooting tips are listed as follows:
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/207191763-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-On-Off-Dim)

View File

@@ -54,27 +54,20 @@ def parse(String description) {
def event = zigbee.getEvent(description) def event = zigbee.getEvent(description)
if (event) { if (event) {
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
if (event.name=="level" && event.value==0) {} if (event.name=="level" && event.value==0) {}
else { else {
sendEvent(event) sendEvent(event)
} }
} }
else { else {
def cluster = zigbee.parse(description) log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
if (cluster.data[0] == 0x00) {
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug "${cluster}"
}
} }
} }
@@ -93,19 +86,24 @@ def setLevel(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.onOffRefresh()
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
} }
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Enrolls device to Device-Watch with 3 x Reporting interval 30min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
} }

View File

@@ -31,8 +31,7 @@
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: "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: "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" 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"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock" }
}
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){ multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
@@ -90,7 +89,7 @@ def configure() {
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
TYPE_U8, 600, 21600, 0x01) TYPE_U8, 600, 21600, 0x01)
log.info "configure() --- cmds: $cmds" log.info "configure() --- cmds: $cmds"
return refresh() + cmds // send refresh cmds as part of config return cmds + refresh() // send refresh cmds as part of config
} }
def refresh() { def refresh() {

View File

@@ -1,154 +0,0 @@
/**
* Copyright 2016 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* 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.
*
* Author: SmartThings
* Date: 2016-01-19
*
* This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature)
*/
metadata {
definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Color Control"
capability "Configuration"
capability "Polling"
capability "Refresh"
capability "Switch"
capability "Switch Level"
capability "Health Check"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
}
// 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.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"color control.setColor"
}
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["switch", "refresh"])
}
}
//Globals
private getATTRIBUTE_HUE() { 0x0000 }
private getATTRIBUTE_SATURATION() { 0x0001 }
private getHUE_COMMAND() { 0x00 }
private getSATURATION_COMMAND() { 0x03 }
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) {
log.debug event
if (event.name=="level" && event.value==0) {}
else {
sendEvent(event)
}
}
else {
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
def cluster = zigbee.parse(description)
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
}
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
}
}
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
if (cluster.data[0] == 0x00){
log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster"
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
log.info "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbeeMap
}
}
}
def on() {
zigbee.on()
}
def off() {
zigbee.off()
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return zigbee.onOffRefresh()
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
}
def configure() {
log.debug "Configuring Reporting and Bindings."
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
// enrolls with default periodic reporting until newer 5 min interval is confirmed
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
}
def setLevel(value) {
zigbee.setLevel(value)
}
def setColor(value){
log.trace "setColor($value)"
zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation)
}
def setHue(value) {
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
}
def setSaturation(value) {
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
}

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,42 +0,0 @@
# OSRAM LIGHTIFY LED RGBW Bulb
Works with:
* [OSRAM LIGHTIFY LED RGBW Bulb](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Battery](#battery-specification)
## Capabilities
* **Actuator** - It represents that a device has commands.
* **Color Control** - It represents that the color attributes of a device can be controlled (hue, saturation, color value).
* **Color Temperature** - It represents color temperature capability measured in degree Kelvin.
* **Polling** - It represents that a device can be polled.
* **Switch** - can detect state (possible values: on/off)
* **Switch Level** - can detect current light level (0-100 in percent)
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Refresh** - _refresh()_ command for status updates
* **Health Check** - indicates ability to get device health notifications
## Device Health
A Category C6 OSRAM LIGHTIFY LED RGBW Bulb with maxReportTime of 5 mins.
Check-in interval is double the value of maxReportTime.
This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
It may also happen that you need to reset the device.
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
* [Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)

View File

@@ -85,6 +85,12 @@ def parse(String description) {
def event = zigbee.getEvent(description) def event = zigbee.getEvent(description)
if (event) { if (event) {
log.debug event log.debug event
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
if (event.name=="level" && event.value==0) {} if (event.name=="level" && event.value==0) {}
else { else {
if (event.name=="colorTemperature") { if (event.name=="colorTemperature") {
@@ -95,11 +101,11 @@ def parse(String description) {
} }
else { else {
def zigbeeMap = zigbee.parseDescriptionAsMap(description) def zigbeeMap = zigbee.parseDescriptionAsMap(description)
def cluster = zigbee.parse(description) log.trace "zigbeeMap : $zigbeeMap"
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed") sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
} }
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
@@ -107,18 +113,8 @@ def parse(String description) {
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false) sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
} }
} }
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
if (cluster.data[0] == 0x00){
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else { else {
log.info "DID NOT PARSE MESSAGE for description : $description" log.info "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbeeMap
} }
} }
} }
@@ -134,21 +130,26 @@ def off() {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.onOffRefresh()
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) zigbee.onOffRefresh() + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
} }
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Enrolls device to Device-Watch with 3 x Reporting interval 30min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh()
} }
def setColorTemperature(value) { def setColorTemperature(value) {
@@ -189,5 +190,5 @@ def setHue(value) {
def setSaturation(value) { def setSaturation(value) {
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
} }

View File

@@ -82,5 +82,5 @@ def refresh() {
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
refresh() zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
} }

View File

@@ -78,5 +78,5 @@ def refresh() {
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
zigbee.onOffRefresh() + zigbee.onOffConfig() zigbee.onOffConfig() + zigbee.onOffRefresh()
} }

View File

@@ -134,5 +134,10 @@ def refresh() {
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
refresh() zigbee.onOffConfig() +
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) +
zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1) +
zigbee.onOffRefresh() +
zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) +
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
} }

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,37 +0,0 @@
# OSRAM Lightify Tunable 60 White
Works with:
* [OSRAM Lightify Tunable 60 White](http://www.osram.com/osram_com/tools-and-services/tools/lightify---smart-connected-light/lightify-for-home---what-is-light-to-you/lightify-products/lightify-classic-a60-tunable-white/index.jsp)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Battery](#battery-specification)
## Capabilities
* **Actuator** - represents that a Device has commands
* **Color Temperature** - represents color temperature, measured in degrees Kelvin.
* **Configuration** - _configure()_ command called when device is installed or device preferences updated.
* **Refresh** - _refresh()_ command for status updates
* **Switch** - can detect state (possible values: on/off)
* **Switch Level** - represents current light level, usually 0-100 in percent
* **Health Check** - indicates ability to get device health notifications
## Device Health
A Category C1 OSRAM Lightify Tunable 60 White with maxReportTime of 5 mins.
Check-in interval is double the value of maxReportTime.
This gives the device twice the amount of time to respond before it is marked as offline.
Check-in interval = 12 mins
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
Other troubleshooting tips are listed as follows:
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/204576454-OSRAM-LIGHTIFY-Tunable-White-60-Bulb)

View File

@@ -74,6 +74,12 @@ def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description) def event = zigbee.getEvent(description)
if (event) { if (event) {
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
if (event.name=="level" && event.value==0) {} if (event.name=="level" && event.value==0) {}
else { else {
if (event.name=="colorTemperature") { if (event.name=="colorTemperature") {
@@ -83,21 +89,8 @@ def parse(String description) {
} }
} }
else { else {
def cluster = zigbee.parse(description) log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
if (cluster.data[0] == 0x00) {
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug "${cluster}"
}
} }
} }
@@ -117,21 +110,26 @@ def setLevel(value) {
* PING is used by Device-Watch in attempt to reach the Device * PING is used by Device-Watch in attempt to reach the Device
* */ * */
def ping() { def ping() {
return zigbee.onOffRefresh()
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
} }
def refresh() { def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
} }
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Enrolls device to Device-Watch with 3 x Reporting interval 30min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh()
} }
def setColorTemperature(value) { def setColorTemperature(value) {

View File

@@ -1,134 +0,0 @@
/**
* Copyright 2016 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* 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: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Color Control"
capability "Configuration"
capability "Polling"
capability "Refresh"
capability "Switch"
capability "Switch Level"
}
// 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.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"color control.setColor"
}
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["switch", "refresh"])
}
}
//Globals
private getATTRIBUTE_HUE() { 0x0000 }
private getATTRIBUTE_SATURATION() { 0x0001 }
private getHUE_COMMAND() { 0x00 }
private getSATURATION_COMMAND() { 0x03 }
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getEvent(description)
if (finalResult) {
log.debug finalResult
sendEvent(finalResult)
}
else {
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
log.trace "zigbeeMap : $zigbeeMap"
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
sendEvent(name: "hue", value: hueValue, displayed:false)
}
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
sendEvent(name: "saturation", value: saturationValue, displayed:false)
}
}
else {
log.info "DID NOT PARSE MESSAGE for description : $description"
}
}
}
def on() {
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
}
def off() {
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
}
def refresh() {
refreshAttributes() + configureAttributes()
}
def poll() {
refreshAttributes()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
configureAttributes() + refreshAttributes()
}
def configureAttributes() {
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
}
def refreshAttributes() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
}
def setLevel(value) {
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
def setColor(value){
log.trace "setColor($value)"
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
}
def setHue(value) {
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
}
def setSaturation(value) {
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
}

View File

@@ -123,7 +123,7 @@ def configureAttributes() {
} }
def refreshAttributes() { def refreshAttributes() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION)
} }
def setColorTemperature(value) { def setColorTemperature(value) {
@@ -141,10 +141,10 @@ def setColor(value){
def setHue(value) { def setHue(value) {
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
} }
def setSaturation(value) { def setSaturation(value) {
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
} }

View File

@@ -65,16 +65,7 @@ void updateSwitch() {
private void updateAll(devices) { private void updateAll(devices) {
def command = request.JSON?.command def command = request.JSON?.command
if (command) { if (command) {
switch(command) { devices."$command"()
case "on":
devices.on()
break
case "off":
devices.off()
break
default:
httpError(403, "Access denied. This command is not supported by current capability.")
}
} }
} }
@@ -86,16 +77,7 @@ private void update(devices) {
if (!device) { if (!device) {
httpError(404, "Device not found") httpError(404, "Device not found")
} else { } else {
switch(command) { device."$command"()
case "on":
device.on()
break
case "off":
device.off()
break
default:
httpError(403, "Access denied. This command is not supported by current capability.")
}
} }
} }
} }

View File

@@ -58,7 +58,7 @@ def authPage() {
if (canInstallLabs()) { if (canInstallLabs()) {
def redirectUrl = getBuildRedirectUrl() def redirectUrl = getBuildRedirectUrl()
// log.debug "Redirect url = ${redirectUrl}" log.debug "Redirect url = ${redirectUrl}"
if (state.authToken) { if (state.authToken) {
description = "Tap 'Next' to proceed" description = "Tap 'Next' to proceed"
@@ -113,13 +113,13 @@ def oauthInitUrl() {
scope: "read_station" scope: "read_station"
] ]
// log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}" log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
} }
def callback() { def callback() {
// log.debug "callback()>> params: $params, params.code ${params.code}" log.debug "callback()>> params: $params, params.code ${params.code}"
def code = params.code def code = params.code
def oauthState = params.state def oauthState = params.state
@@ -135,7 +135,7 @@ def callback() {
scope: "read_station" scope: "read_station"
] ]
// log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}" log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
def tokenUrl = getVendorTokenPath() def tokenUrl = getVendorTokenPath()
def params = [ def params = [
@@ -144,7 +144,7 @@ def callback() {
body: tokenParams body: tokenParams
] ]
// log.debug "PARAMS: ${params}" log.debug "PARAMS: ${params}"
httpPost(params) { resp -> httpPost(params) { resp ->
@@ -156,7 +156,7 @@ def callback() {
state.refreshToken = data.refresh_token state.refreshToken = data.refresh_token
state.authToken = data.access_token state.authToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000) state.tokenExpires = now() + (data.expires_in * 1000)
// log.debug "swapped token: $resp.data" log.debug "swapped token: $resp.data"
} }
} }
@@ -292,7 +292,7 @@ def refreshToken() {
response.data.each {key, value -> response.data.each {key, value ->
def data = slurper.parseText(key); def data = slurper.parseText(key);
// log.debug "Data: $data" log.debug "Data: $data"
state.refreshToken = data.refresh_token state.refreshToken = data.refresh_token
state.accessToken = data.access_token state.accessToken = data.access_token
@@ -387,18 +387,18 @@ def getDeviceList() {
state.deviceDetail = [:] state.deviceDetail = [:]
state.deviceState = [:] state.deviceState = [:]
apiGet("/api/getstationsdata") { response -> apiGet("/api/devicelist") { response ->
response.data.body.devices.each { value -> response.data.body.devices.each { value ->
def key = value._id def key = value._id
deviceList[key] = "${value.station_name}: ${value.module_name}" deviceList[key] = "${value.station_name}: ${value.module_name}"
state.deviceDetail[key] = value state.deviceDetail[key] = value
state.deviceState[key] = value.dashboard_data state.deviceState[key] = value.dashboard_data
value.modules.each { value2 -> }
def key2 = value2._id response.data.body.modules.each { value ->
deviceList[key2] = "${value.station_name}: ${value2.module_name}" def key = value._id
state.deviceDetail[key2] = value2 deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}"
state.deviceState[key2] = value2.dashboard_data state.deviceDetail[key] = value
} state.deviceState[key] = value.dashboard_data
} }
} }

View File

@@ -1,7 +1,7 @@
/** /**
* Smart Windows * Smart Windows
* Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!). * Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!).
* *
* Copyright 2014 Eric Gideon * Copyright 2014 Eric Gideon
* *
* Based in part on the "When it's going to rain" SmartApp by the SmartThings team, * Based in part on the "When it's going to rain" SmartApp by the SmartThings team,
@@ -21,18 +21,13 @@ definition(
name: "Smart Windows", name: "Smart Windows",
namespace: "egid", namespace: "egid",
author: "Eric Gideon", author: "Eric Gideon",
description: "Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.", description: "Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.",
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png" iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
) )
preferences { preferences {
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
}
section( "Set the temperature range for your comfort zone..." ) { section( "Set the temperature range for your comfort zone..." ) {
input "minTemp", "number", title: "Minimum temperature" input "minTemp", "number", title: "Minimum temperature"
input "maxTemp", "number", title: "Maximum temperature" input "maxTemp", "number", title: "Maximum temperature"
@@ -44,11 +39,9 @@ preferences {
input "inTemp", "capability.temperatureMeasurement", title: "Indoor" input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
} }
section( "Set your location" ) {
if (location.channelName != 'samsungtv') { input "zipCode", "text", title: "Zip code"
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } }
}
section( "Notifications" ) { section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "retryPeriod", "number", title: "Minutes between notifications:" input "retryPeriod", "number", title: "Minutes between notifications:"
@@ -79,7 +72,7 @@ def temperatureHandler(evt) {
def currentInTemp = evt.doubleValue def currentInTemp = evt.doubleValue
def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' }
log.trace "Temp event: $evt" log.trace "Temp event: $evt"
log.info "In: $currentInTemp; Out: $currentOutTemp" log.info "In: $currentInTemp; Out: $currentOutTemp"
@@ -105,7 +98,7 @@ def temperatureHandler(evt) {
if ( currentOutTemp < maxTemp && !openWindows ) { if ( currentOutTemp < maxTemp && !openWindows ) {
send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else if ( currentOutTemp > maxTemp && openWindows ) { } else if ( currentOutTemp > maxTemp && openWindows ) {
send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else { } else {
log.debug "No notifications sent. Everything is in the right place." log.debug "No notifications sent. Everything is in the right place."
} }
@@ -132,11 +125,7 @@ def temperatureHandler(evt) {
} }
def weatherCheck() { def weatherCheck() {
def json def json = getWeatherFeature("conditions", zipCode)
if (location.channelName != 'samsungtv')
json = getWeatherFeature("conditions", zipCode)
else
json = getWeatherFeature("conditions")
def currentTemp = json?.current_observation?.temp_f def currentTemp = json?.current_observation?.temp_f
if ( currentTemp ) { if ( currentTemp ) {
@@ -161,4 +150,4 @@ private send(msg) {
} }
log.info msg log.info msg
} }

View File

@@ -1,253 +0,0 @@
/**
* Gideon
*
* Copyright 2016 Nicola Russo
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "Gideon",
namespace: "gideon.api",
author: "Braindrain Solutions",
description: "Gideon AI Smart app allows you to connect and control all of your SmartThings devices through the Gideon AI app, making your SmartThings devices even smarter.",
category: "Family",
iconUrl: "http://s33.postimg.org/t77u7y7v3/logo.png",
iconX2Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
iconX3Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
oauth: [displayName: "Gideon AI API", displayLink: "gideon.ai"])
preferences {
section("Control these switches...") {
input "switches", "capability.switch", multiple:true
}
section("Control these motion sensors...") {
input "motions", "capability.motionSensor", multiple:true
}
section("Control these presence sensors...") {
input "presence_sensors", "capability.presenceSensor", multiple:true
}
section("Control these outlets...") {
input "outlets", "capability.switch", multiple:true
}
section("Control these locks...") {
input "locks", "capability.lock", multiple:true
}
section("Control these locks...") {
input "temperature_sensors", "capability.temperatureMeasurement"
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
// TODO: subscribe to attributes, devices, locations, etc.
subscribe(outlet, "energy", outletHandler)
subscribe(outlet, "switch", outletHandler)
}
// TODO: implement event handlers
def outletHandler(evt) {
log.debug "$outlet.currentEnergy"
//TODO call G API
}
private device(it, type) {
it ? [id: it.id, label: it.label, type: type] : null
}
//API Mapping
mappings {
path("/getalldevices") {
action: [
GET: "getAllDevices"
]
}
path("/doorlocks/:id/:command") {
action: [
GET: "updateDoorLock"
]
}
path("/doorlocks/:id") {
action: [
GET: "getDoorLockStatus"
]
}
path("/tempsensors/:id") {
action: [
GET: "getTempSensorsStatus"
]
}
path("/presences/:id") {
action: [
GET: "getPresenceStatus"
]
}
path("/motions/:id") {
action: [
GET: "getMotionStatus"
]
}
path("/outlets/:id") {
action: [
GET: "getOutletStatus"
]
}
path("/outlets/:id/:command") {
action: [
GET: "updateOutlet"
]
}
path("/switches/:command") {
action: [
PUT: "updateSwitch"
]
}
}
//API Methods
def getAllDevices() {
def locks_list = locks.collect{device(it,"Lock")}
def presences_list = presence_sensors.collect{device(it,"Presence")}
def motions_list = motions.collect{device(it,"Motion")}
def outlets_list = outlets.collect{device(it,"Outlet")}
def switches_list = switches.collect{device(it,"Switch")}
def temp_list = temperature_sensors.collect{device(it,"Temperature")}
return [Locks: locks_list, Presences: presences_list, Motions: motions_list, Outlets: outlets_list, Switches: switches_list, Temperatures: temp_list]
}
//LOCKS
def getDoorLockStatus() {
def device = locks.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
return [Device_state: device.currentValue('lock')]
}
}
def updateDoorLock() {
def command = params.command
def device = locks.find { it.id == params.id }
if (command){
if (!device) {
httpError(404, "Device not found")
} else {
if(command == "toggle")
{
if(device.currentValue('lock') == "locked")
device.unlock();
else
device.lock();
return [Device_id: params.id, result_action: "200"]
}
}
}
}
//PRESENCE
def getPresenceStatus() {
def device = presence_sensors.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
return [Device_state: device.currentValue('presence')]
}
}
//MOTION
def getMotionStatus() {
def device = motions.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
return [Device_state: device.currentValue('motion')]
}
}
//OUTLET
def getOutletStatus() {
def device = outlets.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
return [Device_state: device.currentSwitch, Current_watt: device.currentValue("energy")]
}
}
def updateOutlet() {
def command = params.command
def device = outlets.find { it.id == params.id }
if (command){
if (!device) {
httpError(404, "Device not found")
} else {
if(command == "toggle")
{
if(device.currentSwitch == "on")
device.off();
else
device.on();
return [Device_id: params.id, result_action: "200"]
}
}
}
}
//SWITCH
def updateSwitch() {
def command = params.command
def device = switches.find { it.id == params.id }
if (command){
if (!device) {
httpError(404, "Device not found")
} else {
if(command == "toggle")
{
if(device.currentSwitch == "on")
device.off();
else
device.on();
return [Device_id: params.id, result_action: "200"]
}
}
}
}
//TEMPERATURE
def getTempSensorsStatus() {
def device = temperature_sensors.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
return [Device_state: device.currentValue('temperature')]
}
}

View File

@@ -18,13 +18,8 @@ definition(
) )
preferences { preferences {
section("Zip code?") {
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { input "zipcode", "text", title: "Zipcode?"
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
}
if (location.channelName != 'samsungtv') {
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
} }
section("Things to check?") { section("Things to check?") {
@@ -65,11 +60,7 @@ def scheduleCheck(evt) {
// Only need to poll if we haven't checked in a while - and if something is left open. // Only need to poll if we haven't checked in a while - and if something is left open.
if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) { if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) {
log.info("Something's open - let's check the weather.") log.info("Something's open - let's check the weather.")
def response def response = getWeatherFeature("forecast", zipcode)
if (location.channelName != 'samsungtv')
response = getWeatherFeature("forecast", zipCode)
else
response = getWeatherFeature("forecast")
def weather = isStormy(response) def weather = isStormy(response)
if(weather) { if(weather) {

View File

@@ -26,9 +26,9 @@ preferences {
} }
section("Temperature monitor?") { section("Temperature monitor?") {
input "temp", "capability.temperatureMeasurement", title: "Temperature Sensor", required: false input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false
input "maxTemp", "number", title: "Max Temperature (°${location.temperatureScale})", required: false input "maxTemp", "number", title: "Max Temp?", required: false
input "minTemp", "number", title: "Min Temperature (°${location.temperatureScale})", required: false input "minTemp", "number", title: "Min Temp?", required: false
} }
section("When which people are away?") { section("When which people are away?") {

View File

@@ -4,9 +4,6 @@
* Author: Juan Risso * Author: Juan Risso
* Date: 2013-12-19 * Date: 2013-12-19
*/ */
include 'asynchttp_v1'
definition( definition(
name: "Jawbone UP (Connect)", name: "Jawbone UP (Connect)",
namespace: "juano2310", namespace: "juano2310",
@@ -31,7 +28,7 @@ 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/initialize") {action: [GET: "oauthInitUrl"]}
path("/oauth/callback") { action: [ GET: "callback" ] } path("/oauth/callback") { action: [ GET: "callback" ] }
} }
@@ -47,7 +44,7 @@ def callback() {
} else { } else {
log.warn "No authQueryString" log.warn "No authQueryString"
} }
if (state.JawboneAccessToken) { if (state.JawboneAccessToken) {
log.debug "Access token already exists" log.debug "Access token already exists"
setup() setup()
@@ -76,7 +73,7 @@ def callback() {
def authPage() { def authPage() {
log.debug "authPage" log.debug "authPage"
def description = null def description = null
if (state.JawboneAccessToken == null) { if (state.JawboneAccessToken == null) {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
@@ -85,13 +82,12 @@ def authPage() {
description = "Click to enter Jawbone Credentials" description = "Click to enter Jawbone Credentials"
def redirectUrl = buildRedirectUrl def redirectUrl = buildRedirectUrl
log.debug "RedirectURL = ${redirectUrl}" log.debug "RedirectURL = ${redirectUrl}"
def donebutton= state.JawboneAccessToken != null def donebutton= state.JawboneAccessToken != null
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) { return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,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."
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) { return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description } section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
} }
@@ -111,7 +107,7 @@ def receiveToken(redirectUrl = null) {
def params = [ def params = [
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
] ]
httpGet(params) { response -> httpGet(params) { response ->
log.debug "${response.data}" log.debug "${response.data}"
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}" log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
state.JawboneAccessToken = response.data.access_token state.JawboneAccessToken = response.data.access_token
@@ -153,7 +149,7 @@ def connectionStatus(message, redirectUrl = null) {
<meta http-equiv="refresh" content="3; url=${redirectUrl}" /> <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
""" """
} }
def html = """ def html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -233,12 +229,12 @@ 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=${appSettings.clientSecret}"
try { try {
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
if (response.status == 200) { if (response.status == 200) {
log.debug "${response.data}" log.debug "${response.data}"
log.debug "Setting refresh token" log.debug "Setting refresh token to ${response.data.data.refresh_token}"
state.refreshToken = response.data.data.refresh_token state.refreshToken = response.data.data.refresh_token
} }
} }
@@ -262,7 +258,7 @@ def validateCurrentToken() {
state.remove("refreshToken") state.remove("refreshToken")
} }
} else { } else {
log.debug "Setting access token" log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}"
state.JawboneAccessToken = data.access_token state.JawboneAccessToken = data.access_token
state.refreshToken = data.refresh_token state.refreshToken = data.refresh_token
} }
@@ -275,10 +271,10 @@ def validateCurrentToken() {
} }
def initialize() { def initialize() {
log.debug "Callback URL - Webhook" log.debug "Callback URL - Webhook"
def localServerUrl = getApiServerUrl() def localServerUrl = getApiServerUrl()
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" 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"
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
} }
@@ -288,16 +284,16 @@ def setup() {
if (state.JawboneAccessToken) { if (state.JawboneAccessToken) {
def urlmember = "https://jawbone.com/nudge/api/users/@me/" def urlmember = "https://jawbone.com/nudge/api/users/@me/"
def member = null def member = null
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
member = response.data.data member = response.data.data
} }
if (member) { if (member) {
state.member = member state.member = member
def externalId = "${app.id}.${member.xid}" def externalId = "${app.id}.${member.xid}"
// find the appropriate child device based on my app id and the device network id // find the appropriate child device based on my app id and the device network id
def deviceWrapper = getChildDevice("${externalId}") def deviceWrapper = getChildDevice("${externalId}")
// invoke the generatePresenceEvent method on the child device // invoke the generatePresenceEvent method on the child device
@@ -306,8 +302,7 @@ def setup() {
def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true]) def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
if (childDevice) { if (childDevice) {
log.debug "Child Device Successfully Created" log.debug "Child Device Successfully Created"
childDevice?.generateSleepingEvent(false) generateInitialEvent (member, childDevice)
pollChild(childDevice)
} }
} }
} }
@@ -317,7 +312,7 @@ def setup() {
} }
def installed() { def installed() {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
@@ -329,7 +324,7 @@ def installed() {
} }
def updated() { def updated() {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
@@ -353,128 +348,128 @@ def uninstalled() {
} }
def pollChild(childDevice) { def pollChild(childDevice) {
def childMap = [ value: "$childDevice.device.deviceNetworkId}"] def member = state.member
generatePollingEvents (member, childDevice)
def params = [
uri: 'https://jawbone.com',
path: '/nudge/api/users/@me/goals',
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
contentType: 'application/json'
]
asynchttp_v1.get('responseGoals', params, childMap)
def params2 = [
uri: 'https://jawbone.com',
path: '/nudge/api/users/@me/moves',
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
contentType: 'application/json'
]
asynchttp_v1.get('responseMoves', params2, childMap)
} }
def responseGoals(response, dni) { def generatePollingEvents (member, childDevice) {
if (response.hasError()) { // lets figure out if the member is currently "home" (At the place)
log.error "response has error: $response.errorMessage" def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
} else { def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def goals def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
try { def goals = null
// json response already parsed into JSONElement object def moves = null
goals = response.json.data def sleeps = null
} catch (e) { httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
log.error "error parsing json from response: $e" goals = response.data.data
} }
if (goals) { httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
def childDevice = getChildDevice(dni.value) moves = response.data.data.items[0]
log.debug "Goal = ${goals.move_steps} Steps" }
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
} else { try { // we are going to just ignore any errors
log.debug "did not get json results from response body: $response.data" log.debug "Member = ${member.first}"
} log.debug "Moves Goal = ${goals.move_steps} Steps"
} log.debug "Moves = ${moves.details.steps} Steps"
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice)
}
catch (e) {
// eat it
}
} }
def responseMoves(response, dni) { def generateInitialEvent (member, childDevice) {
if (response.hasError()) { // lets figure out if the member is currently "home" (At the place)
log.error "response has error: $response.errorMessage" def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
} else { def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def moves def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
try { def goals = null
// json response already parsed into JSONElement object def moves = null
moves = response.json.data.items[0] def sleeps = null
} catch (e) { httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
log.error "error parsing json from response: $e" goals = response.data.data
} }
if (moves) { httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
def childDevice = getChildDevice(dni.value) moves = response.data.data.items[0]
log.debug "Moves = ${moves.details.steps} Steps" }
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
} else { try { // we are going to just ignore any errors
log.debug "did not get json results from response body: $response.data" log.debug "Member = ${member.first}"
} log.debug "Moves Goal = ${goals.move_steps} Steps"
} log.debug "Moves = ${moves.details.steps} Steps"
log.debug "Sleeping state = false"
childDevice?.generateSleepingEvent(false)
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice)
}
catch (e) {
// eat it
}
} }
def setColor (steps,goal,childDevice) { def setColor (steps,goal,childDevice) {
def result = steps * 100 / goal def result = steps * 100 / goal
if (result < 25) if (result < 25)
childDevice?.sendEvent(name:"steps", value: "steps", label: steps) childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
else if ((result >= 25) && (result < 50)) else if ((result >= 25) && (result < 50))
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
else if ((result >= 50) && (result < 75)) else if ((result >= 50) && (result < 75))
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
else if (result >= 75) else if (result >= 75)
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
} }
def hookEventHandler() { def hookEventHandler() {
// log.debug "In hookEventHandler method." // log.debug "In hookEventHandler method."
log.debug "request = ${request}" log.debug "request = ${request}"
def json = request.JSON def json = request.JSON
// get some stuff we need // get some stuff we need
def userId = json.events.user_xid[0] def userId = json.events.user_xid[0]
def json_type = json.events.type[0] def json_type = json.events.type[0]
def json_action = json.events.action[0] def json_action = json.events.action[0]
//log.debug json //log.debug json
log.debug "Userid = ${userId}" log.debug "Userid = ${userId}"
log.debug "Notification Type: " + json_type log.debug "Notification Type: " + json_type
log.debug "Notification Action: " + json_action log.debug "Notification Action: " + json_action
// find the appropriate child device based on my app id and the device network id // find the appropriate child device based on my app id and the device network id
def externalId = "${app.id}.${userId}" def externalId = "${app.id}.${userId}"
def childDevice = getChildDevice("${externalId}") def childDevice = getChildDevice("${externalId}")
if (childDevice) { if (childDevice) {
switch (json_action) { switch (json_action) {
case "enter_sleep_mode": case "enter_sleep_mode":
childDevice?.generateSleepingEvent(true) childDevice?.generateSleepingEvent(true)
break break
case "exit_sleep_mode": case "exit_sleep_mode":
childDevice?.generateSleepingEvent(false) childDevice?.generateSleepingEvent(false)
break break
case "creation": case "creation":
childDevice?.sendEvent(name:"steps", value: 0) childDevice?.sendEvent(name:"steps", value: 0)
break break
case "updation": case "updation":
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def goals = null def goals = null
def moves = null def moves = null
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
goals = response.data.data goals = response.data.data
} }
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
moves = response.data.data.items[0] moves = response.data.data.items[0]
} }
log.debug "Goal = ${goals.move_steps} Steps" log.debug "Goal = ${goals.move_steps} Steps"
log.debug "Steps = ${moves.details.steps} Steps" log.debug "Steps = ${moves.details.steps} Steps"
childDevice?.sendEvent(name:"steps", value: moves.details.steps) childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps) childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice) //setColor(moves.details.steps,goals.move_steps,childDevice)
break break
case "deletion": case "deletion":
app.delete() app.delete()

View File

@@ -14,7 +14,7 @@
* for the specific language governing permissions and limitations under the License. * for the specific language governing permissions and limitations under the License.
* *
*/ */
definition( definition(
name: "Smart Home Ventilation", name: "Smart Home Ventilation",
namespace: "MichaelStruck", namespace: "MichaelStruck",
@@ -164,7 +164,7 @@ def installed() {
def updated() { def updated() {
unschedule() unschedule()
turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
unsubscribe() unsubscribe
log.debug "Updated with settings: ${settings}" log.debug "Updated with settings: ${settings}"
init() init()
} }
@@ -174,12 +174,12 @@ def init() {
schedule (midnightTime, midNight) schedule (midnightTime, midNight)
subscribe(location, "mode", locationHandler) subscribe(location, "mode", locationHandler)
startProcess() startProcess()
} }
// Common methods // Common methods
def startProcess () { def startProcess () {
createDayArray() createDayArray()
state.dayCount=state.data.size() state.dayCount=state.data.size()
if (state.dayCount){ if (state.dayCount){
state.counter = 0 state.counter = 0
@@ -190,7 +190,7 @@ def startProcess () {
def startDay() { def startDay() {
def start = convertEpoch(state.data[state.counter].start) def start = convertEpoch(state.data[state.counter].start)
def stop = convertEpoch(state.data[state.counter].stop) def stop = convertEpoch(state.data[state.counter].stop)
runOnce(start, turnOnSwitch, [overwrite: true]) runOnce(start, turnOnSwitch, [overwrite: true])
runOnce(stop, incDay, [overwrite: true]) runOnce(stop, incDay, [overwrite: true])
} }
@@ -218,7 +218,7 @@ def locationHandler(evt) {
} }
if (!result) { if (!result) {
startProcess() startProcess()
} }
} }
def midNight(){ def midNight(){
@@ -238,7 +238,7 @@ def turnOffSwitch() {
} }
log.debug "Home ventilation switches are off." log.debug "Home ventilation switches are off."
} }
def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
def title = "" def title = ""
def dayListClean = "On " def dayListClean = "On "
@@ -252,7 +252,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
dayListClean = "${dayListClean}, " dayListClean = "${dayListClean}, "
} }
} }
} }
else { else {
dayListClean = "Every day" dayListClean = "Every day"
} }
@@ -272,7 +272,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
modeListClean = "${modeListClean} ${modePrefix}" modeListClean = "${modeListClean} ${modePrefix}"
} }
} }
} }
else { else {
modeListClean = "${modeListClean}all modes" modeListClean = "${modeListClean}all modes"
} }
@@ -283,16 +283,16 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}" title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}"
} }
if (on3 && off3) { if (on3 && off3) {
title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}"
} }
if (on4 && off4) { if (on4 && off4) {
title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}"
} }
if (on1 || on2 || on3 || on4) { if (on1 || on2 || on3 || on4) {
title += "\n$modeListClean" title += "\n$modeListClean"
title += "\n$dayListClean" title += "\n$dayListClean"
} }
if (!on1 && !on2 && !on3 && !on4) { if (!on1 && !on2 && !on3 && !on4) {
title="Click to configure scenario" title="Click to configure scenario"
} }
@@ -374,7 +374,7 @@ def createDayArray() {
timeOk(timeOnD1, timeOffD1) timeOk(timeOnD1, timeOffD1)
timeOk(timeOnD2, timeOffD2) timeOk(timeOnD2, timeOffD2)
timeOk(timeOnD3, timeOffD3) timeOk(timeOnD3, timeOffD3)
timeOk(timeOnD4, timeOffD4) timeOk(timeOnD4, timeOffD4)
} }
} }
state.data.sort{it.start} state.data.sort{it.start}
@@ -384,7 +384,7 @@ def createDayArray() {
private def textAppName() { private def textAppName() {
def text = "Smart Home Ventilation" def text = "Smart Home Ventilation"
} }
private def textVersion() { private def textVersion() {
def text = "Version 2.1.2 (05/31/2015)" def text = "Version 2.1.2 (05/31/2015)"
@@ -416,4 +416,4 @@ private def textHelp() {
"that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " + "that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " +
"avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " + "avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " +
"at 12:15 am or later." "at 12:15 am or later."
} }

View File

@@ -17,7 +17,7 @@ definition(
name: "Monitor on Sense", name: "Monitor on Sense",
namespace: "resteele", namespace: "resteele",
author: "Rachel Steele", author: "Rachel Steele",
description: "Turn on switch when vibration is sensed", description: "Turn on Monitor when vibration is sensed",
category: "My Apps", category: "My Apps",
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",
@@ -25,10 +25,10 @@ definition(
preferences { preferences {
section("When vibration is sensed...") { section("When the keyboard is used...") {
input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?" input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?"
} }
section("Turn on switch...") { section("Turn on/off a light...") {
input "switch1", "capability.switch" input "switch1", "capability.switch"
} }
} }
@@ -47,3 +47,5 @@ def updated() {
def accelerationActiveHandler(evt) { def accelerationActiveHandler(evt) {
switch1.on() switch1.on()
} }

View File

@@ -114,16 +114,13 @@ def beaconHandler(evt) {
if (allOk) { if (allOk) {
def data = new groovy.json.JsonSlurper().parseText(evt.data) def data = new groovy.json.JsonSlurper().parseText(evt.data)
// removed logging of device names. can be added back for debugging log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
//log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
def beaconName = getBeaconName(evt) def beaconName = getBeaconName(evt)
// removed logging of device names. can be added back for debugging log.debug "<beacon-control> beaconName: $beaconName"
//log.debug "<beacon-control> beaconName: $beaconName"
def phoneName = getPhoneName(data) def phoneName = getPhoneName(data)
// removed logging of device names. can be added back for debugging log.debug "<beacon-control> phoneName: $phoneName"
//log.debug "<beacon-control> phoneName: $phoneName"
if (phoneName != null) { if (phoneName != null) {
def action = data.presence == "1" ? "arrived" : "left" def action = data.presence == "1" ? "arrived" : "left"
def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName" def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"

View File

@@ -49,15 +49,13 @@ preferences {
def installed() { def installed() {
log.debug "Installed with settings: ${settings}" log.debug "Installed with settings: ${settings}"
// commented out log statement because presence sensor label could contain user's name log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
subscribe(people, "presence", presence) subscribe(people, "presence", presence)
} }
def updated() { def updated() {
log.debug "Updated with settings: ${settings}" log.debug "Updated with settings: ${settings}"
// commented out log statement because presence sensor label could contain user's name log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
unsubscribe() unsubscribe()
subscribe(people, "presence", presence) subscribe(people, "presence", presence)
} }

View File

@@ -64,12 +64,10 @@ def meterHandler(evt) {
def lastValue = atomicState.lastValue as double def lastValue = atomicState.lastValue as double
atomicState.lastValue = meterValue atomicState.lastValue = meterValue
def dUnit = evt.unit ?: "Watts"
def aboveThresholdValue = aboveThreshold as int def aboveThresholdValue = aboveThreshold as int
if (meterValue > aboveThresholdValue) { if (meterValue > aboveThresholdValue) {
if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
def msg = "${meter} reported ${evt.value} ${dUnit} which is above your threshold of ${aboveThreshold}." def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}."
sendMessage(msg) sendMessage(msg)
} else { } else {
// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed" // log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
@@ -80,7 +78,7 @@ def meterHandler(evt) {
def belowThresholdValue = belowThreshold as int def belowThresholdValue = belowThreshold as int
if (meterValue < belowThresholdValue) { if (meterValue < belowThresholdValue) {
if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
def msg = "${meter} reported ${evt.value} ${dUnit} which is below your threshold of ${belowThreshold}." def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}."
sendMessage(msg) sendMessage(msg)
} else { } else {
// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed" // log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"

View File

@@ -54,10 +54,10 @@ def waterWetHandler(evt) {
def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1 def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1
if (alreadySentSms) { if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaSeconds seconds" log.debug "SMS already sent to $phone within the last $deltaSeconds seconds"
} else { } else {
def msg = "${alarm.displayName} is wet!" def msg = "${alarm.displayName} is wet!"
log.debug "$alarm is wet, texting phone number" log.debug "$alarm is wet, texting $phone"
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts(msg, recipients) sendNotificationToContacts(msg, recipients)

View File

@@ -90,7 +90,7 @@ def takeAction(){
} }
def sendTextMessage() { def sendTextMessage() {
log.debug "$multisensor was open too long, texting phone" log.debug "$multisensor was open too long, texting $phone"
updateSmsHistory() updateSmsHistory()
def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1) def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1)

View File

@@ -454,23 +454,17 @@ def sendStopEvent(source) {
eventData.value += "cancelled" eventData.value += "cancelled"
} }
// send 100% completion event
sendTimeRemainingEvent(100)
// send a non-displayed 0% completion to reset tiles
sendTimeRemainingEvent(0, false)
// send sessionStatus event last so the event feed is ordered properly
sendControllerEvent(eventData) sendControllerEvent(eventData)
sendTimeRemainingEvent(0)
} }
def sendTimeRemainingEvent(percentComplete, displayed = true) { def sendTimeRemainingEvent(percentComplete) {
log.trace "sendTimeRemainingEvent(${percentComplete})" log.trace "sendTimeRemainingEvent(${percentComplete})"
def percentCompleteEventData = [ def percentCompleteEventData = [
name: "percentComplete", name: "percentComplete",
value: percentComplete as int, value: percentComplete as int,
displayed: displayed, displayed: true,
isStateChange: true isStateChange: true
] ]
sendControllerEvent(percentCompleteEventData) sendControllerEvent(percentCompleteEventData)
@@ -480,7 +474,7 @@ def sendTimeRemainingEvent(percentComplete, displayed = true) {
def timeRemainingEventData = [ def timeRemainingEventData = [
name: "timeRemaining", name: "timeRemaining",
value: displayableTime(timeRemaining), value: displayableTime(timeRemaining),
displayed: displayed, displayed: true,
isStateChange: true isStateChange: true
] ]
sendControllerEvent(timeRemainingEventData) sendControllerEvent(timeRemainingEventData)
@@ -614,6 +608,8 @@ private completion() {
handleCompletionMessaging() handleCompletionMessaging()
handleCompletionModesAndPhrases() handleCompletionModesAndPhrases()
sendTimeRemainingEvent(100)
} }
private handleCompletionSwitches() { private handleCompletionSwitches() {
@@ -765,7 +761,7 @@ String displayableTime(timeRemaining) {
return "${minutes}:00" return "${minutes}:00"
} }
def fraction = "0.${parts[1]}" as double def fraction = "0.${parts[1]}" as double
def seconds = "${60 * fraction as int}".padLeft(2, "0") def seconds = "${60 * fraction as int}".padRight(2, "0")
return "${minutes}:${seconds}" return "${minutes}:${seconds}"
} }
@@ -1105,4 +1101,4 @@ def hasStartLevel() {
def hasEndLevel() { def hasEndLevel() {
return (endLevel != null && endLevel != "") return (endLevel != null && endLevel != "")
} }

View File

@@ -47,13 +47,13 @@ preferences {
def installed() { def installed() {
log.debug "Installed with settings: ${settings}" log.debug "Installed with settings: ${settings}"
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
subscribe(people, "presence", presence) subscribe(people, "presence", presence)
} }
def updated() { def updated() {
log.debug "Updated with settings: ${settings}" log.debug "Updated with settings: ${settings}"
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
unsubscribe() unsubscribe()
subscribe(people, "presence", presence) subscribe(people, "presence", presence)
} }
@@ -71,10 +71,11 @@ def presence(evt)
def person = getPerson(evt) def person = getPerson(evt)
def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"} def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"}
if (recentNotPresent) { if (recentNotPresent) {
log.debug "skipping notification of arrival of Person because last departure was only ${now() - recentNotPresent.date.time} msec ago" log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago"
} }
else { else {
def message = "${person.displayName} arrived at home, changing mode to '${newMode}'" def message = "${person.displayName} arrived at home, changing mode to '${newMode}'"
log.info message
send(message) send(message)
setLocationMode(newMode) setLocationMode(newMode)
} }
@@ -105,4 +106,6 @@ private send(msg) {
sendSms(phone, msg) sendSms(phone, msg)
} }
} }
log.debug msg
} }

View File

@@ -57,11 +57,12 @@ def scheduleCheck()
def message = message1 ?: "SmartThings - Habit Helper Reminder!" def message = message1 ?: "SmartThings - Habit Helper Reminder!"
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
log.debug "Texting reminder to contacts:${recipients?.size()}" log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}"
sendNotificationToContacts(message, recipients) sendNotificationToContacts(message, recipients)
} }
else { else {
log.debug "Texting reminder"
log.debug "Texting reminder: ($message) to $phone1"
sendSms(phone1, message) sendSms(phone1, message)
} }
} }

View File

@@ -83,7 +83,7 @@ def bridgeDiscovery(params=[:])
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) { return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
section("Please wait while we discover your Hue Bridge. 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 Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options, submitOnChange: true input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
} }
} }
} }
@@ -95,7 +95,8 @@ def bridgeDiscoveryFailed() {
} }
} }
def bridgeLinking() { def bridgeLinking()
{
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
state.linkRefreshcount = linkRefreshcount + 1 state.linkRefreshcount = linkRefreshcount + 1
def refreshInterval = 3 def refreshInterval = 3
@@ -131,7 +132,10 @@ def bulbDiscovery() {
def refreshInterval = 3 def refreshInterval = 3
state.inBulbDiscovery = true state.inBulbDiscovery = true
def bridge = null def bridge = null
if (selectedHue) {
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListData)
}
state.bridgeRefreshCount = 0 state.bridgeRefreshCount = 0
def allLightsFound = bulbsDiscovered() ?: [:] def allLightsFound = bulbsDiscovered() ?: [:]
@@ -167,7 +171,7 @@ def bulbDiscovery() {
return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Hue Lights. 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 Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, submitOnChange: true, options:newLights input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, options:newLights
paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription
} }
section { section {
@@ -256,6 +260,10 @@ Map bulbsDiscovered() {
return bulbmap return bulbmap
} }
def bulbListData(evt) {
state.bulbs = evt.jsonData
}
Map getHueBulbs() { Map getHueBulbs() {
state.bulbs = state.bulbs ?: [:] state.bulbs = state.bulbs ?: [:]
} }
@@ -309,6 +317,29 @@ def uninstalled(){
state.username = null state.username = null
} }
// Handles events to add new bulbs
def bulbListHandler(hub, data = "") {
def msg = "Bulbs list not processed. Only while in settings menu."
def bulbs = [:]
if (state.inBulbDiscovery) {
def logg = ""
log.trace "Adding bulbs to state..."
state.bridgeProcessedLightList = true
def object = new groovy.json.JsonSlurper().parseText(data)
object.each { k,v ->
if (v instanceof Map)
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub]
}
}
def bridge = null
if (selectedHue) {
bridge = getChildDevice(selectedHue)
}
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
msg = "${bulbs.size()} bulbs found. ${bulbs}"
return msg
}
private upgradeDeviceType(device, newHueType) { private upgradeDeviceType(device, newHueType) {
def deviceType = getDeviceType(newHueType) def deviceType = getDeviceType(newHueType)
@@ -329,6 +360,8 @@ private getDeviceType(hueType) {
return "Hue Bloom" return "Hue Bloom"
else if (hueType?.equalsIgnoreCase("Color Temperature Light")) else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
return "Hue White Ambiance Bulb" return "Hue White Ambiance Bulb"
else if (hueType?.equalsIgnoreCase("Dimmable plug-in unit"))
return "Hue Lux Bulb"
else else
return null return null
} }
@@ -417,6 +450,7 @@ def addBridge() {
updateBridgeStatus(childDevice) updateBridgeStatus(childDevice)
childDevice.sendEvent(name: "idNumber", value: idNumber) childDevice.sendEvent(name: "idNumber", value: idNumber)
if (vbridge.value.ip && vbridge.value.port) { if (vbridge.value.ip && vbridge.value.port) {
if (vbridge.value.ip.contains(".")) { if (vbridge.value.ip.contains(".")) {
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
@@ -460,25 +494,24 @@ def ssdpBridgeHandler(evt) {
def host = ip + ":" + port def host = ip + ":" + port
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host." log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
def dniReceived = "${parsedEvent.mac}" def dni = "${parsedEvent.mac}"
def currentDni = dstate.mac def d = getChildDevice(dni)
def d = getChildDevice(dniReceived)
def networkAddress = null def networkAddress = null
if (!d) { if (!d) {
// There might be a mismatch between bridge DNI and the actual bridge mac address, correct that childDevices.each {
log.debug "Bridge with $dniReceived not found" if (it.getDeviceDataByName("mac")) {
def bridge = childDevices.find { it.deviceNetworkId == currentDni } def newDNI = "${it.getDeviceDataByName("mac")}"
if (bridge != null) { d = it
log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived" if (newDNI != it.deviceNetworkId) {
bridge.setDeviceNetworkId("${dniReceived}") def oldDNI = it.deviceNetworkId
dstate.mac = dniReceived log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
// Check to see if selectedHue is a valid bridge, otherwise update it it.setDeviceNetworkId("${newDNI}")
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue} if (oldDNI == selectedHue) {
if (isSelectedValid == null) { app.updateSetting("selectedHue", newDNI)
log.warn "Correcting selectedHue in state" }
app.updateSetting("selectedHue", dniReceived) doDeviceSync()
}
} }
doDeviceSync()
} }
} else { } else {
updateBridgeStatus(d) updateBridgeStatus(d)
@@ -496,18 +529,6 @@ def ssdpBridgeHandler(evt) {
d.sendEvent(name:"networkAddress", value: host) d.sendEvent(name:"networkAddress", value: host)
d.updateDataValue("networkAddress", host) d.updateDataValue("networkAddress", host)
} }
if (dstate.mac != dniReceived) {
log.warn "Correcting bridge mac address in state"
dstate.mac = dniReceived
}
if (selectedHue != dniReceived) {
// Check to see if selectedHue is a valid bridge, otherwise update it
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
if (isSelectedValid == null) {
log.warn "Correcting selectedHue in state"
app.updateSetting("selectedHue", dniReceived)
}
}
} }
} }
} }
@@ -540,8 +561,11 @@ void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
if (isValidSource(hubResponse.mac)) { if (isValidSource(hubResponse.mac)) {
def body = hubResponse.json def body = hubResponse.json
if (!body?.state?.on) { //check if first time poll made it here by mistake if (!body?.state?.on) { //check if first time poll made it here by mistake
def bulbs = getHueBulbs()
log.debug "Adding bulbs to state!" log.debug "Adding bulbs to state!"
updateBulbState(body, hubResponse.hubId) body.each { k, v ->
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hubResponse.hubId]
}
} }
} }
} }
@@ -627,7 +651,8 @@ def locationHandler(evt) {
} }
} }
} }
} else if (parsedEvent.headers && parsedEvent.body) { }
else if (parsedEvent.headers && parsedEvent.body) {
log.trace "HUE BRIDGE RESPONSES" log.trace "HUE BRIDGE RESPONSES"
def headerString = parsedEvent.headers.toString() def headerString = parsedEvent.headers.toString()
if (headerString?.contains("xml")) { if (headerString?.contains("xml")) {
@@ -657,8 +682,11 @@ def locationHandler(evt) {
} else { } else {
//GET /api/${state.username}/lights response (application/json) //GET /api/${state.username}/lights response (application/json)
if (!body?.state?.on) { //check if first time poll made it here by mistake if (!body?.state?.on) { //check if first time poll made it here by mistake
def bulbs = getHueBulbs()
log.debug "Adding bulbs to state!" log.debug "Adding bulbs to state!"
updateBulbState(body, parsedEvent.hub) body.each { k,v ->
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub]
}
} }
} }
} }
@@ -701,13 +729,13 @@ private void updateBridgeStatus(childDevice) {
} }
/** /**
* Check if all Hue bridges have been heard from in the last 11 minutes, if not an Offline event will be sent * Check if all Hue bridges have been heard from in the last 16 minutes, if not an Offline event will be sent
* for the bridge and all connected lights. Also, set ID number on bridge if not done previously. * for the bridge. Also, set ID number on bridge if not done previously.
*/ */
private void checkBridgeStatus() { private void checkBridgeStatus() {
def bridges = getHueBridges() def bridges = getHueBridges()
// Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer) // Check if each bridge has been heard from within the last 16 minutes (3 poll intervals times 5 minutes plus buffer)
def time = now() - (1000 * 60 * 11) def time = now() - (1000 * 60 * 16)
bridges.each { bridges.each {
def d = getChildDevice(it.value.mac) def d = getChildDevice(it.value.mac)
if(d) { if(d) {
@@ -717,21 +745,14 @@ private void checkBridgeStatus() {
d.sendEvent(name: "idNumber", value: it.value.idNumber) d.sendEvent(name: "idNumber", value: it.value.idNumber)
} }
if (it.value.lastActivity < time) { // it.value.lastActivity != null && if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
log.warn "Bridge $it.value.idNumber is Offline" log.warn "Bridge $it.key is Offline"
d.sendEvent(name: "status", value: "Offline") d.sendEvent(name: "status", value: "Offline")
} else {
state.bulbs?.each {
it.value.online = false
}
getChildDevices().each {
it.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", isStateChange: true, displayed: false)
}
} else {
d.sendEvent(name: "status", value: "Online")//setOnline(false) d.sendEvent(name: "status", value: "Online")//setOnline(false)
} }
} }
} }
} }
def isValidSource(macAddress) { def isValidSource(macAddress) {
@@ -739,35 +760,6 @@ def isValidSource(macAddress) {
return (vbridges?.find {"${it.value.mac}" == macAddress}) != null return (vbridges?.find {"${it.value.mac}" == macAddress}) != null
} }
def isInBulbDiscovery() {
return state.inBulbDiscovery
}
private updateBulbState(messageBody, hub) {
def bulbs = getHueBulbs()
// Copy of bulbs used to locate old lights in state that are no longer on bridge
def toRemove = [:]
toRemove << bulbs
messageBody.each { k,v ->
if (v instanceof Map) {
if (bulbs[k] == null) {
bulbs[k] = [:]
}
bulbs[k] << [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub, remove: false]
toRemove.remove(k)
}
}
// Remove bulbs from state that are no longer discovered
toRemove.each { k,v ->
log.warn "${bulbs[k].name} no longer exists on bridge, removing"
bulbs.remove(k)
}
}
///////////////////////////////////// /////////////////////////////////////
//CHILD DEVICE METHODS //CHILD DEVICE METHODS
///////////////////////////////////// /////////////////////////////////////
@@ -791,7 +783,8 @@ def parse(childDevice, description) {
if (body instanceof java.util.Map) { if (body instanceof java.util.Map) {
// get (poll) reponse // get (poll) reponse
return handlePoll(body) return handlePoll(body)
} else { }
else {
//put response //put response
return handleCommandResponse(body) return handleCommandResponse(body)
} }
@@ -803,12 +796,10 @@ def parse(childDevice, description) {
} }
// Philips Hue priority for color is xy > ct > hs // Philips Hue priority for color is xy > ct > hs
// For SmartThings, try to always send hue, sat and hex
private sendColorEvents(device, xy, hue, sat, ct, colormode = null) { private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
if (device == null || (xy == null && hue == null && sat == null && ct == null)) if (device == null || (xy == null && hue == null && sat == null && ct == null))
return return
def events = [:]
// For now, only care about changing color temperature if requested by user // For now, only care about changing color temperature if requested by user
if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) { if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
// for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below // for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
@@ -822,13 +813,13 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
if (hue != null) { if (hue != null) {
// 0-65535 // 0-65535
def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false] device.sendEvent([name: "hue", value: value, descriptionText: "Color has changed"])
} }
if (sat != null) { if (sat != null) {
// 0-254 // 0-254
def value = Math.round(sat * 100 / 254) as int def value = Math.round(sat * 100 / 254) as int
events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false] device.sendEvent([name: "saturation", value: value, descriptionText: "Color has changed"])
} }
// Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex // Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
@@ -840,28 +831,17 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
def model = state.bulbs[id]?.modelid def model = state.bulbs[id]?.modelid
def hex = colorFromXY(xy, model) def hex = colorFromXY(xy, model)
// Create Hue and Saturation events if not previously existing // TODO Disabled until a solution for the jumping color picker can be figured out
def hsv = hexToHsv(hex) //device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: false])
if (events["hue"] == null)
events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
if (events["saturation"] == null)
events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
} else if (colormode == "hs" || colormode == null) { } else if (colormode == "hs" || colormode == null) {
// colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above // colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
// TODO Disabled until the standard behavior of lights is defined (hue and sat events are sent above)
def hex = hsvToHex(hueValue, satValue) //def hex = colorUtil.hslToHex((int) device.currentHue, (int) device.currentSaturation)
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true] // device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed"])
} }
boolean sendColorChanged = false return debug
events.each {
device.sendEvent(it.value)
}
} }
private sendBasicEvents(device, param, value) { private sendBasicEvents(device, param, value) {
@@ -901,38 +881,36 @@ private handleCommandResponse(body) {
// scan entire response before sending events to make sure they are always in the same order // scan entire response before sending events to make sure they are always in the same order
def updates = [:] def updates = [:]
body.each { payload -> body.each { payload ->
log.debug $payload
if (payload?.success) { if (payload?.success) {
def childDeviceNetworkId = app.id + "/" def childDeviceNetworkId = app.id + "/"
def eventType def eventType
payload.success.each { k, v -> payload.success.each { k, v ->
def data = k.split("/") def data = k.split("/")
if (data.length == 5) { if (data.length == 5) {
childDeviceNetworkId = app.id + "/" + k.split("/")[2] childDeviceNetworkId = app.id + "/" + k.split("/")[2]
if (!updates[childDeviceNetworkId]) if (!updates[childDeviceNetworkId])
updates[childDeviceNetworkId] = [:] updates[childDeviceNetworkId] = [:]
eventType = k.split("/")[4] eventType = k.split("/")[4]
updates[childDeviceNetworkId]."$eventType" = v updates[childDeviceNetworkId]."$eventType" = v
} }
} }
} else if (payload.error) { } else if (payload.error) {
log.warn "Error returned from Hue bridge error = ${body?.error}" log.warn "Error returned from Hue bridge error = ${body?.error}"
} }
} }
// send events for each update found above (order of events should be same as handlePoll()) // send events for each update found above (order of events should be same as handlePoll())
updates.each { childDeviceNetworkId, params -> updates.each { childDeviceNetworkId, params ->
def device = getChildDevice(childDeviceNetworkId) def device = getChildDevice(childDeviceNetworkId)
def id = getId(device) sendBasicEvents(device, "on", params.on)
// If device is offline, then don't send events which will update device watch sendBasicEvents(device, "bri", params.bri)
if (isOnline(id)) { sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
sendBasicEvents(device, "on", params.on) }
sendBasicEvents(device, "bri", params.bri)
sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
}
}
return [] return []
} }
/** /**
* Handles a response to a poll (GET) sent to the Hue Bridge. * Handles a response to a poll (GET) sent to the Hue Bridge.
@@ -952,53 +930,26 @@ private handleCommandResponse(body) {
* @return empty array * @return empty array
*/ */
private handlePoll(body) { private handlePoll(body) {
// Used to track "unreachable" time if (state.updating) {
// Device is considered "offline" if it has been in the "unreachable" state for // If user just executed commands, then ignore poll to not confuse the turning on/off state
// 11 minutes (e.g. two poll intervals) return []
// Note, Hue Bridge marks devices as "unreachable" often even when they accept commands }
Calendar time11 = Calendar.getInstance()
time11.add(Calendar.MINUTE, -11)
Calendar currentTime = Calendar.getInstance()
def bulbs = getChildDevices() def bulbs = getChildDevices()
for (bulb in body) { for (bulb in body) {
def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
if (device) { if (device) {
if (bulb.value.state?.reachable) { if (bulb.value.state?.reachable) {
if (state.bulbs[bulb.key]?.online == false) { sendBasicEvents(device, "on", bulb.value?.state?.on)
// light just came back online, notify device watch sendBasicEvents(device, "bri", bulb.value?.state?.bri)
def lastActivity = now() sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true)
log.debug "$device is Online"
}
// Mark light as "online"
state.bulbs[bulb.key]?.unreachableSince = null
state.bulbs[bulb.key]?.online = true
// If user just executed commands, then do not send events to avoid confusing the turning on/off state
if (!state.updating) {
sendBasicEvents(device, "on", bulb.value?.state?.on)
sendBasicEvents(device, "bri", bulb.value?.state?.bri)
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
}
} else { } else {
if (state.bulbs[bulb.key]?.unreachableSince == null) { log.warn "$device is not reachable by Hue bridge"
// Store the first time where device was reported as "unreachable" }
state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis() }
} else if (state.bulbs[bulb.key]?.online) { }
// Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary return []
if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis()) {
log.warn "$device went Offline"
state.bulbs[bulb.key]?.online = false
device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true)
}
}
log.warn "$device may not reachable by Hue bridge"
}
}
} }
return []
}
private updateInProgress() { private updateInProgress() {
state.updating = true state.updating = true
@@ -1027,25 +978,22 @@ def hubVerification(bodytext) {
def on(childDevice) { def on(childDevice) {
log.debug "Executing 'on'" log.debug "Executing 'on'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [on: true]) put("lights/${getId(childDevice)}/state", [on: true])
return "Bulb is turning On" return "Bulb is turning On"
} }
def off(childDevice) { def off(childDevice) {
log.debug "Executing 'off'" log.debug "Executing 'off'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "off") createSwitchEvent(childDevice, "off")
put("lights/$id/state", [on: false]) put("lights/${getId(childDevice)}/state", [on: false])
return "Bulb is turning Off" return "Bulb is turning Off"
} }
def setLevel(childDevice, percent) { def setLevel(childDevice, percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
// 1 - 254 // 1 - 254
def level def level
@@ -1060,51 +1008,48 @@ def setLevel(childDevice, percent) {
// that means that the light will still be on when on is called next time // that means that the light will still be on when on is called next time
// Lets emulate that here // Lets emulate that here
if (percent > 0) { if (percent > 0) {
put("lights/$id/state", [bri: level, on: true]) put("lights/${getId(childDevice)}/state", [bri: level, on: true])
} else { } else {
put("lights/$id/state", [on: false]) put("lights/${getId(childDevice)}/state", [on: false])
} }
return "Setting level to $percent" return "Setting level to $percent"
} }
def setSaturation(childDevice, percent) { def setSaturation(childDevice, percent) {
log.debug "Executing 'setSaturation($percent)'" log.debug "Executing 'setSaturation($percent)'"
def id = getId(childDevice) updateInProgress()
updateInProgress()
// 0 - 254 // 0 - 254
def level = Math.min(Math.round(percent * 254 / 100), 254) def level = Math.min(Math.round(percent * 254 / 100), 254)
// TODO should this be done by app only or should we default to on? // TODO should this be done by app only or should we default to on?
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [sat: level, on: true]) put("lights/${getId(childDevice)}/state", [sat: level, on: true])
return "Setting saturation to $percent" return "Setting saturation to $percent"
} }
def setHue(childDevice, percent) { def setHue(childDevice, percent) {
log.debug "Executing 'setHue($percent)'" log.debug "Executing 'setHue($percent)'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
// 0 - 65535 // 0 - 65535
def level = Math.min(Math.round(percent * 65535 / 100), 65535) def level = Math.min(Math.round(percent * 65535 / 100), 65535)
// TODO should this be done by app only or should we default to on? // TODO should this be done by app only or should we default to on?
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [hue: level, on: true]) put("lights/${getId(childDevice)}/state", [hue: level, on: true])
return "Setting hue to $percent" return "Setting hue to $percent"
} }
def setColorTemperature(childDevice, huesettings) { def setColorTemperature(childDevice, huesettings) {
log.debug "Executing 'setColorTemperature($huesettings)'" log.debug "Executing 'setColorTemperature($huesettings)'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
// 153 (6500K) to 500 (2000K) // 153 (6500K) to 500 (2000K)
def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings) def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings)
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [ct: ct, on: true]) put("lights/${getId(childDevice)}/state", [ct: ct, on: true])
return "Setting color temperature to $percent" return "Setting color temperature to $percent"
} }
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def id = getId(childDevice)
updateInProgress() updateInProgress()
def value = [:] def value = [:]
@@ -1112,22 +1057,26 @@ def setColor(childDevice, huesettings) {
def sat = null def sat = null
def xy = null def xy = null
// Prefer hue/sat over hex to make sure it works with the majority of the smartapps // For now ignore model to get a consistent color if same color is set across multiple devices
if (huesettings.hue != null || huesettings.sat != null) { // def model = state.bulbs[getId(childDevice)]?.modelid
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to if (huesettings.hex != null) {
// generate hue/sat events even though bridge will prioritize XY when setting color
if (huesettings.hue != null)
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
} else if (huesettings.hex != null) {
// For now ignore model to get a consistent color if same color is set across multiple devices
// def model = state.bulbs[getId(childDevice)]?.modelid
// value.xy = calculateXY(huesettings.hex, model) // value.xy = calculateXY(huesettings.hex, model)
// Once groups, or scenes are introduced it might be a good idea to use unique models again // Once groups, or scenes are introduced it might be a good idea to use unique models again
value.xy = calculateXY(huesettings.hex) value.xy = calculateXY(huesettings.hex)
} }
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
// generate hue/sat events even though bridge will prioritize XY when setting color
if (huesettings.hue != null)
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
else
value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
else
value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254)
/* Disabled for now due to bad behavior via Lightning Wizard /* Disabled for now due to bad behavior via Lightning Wizard
if (!value.xy) { if (!value.xy) {
// Below will translate values to hex->XY to take into account the color support of the different hue types // Below will translate values to hex->XY to take into account the color support of the different hue types
@@ -1157,30 +1106,15 @@ def setColor(childDevice, huesettings) {
value.on = false value.on = false
createSwitchEvent(childDevice, value.on ? "on" : "off") createSwitchEvent(childDevice, value.on ? "on" : "off")
put("lights/$id/state", value) put("lights/${getId(childDevice)}/state", value)
return "Setting color to $value" return "Setting color to $value"
} }
def ping(childDevice) {
if (childDevice.device?.deviceNetworkId?.equalsIgnoreCase(selectedHue)) {
if (childDevice.device?.currentValue("status")?.equalsIgnoreCase("Online")) {
childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Bridge is reachable", displayed: false, isStateChange: true)
return "Bridge is Online"
} else {
return "Bridge is Offline"
}
} else if (isOnline(getId(childDevice))) {
childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true)
return "Device is Online"
} else {
return "Device is Offline"
}
}
private getId(childDevice) { private getId(childDevice) {
if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
return childDevice.device?.deviceNetworkId[3..-1] return childDevice.device?.deviceNetworkId[3..-1]
} else { }
else {
return childDevice.device?.deviceNetworkId.split("/")[-1] return childDevice.device?.deviceNetworkId.split("/")[-1]
} }
} }
@@ -1189,12 +1123,10 @@ private poll() {
def host = getBridgeIP() def host = getBridgeIP()
def uri = "/api/${state.username}/lights/" def uri = "/api/${state.username}/lights/"
log.debug "GET: $host$uri" log.debug "GET: $host$uri"
sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" + sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
"HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue)) HOST: ${host}
}
private isOnline(id) { """, physicalgraph.device.Protocol.LAN, selectedHue))
return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null
} }
private put(path, body) { private put(path, body) {
@@ -1206,11 +1138,13 @@ private put(path, body) {
log.debug "PUT: $host$uri" log.debug "PUT: $host$uri"
log.debug "BODY: ${bodyJSON}" log.debug "BODY: ${bodyJSON}"
sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" + sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
"HOST: ${host}\r\n" + HOST: ${host}
"Content-Length: ${length}\r\n" + Content-Length: ${length}
"\r\n" +
"${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}")) ${bodyJSON}
""", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
} }
/* /*
@@ -1231,7 +1165,7 @@ private getBridgeIP() {
if (d) { if (d) {
if (d.getDeviceDataByName("networkAddress")) if (d.getDeviceDataByName("networkAddress"))
host = d.getDeviceDataByName("networkAddress") host = d.getDeviceDataByName("networkAddress")
else else
host = d.latestState('networkAddress').stringValue host = d.latestState('networkAddress').stringValue
} }
if (host == null || host == "") { if (host == null || host == "") {
@@ -1261,8 +1195,8 @@ def convertBulbListToMap() {
try { try {
if (state.bulbs instanceof java.util.List) { if (state.bulbs instanceof java.util.List) {
def map = [:] def map = [:]
state.bulbs?.unique {it.id}.each { bulb -> state.bulbs.unique {it.id}.each { bulb ->
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub, "online": bulb.online]] map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]]
} }
state.bulbs = map state.bulbs = map
} }
@@ -1668,101 +1602,3 @@ private boolean checkPointInLampsReach(p, colorPoints) {
return false; return false;
} }
} }
/**
* Converts an RGB color in hex to HSV/HSB.
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
*
* @param colorStr color value in hex (#ff03d3)
*
* @return HSV representation in an array (0-100) [hue, sat, value]
*/
def hexToHsv(colorStr){
def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255
def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255
def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255
def max = Math.max(Math.max(r, g), b)
def min = Math.min(Math.min(r, g), b)
def h, s, v = max
def d = max - min
s = max == 0 ? 0 : d / max
if(max == min){
h = 0
}else{
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break
case g: h = (b - r) / d + 2; break
case b: h = (r - g) / d + 4; break
}
h /= 6;
}
return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
}
/**
* Converts HSV/HSB color to RGB in hex.
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
*
* @param hue hue 0-100
* @param sat saturation 0-100
* @param value value 0-100 (defaults to 100)
* @return the color in hex (#ff03d3)
*/
def hsvToHex(hue, sat, value = 100){
def r, g, b;
def h = hue / 100
def s = sat / 100
def v = value / 100
def i = Math.floor(h * 6)
def f = h * 6 - i
def p = v * (1 - s)
def q = v * (1 - f * s)
def t = v * (1 - (1 - f) * s)
switch (i % 6) {
case 0:
r = v
g = t
b = p
break
case 1:
r = q
g = v
b = p
break
case 2:
r = p
g = v
b = t
break
case 3:
r = p
g = q
b = v
break
case 4:
r = t
g = p
b = v
break
case 5:
r = v
g = p
b = q
break
}
// Converting float components to int components.
def r1 = String.format("%02X", (int) (r * 255.0f))
def g1 = String.format("%02X", (int) (g * 255.0f))
def b1 = String.format("%02X", (int) (b * 255.0f))
return "#$r1$g1$b1"
}

View File

@@ -53,14 +53,14 @@ def accelerationActiveHandler(evt) {
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
if (alreadySentSms) { if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaSeconds seconds" log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
} else { } else {
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
log.debug "accelerationSensor has moved, texting contacts: ${recipients?.size()}" log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}"
sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients) sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients)
} }
else { else {
log.debug "accelerationSensor has moved, sending text message" log.debug "$accelerationSensor has moved, texting $phone1"
sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved") sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved")
} }
} }

View File

@@ -69,10 +69,10 @@ def temperatureHandler(evt) {
def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1 def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1
if (alreadySentSms) { if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaMinutes minutes" log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes"
// TODO: Send "Temperature back to normal" SMS, turn switch off // TODO: Send "Temperature back to normal" SMS, turn switch off
} else { } else {
log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch" log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
def tempScale = location.temperatureScale ?: "F" def tempScale = location.temperatureScale ?: "F"
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
switch1?.on() switch1?.on()

View File

@@ -50,9 +50,9 @@ def authPage() {
} }
def description = "Tap to enter LIFX credentials" def description = "Tap to enter LIFX credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
// def redirectUrl = "${apiServerUrl}" // def redirectUrl = "${apiServerUrl}"
// log.debug "app id: ${app.id}" log.debug "app id: ${app.id}"
// log.debug "redirect url: ${redirectUrl}"s log.debug "redirect url: ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) { return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
section { section {
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
@@ -372,7 +372,7 @@ def updateDevices() {
def childDevice = getChildDevice(device.id) def childDevice = getChildDevice(device.id)
selectors.add("${device.id}") selectors.add("${device.id}")
if (!childDevice) { if (!childDevice) {
// log.info("Adding device ${device.id}: ${device.product}") log.info("Adding device ${device.id}: ${device.product}")
def data = [ def data = [
label: device.label, label: device.label,
level: Math.round((device.brightness ?: 1) * 100), level: Math.round((device.brightness ?: 1) * 100),

View File

@@ -34,7 +34,6 @@
* locks | lock | lock, unlock | locked, unlocked * locks | lock | lock, unlock | locked, unlocked
* ---------------------+-------------------+-----------------------------+------------------------------------ * ---------------------+-------------------+-----------------------------+------------------------------------
*/ */
include 'asynchttp_v1'
definition( definition(
name: "Logitech Harmony (Connect)", name: "Logitech Harmony (Connect)",
@@ -52,7 +51,7 @@ definition(
} }
preferences(oauthPage: "deviceAuthorization") { preferences(oauthPage: "deviceAuthorization") {
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) { page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
section("Allow Logitech Harmony to control these things...") { section("Allow Logitech Harmony to control these things...") {
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
@@ -97,41 +96,38 @@ def authPage() {
def description = null def description = null
if (!state.HarmonyAccessToken) { if (!state.HarmonyAccessToken) {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "Harmony - About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} }
description = "Click to enter Harmony Credentials" description = "Click to enter Harmony Credentials"
def redirectUrl = buildRedirectUrl def redirectUrl = buildRedirectUrl
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 { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
} }
} else { } else {
//device discovery request every 5 //25 seconds //device discovery request every 5 //25 seconds
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + 1 state.deviceRefreshCount = deviceRefreshCount + 1
def refreshInterval = 5 def refreshInterval = 3
def huboptions = state.HarmonyHubs ?: [] def huboptions = state.HarmonyHubs ?: []
def actoptions = state.HarmonyActivities ?: [] def actoptions = state.HarmonyActivities ?: []
def numFoundHub = huboptions.size() ?: 0 def numFoundHub = huboptions.size() ?: 0
def numFoundAct = actoptions.size() ?: 0 def numFoundAct = actoptions.size() ?: 0
if((deviceRefreshCount % 5) == 0) { if((deviceRefreshCount % 5) == 0) {
discoverDevices() discoverDevices()
} }
return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
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, submitOnChange: true, options:huboptions input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
} }
// Virtual activity flag // Virtual activity flag
if (numFoundHub > 0 && numFoundAct > 0 && true) 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, submitOnChange: true, options:actoptions input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
} }
if (state.resethub) if (state.resethub)
section("Connection to the hub timed out. Please restart the hub and try again.") {} section("Connection to the hub timed out. Please restart the hub and try again.") {}
} }
} }
@@ -141,13 +137,13 @@ def callback() {
def redirectUrl = null def redirectUrl = null
if (params.authQueryString) { if (params.authQueryString) {
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
log.debug "Harmony - redirectUrl: ${redirectUrl}" log.debug "redirectUrl: ${redirectUrl}"
} else { } else {
log.warn "Harmony - No authQueryString" log.warn "No authQueryString"
} }
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
log.debug "Harmony - Access token already exists" log.debug "Access token already exists"
discovery() discovery()
success() success()
} else { } else {
@@ -155,27 +151,27 @@ def callback() {
if (code) { if (code) {
if (code.size() > 6) { if (code.size() > 6) {
// Harmony code // Harmony code
log.debug "Harmony - Exchanging code for access token" log.debug "Exchanging code for access token"
receiveToken(redirectUrl) receiveToken(redirectUrl)
} else { } else {
// Initiate the Harmony OAuth flow. // Initiate the Harmony OAuth flow.
init() init()
} }
} else { } else {
log.debug "Harmony - This code should be unreachable" log.debug "This code should be unreachable"
success() success()
} }
} }
} }
def init() { def init() {
log.debug "Harmony - Requesting Code" log.debug "Requesting Code"
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ] def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ]
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) {
log.debug "Harmony - receiveToken" log.debug "receiveToken"
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ] def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ]
def params = [ def params = [
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
@@ -186,7 +182,7 @@ def receiveToken(redirectUrl = null) {
} }
} catch (java.util.concurrent.TimeoutException e) { } catch (java.util.concurrent.TimeoutException e) {
fail(e) fail(e)
log.warn "Harmony - Connection timed out, please try again later." log.warn "Connection timed out, please try again later."
} }
discovery() discovery()
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
@@ -310,7 +306,7 @@ def buildRedirectUrl(page) {
def installed() { def installed() {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "Harmony - About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} else { } else {
initialize() initialize()
@@ -318,8 +314,10 @@ def installed() {
} }
def updated() { def updated() {
unsubscribe()
unschedule()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "Harmony - About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} else { } else {
initialize() initialize()
@@ -330,9 +328,9 @@ def uninstalled() {
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
try { try {
state.HarmonyAccessToken = "" state.HarmonyAccessToken = ""
log.debug "Harmony - Success disconnecting Harmony from SmartThings" log.debug "Success disconnecting Harmony from SmartThings"
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}" log.error "Error disconnecting Harmony from SmartThings: ${e.statusCode}"
} }
} }
} }
@@ -341,8 +339,7 @@ def initialize() {
state.aux = 0 state.aux = 0
if (selectedhubs || selectedactivities) { if (selectedhubs || selectedactivities) {
addDevice() addDevice()
runEvery5Minutes("poll") runEvery5Minutes("poll")
getActivityList()
} }
} }
@@ -351,7 +348,7 @@ def getHarmonydevices() {
} }
Map discoverDevices() { Map discoverDevices() {
log.trace "Harmony - Discovering devices..." log.trace "Discovering devices..."
discovery() discovery()
if (getHarmonydevices() != []) { if (getHarmonydevices() != []) {
def devices = state.Harmonydevices.hubs def devices = state.Harmonydevices.hubs
@@ -363,7 +360,7 @@ Map discoverDevices() {
def hubname = getHubName(it.key) def hubname = getHubName(it.key)
def hubvalue = "${hubname}" def hubvalue = "${hubname}"
hubs["harmony-${hubkey}"] = hubvalue hubs["harmony-${hubkey}"] = hubvalue
it.value.response.data.activities.each { it.value.response.data.activities.each {
def value = "${it.value.name}" def value = "${it.value.name}"
def key = "harmony-${hubkey}-${it.key}" def key = "harmony-${hubkey}-${it.key}"
activities["${key}"] = value activities["${key}"] = value
@@ -381,177 +378,164 @@ def discovery() {
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
if (response.status == 200) { if (response.status == 200) {
log.debug "Harmony - valid Token" log.debug "valid Token"
state.Harmonydevices = response.data state.Harmonydevices = response.data
state.resethub = false state.resethub = false
getActivityList()
poll()
} else { } else {
log.debug "Harmony - Error: $response.status" log.debug "Error: $response.status"
} }
} }
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken") state.remove("HarmonyAccessToken")
log.warn "Harmony - Harmony Access token has expired" log.warn "Harmony Access token has expired"
} }
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {
log.warn "Harmony - Connection to the hub timed out. Please restart the hub and try again." log.warn "Connection to the hub timed out. Please restart the hub and try again."
state.resethub = true state.resethub = true
} catch (e) { } catch (e) {
log.info "Harmony - Error: $e" log.info "Logitech Harmony - Error: $e"
} }
return null return null
} }
def addDevice() { def addDevice() {
log.trace "Harmony - Adding Hubs" log.trace "Adding Hubs"
selectedhubs.each { dni -> selectedhubs.each { dni ->
def d = getChildDevice(dni) def d = getChildDevice(dni)
if(!d) { if(!d) {
def newAction = state.HarmonyHubs.find { it.key == dni } def newAction = state.HarmonyHubs.find { it.key == dni }
d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"]) d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"])
log.trace "Harmony - Created ${d.displayName} with id $dni" log.trace "created ${d.displayName} with id $dni"
poll() poll()
} else { } else {
log.trace "Harmony - Found ${d.displayName} with id $dni already exists" log.trace "found ${d.displayName} with id $dni already exists"
} }
} }
log.trace "Harmony - Adding Activities" log.trace "Adding Activities"
selectedactivities.each { dni -> selectedactivities.each { dni ->
def d = getChildDevice(dni) def d = getChildDevice(dni)
if(!d) { if(!d) {
def newAction = state.HarmonyActivities.find { it.key == dni } def newAction = state.HarmonyActivities.find { it.key == dni }
if (newAction) { if (newAction) {
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
log.trace "Harmony - Created ${d.displayName} with id $dni" log.trace "created ${d.displayName} with id $dni"
poll() poll()
} }
} else { } else {
log.trace "Harmony - Found ${d.displayName} with id $dni already exists" log.trace "found ${d.displayName} with id $dni already exists"
} }
} }
} }
def activity(dni,mode) { def activity(dni,mode) {
def tokenParam = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url def msg = "Command failed"
def url = ''
if (dni == "all") { if (dni == "all") {
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}" url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
} else { } else {
def aux = dni.split('-') def aux = dni.split('-')
def hubId = aux[1] def hubId = aux[1]
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
} else { } else {
def activityId = aux[2] def activityId = aux[2]
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
} }
} }
def params = [ try {
uri: url, httpPostJson(uri: url) { response ->
contentType: 'application/json' if (response.data.code == 200 || dni == "all") {
] msg = "Command sent succesfully"
asynchttp_v1.post('activityResponse', params) state.aux = 0
return "Command Sent" } else {
} msg = "Command failed. Error: $response.data.code"
}
def activityResponse(response, data) { }
if (response.hasError()) { } catch (groovyx.net.http.HttpResponseException ex) {
log.error "Harmony - response has error: $response.errorMessage" log.error ex
if (response.status == 401) { // token is expired if (state.aux == 0) {
state.remove("HarmonyAccessToken") state.aux = 1
log.warn "Harmony - Access token has expired" activity(dni,mode)
} else {
msg = ex
state.aux = 0
}
} catch(Exception ex) {
msg = ex
} }
} else { runIn(10, "poll", [overwrite: true])
if (response.status == 200) { return msg
log.trace "Harmony - Command sent succesfully"
poll()
} else {
log.trace "Harmony - Command failed. Error: $response.status"
}
}
} }
def poll() { def poll() {
// GET THE LIST OF ACTIVITIES // GET THE LIST OF ACTIVITIES
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def tokenParam = [auth: state.HarmonyAccessToken] getActivityList()
def params = [ def Params = [auth: state.HarmonyAccessToken]
uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}", def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
headers: ["Accept": "application/json"], try {
contentType: 'application/json' httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
] def map = [:]
asynchttp_v1.get('pollResponse', params) response.data.hubs.each {
} else { if (it.value.message == "OK") {
log.warn "Harmony - Access token has expired" map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
} def hub = getChildDevice("harmony-${it.key}")
} if (hub) {
if (it.value.response.data.currentAvActivity == "-1") {
def pollResponse(response, data) { hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
if (response.hasError()) { } else {
log.error "Harmony - response has error: $response.errorMessage" def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
if (response.status == 401) { // token is expired hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
state.remove("HarmonyAccessToken") }
log.warn "Harmony - Access token has expired" }
} } else {
} else { log.trace it.value.message
def ResponseValues }
try { }
// json response already parsed into JSONElement object def activities = getChildDevices()
ResponseValues = response.json def activitynotrunning = true
} catch (e) { activities.each { activity ->
log.error "Harmony - error parsing json from response: $e" def act = activity.deviceNetworkId.split('-')
} if (act.size() > 2) {
if (ResponseValues) { def aux = map.find { it.key == act[1] }
def map = [:] if (aux) {
ResponseValues.hubs.each { def aux2 = aux.value.split(',')
if (it.value.message == "OK") { def childDevice = getChildDevice(activity.deviceNetworkId)
map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
def hub = getChildDevice("harmony-${it.key}") childDevice?.sendEvent(name: "switch", value: "on")
if (hub) { if (aux2[1] == "1")
if (it.value.response.data.currentAvActivity == "-1") { runIn(5, "poll", [overwrite: true])
hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false) } else {
} else { childDevice?.sendEvent(name: "switch", value: "off")
def currentActivity if (aux2[1] == "3")
def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}") runIn(5, "poll", [overwrite: true])
if (activityDTH) }
currentActivity = activityDTH.device.displayName }
else
currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
}
}
} else {
log.trace "Harmony - error response: $it.value.message"
}
}
def activities = getChildDevices()
def activitynotrunning = true
activities.each { activity ->
def act = activity.deviceNetworkId.split('-')
if (act.size() > 2) {
def aux = map.find { it.key == act[1] }
if (aux) {
def aux2 = aux.value.split(',')
def childDevice = getChildDevice(activity.deviceNetworkId)
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
childDevice?.sendEvent(name: "switch", value: "on")
if (aux2[1] == "1")
runIn(5, "poll", [overwrite: true])
} else {
childDevice?.sendEvent(name: "switch", value: "off")
if (aux2[1] == "3")
runIn(5, "poll", [overwrite: true])
} }
} }
return "Poll completed $map - $state.hubs"
} }
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken")
log.warn "Harmony Access token has expired"
}
} catch (java.net.SocketTimeoutException e) {
log.warn "Connection to the hub timed out. Please restart the hub and try again."
state.resethub = true
} catch (e) {
log.info "Logitech Harmony - Error: $e"
} }
} else { }
log.debug "Harmony - did not get json results from response body: $response.data"
}
}
} }
def getActivityList() { def getActivityList() {
// GET ACTIVITY'S NAME
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
@@ -568,19 +552,21 @@ def getActivityList() {
[id: it.key, name: it.value['name'], type: it.value['type']] [id: it.key, name: it.value['name'], type: it.value['type']]
} }
activities += [id: "off", name: "Activity OFF", type: "0"] activities += [id: "off", name: "Activity OFF", type: "0"]
log.trace activities
} }
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
} }
} }
} }
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
log.trace e log.trace e
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {
log.trace e log.trace e
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
return activity
} }
def getActivityName(activity,hubId) { def getActivityName(activity,hubId) {
@@ -643,7 +629,7 @@ def sendNotification(msg) {
def hookEventHandler() { def hookEventHandler() {
// log.debug "In hookEventHandler method." // log.debug "In hookEventHandler method."
log.debug "Harmony - request = ${request}" log.debug "request = ${request}"
def json = request.JSON def json = request.JSON
@@ -652,14 +638,14 @@ def hookEventHandler() {
} }
def listDevices() { def listDevices() {
log.debug "Harmony - getDevices(), params: ${params}" log.debug "getDevices, params: ${params}"
allDevices.collect { allDevices.collect {
deviceItem(it) deviceItem(it)
} }
} }
def getDevice() { def getDevice() {
log.debug "Harmony - getDevice(), params: ${params}" log.debug "getDevice, params: ${params}"
def device = allDevices.find { it.id == params.id } def device = allDevices.find { it.id == params.id }
if (!device) { if (!device) {
render status: 404, data: '{"msg": "Device not found"}' render status: 404, data: '{"msg": "Device not found"}'
@@ -672,7 +658,7 @@ def updateDevice() {
def data = request.JSON def data = request.JSON
def command = data.command def command = data.command
def arguments = data.arguments def arguments = data.arguments
log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}" log.debug "updateDevice, params: ${params}, request: ${data}"
if (!command) { if (!command) {
render status: 400, data: '{"msg": "command is required"}' render status: 400, data: '{"msg": "command is required"}'
} else { } else {
@@ -740,7 +726,7 @@ def getDeviceCapabilityCommands(deviceCapabilities) {
} }
def listSubscriptions() { def listSubscriptions() {
log.debug "Harmony - listSubscriptions()" log.debug "listSubscriptions()"
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect { app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
def deviceInfo = state[it.device.id] def deviceInfo = state[it.device.id]
def response = [ def response = [
@@ -761,17 +747,17 @@ def addSubscription() {
def attribute = data.attributeName def attribute = data.attributeName
def callbackUrl = data.callbackUrl def callbackUrl = data.callbackUrl
log.debug "Harmony - addSubscription, params: ${params}, request: ${data}" log.debug "addSubscription, params: ${params}, request: ${data}"
if (!attribute) { if (!attribute) {
render status: 400, data: '{"msg": "attributeName is required"}' render status: 400, data: '{"msg": "attributeName is required"}'
} else { } else {
def device = allDevices.find { it.id == data.deviceId } def device = allDevices.find { it.id == data.deviceId }
if (device) { if (device) {
if (!state.harmonyHubs) { if (!state.harmonyHubs) {
log.debug "Harmony - Adding callbackUrl: $callbackUrl" log.debug "Adding callbackUrl: $callbackUrl"
state[device.id] = [callbackUrl: callbackUrl] state[device.id] = [callbackUrl: callbackUrl]
} }
log.debug "Harmony - Adding subscription" log.debug "Adding subscription"
def subscription = subscribe(device, attribute, deviceHandler) def subscription = subscribe(device, attribute, deviceHandler)
if (!subscription || !subscription.eventSubscription) { if (!subscription || !subscription.eventSubscription) {
subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
@@ -799,7 +785,7 @@ def removeSubscription() {
log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
if (device) { if (device) {
log.debug "Harmony - Removing subscription for device: ${device.id}" log.debug "Removing subscription for device: ${device.id}"
state.remove(device.id) state.remove(device.id)
unsubscribe(device) unsubscribe(device)
} }
@@ -823,17 +809,16 @@ def deviceHandler(evt) {
def deviceInfo = state[evt.deviceId] def deviceInfo = state[evt.deviceId]
if (state.harmonyHubs) { if (state.harmonyHubs) {
state.harmonyHubs.each { harmonyHub -> state.harmonyHubs.each { harmonyHub ->
log.trace "Harmony - Sending data to $harmonyHub.name"
sendToHarmony(evt, harmonyHub.callbackUrl) sendToHarmony(evt, harmonyHub.callbackUrl)
} }
} else if (deviceInfo) { } else if (deviceInfo) {
if (deviceInfo.callbackUrl) { if (deviceInfo.callbackUrl) {
sendToHarmony(evt, deviceInfo.callbackUrl) sendToHarmony(evt, deviceInfo.callbackUrl)
} else { } else {
log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}" log.warn "No callbackUrl set for device: ${evt.deviceId}"
} }
} else { } else {
log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}" log.warn "No subscribed device found for device: ${evt.deviceId}"
} }
} }
@@ -857,12 +842,12 @@ def sendToHarmony(evt, String callbackUrl) {
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
] ]
try { try {
log.debug "Harmony - Sending data to Harmony Cloud: $params" log.debug "Sending data to Harmony Cloud: $params"
httpPostJson(params) { resp -> httpPostJson(params) { resp ->
log.debug "Harmony - Cloud Response: ${resp.status}" log.debug "Harmony Cloud - Response: ${resp.status}"
} }
} catch (e) { } catch (e) {
log.error "Harmony - Cloud Something went wrong: $e" log.error "Harmony Cloud - Something went wrong: $e"
} }
} }
} }
@@ -887,10 +872,10 @@ def activityCallback() {
if (data.errorCode == "200") { if (data.errorCode == "200") {
device.setCurrentActivity(data.currentActivityId) device.setCurrentActivity(data.currentActivityId)
} else { } else {
log.warn "Harmony - Activity callback error: ${data}" log.warn "Activity callback error: ${data}"
} }
} else { } else {
log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}" log.warn "Activity callback sent to non-existant dni: ${params.dni}"
} }
render status: 200, data: '{"msg": "Successfully received callbackUrl"}' render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
} }
@@ -924,13 +909,13 @@ def harmony() {
} }
def deleteHarmony() { def deleteHarmony() {
log.debug "Harmony - Trying to delete Harmony hub with mac: ${params.mac}" log.debug "Trying to delete Harmony hub with mac: ${params.mac}"
def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac } def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac }
if (harmonyHub) { if (harmonyHub) {
log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}" log.debug "Deleting Harmony hub with mac: ${params.mac}"
state.harmonyHubs.remove(harmonyHub) state.harmonyHubs.remove(harmonyHub)
} else { } else {
log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}" log.debug "Couldn't find Harmony hub with mac: ${params.mac}"
} }
render status: 204, data: "{}" render status: 204, data: "{}"
} }

View File

@@ -111,23 +111,16 @@ private sendMessage(evt) {
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts(msg, recipients, options) sendNotificationToContacts(msg, recipients, options)
} else { } else {
if (pushAndPhone != 'No') {
log.debug 'sending push'
options.method = 'push'
sendNotification(msg, options)
}
if (phone) { if (phone) {
options.phone = phone options.phone = phone
if (pushAndPhone != 'No') { log.debug 'sending SMS'
log.debug 'Sending push and SMS' sendNotification(msg, options)
options.method = 'both'
} else {
log.debug 'Sending SMS'
options.method = 'phone'
}
} else if (pushAndPhone != 'No') {
log.debug 'Sending push'
options.method = 'push'
} else {
log.debug 'Sending nothing'
options.method = 'none'
} }
sendNotification(msg, options)
} }
if (frequency) { if (frequency) {
state[evt.deviceId] = now() state[evt.deviceId] = now()

View File

@@ -41,10 +41,10 @@ def updated() {
def presenceHandler(evt) { def presenceHandler(evt) {
if (evt.value == "present") { if (evt.value == "present") {
// log.debug "${presence.label ?: presence.name} has arrived at the ${location}" log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
sendPush("${presence.label ?: presence.name} has arrived at the ${location}") sendPush("${presence.label ?: presence.name} has arrived at the ${location}")
} else if (evt.value == "not present") { } else if (evt.value == "not present") {
// log.debug "${presence.label ?: presence.name} has left the ${location}" log.debug "${presence.label ?: presence.name} has left the ${location}"
sendPush("${presence.label ?: presence.name} has left the ${location}") sendPush("${presence.label ?: presence.name} has left the ${location}")
} }
} }

View File

@@ -47,7 +47,7 @@ def updated() {
def presenceHandler(evt) { def presenceHandler(evt) {
if (evt.value == "present") { if (evt.value == "present") {
// log.debug "${presence.label ?: presence.name} has arrived at the ${location}" log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients) sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients)
@@ -56,7 +56,7 @@ def presenceHandler(evt) {
sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}") sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}")
} }
} else if (evt.value == "not present") { } else if (evt.value == "not present") {
// log.debug "${presence.label ?: presence.name} has left the ${location}" log.debug "${presence.label ?: presence.name} has left the ${location}"
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients) sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients)

View File

@@ -67,7 +67,7 @@ def updated() {
} }
def subscribe() { def subscribe() {
// log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}"
subscribe(doorSensor, "contact", garageDoorContact) subscribe(doorSensor, "contact", garageDoorContact)
subscribe(cars, "presence", carPresence) subscribe(cars, "presence", carPresence)

View File

@@ -26,22 +26,17 @@ definition(
) )
preferences { preferences {
section ("In addition to push notifications, send text alerts to...") {
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { input("recipients", "contact", title: "Send notifications to") {
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } input "phone1", "phone", title: "Phone Number 1", required: false
input "phone2", "phone", title: "Phone Number 2", required: false
input "phone3", "phone", title: "Phone Number 3", required: false
}
} }
if (location.channelName != 'samsungtv') { section ("Zip code (optional, defaults to location coordinates)...") {
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } input "zipcode", "text", title: "Zip Code", required: false
} }
section ("In addition to push notifications, send text alerts to...") {
input("recipients", "contact", title: "Send notifications to") {
input "phone1", "phone", title: "Phone Number 1", required: false
input "phone2", "phone", title: "Phone Number 2", required: false
input "phone3", "phone", title: "Phone Number 3", required: false
}
}
} }
def installed() { def installed() {
@@ -66,7 +61,7 @@ def checkForSevereWeather() {
def alerts def alerts
if(locationIsDefined()) { if(locationIsDefined()) {
if(zipcodeIsValid()) { if(zipcodeIsValid()) {
alerts = getWeatherFeature("alerts", zipCode)?.alerts alerts = getWeatherFeature("alerts", zipcode)?.alerts
} else { } else {
log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode"
alerts = getWeatherFeature("alerts")?.alerts alerts = getWeatherFeature("alerts")?.alerts

View File

@@ -71,7 +71,7 @@ def updated() {
private subscribeToEvents() private subscribeToEvents()
{ {
subscribe intrusionMotions, "motion", intruderMotion subscribe intrusionMotions, "motion", intruderMotion
// subscribe residentMotions, "motion", residentMotion subscribe residentMotions, "motion", residentMotion
subscribe intrusionContacts, "contact", contact subscribe intrusionContacts, "contact", contact
subscribe alarms, "alarm", alarm subscribe alarms, "alarm", alarm
subscribe(app, appTouch) subscribe(app, appTouch)
@@ -156,7 +156,6 @@ def residentMotion(evt)
// startReArmSequence() // startReArmSequence()
// } // }
//} //}
unsubscribe(residentMotions)
} }
def contact(evt) def contact(evt)
@@ -215,7 +214,7 @@ def checkForReArm()
} }
else { else {
log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection" log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection"
} }
} }
private startAlarmSequence() private startAlarmSequence()

View File

@@ -48,7 +48,7 @@ def updated()
def contactOpenHandler(evt) { def contactOpenHandler(evt) {
log.trace "$evt.value: $evt, $settings" log.trace "$evt.value: $evt, $settings"
log.debug "$contact1 was opened, sending text" log.debug "$contact1 was opened, texting $phone1"
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients) sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients)
} }

View File

@@ -50,7 +50,7 @@ def updated() {
def motionActiveHandler(evt) { def motionActiveHandler(evt) {
log.trace "$evt.value: $evt, $settings" log.trace "$evt.value: $evt, $settings"
if (presence1.latestValue("presence") == "not present") { if (presence1.latestValue("presence") == "not present") {
// Don't send a continuous stream of text messages // Don't send a continuous stream of text messages
def deltaSeconds = 10 def deltaSeconds = 10
@@ -60,14 +60,14 @@ def motionActiveHandler(evt) {
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
if (alreadySentSms) { if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaSeconds seconds" log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
} else { } else {
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}" log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}"
sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients) sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients)
} }
else { else {
log.debug "$motion1 has moved while you were out, sending text" log.debug "$motion1 has moved while you were out, texting $phone1"
sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out") sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out")
} }
} }

View File

@@ -53,13 +53,13 @@ def accelerationActiveHandler(evt) {
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
if (alreadySentSms) { if (alreadySentSms) {
log.debug "SMS already sent to phone within the last $deltaSeconds seconds" log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
} else { } else {
if (location.contactBookEnabled) { if (location.contactBookEnabled) {
sendNotificationToContacts("Gun case has moved!", recipients) sendNotificationToContacts("Gun case has moved!", recipients)
} }
else { else {
log.debug "$accelerationSensor has moved, texting phone" log.debug "$accelerationSensor has moved, texting $phone1"
sendSms(phone1, "Gun case has moved!") sendSms(phone1, "Gun case has moved!")
} }
} }

View File

@@ -86,7 +86,6 @@ def firstPage()
def lightSwitchesDiscovered = lightSwitchesDiscovered() def lightSwitchesDiscovered = lightSwitchesDiscovered()
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
section("Select a device...") { section("Select a device...") {
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
@@ -682,4 +681,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
private List getRealHubFirmwareVersions() { private List getRealHubFirmwareVersions() {
return location.hubs*.firmwareVersionString.findAll { it } return location.hubs*.firmwareVersionString.findAll { it }
} }

View File

@@ -60,7 +60,7 @@ def authPage() {
def oauthInitUrl() { def oauthInitUrl() {
def token = getToken() def token = getToken()
//log.debug "initiateOauth got token: $token" log.debug "initiateOauth got token: $token"
// store these for validate after the user takes the oauth journey // store these for validate after the user takes the oauth journey
state.oauth_request_token = token.oauth_token state.oauth_request_token = token.oauth_token
@@ -76,7 +76,7 @@ def getToken() {
] ]
def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token" def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token"
def url = buildSignedUrl(requestTokenBaseUrl, params) def url = buildSignedUrl(requestTokenBaseUrl, params)
//log.debug "getToken - url: $url" log.debug "getToken - url: $url"
return getJsonFromUrl(url) return getJsonFromUrl(url)
} }
@@ -182,7 +182,7 @@ def exchangeToken() {
def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token" def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token"
def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret) def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret)
//log.debug "signed url: $url with secret $tokenSecret" log.debug "signed url: $url with secret $tokenSecret"
def token = getJsonFromUrl(url) def token = getJsonFromUrl(url)
@@ -198,8 +198,8 @@ def exchangeToken() {
def load() { def load() {
def json = get(getMeasurement(new Date() - 30)) def json = get(getMeasurement(new Date() - 30))
// removed logging of actual json payload. Can be put back for debugging
log.debug "swapped, then received json" log.debug "swapped, then received: $json"
parse(data:json) parse(data:json)
def html = """ def html = """