Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Pearce
309439a391 MSA-1474: Tim,
As requested, My Danfoss DTH code for certification. 

Regards
Richard
2016-09-15 12:19:27 -05:00
65 changed files with 1225 additions and 1854 deletions

View File

@@ -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"

View File

@@ -0,0 +1,284 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*
*/
//metadata for smartapps
metadata {
definition (name: "Danfoss LC13 TRV V0.7", namespace: "richdp", author: "RichardP") {
capability "Actuator"
capability "Temperature Measurement"
capability "Relative Humidity Measurement"
capability "Thermostat"
capability "Configuration"
capability "Polling"
capability "Sensor"
attribute "thermostatFanState", "string"
command "switchMode"
command "switchFanMode"
command "quickSetCool"
command "quickSetHeat"
fingerprint deviceId: "0x0804"
fingerprint inClusters: "0x80, 0x46, 0x81, 0x72, 0x8F, 0x75, 0x43, 0x86, 0x84"
}
// simulator metadata
simulator {
status "off" : "command: 4003, payload: 00"
status "heat" : "command: 4003, payload: 01"
status "cool" : "command: 4003, payload: 02"
status "auto" : "command: 4003, payload: 03"
status "emergencyHeat" : "command: 4003, payload: 04"
status "fanAuto" : "command: 4403, payload: 00"
status "fanOn" : "command: 4403, payload: 01"
status "fanCirculate" : "command: 4403, payload: 06"
status "heat 60" : "command: 4303, payload: 01 09 3C"
status "heat 68" : "command: 4303, payload: 01 09 44"
status "heat 72" : "command: 4303, payload: 01 09 48"
status "cool 72" : "command: 4303, payload: 02 09 48"
status "cool 76" : "command: 4303, payload: 02 09 4C"
status "cool 80" : "command: 4303, payload: 02 09 50"
status "temp 58" : "command: 3105, payload: 01 2A 02 44"
status "temp 62" : "command: 3105, payload: 01 2A 02 6C"
status "temp 70" : "command: 3105, payload: 01 2A 02 BC"
status "temp 74" : "command: 3105, payload: 01 2A 02 E4"
status "temp 78" : "command: 3105, payload: 01 2A 03 0C"
status "temp 82" : "command: 3105, payload: 01 2A 03 34"
status "idle" : "command: 4203, payload: 00"
status "heating" : "command: 4203, payload: 01"
status "cooling" : "command: 4203, payload: 02"
status "fan only" : "command: 4203, payload: 03"
status "pending heat" : "command: 4203, payload: 04"
status "pending cool" : "command: 4203, payload: 05"
status "vent economizer": "command: 4203, payload: 06"
// reply messages
reply "2502": "command: 2503, payload: FF"
}
//UI configuration
tiles {
// Main Tile displays current set point recieved from thermostat.
// Background color changes with temp.
valueTile("temperature", "device.currentHeatingSetpoint", width: 3, height: 2) {
state("device.currentHeatingSetpoint", label:'${currentValue}°',
backgroundColors:[
[value: 0, color: "#ededed"],
[value: 4, color: "#153591"], //blue
[value: 16, color: "#178998"],
[value: 18, color: "#199f5c"],
[value: 20, color: "#2da71c"],//green
[value: 21, color: "#5baa1d"],
[value: 22, color: "#8aae1e"],
[value: 23, color: "#b1a81f"],
[value: 24, color: "#b57d20"],
[value: 26, color: "#b85122"],
[value: 28, color: "#bc2323"] //red
]
)
}
// Slider control for controling the set point which is next sent to the thermostat
controlTile("heatSliderControl", "device.nextHeatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range:"(4..28)" ) {
state "setHeatingSetpoint", action: "quickSetHeat", backgroundColor:"#d04e00"
}
// Tile to indicate the set point that will next be sent to the thermostat. Controlled by controlTile above.
valueTile("nextHeatingSetpoint", "device.nextHeatingSetpoint", inactiveLabel: false, decoration: "flat") {
state "heat", label:'${currentValue}° next', backgroundColor:"#ffffff"
}
// Display battery %
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
tileAttribute ("device.battery", key: "PRIMARY_CONTROL"){
state "battery", label:'${currentValue}% battery', unit:""}
}
// Standard Refresh Tile
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
state "default", action:"polling.poll", icon:"st.secondary.refresh"
}
// Standard Config Tile
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
// Order of Tiles
main "temperature"
details(["temperature", "heatSliderControl", "nextHeatingSetpoint", "battery", "refresh", "configure"])
}
} //end of UI config
//Parse string to recieve information from device
def parse(String description) {
state.count = 0
def results = []
log.debug("RAW command: $description")
if (description.startsWith("Err")) {
log.debug("An error has occurred")
}
else {
def cmd = zwave.parse(description,[0x80: 1, 0x46: 1, 0x81: 1, 0x72: 2, 0x8F: 1, 0x75: 2, 0x43: 2, 0x86: 1, 0x84: 2])
log.debug "Parsed Command: $cmd"
if (cmd) {
results = zwaveEvent(cmd)
}
}
}
// Event Generation
// Event - Get Battery Report
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
log.debug ("Battery: $cmd.batteryLevel")
}
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
state.lastbatt = new Date().time
createEvent(map)
}
// Event - Get Override Schedule
def zwaveEvent(physicalgraph.zwave.commands.climatecontrolschedulev1.ScheduleOverrideReport cmd){
log.debug "--- Schedule Override Report received: $cmd"
}
// Event - Thermostat Report
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) {
log.debug "--- Thermostat Report recieved: $cmd"
def cmdScale = cmd.scale == 1 ? "F" : "C"
def map = [:]
map.value = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision)
map.unit = getTemperatureScale()
map.displayed = false
map.name = "currentHeatingSetpoint"
log.debug ("Parsed Thermostat Set Point: $map")
// So we can respond with same format
state.size = cmd.size
state.scale = cmd.scale
state.precision = cmd.precision
map
sendEvent(map)
}
// Event for waking up the battery powered device
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: true)]
// Only ask for battery if we haven't had a BatteryReport in a while
if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
result << response(zwave.batteryV1.batteryGet())
result << response("delay 1200") // leave time for device to respond to batteryGet
}
//Send new set point to TRV on wake up
result << response (configureSetPoint())
result << response ("delay 2000")
//Confirm current set point and update UI
result << response (updateUI())
result << response ("delay 2000")
// wait and then send no more information confirmation
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
log.debug "Zwave event received: $cmd"
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "Unexpected zwave command $cmd"
}
// Command Implementations
def poll() {
delayBetween([
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(), //chaged to V1 for testing
], standardDelay)
}
def quickSetHeat(degrees) {
setHeatingSetpoint(degrees, 1000)
}
def nextHeatingSetpoint(degrees, delay = 3600) {
setHeatingSetpoint(degrees.toDouble(), delay)
}
def setHeatingSetpoint(Double degrees, Integer delay = 3600) {
log.trace "setNextHeatingSetpoint($degrees, $delay)"
def deviceScale = state.scale ?: 2
def deviceScaleString = deviceScale == 2 ? "C" : "F"
def locationScale = getTemperatureScale()
def p = (state.precision == null) ? 1 : state.precision
def convertedDegrees
if (locationScale == "C" && deviceScaleString == "F") {
convertedDegrees = celsiusToFahrenheit(degrees)
} else if (locationScale == "F" && deviceScaleString == "C") {
convertedDegrees = fahrenheitToCelsius(degrees)
} else {
convertedDegrees = degrees
}
sendEvent(name:"nextHeatingSetpoint", value:convertedDegrees, isStateChange: true)
log.debug ("Value to be sent to thermostat on next wakeup: $convertedDegrees")
state.nextHeatingSetpoint = convertedDegrees
}
//send new set point to thermostat
def configureSetPoint(){
log.debug "*** sending new setpoint"
def sendHeatingSetpoint = state.nextHeatingSetpoint
delayBetween([
zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: 0, precision: 2, scaledValue: sendHeatingSetpoint).format(),
], 2000)
}
//get the current setpoint and update the UI
def updateUI(){
delayBetween([
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()
], 2000)
}
//configuration
def configure() {
delayBetween([
zwave.configurationV1.configurationSet(parameterNumber:1, size:2, scaledConfigurationValue:100).format(),
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
zwave.wakeUpV1.wakeUpIntervalSet(seconds:3600, nodeid:zwaveHubNodeId).format()
], standardDelay)
}
//standardDelay
private getStandardDelay() {
1000
}

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"
@@ -93,19 +94,17 @@ def ping() {
} }
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") // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// Device-Watch allows 2 check-in misses from device sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
zigbee.onOffRefresh() + zigbee.levelRefresh() zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
} }

