Compare commits

..

1 Commits

Author SHA1 Message Date
Prof Genius
ca5443d4dc MSA-1333: Device handler for Kudled zigbee bulb.
According to forum discussion here:
https://community.smartthings.com/t/successfully-paired-kudled-color-bulb-chinese-hue-a19-bulb-copy-cat-23-with-philips-hue-bridge/20515/105

Hope this can be useful.
2016-06-03 01:36:36 -05:00
4 changed files with 367 additions and 809 deletions

View File

@@ -0,0 +1,262 @@
/*
Osram Tunable White 60 A19 bulb
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
that issue by using state variables
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "Kudled white bulb", namespace: "smartthings", author: "SmartThings") {
capability "Color Temperature"
capability "Actuator"
capability "Switch"
capability "Switch Level"
capability "Configuration"
capability "Refresh"
capability "Sensor"
attribute "colorName", "string"
// indicates that device keeps track of heartbeat (in state.heartbeat)
attribute "heartbeat", "string"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
state "colorTemperature", label: '${currentValue} K'
}
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
state "colorName", label: '${currentValue}'
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
main(["switch"])
details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
//log.trace description
// save heartbeat (i.e. last time we got a message from device)
state.heartbeat = Calendar.getInstance().getTimeInMillis()
if (description?.startsWith("catchall:")) {
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
{
def result = createEvent(name: "switch", value: "on")
log.debug "Parse returned ${result?.descriptionText}"
return result
}
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
{
def result = createEvent(name: "switch", value: "off")
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
else if (description?.startsWith("read attr -")) {
def descMap = parseDescriptionAsMap(description)
log.trace "descMap : $descMap"
if (descMap.cluster == "0300") {
log.debug descMap.value
def tempInMired = convertHexToInt(descMap.value)
def tempInKelvin = Math.round(1000000/tempInMired)
log.trace "temp in kelvin: $tempInKelvin"
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
}
else if(descMap.cluster == "0008"){
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
log.debug "dimmer value is $dimmerValue"
sendEvent(name: "level", value: dimmerValue)
}
}
else {
def name = description?.startsWith("on/off: ") ? "switch" : null
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
def on() {
log.debug "on()"
sendEvent(name: "switch", value: "on")
setLevel(state?.levelValue)
}
def off() {
log.debug "off()"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
def refresh() {
sendEvent(name: "heartbeat", value: "alive", displayed:false)
[
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7"
]
}
def configure() {
state.levelValue = 100
log.debug "Configuring Reporting and Bindings."
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
]
return onOffConfig() + levelConfig() + configCmds + refresh() // send refresh cmds as part of config
}
def onOffConfig() {
[
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 6 0 0x10 0 300 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
]
}
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
//min level change is 01
def levelConfig() {
[
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 8 0 0x20 5 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
]
}
def setColorTemperature(value) {
if(value<101){
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
}
def tempInMired = Math.round(1000000/value)
def finalHex = swapEndianHex(hex(tempInMired, 4))
def genericName = getGenericName(value)
log.debug "generic name is : $genericName"
def cmds = []
sendEvent(name: "colorTemperature", value: value, displayed:false)
sendEvent(name: "colorName", value: genericName)
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
cmds
}
def parseDescriptionAsMap(description) {
(description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
def setLevel(value) {
state.levelValue = (value==null) ? 100 : value
log.trace "setLevel($value)"
def cmds = []
if (value == 0) {
sendEvent(name: "switch", value: "off")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
else if (device.latestValue("switch") == "off") {
sendEvent(name: "switch", value: "on")
}
sendEvent(name: "level", value: state.levelValue)
def level = hex(state.levelValue * 254 / 100)
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
//log.debug cmds
cmds
}
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
private getGenericName(value){
def genericName = "White"
if(value < 3300){
genericName = "Soft White"
} else if(value < 4150){
genericName = "Moonlight"
} else if(value < 5000){
genericName = "Cool White"
} else if(value <= 6500){
genericName = "Daylight"
}
genericName
}
private getEndpointId() {
//new BigInteger(device.endpointId, 16).toString()
"0x0B"
}
private hex(value, width=2) {
def s = new BigInteger(Math.round(value).toString()).toString(16)
while (s.size() < width) {
s = "0" + s
}
s
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
//Need to reverse array of size 2
private byte[] reverseArray(byte[] array) {
byte tmp;
tmp = array[1];
array[1] = array[0];
array[0] = tmp;
return array
}

View File

@@ -13,14 +13,14 @@
* Documented Header
*
* Change 2: 2014-03-15
* Fixed bug where we weren't coming on when changing
* Fixed bug where we weren't coming on when changing
* levels down.
*
* Change 3: 2014-04-02 (lieberman)
* Change 3: 2014-04-02 (lieberman)
* Changed sendEvent() to createEvent() in parse()
*
* Change 4: 2014-04-12 (wackford)
* Added current power usage tile
* Added current power usage tile
*
* Change 5: 2014-09-14 (wackford)
* a. Changed createEvent() to sendEvent() in parse() to
@@ -33,7 +33,7 @@
* b. added refresh on udate
* c. added uninstallFromChildDevice to handle removing from settings
* d. Changed to allow bulb to 100%, was possible to get past logic at 99
*
*
* Change 7: 2014-11-09 (wackford)
* a. Added bulbpower calcs to device. TCP is broken
* b. Changed to set dim level first then on. Much easier on the eys coming from bright.
@@ -42,7 +42,7 @@
* Code
*****************************************************************
*/
// for the UI
// for the UI
metadata {
definition (name: "TCP Bulb", namespace: "wackford", author: "Todd Wackford") {
capability "Switch"
@@ -52,26 +52,28 @@ metadata {
capability "Switch Level"
attribute "stepsize", "string"
command "levelUp"
command "levelDown"
command "on"
command "off"
command "setBulbPower"
command "on"
command "off"
command "setBulbPower"
}
simulator {
// TODO: define status and reply messages here
}
preferences {
preferences {
input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5
}
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#79b821"
state "off", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff"
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
@@ -82,15 +84,15 @@ metadata {
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright"
}
standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
state "default", action:"levelDown", icon:"st.illuminance.illuminance.light"
}
valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") {
state "power", label: '${currentValue} Watts'
}
standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright"
}
standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
state "default", action:"levelDown", icon:"st.illuminance.illuminance.light"
}
valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") {
state "power", label: '${currentValue} Watts'
}
main(["switch"])
details(["switch", "lUp", "lDown", "levelSliderControl", "level" , "power", "refresh" ])
@@ -101,10 +103,10 @@ metadata {
def parse(description) {
//log.debug "parse() - $description"
def results = []
if ( description == "updated" )
return
if ( description == "updated" )
return
if (description?.name && description?.value)
{
results << createEvent(name: "${description?.name}", value: "${description?.value}")
@@ -114,73 +116,73 @@ def parse(description) {
// handle commands
def setBulbPower(value) {
state.bulbPower = value
log.debug "In child with bulbPower of ${state.bulbPower}"
log.debug "In child with bulbPower of ${state.bulbPower}"
}
def on() {
log.debug "Executing 'on'"
sendEvent(name:"switch",value:on)
sendEvent(name:"switch",value:on)
parent.on(this)
def levelSetting = device.latestValue("level") as Float ?: 1.0
//def bulbPowerMax = device.latestValue("setBulbPower") as Float
//def calculatedPower = bulbPowerMax * (levelSetting / 100)
//sendEvent(name: "power", value: calculatedPower.round(1))
if (device.latestValue("level") == null) {
def bulbPowerMax = device.latestValue("setBulbPower") as Float
def calculatedPower = bulbPowerMax * (levelSetting / 100)
sendEvent(name: "power", value: calculatedPower.round(1))
if (device.latestValue("level") == null) {
sendEvent( name: "level", value: 1.0 )
}
}
}
def off() {
log.debug "Executing 'off'"
sendEvent(name:"switch",value:off)
sendEvent(name:"switch",value:off)
parent.off(this)
sendEvent(name: "power", value: 0.0)
sendEvent(name: "power", value: 0.0)
}
def levelUp() {
def level = device.latestValue("level") as Integer ?: 0
def step = state.stepsize as float
level+= step
if ( level > 100 )
level = 100
setLevel(level)
def step = state.stepsize as float
level+= step
if ( level > 100 )
level = 100
setLevel(level)
}
def levelDown() {
def level = device.latestValue("level") as Integer ?: 0
def step = state.stepsize as float
level-= step
def step = state.stepsize as float
level-= step
if ( level < 1 )
level = 1
setLevel(level)
level = 1
setLevel(level)
}
def setLevel(value) {
log.debug "in setLevel with value: ${value}"
def level = value as Integer
sendEvent( name: "level", value: level )
sendEvent( name: "switch.setLevel", value:level )
parent.setLevel( this, level )
if (( level > 0 ) && ( level <= 100 ))
on()
else
off()
//def levelSetting = level as float
//def bulbPowerMax = device.latestValue("setBulbPower") as float
//def calculatedPower = bulbPowerMax * (levelSetting / 100)
//sendEvent(name: "power", value: calculatedPower.round(1))
sendEvent( name: "level", value: level )
sendEvent( name: "switch.setLevel", value:level )
parent.setLevel( this, level )
if (( level > 0 ) && ( level <= 100 ))
on()
else
off()
def levelSetting = level as float
def bulbPowerMax = device.latestValue("setBulbPower") as float
def calculatedPower = bulbPowerMax * (levelSetting / 100)
sendEvent(name: "power", value: calculatedPower.round(1))
}
def poll() {
@@ -198,29 +200,29 @@ def installed() {
}
def updated() {
initialize()
refresh()
initialize()
refresh()
}
def initialize() {
if ( !settings.stepsize )
state.stepsize = 10 //set the default stepsize
else
state.stepsize = settings.stepsize
state.stepsize = 10 //set the default stepsize
else
state.stepsize = settings.stepsize
}
/*******************************************************************************
Method :uninstalled(args)
(args) :none
returns:Nothing
ERRORS :No error handling is done
Purpose:This is standard ST method.
Gets called when "remove" is selected in child device "preferences"
tile. It also get's called when "deleteChildDevice(child)" is
called from parent service manager app.
*******************************************************************************/
Method :uninstalled(args)
(args) :none
returns:Nothing
ERRORS :No error handling is done
Purpose:This is standard ST method.
Gets called when "remove" is selected in child device "preferences"
tile. It also get's called when "deleteChildDevice(child)" is
called from parent service manager app.
*******************************************************************************/
def uninstalled() {
log.debug "Executing 'uninstall' in device type"
parent.uninstallFromChildDevice(this)
parent.uninstallFromChildDevice(this)
}

View File

@@ -1,679 +0,0 @@
/**
* TCP Bulbs (Connect)
*
* Copyright 2014 Todd Wackford
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import java.security.MessageDigest;
private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" }
definition(
name: "TCP Bulbs - more reliable",
namespace: "mmacaula",
author: "Mike Macaulay",
description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png",
singleInstance: true
)
preferences {
def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials.
Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs."""
page(name: "selectDevices", title: "Connect Your TCP Lights to SmartThings", install: false, uninstall: true, nextPage: "chooseBulbs") {
section("TCP Connected Remote Credentials") {
input "username", "text", title: "Enter TCP Remote Email/UserName", required: true
input "password", "password", title: "Enter TCP Remote Password", required: true
paragraph msg
}
}
page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize")
}
def installed() {
debugOut "Installed with settings: ${settings}"
unschedule()
unsubscribe()
setupBulbs()
log.debug "schedule every 5 minutes syncronizeDevices)"
runEvery5Minutes(syncronizeDevices)
}
def updated() {
debugOut "Updated with settings: ${settings}"
unschedule()
setupBulbs()
log.debug "schedule update every 5 minutes syncronizeDevices)"
runEvery5Minutes(syncronizeDevices)
}
def uninstalled()
{
unschedule() //in case we have hanging runIn()'s
}
private removeChildDevices(delete)
{
debugOut "deleting ${delete.size()} bulbs"
debugOut "deleting ${delete}"
delete.each {
deleteChildDevice(it.device.deviceNetworkId)
}
}
def uninstallFromChildDevice(childDevice)
{
def errorMsg = "uninstallFromChildDevice was called and "
if (!settings.selectedBulbs) {
debugOut errorMsg += "had empty list passed in"
return
}
def dni = childDevice.device.deviceNetworkId
if ( !dni ) {
debugOut errorMsg += "could not find dni of device"
return
}
def newDeviceList = settings.selectedBulbs - dni
app.updateSetting("selectedBulbs", newDeviceList)
debugOut errorMsg += "completed succesfully"
}
def setupBulbs() {
debugOut "In setupBulbs"
def bulbs = state.devices
def deviceFile = "TCP Bulb"
selectedBulbs.each { did ->
//see if this is a selected bulb and install it if not already
def d = getChildDevice(did)
if(!d) {
def newBulb = bulbs.find { (it.did) == did }
d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true])
/*if ( isRoom(did) ) { //change to the multi light group icon for a room device
d.setIcon("switch", "on", "st.lights.multi-light-bulb-on")
d.setIcon("switch", "off", "st.lights.multi-light-bulb-off")
d.save()
}*/
} else {
debugOut "We already added this device"
}
}
// Delete any that are no longer in settings
def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) }
removeChildDevices(delete)
//we want to ensure syncronization between rooms and bulbs
//syncronizeDevices()
}
def initialize() {
atomicState.token = ""
getToken()
if ( atomicState.token == "error" ) {
return dynamicPage(name:"chooseBulbs", title:"TCP Login Failed!\r\nTap 'Done' to try again", nextPage:"", install:false, uninstall: false) {
section("") {}
}
} else {
"we're good to go"
debugOut "We have Token."
}
//getGatewayData() //we really don't need anything from the gateway
deviceDiscovery()
def options = devicesDiscovered() ?: []
def msg = """Tap 'Done' after you have selected the desired devices."""
return dynamicPage(name:"chooseBulbs", title:"TCP and SmartThings Connected!", nextPage:"", install:true, uninstall: true) {
section("Tap Below to View Device List") {
input "selectedBulbs", "enum", required:false, title:"Select Bulb/Fixture", multiple:true, options:options
paragraph msg
}
}
}
def deviceDiscovery() {
def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
def Params = [
cmd: "RoomGetCarousel",
data: "${data}",
fmt: "json"
]
def cmd = toQueryString(Params)
def rooms = ""
log.debug 'trying to discover devices'
apiPost(cmd) { response ->
rooms = response.data.gip.room
}
debugOut "rooms data = ${rooms}"
def devices = []
def bulbIndex = 1
def lastRoomName = null
def deviceList = []
if ( rooms[1] == null ) {
def roomId = rooms.rid
def roomName = rooms.name
devices = rooms.device
if ( devices[1] != null ) {
debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
//deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"]
devices.each({
debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
})
} else {
debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
}
} else {
rooms.each({
devices = it.device
def roomName = it.name
if ( devices[1] != null ) {
def roomId = it?.rid
debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
//deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"]
devices.each({
debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"]
})
} else {
debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}"
deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"]
}
})
}
devices = ["devices" : deviceList]
state.devices = devices.devices
}
Map devicesDiscovered() {
def devices = state.devices
def map = [:]
if (devices instanceof java.util.Map) {
devices.each {
def value = "${it?.name}"
def key = it?.did
map["${key}"] = value
}
} else { //backwards compatable
devices.each {
def value = "${it?.name}"
def key = it?.did
map["${key}"] = value
}
}
map
}
def getGatewayData() {
debugOut "In getGatewayData"
def data = "<gip><version>1</version><token>${atomicState.token}</token></gip>"
def qParams = [
cmd: "GatewayGetInfo",
data: "${data}",
fmt: "json"
]
def cmd = toQueryString(qParams)
apiPost(cmd) { response ->
debugOut "the gateway reponse is ${response.data.gip.gateway}"
}
}
def getToken(Closure callback) {
atomicState.token = ""
if (password) {
def hashedPassword = generateMD5(password)
def data = "<gip><version>1</version><email>${username}</email><password>${hashedPassword}</password></gip>"
def qParams = [
cmd : "GWRLogin",
data: "${data}",
fmt : "json"
]
def cmd = toQueryString(qParams)
apiPost(cmd) { response ->
def status = response.data.gip.rc
//sendNotificationEvent("Get token status ${status}")
if (status != "200") {//success code = 200
def errorText = response.data.gip.error
debugOut "Error logging into TCP Gateway. Error = ${errorText}"
atomicState.token = "error"
} else {
atomicState.token = response.data.gip.token
if(callback){
callback.call()
}
}
}
} else {
log.warn "Unable to log into TCP Gateway. Error = Password is null"
atomicState.token = "error"
}
}
def apiPost(String data, Integer retryCount = 0, Closure callback) {
debugOut "In apiPost with data: ${data}"
def params = [
uri: apiUrl(),
body: data
]
httpPost(params) {
response ->
def rc = response.data.gip.rc
if ( rc == "200" ) {
debugOut ("Return Code = ${rc} = Command Succeeded.")
callback.call(response)
} else if ( rc.startsWith("4") || rc.startsWith("5") ) {
debugOut "Return Code = ${rc} = Error: Something happened!" //Error code from gateway
sendNotificationEvent("Return Code = ${rc} = Error: Something happened! Retry # ${retryCount}" )
log.debug "Refreshing Token"
if(retryCount > 5){
// give up, send a notification
sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Gave up after ${retryCount} tries")
}
getToken({ ->
def updatedTokenData = data.replaceFirst("<token>[^<]*</token>", '<token>${atomicState.token}</token>')
// try again if we got our token
sendNotificationEvent('re-fetched token, trying again')
apiPost(updatedTokenData, retryCount++, callback)
})
//callback.call(response) //stubbed out so getToken works (we had race issue)
} else {
log.error "Return Code = ${rc} = Error!" //Error code from gateway
sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Check that TCP Gateway is online")
callback.call(response)
}
}
}
//this is not working. TCP power reporting is broken. Leave it here for future fix
def calculateCurrentPowerUse(deviceCapability, usePercentage) {
debugOut "In calculateCurrentPowerUse()"
debugOut "deviceCapability: ${deviceCapability}"
debugOut "usePercentage: ${usePercentage}"
def calcPower = usePercentage * 1000
def reportPower = calcPower.round(1) as String
debugOut "report power = ${reportPower}"
return reportPower
}
def generateSha256(String s) {
MessageDigest digest = MessageDigest.getInstance("SHA-256")
digest.update(s.bytes)
new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0')
}
def generateMD5(String s) {
MessageDigest digest = MessageDigest.getInstance("MD5")
digest.update(s.bytes);
new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
}
String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
def checkDevicesOnline(bulbs) {
debugOut "In checkDevicesOnline()"
def onlineBulbs = []
def thisBulb = []
bulbs.each {
def dni = it?.did
thisBulb = it
def data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
def qParams = [
cmd: "DeviceGetInfo",
data: "${data}",
fmt: "json"
]
def cmd = toQueryString(qParams)
def bulbData = []
apiPost(cmd) { response ->
bulbData = response.data.gip
}
if ( bulbData?.offline == "1" ) {
debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}"
} else {
debugOut "${it?.name} is online with offline value of ${bulbData?.offline}"
onlineBulbs += thisBulb
}
}
return onlineBulbs
}
def syncronizeDevices() {
debugOut "In syncronizeDevices"
def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) }
update.each {
def dni = getChildDevice( it.deviceNetworkId )
debugOut "dni = ${dni}"
if (isRoom(dni)) {
pollRoom(dni)
} else {
poll(dni)
}
}
getToken()
}
boolean isRoom(dni) {
def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))}
}
boolean isBulb(dni) {
def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))}
}
def debugEvent(message, displayEvent) {
def results = [
name: "appdebug",
descriptionText: message,
displayed: displayEvent
]
log.debug "Generating AppDebug Event: ${results}"
sendEvent (results)
}
def debugOut(msg) {
log.debug msg
//sendNotificationEvent(msg) //Uncomment this for troubleshooting only
}
/**************************************************************************
Child Device Call In Methods
**************************************************************************/
def on(childDevice) {
debugOut "On request from child device"
def dni = childDevice.device.deviceNetworkId
def data = ""
def cmd = ""
if ( isRoom(dni) ) { // this is a room, not a bulb
data = "<gip><version>1</version><token>$atomicState.token</token><rid>${dni}</rid><type>power</type><value>1</value></gip>"
cmd = "RoomSendCommand"
} else {
data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>1</value></gip>"
cmd = "DeviceSendCommand"
}
def qParams = [
cmd: cmd,
data: "${data}",
fmt: "json"
]
cmd = toQueryString(qParams)
apiPost(cmd) { response ->
debugOut "ON result: ${response.data}"
}
//we want to ensure syncronization between rooms and bulbs
//runIn(2, "syncronizeDevices")
}
def off(childDevice) {
debugOut "Off request from child device"
def dni = childDevice.device.deviceNetworkId
def data = ""
def cmd = ""
if ( isRoom(dni) ) { // this is a room, not a bulb
data = "<gip><version>1</version><token>$atomicState.token</token><rid>${dni}</rid><type>power</type><value>0</value></gip>"
cmd = "RoomSendCommand"
} else {
data = "<gip><version>1</version><token>$atomicState.token</token><did>${dni}</did><type>power</type><value>0</value></gip>"
cmd = "DeviceSendCommand"
}
def qParams = [
cmd: cmd,
data: "${data}",
fmt: "json"
]
cmd = toQueryString(qParams)
apiPost(cmd) { response ->
debugOut "${response.data}"
}
//we want to ensure syncronization between rooms and bulbs
//runIn(2, "syncronizeDevices")
}
def setLevel(childDevice, value) {
debugOut "setLevel request from child device"
def dni = childDevice.device.deviceNetworkId
def data = ""
def cmd = ""
if ( isRoom(dni) ) { // this is a room, not a bulb
data = "<gip><version>1</version><token>${atomicState.token}</token><rid>${dni}</rid><type>level</type><value>${value}</value></gip>"
cmd = "RoomSendCommand"
} else {
data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did><type>level</type><value>${value}</value></gip>"
cmd = "DeviceSendCommand"
}
def qParams = [
cmd: cmd,
data: "${data}",
fmt: "json"
]
cmd = toQueryString(qParams)
apiPost(cmd) { response ->
debugOut "${response.data}"
}
//we want to ensure syncronization between rooms and bulbs
//runIn(2, "syncronizeDevices")
}
// Really not called from child, but called from poll() if it is a room
def pollRoom(dni) {
debugOut "In pollRoom"
def data = ""
def cmd = ""
def roomDeviceData = []
data = "<gip><version>1</version><token>${atomicState.token}</token><rid>${dni}</rid><fields>name,power,control,status,state</fields></gip>"
cmd = "RoomGetDevices"
def qParams = [
cmd: cmd,
data: "${data}",
fmt: "json"
]
cmd = toQueryString(qParams)
apiPost(cmd) { response ->
roomDeviceData = response.data.gip
}
debugOut "Room Data: ${roomDeviceData}"
def totalPower = 0
def totalLevel = 0
def cnt = 0
def onCnt = 0 //used to tally on/off states
roomDeviceData.device.each({
if ( getChildDevice(it.did) ) {
totalPower += it.other.bulbpower.toInteger()
totalLevel += it.level.toInteger()
onCnt += it.state.toInteger()
cnt += 1
}
})
def avgLevel = totalLevel/cnt
def usingPower = totalPower * (avgLevel / 100) as float
def room = getChildDevice( dni )
//the device is a room but we use same type file
sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs
//if all devices in room are on, room is on
if ( cnt == onCnt ) { // all devices are on
sendEvent( dni, [name: "switch",value:"on"] )
sendEvent( dni, [name: "power",value:usingPower.round(1)] )
} else { //if any device in room is off, room is off
sendEvent( dni, [name: "switch",value:"off"] )
sendEvent( dni, [name: "power",value:0.0] )
}
debugOut "Room Using Power: ${usingPower.round(1)}"
}
def poll(childDevice) {
debugOut "In poll() with ${childDevice}"
def dni = childDevice.device.deviceNetworkId
def bulbData = []
def data = ""
def cmd = ""
if ( isRoom(dni) ) { // this is a room, not a bulb
pollRoom(dni)
return
}
data = "<gip><version>1</version><token>${atomicState.token}</token><did>${dni}</did></gip>"
cmd = "DeviceGetInfo"
def qParams = [
cmd: cmd,
data: "${data}",
fmt: "json"
]
cmd = toQueryString(qParams)
apiPost(cmd) { response ->
bulbData = response.data.gip
debugOut "This Bulbs Data Return = ${bulbData}"
def bulb = getChildDevice( dni )
//set the devices power max setting to do calcs within the device type
if ( bulbData.other.bulbpower )
sendEvent( dni, [name: "setBulbPower",value:"${bulbData.other.bulbpower}"] )
if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" ))
sendEvent( dni, [name: "switch",value:"on"] )
if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" ))
sendEvent( dni, [name: "switch",value:"off"] )
//if ( bulbData.level != bulb?.currentValue("level")) {
// sendEvent( dni, [name: "level",value: "${bulbData.level}"] )
// sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] )
//}
if (( bulbData.state == "1" ) && ( bulbData.other.bulbpower )) {
def levelSetting = bulbData.level as float
def bulbPowerMax = bulbData.other.bulbpower as float
def calculatedPower = bulbPowerMax * (levelSetting / 100)
sendEvent( dni, [name: "power", value: calculatedPower.round(1)] )
}
if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower ))
sendEvent( dni, [name: "power", value: 0.0] )
}
}

View File

@@ -30,7 +30,6 @@ definition(
preferences {
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5)
page(name:"bridgeDiscoveryFailed", title:"Bridge Discovery Failed", content:"bridgeDiscoveryFailed", refreshTimeout:0)
page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
}
@@ -54,21 +53,12 @@ def bridgeDiscovery(params=[:])
def options = bridges ?: []
def numFound = options.size() ?: 0
if (numFound == 0) {
if (state.bridgeRefreshCount == 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
app.updateSetting("selectedHue", "")
} else if (state.bridgeRefreshCount > 100) {
// five minutes have passed, give up
// there seems to be a problem going back from discovey failed page in some instances (compared to pressing next)
// however it is probably a SmartThings settings issue
state.bridges = [:]
app.updateSetting("selectedHue", "")
state.bridgeRefreshCount = 0
return bridgeDiscoveryFailed()
}
}
if (numFound == 0 && state.bridgeRefreshCount > 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
state.bridgeRefreshCount = 0
app.updateSetting("selectedHue", "")
}
ssdpSubscribe()
@@ -89,13 +79,6 @@ def bridgeDiscovery(params=[:])
}
}
def bridgeDiscoveryFailed() {
return dynamicPage(name:"bridgeDiscoveryFailed", title: "Bridge Discovery Failed", nextPage: "bridgeDiscovery") {
section("Failed to discover any Hue Bridges. Please confirm that the Hue Bridge is connected to the same network as your SmartThings Hub, and that it has power.") {
}
}
}
def bridgeLinking()
{
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
@@ -105,15 +88,19 @@ def bridgeLinking()
def nextPage = ""
def title = "Linking with your Hue"
def paragraphText
def hueimage = null
if (selectedHue) {
paragraphText = "Press the button on your Hue Bridge to setup a link. "
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
} else {
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
hueimage = null
}
if (state.username) { //if discovery worked
nextPage = "bulbDiscovery"
title = "Success!"
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
hueimage = null
}
if((linkRefreshcount % 2) == 0 && !state.username) {
@@ -123,6 +110,8 @@ def bridgeLinking()
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
section("") {
paragraph """${paragraphText}"""
if (hueimage != null)
image "${hueimage}"
}
}
}
@@ -146,14 +135,13 @@ def bulbDiscovery() {
if((bulbRefreshCount % 5) == 0) {
discoverHueBulbs()
}
def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
def title = selectedBridge?.value?.name ?: "Find bridges"
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
}
section {
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
}
@@ -360,19 +348,18 @@ def addBulbs() {
def newHueBulb
if (bulbs instanceof java.util.Map) {
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
if (newHueBulb != null) {
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
if (newHueBulb != null) {
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
if (d) {
log.debug "created ${d.displayName} with id $dni"
d.completedSetup = true
d.refresh()
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else {
//backwards compatable
//backwards compatable
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
d?.completedSetup = true
@@ -382,7 +369,7 @@ def addBulbs() {
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
if (bulbs instanceof java.util.Map) {
// Update device type if incorrect
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
upgradeDeviceType(d, newHueBulb?.value?.type)
}
}
@@ -502,21 +489,7 @@ void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
def bridges = getHueBridges()
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
if (bridge) {
// serialNumber from API is in format of 0017882413ad (mac address), however on the actual bridge only last six
// characters are printed on the back so using that to identify bridge
def idNumber = body?.device?.serialNumber?.text()
if (idNumber?.size() >= 6)
idNumber = idNumber[-6..-1].toUpperCase()
// usually in form of bridge name followed by (ip), i.e. defaults to Philips Hue (192.168.1.2)
// replace IP with serial number to make it easier for user to identify
def name = body?.device?.friendlyName?.text()
def index = name?.indexOf('(')
if (index != -1) {
name = name.substring(0,index)
name += " ($idNumber)"
}
bridge.value << [name:name, serialNumber:body?.device?.serialNumber?.text(), verified: true]
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
} else {
log.error "/description.xml returned a bridge that didn't exist"
}