View File

@@ -67,6 +67,6 @@ def refresh() {
void poll() { void poll() {
log.debug "Executing 'poll' using parent SmartApp" log.debug "Executing 'poll' using parent SmartApp"
parent.pollChild() parent.poll()
} }

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 {
@@ -133,7 +133,7 @@ def refresh() {
void poll() { void poll() {
log.debug "Executing 'poll' using parent SmartApp" log.debug "Executing 'poll' using parent SmartApp"
parent.pollChild() parent.poll()
} }
def generateEvent(Map results) { def generateEvent(Map results) {
@@ -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 * 15, 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}'"
@@ -76,8 +70,13 @@ 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.isInBulbDiscovery())
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)
} }
@@ -86,7 +85,3 @@ def parse(description) {
} }
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 * 15, 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 * 15, 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 * 15, 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

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"
@@ -104,21 +104,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}"
}
} }
} }
@@ -141,12 +128,10 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh() + zigbee.onOffConfig(0, 300) + powerConfig() zigbee.onOffConfig(0, 300) + powerConfig() + refresh()
} }
//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"
@@ -118,28 +118,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 +135,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 +180,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
} }
} }
@@ -304,13 +292,19 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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."
def enrollCmds = [
"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 // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default // battery minReport 30 seconds, maxReportTime 6 hrs by default
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + 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"
@@ -122,37 +122,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 +144,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 +194,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
} }
} }
@@ -319,13 +303,19 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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."
def enrollCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
]
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default // battery minReport 30 seconds, maxReportTime 6 hrs by default
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {

View File

@@ -255,9 +255,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 +270,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"
@@ -147,33 +147,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 +169,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 +261,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
} }
} }
@@ -412,22 +401,22 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default // battery minReport 30 seconds, maxReportTime 6 hrs by default
def configCmds = zigbee.batteryConfig() + def configCmds = enrollResponse() +
zigbee.batteryConfig() +
zigbee.temperatureConfig(30, 300) + 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

@@ -277,8 +277,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 +295,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"
@@ -109,28 +109,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 +127,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
} }
@@ -266,15 +255,19 @@ def refresh() {
} }
def configure() { def configure() {
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 enrollCmds = [
"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 // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default // battery minReport 30 seconds, maxReportTime 6 hrs by default
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + 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"
@@ -93,37 +93,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 +116,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
} }
@@ -250,7 +235,11 @@ 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: '%'
]
} }
/** /**
@@ -263,16 +252,20 @@ def ping() {
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) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 humidityConfigCmds = [
@@ -283,7 +276,7 @@ def configure() {
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
// battery minReport 30 seconds, maxReportTime 6 hrs by default // 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 return humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // 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

@@ -60,21 +60,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}"
}
} }
} }
@@ -97,15 +84,13 @@ def ping() {
} }
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) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
} }

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

@@ -95,7 +95,7 @@ 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
@@ -107,18 +107,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
} }
} }
} }
@@ -138,17 +128,15 @@ def ping() {
} }
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) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh() 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.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)
} }
def setColorTemperature(value) { def setColorTemperature(value) {
@@ -189,5 +177,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

@@ -83,21 +83,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}"
}
} }
} }
@@ -121,17 +108,15 @@ def ping() {
} }
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) // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
// enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
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 // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
refresh() zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
} }
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

@@ -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

@@ -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

@@ -20,6 +20,8 @@
* JLH - 02-15-2014 - Fuller use of ecobee API * JLH - 02-15-2014 - Fuller use of ecobee API
* 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines
*/ */
include 'asynchttp_v1'
definition( definition(
name: "Ecobee (Connect)", name: "Ecobee (Connect)",
namespace: "smartthings", namespace: "smartthings",
@@ -244,9 +246,7 @@ def getEcobeeThermostats() {
uri: apiEndpoint, uri: apiEndpoint,
path: "/1/thermostat", path: "/1/thermostat",
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
// TODO - the query string below is not consistent with the Ecobee docs: query: [json: toJson(bodyParams)]
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
query: [format: 'json', body: toJson(bodyParams)]
] ]
def stats = [:] def stats = [:]
@@ -265,9 +265,8 @@ def getEcobeeThermostats() {
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
log.trace "Exception polling children: " + e.response.data.status log.trace "Exception polling children: " + e.response.data.status
if (e.response.data.status.code == 14) { if (e.response.data.status.code == 14) {
atomicState.action = "getEcobeeThermostats"
log.debug "Refreshing your auth_token!" log.debug "Refreshing your auth_token!"
refreshAuthToken() refreshAuthToken([async: false, nextAction: "getEcobeeThermostats"])
} }
} }
atomicState.thermostats = stats atomicState.thermostats = stats
@@ -358,16 +357,22 @@ def initialize() {
atomicState.timeSendPush = null atomicState.timeSendPush = null
atomicState.reAttempt = 0 atomicState.reAttempt = 0
pollHandler() //first time polling data data from thermostat initialPoll() //first time polling data data from thermostat
//automatically update devices status every 5 mins //automatically update devices status every 5 mins
runEvery5Minutes("poll") runEvery5Minutes("poll")
} }
def pollHandler() { /**
log.debug "pollHandler()" * Polls the child devices (synchronously).
pollChildren(null) // Hit the ecobee API for update on all thermostats * This is used during app install/update, and is synchronous
* to maintain current behavior that will cause install/update to fail
* if polling fails.
*/
def initialPoll() {
log.debug "initialPoll()"
pollChildrenSync() // Hit the ecobee API for update on all thermostats
atomicState.thermostats.each {stat -> atomicState.thermostats.each {stat ->
def dni = stat.key def dni = stat.key
@@ -380,36 +385,38 @@ def pollHandler() {
} }
} }
def pollChildren(child = null) { /**
def thermostatIdsString = getChildDeviceIdsString() * Polls Ecobee (asynchronously) for updated device state data.
* Called from within this Connect SmartApp as well as the child
* devices.
*/
def poll() {
log.debug "polling asynchronously"
asynchttp_v1.get('asyncPollResponseHandler', getPollParams())
}
/**
* Makes a (synchronous) request to the Ecobee API to get the data for the thermostats.
* This request is made synchronously here because it is called as part of the
* install/updated lifecycle, and changing it to asynchronous during the install/update
* lifecycle may change the behavior if there is an error in polling.
*
* If further analysis shows that polling can be done asynchronously during
* install/update without any adverse consequences, this should then be made
* asynchronous just as the scheduled polling is.
*/
def pollChildrenSync() {
log.debug "polling children: $thermostatIdsString" log.debug "polling children: $thermostatIdsString"
def requestBody = [ def params = getPollParams()
selection: [ params.query << ["Content-Type": "application/json"]
selectionType: "thermostats",
selectionMatch: thermostatIdsString,
includeExtendedRuntime: true,
includeSettings: true,
includeRuntime: true,
includeSensors: true
]
]
def result = false def result = false
log.debug "making synchronous poll request"
def pollParams = [
uri: apiEndpoint,
path: "/1/thermostat",
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
// TODO - the query string below is not consistent with the Ecobee docs:
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
query: [format: 'json', body: toJson(requestBody)]
]
try{ try{
httpGet(pollParams) { resp -> httpGet(params) { resp ->
if(resp.status == 200) { if(resp.status == 200) {
log.debug "poll results returned resp.data ${resp.data}"
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
updateSensorData() updateSensorData()
storeThermostatData(resp.data.thermostatList) storeThermostatData(resp.data.thermostatList)
@@ -420,40 +427,95 @@ def pollChildren(child = null) {
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
log.trace "Exception polling children: " + e.response.data.status log.trace "Exception polling children: " + e.response.data.status
if (e.response.data.status.code == 14) { if (e.response.data.status.code == 14) {
atomicState.action = "pollChildren"
log.debug "Refreshing your auth_token!" log.debug "Refreshing your auth_token!"
refreshAuthToken() refreshAuthToken([async: false, nextAction: "pollChildrenSync"])
} }
} }
return result return result
} }
// Poll Child is invoked from the Child Device itself as part of the Poll Capability /**
def pollChild() { * Response handler for asynchronous request to get thermostat data.
def devices = getChildDevices() * Given a successful response, updates the sensor data, stores the thermostat
* data, and generates child device events.
if (pollChildren()) { *
devices.each { child -> * If the access token has expired, will issue a request to refresh the token
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { * (and pending successful token refresh, the poll request will be made again).
if(atomicState.thermostats[child.device.deviceNetworkId] != null) { */
def tData = atomicState.thermostats[child.device.deviceNetworkId] def asyncPollResponseHandler(response, data) {
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" log.trace "async poll response handler"
child.generateEvent(tData.data) //parse received message from parent if (!response.hasError()) {
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { if (response.status == 200) {
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" def json
return null try {
} json = response.getJson()
} } catch (e) {
} log.error ("error parsing JSON", e)
} else { }
log.info "ERROR: pollChildren()" if (json) {
return null atomicState.remoteSensors = json.thermostatList.remoteSensors
} updateSensorData()
storeThermostatData(json.thermostatList)
generateChildThermostatEvent()
}
} else {
log.warn "Response returned non-200 response. Status: ${response.status}, data: ${response.getData()}"
}
} else {
log.trace "Exception polling children: ${response.getErrorMessage()}"
def errorJson
try {
errorJson = response.getErrorJson()
} catch (e) {
log.error("Unable to parse error json response", e)
}
if (errorJson?.status?.code == 14) {
log.debug "Refreshing your auth_token!"
refreshAuthToken([async: true, nextAction: "poll"])
} else {
log.warn "Error polling children that is not due to an expired token. Response: ${response.getErrorData()}"
}
}
} }
void poll() { private getPollParams() {
pollChild() def thermostatIdsString = getChildDeviceIdsString()
def requestBody = [
selection: [
selectionType: "thermostats",
selectionMatch: thermostatIdsString,
includeExtendedRuntime: true,
includeSettings: true,
includeRuntime: true,
includeSensors: true
]
]
return [
uri: apiEndpoint,
path: "/1/thermostat",
headers: ["Authorization": "Bearer ${atomicState.authToken}"],
query: [json: toJson(requestBody)]
]
}
/**
* Calls each child thermostat device to generate an event with the thermostat
* data.
*/
def generateChildThermostatEvent() {
log.trace("generateChildThermostatEvent")
getChildDevices().each { child ->
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
def tData = atomicState.thermostats[child.device.deviceNetworkId]
log.debug "calling child.generateEvent($tData.data)"
child.generateEvent(tData.data) //parse received message from parent
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
return null
}
}
}
} }
def availableModes(child) { def availableModes(child) {
@@ -553,47 +615,104 @@ def toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
} }
private refreshAuthToken() { /**
log.debug "refreshing auth token" * Uses the refresh token to get a new access token, then executes the nextAction.
* @param options - a map of options. valid options are async: true/false, which
* specifies if the refresh token request will be done asynchronously or not (default is false)
* nextAction: "nameOfMethod" specifies what method to execute after
* the token is refreshed (not required).
* (note: using a map as the parameter because we need to call it from a schedueled
* execution and we can only pass a data map to scheduled executions)
*/
private void refreshAuthToken(options) {
if(!atomicState.refreshToken) {
log.warn "Cannot not refresh OAuth token since there is no refreshToken stored"
} else {
def refreshParams = [
uri : apiEndpoint,
path : "/token",
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
]
if (options.async) {
refreshAuthTokenAsync(refreshParams, options.nextAction)
} else {
refreshAuthTokenSync(refreshParams, options.nextAction)
}
}
}
if(!atomicState.refreshToken) { private void refreshAuthTokenSync(params, nextAction = null) {
log.warn "Can not refresh OAuth token since there is no refreshToken stored" try {
} else { httpPost(refreshParams) { resp ->
def refreshParams = [ if(resp.status == 200) {
method: 'POST', log.debug "Token refreshed...calling saved RestAction now!"
uri : apiEndpoint, debugEvent("Token refreshed ... calling saved RestAction now!")
path : "/token", saveTokenAndResumeAction(resp.data, nextAction)
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
]
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
//changed to httpPost
try {
def jsonMap
httpPost(refreshParams) { resp ->
if(resp.status == 200) {
log.debug "Token refreshed...calling saved RestAction now!"
debugEvent("Token refreshed ... calling saved RestAction now!")
saveTokenAndResumeAction(resp.data)
}
} }
} catch (groovyx.net.http.HttpResponseException e) { }
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" } catch (groovyx.net.http.HttpResponseException e) {
def reAttemptPeriod = 300 // in sec log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc. reauthTokenErrorHandler(e.statusCode)
runIn(reAttemptPeriod, "refreshAuthToken") }
} else if (e.statusCode == 401) { // unauthorized }
atomicState.reAttempt = atomicState.reAttempt + 1
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}" private void refreshAuthTokenAsync(refreshParams, nextAction = null) {
if (atomicState.reAttempt <= 3) { log.debug "making asynchronous refresh request"
runIn(reAttemptPeriod, "refreshAuthToken") asynchttp_v1.post('refreshTokenResponseHandler', refreshParams, [nextAction: nextAction])
} else { }
sendPushAndFeeds(notificationMessage)
atomicState.reAttempt = 0 /**
} * The response handler for the request to refresh the authorization handler.
} * Stores the new authorization token and refresh token, and executes any action
} * (method) that failed due to the authorization token expiring.
} */
private void refreshTokenResponseHandler(response, data) {
if (!response.hasError()) {
if (response.status == 200) {
def json
try {
json = response.getJson()
} catch (e) {
log.error "error parsing json from response data: $response.data"
}
if (json) {
log.debug "asnyc refreshTokenHandler: Token refreshed...calling saved RestAction now!"
debugEvent("async Token refreshed ... calling saved RestAction now!")
saveTokenAndResumeAction(json, data.nextAction)
} else {
log.warn "successfully parsed json but result is empty or null"
}
} else {
log.debug "Non 200 response returned. Response code: ${response.code}, data: ${response.getData()}"
}
} else {
log.debug "async refreshTokenHandler: RESPONSE ERROR: ${response.getErrorJson()}"
reauthTokenErrorHandler(response.getErrorJson().code)
}
}
/**
* Retries refreshing the authorization token. Will attempt to get the refresh
* token later, in case there were errors retrieving it.
* Will retry a fixed number of times before sending a push notification to the
* user instructing them to reauthenticate
*/
private void reauthTokenErrorHandler(responseCode) {
def retryInterval = 300 // in seconds
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
// might get non-401 error from exceeding 20 second app limit, connectivity issues, etc.
if (responseCode != 401) {
runIn(retryInterval, "refreshAuthToken", [async: true])
} else if (responseCode == 401) { // unauthorized
atomicState.reAttempt = atomicState.reAttempt + 1
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
if (atomicState.reAttempt <= 3) {
runIn(retryInterval, "refreshAuthToken", [async: true])
} else {
sendPushAndFeeds(notificationMessage)
atomicState.reAttempt = 0
}
}
} }
/** /**
@@ -603,20 +722,20 @@ private refreshAuthToken() {
* *
* @param json - an object representing the parsed JSON response from Ecobee * @param json - an object representing the parsed JSON response from Ecobee
*/ */
private void saveTokenAndResumeAction(json) { private void saveTokenAndResumeAction(json, String nextAction) {
log.debug "token response json: $json" def debugMessage = "token response, scope: ${json?.scope}, expires_in: ${json?.expires_in}, token_type: ${json?.token_type}"
log.debug "debugMessage"
if (json) { if (json) {
debugEvent("Response = $json") debugEvent(debugMessage)
atomicState.refreshToken = json?.refresh_token atomicState.refreshToken = json?.refresh_token
atomicState.authToken = json?.access_token atomicState.authToken = json?.access_token
if (atomicState.action) { if (nextAction) {
log.debug "got refresh token, executing next action: ${atomicState.action}" log.debug "got refresh token, will execute next action (passed in!): $nextAction"
"${atomicState.action}"() "$nextAction"()
} }
} else { } else {
log.warn "did not get response body from refresh token response" log.warn "did not get response body from refresh token response"
} }
atomicState.action = ""
} }
/** /**
@@ -756,7 +875,6 @@ private boolean sendCommandToEcobee(Map bodyParams) {
try{ try{
httpPost(cmdParams) { resp -> httpPost(cmdParams) { resp ->
if(resp.status == 200) { if(resp.status == 200) {
log.debug "updated ${resp.data}"
def returnStatus = resp.data.status.code def returnStatus = resp.data.status.code
if (returnStatus == 0) { if (returnStatus == 0) {
log.debug "Successful call to ecobee API." log.debug "Successful call to ecobee API."
@@ -771,11 +889,10 @@ private boolean sendCommandToEcobee(Map bodyParams) {
log.trace "Exception Sending Json: " + e.response.data.status log.trace "Exception Sending Json: " + e.response.data.status
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
if (e.response.data.status.code == 14) { if (e.response.data.status.code == 14) {
// TODO - figure out why we're setting the next action to be pollChildren // TODO - figure out why we're setting the next action to be poll
// after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error?
atomicState.action = "pollChildren"
log.debug "Refreshing your auth_token!" log.debug "Refreshing your auth_token!"
refreshAuthToken() refreshAuthToken([async: true, nextAction: "poll"])
} else { } else {
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
log.error "Authentication error, invalid authentication method, lack of credentials, etc." log.error "Authentication error, invalid authentication method, lack of credentials, etc."

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() {

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
} }
} }
} }
@@ -131,7 +131,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() ?: [:]
@@ -256,6 +259,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 +316,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, online: v.state?.reachable]
}
}
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)
@@ -460,25 +490,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 +525,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 +557,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]
}
} }
} }
} }
@@ -657,8 +677,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]
}
} }
} }
} }
@@ -718,7 +741,7 @@ private void checkBridgeStatus() {
} }
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")
state.bulbs?.each { state.bulbs?.each {
@@ -743,31 +766,6 @@ def isInBulbDiscovery() {
return state.inBulbDiscovery 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
///////////////////////////////////// /////////////////////////////////////
@@ -803,12 +801,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 +818,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 +836,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) {
@@ -902,6 +887,8 @@ private handleCommandResponse(body) {
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
@@ -952,14 +939,6 @@ private handleCommandResponse(body) {
* @return empty array * @return empty array
*/ */
private handlePoll(body) { private handlePoll(body) {
// Used to track "unreachable" time
// Device is considered "offline" if it has been in the "unreachable" state for
// 11 minutes (e.g. two poll intervals)
// 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}"}
@@ -969,10 +948,7 @@ private handlePoll(body) {
// light just came back online, notify device watch // light just came back online, notify device watch
def lastActivity = now() def lastActivity = now()
device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true) 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 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 user just executed commands, then do not send events to avoid confusing the turning on/off state
@@ -982,18 +958,9 @@ private handlePoll(body) {
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) 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) { state.bulbs[bulb.key]?.online = false
// Store the first time where device was reported as "unreachable" log.warn "$device is not reachable by Hue bridge"
state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis() device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true)
} else if (state.bulbs[bulb.key]?.online) {
// Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
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"
} }
} }
} }
@@ -1028,6 +995,9 @@ def hubVerification(bodytext) {
def on(childDevice) { def on(childDevice) {
log.debug "Executing 'on'" log.debug "Executing 'on'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [on: true]) put("lights/$id/state", [on: true])
@@ -1037,6 +1007,9 @@ def on(childDevice) {
def off(childDevice) { def off(childDevice) {
log.debug "Executing 'off'" log.debug "Executing 'off'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "off") createSwitchEvent(childDevice, "off")
put("lights/$id/state", [on: false]) put("lights/$id/state", [on: false])
@@ -1046,6 +1019,9 @@ def off(childDevice) {
def setLevel(childDevice, percent) { def setLevel(childDevice, percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
// 1 - 254 // 1 - 254
def level def level
@@ -1070,6 +1046,10 @@ def setLevel(childDevice, percent) {
def setSaturation(childDevice, percent) { def setSaturation(childDevice, percent) {
log.debug "Executing 'setSaturation($percent)'" log.debug "Executing 'setSaturation($percent)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
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)
@@ -1082,6 +1062,9 @@ def setSaturation(childDevice, percent) {
def setHue(childDevice, percent) { def setHue(childDevice, percent) {
log.debug "Executing 'setHue($percent)'" log.debug "Executing 'setHue($percent)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
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)
@@ -1094,6 +1077,9 @@ def setHue(childDevice, percent) {
def setColorTemperature(childDevice, huesettings) { def setColorTemperature(childDevice, huesettings) {
log.debug "Executing 'setColorTemperature($huesettings)'" log.debug "Executing 'setColorTemperature($huesettings)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
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)
@@ -1105,6 +1091,9 @@ def setColorTemperature(childDevice, huesettings) {
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
def value = [:] def value = [:]
@@ -1112,22 +1101,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
@@ -1162,14 +1155,7 @@ def setColor(childDevice, huesettings) {
} }
def ping(childDevice) { def ping(childDevice) {
if (childDevice.device?.deviceNetworkId?.equalsIgnoreCase(selectedHue)) { if (isOnline(getId(childDevice))) {
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) childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true)
return "Device is Online" return "Device is Online"
} else { } else {
@@ -1189,12 +1175,13 @@ 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}
""", physicalgraph.device.Protocol.LAN, selectedHue))
} }
private isOnline(id) { private isOnline(id) {
return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null 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 +1193,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 +1220,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,7 +1250,7 @@ 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, "online": bulb.online]]
} }
state.bulbs = map state.bulbs = map
@@ -1668,101 +1657,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

@@ -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

@@ -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

@@ -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 }
} }