mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-18 05:10:52 +00:00
Compare commits
9 Commits
PROD_2016.
...
MSA-1338-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d787c1b41e | ||
|
|
5d1b033486 | ||
|
|
31f77513da | ||
|
|
c6f706e47a | ||
|
|
cb26f055d7 | ||
|
|
cc2d19e951 | ||
|
|
031a15ec86 | ||
|
|
fc2db2575d | ||
|
|
32f0385e30 |
@@ -13,14 +13,14 @@
|
|||||||
* Documented Header
|
* Documented Header
|
||||||
*
|
*
|
||||||
* Change 2: 2014-03-15
|
* 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.
|
* levels down.
|
||||||
*
|
*
|
||||||
* Change 3: 2014-04-02 (lieberman)
|
* Change 3: 2014-04-02 (lieberman)
|
||||||
* Changed sendEvent() to createEvent() in parse()
|
* Changed sendEvent() to createEvent() in parse()
|
||||||
*
|
*
|
||||||
* Change 4: 2014-04-12 (wackford)
|
* Change 4: 2014-04-12 (wackford)
|
||||||
* Added current power usage tile
|
* Added current power usage tile
|
||||||
*
|
*
|
||||||
* Change 5: 2014-09-14 (wackford)
|
* Change 5: 2014-09-14 (wackford)
|
||||||
* a. Changed createEvent() to sendEvent() in parse() to
|
* a. Changed createEvent() to sendEvent() in parse() to
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
* b. added refresh on udate
|
* b. added refresh on udate
|
||||||
* c. added uninstallFromChildDevice to handle removing from settings
|
* c. added uninstallFromChildDevice to handle removing from settings
|
||||||
* d. Changed to allow bulb to 100%, was possible to get past logic at 99
|
* d. Changed to allow bulb to 100%, was possible to get past logic at 99
|
||||||
*
|
*
|
||||||
* Change 7: 2014-11-09 (wackford)
|
* Change 7: 2014-11-09 (wackford)
|
||||||
* a. Added bulbpower calcs to device. TCP is broken
|
* 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.
|
* b. Changed to set dim level first then on. Much easier on the eys coming from bright.
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
* Code
|
* Code
|
||||||
*****************************************************************
|
*****************************************************************
|
||||||
*/
|
*/
|
||||||
// for the UI
|
// for the UI
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "TCP Bulb", namespace: "wackford", author: "Todd Wackford") {
|
definition (name: "TCP Bulb", namespace: "wackford", author: "Todd Wackford") {
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
@@ -52,28 +52,26 @@ metadata {
|
|||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
attribute "stepsize", "string"
|
attribute "stepsize", "string"
|
||||||
|
|
||||||
command "levelUp"
|
command "levelUp"
|
||||||
command "levelDown"
|
command "levelDown"
|
||||||
command "on"
|
command "on"
|
||||||
command "off"
|
command "off"
|
||||||
command "setBulbPower"
|
command "setBulbPower"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
// TODO: define status and reply messages here
|
// TODO: define status and reply messages here
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5
|
input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
state "on", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#79b821"
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
state "off", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff"
|
||||||
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) {
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
state "level", action:"switch level.setLevel"
|
state "level", action:"switch level.setLevel"
|
||||||
@@ -84,15 +82,15 @@ metadata {
|
|||||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
state "level", label: 'Level ${currentValue}%'
|
state "level", label: 'Level ${currentValue}%'
|
||||||
}
|
}
|
||||||
standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
|
standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
|
||||||
state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright"
|
state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright"
|
||||||
}
|
}
|
||||||
standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
|
standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) {
|
||||||
state "default", action:"levelDown", icon:"st.illuminance.illuminance.light"
|
state "default", action:"levelDown", icon:"st.illuminance.illuminance.light"
|
||||||
}
|
}
|
||||||
valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") {
|
valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
state "power", label: '${currentValue} Watts'
|
state "power", label: '${currentValue} Watts'
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
details(["switch", "lUp", "lDown", "levelSliderControl", "level" , "power", "refresh" ])
|
details(["switch", "lUp", "lDown", "levelSliderControl", "level" , "power", "refresh" ])
|
||||||
@@ -103,10 +101,10 @@ metadata {
|
|||||||
def parse(description) {
|
def parse(description) {
|
||||||
//log.debug "parse() - $description"
|
//log.debug "parse() - $description"
|
||||||
def results = []
|
def results = []
|
||||||
|
|
||||||
if ( description == "updated" )
|
if ( description == "updated" )
|
||||||
return
|
return
|
||||||
|
|
||||||
if (description?.name && description?.value)
|
if (description?.name && description?.value)
|
||||||
{
|
{
|
||||||
results << createEvent(name: "${description?.name}", value: "${description?.value}")
|
results << createEvent(name: "${description?.name}", value: "${description?.value}")
|
||||||
@@ -116,73 +114,73 @@ def parse(description) {
|
|||||||
// handle commands
|
// handle commands
|
||||||
def setBulbPower(value) {
|
def setBulbPower(value) {
|
||||||
state.bulbPower = value
|
state.bulbPower = value
|
||||||
log.debug "In child with bulbPower of ${state.bulbPower}"
|
log.debug "In child with bulbPower of ${state.bulbPower}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def on() {
|
def on() {
|
||||||
log.debug "Executing 'on'"
|
log.debug "Executing 'on'"
|
||||||
sendEvent(name:"switch",value:on)
|
sendEvent(name:"switch",value:on)
|
||||||
parent.on(this)
|
parent.on(this)
|
||||||
|
|
||||||
def levelSetting = device.latestValue("level") as Float ?: 1.0
|
def levelSetting = device.latestValue("level") as Float ?: 1.0
|
||||||
def bulbPowerMax = device.latestValue("setBulbPower") as Float
|
//def bulbPowerMax = device.latestValue("setBulbPower") as Float
|
||||||
def calculatedPower = bulbPowerMax * (levelSetting / 100)
|
//def calculatedPower = bulbPowerMax * (levelSetting / 100)
|
||||||
sendEvent(name: "power", value: calculatedPower.round(1))
|
//sendEvent(name: "power", value: calculatedPower.round(1))
|
||||||
|
|
||||||
if (device.latestValue("level") == null) {
|
if (device.latestValue("level") == null) {
|
||||||
sendEvent( name: "level", value: 1.0 )
|
sendEvent( name: "level", value: 1.0 )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
log.debug "Executing 'off'"
|
log.debug "Executing 'off'"
|
||||||
sendEvent(name:"switch",value:off)
|
sendEvent(name:"switch",value:off)
|
||||||
parent.off(this)
|
parent.off(this)
|
||||||
sendEvent(name: "power", value: 0.0)
|
sendEvent(name: "power", value: 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
def levelUp() {
|
def levelUp() {
|
||||||
def level = device.latestValue("level") as Integer ?: 0
|
def level = device.latestValue("level") as Integer ?: 0
|
||||||
def step = state.stepsize as float
|
def step = state.stepsize as float
|
||||||
|
|
||||||
level+= step
|
level+= step
|
||||||
|
|
||||||
if ( level > 100 )
|
if ( level > 100 )
|
||||||
level = 100
|
level = 100
|
||||||
|
|
||||||
setLevel(level)
|
setLevel(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
def levelDown() {
|
def levelDown() {
|
||||||
def level = device.latestValue("level") as Integer ?: 0
|
def level = device.latestValue("level") as Integer ?: 0
|
||||||
def step = state.stepsize as float
|
def step = state.stepsize as float
|
||||||
|
|
||||||
level-= step
|
level-= step
|
||||||
|
|
||||||
if ( level < 1 )
|
if ( level < 1 )
|
||||||
level = 1
|
level = 1
|
||||||
|
|
||||||
setLevel(level)
|
setLevel(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
log.debug "in setLevel with value: ${value}"
|
log.debug "in setLevel with value: ${value}"
|
||||||
def level = value as Integer
|
def level = value as Integer
|
||||||
|
|
||||||
sendEvent( name: "level", value: level )
|
sendEvent( name: "level", value: level )
|
||||||
sendEvent( name: "switch.setLevel", value:level )
|
sendEvent( name: "switch.setLevel", value:level )
|
||||||
parent.setLevel( this, level )
|
parent.setLevel( this, level )
|
||||||
|
|
||||||
|
|
||||||
|
if (( level > 0 ) && ( level <= 100 ))
|
||||||
if (( level > 0 ) && ( level <= 100 ))
|
on()
|
||||||
on()
|
else
|
||||||
else
|
off()
|
||||||
off()
|
|
||||||
|
//def levelSetting = level as float
|
||||||
def levelSetting = level as float
|
//def bulbPowerMax = device.latestValue("setBulbPower") as float
|
||||||
def bulbPowerMax = device.latestValue("setBulbPower") as float
|
//def calculatedPower = bulbPowerMax * (levelSetting / 100)
|
||||||
def calculatedPower = bulbPowerMax * (levelSetting / 100)
|
//sendEvent(name: "power", value: calculatedPower.round(1))
|
||||||
sendEvent(name: "power", value: calculatedPower.round(1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
@@ -200,29 +198,29 @@ def installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
initialize()
|
initialize()
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
if ( !settings.stepsize )
|
if ( !settings.stepsize )
|
||||||
state.stepsize = 10 //set the default stepsize
|
state.stepsize = 10 //set the default stepsize
|
||||||
else
|
else
|
||||||
state.stepsize = settings.stepsize
|
state.stepsize = settings.stepsize
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
Method :uninstalled(args)
|
Method :uninstalled(args)
|
||||||
(args) :none
|
(args) :none
|
||||||
returns:Nothing
|
returns:Nothing
|
||||||
ERRORS :No error handling is done
|
ERRORS :No error handling is done
|
||||||
|
|
||||||
Purpose:This is standard ST method.
|
Purpose:This is standard ST method.
|
||||||
Gets called when "remove" is selected in child device "preferences"
|
Gets called when "remove" is selected in child device "preferences"
|
||||||
tile. It also get's called when "deleteChildDevice(child)" is
|
tile. It also get's called when "deleteChildDevice(child)" is
|
||||||
called from parent service manager app.
|
called from parent service manager app.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
def uninstalled() {
|
def uninstalled() {
|
||||||
log.debug "Executing 'uninstall' in device type"
|
log.debug "Executing 'uninstall' in device type"
|
||||||
parent.uninstallFromChildDevice(this)
|
parent.uninstallFromChildDevice(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,679 @@
|
|||||||
|
/**
|
||||||
|
* 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] )
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ definition(
|
|||||||
preferences {
|
preferences {
|
||||||
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
|
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
|
||||||
page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", 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:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
|
||||||
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
|
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
|
||||||
}
|
}
|
||||||
@@ -53,12 +54,21 @@ def bridgeDiscovery(params=[:])
|
|||||||
def options = bridges ?: []
|
def options = bridges ?: []
|
||||||
def numFound = options.size() ?: 0
|
def numFound = options.size() ?: 0
|
||||||
|
|
||||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
if (numFound == 0) {
|
||||||
log.trace "Cleaning old bridges memory"
|
if (state.bridgeRefreshCount == 25) {
|
||||||
state.bridges = [:]
|
log.trace "Cleaning old bridges memory"
|
||||||
state.bridgeRefreshCount = 0
|
state.bridges = [:]
|
||||||
app.updateSetting("selectedHue", "")
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ssdpSubscribe()
|
ssdpSubscribe()
|
||||||
|
|
||||||
@@ -79,6 +89,13 @@ 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()
|
def bridgeLinking()
|
||||||
{
|
{
|
||||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||||
@@ -88,19 +105,15 @@ def bridgeLinking()
|
|||||||
def nextPage = ""
|
def nextPage = ""
|
||||||
def title = "Linking with your Hue"
|
def title = "Linking with your Hue"
|
||||||
def paragraphText
|
def paragraphText
|
||||||
def hueimage = null
|
|
||||||
if (selectedHue) {
|
if (selectedHue) {
|
||||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
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 {
|
} else {
|
||||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
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
|
if (state.username) { //if discovery worked
|
||||||
nextPage = "bulbDiscovery"
|
nextPage = "bulbDiscovery"
|
||||||
title = "Success!"
|
title = "Success!"
|
||||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||||
hueimage = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||||
@@ -110,8 +123,6 @@ def bridgeLinking()
|
|||||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||||
section("") {
|
section("") {
|
||||||
paragraph """${paragraphText}"""
|
paragraph """${paragraphText}"""
|
||||||
if (hueimage != null)
|
|
||||||
image "${hueimage}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,13 +146,14 @@ def bulbDiscovery() {
|
|||||||
if((bulbRefreshCount % 5) == 0) {
|
if((bulbRefreshCount % 5) == 0) {
|
||||||
discoverHueBulbs()
|
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) {
|
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.") {
|
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
|
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
|
||||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -348,26 +360,29 @@ def addBulbs() {
|
|||||||
def newHueBulb
|
def newHueBulb
|
||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||||
if (newHueBulb != null) {
|
|
||||||
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) {
|
if (d) {
|
||||||
log.debug "created ${d.displayName} with id $dni"
|
log.debug "created ${d.displayName} with id $dni"
|
||||||
|
d.completedSetup = true
|
||||||
d.refresh()
|
d.refresh()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//backwards compatable
|
//backwards compatable
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||||
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||||
|
d?.completedSetup = true
|
||||||
d?.refresh()
|
d?.refresh()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
// Update device type if incorrect
|
// 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)
|
upgradeDeviceType(d, newHueBulb?.value?.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,6 +414,7 @@ def addBridge() {
|
|||||||
}
|
}
|
||||||
if (newbridge) {
|
if (newbridge) {
|
||||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||||
|
d?.completedSetup = true
|
||||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||||
@@ -486,7 +502,21 @@ void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
|
|||||||
def bridges = getHueBridges()
|
def bridges = getHueBridges()
|
||||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||||
if (bridge) {
|
if (bridge) {
|
||||||
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
// 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]
|
||||||
} else {
|
} else {
|
||||||
log.error "/description.xml returned a bridge that didn't exist"
|
log.error "/description.xml returned a bridge that didn't exist"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ mappings {
|
|||||||
path("/locks") {
|
path("/locks") {
|
||||||
action: [
|
action: [
|
||||||
GET: "listLocks",
|
GET: "listLocks",
|
||||||
PUT: "updateLock",
|
PUT: "updateLocks",
|
||||||
POST: "updateLock"
|
POST: "updateLocks"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
path("/locks/:id") {
|
path("/locks/:id") {
|
||||||
@@ -442,31 +442,87 @@ def executePhrase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateAll(devices) {
|
private void updateAll(devices) {
|
||||||
|
def type = params.param1
|
||||||
def command = request.JSON?.command
|
def command = request.JSON?.command
|
||||||
if (command)
|
if (!devices) {
|
||||||
{
|
httpError(404, "Devices not found")
|
||||||
command = command.toLowerCase()
|
}
|
||||||
devices."$command"()
|
if (command){
|
||||||
|
devices.each { device ->
|
||||||
|
executeCommand(device, type, command)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(devices) {
|
private void update(devices) {
|
||||||
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
|
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
|
||||||
//def command = request.JSON?.command
|
def type = params.param1
|
||||||
def command = params.command
|
def command = request.JSON?.command
|
||||||
if (command)
|
def device = devices?.find { it.id == params.id }
|
||||||
{
|
|
||||||
command = command.toLowerCase()
|
if (!device) {
|
||||||
def device = devices.find { it.id == params.id }
|
|
||||||
if (!device)
|
|
||||||
{
|
|
||||||
httpError(404, "Device not found")
|
httpError(404, "Device not found")
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
device."$command"()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command) {
|
||||||
|
executeCommand(device, type, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validating the command passed by the user based on capability.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
def validateCommand(device, deviceType, command) {
|
||||||
|
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
|
||||||
|
def currentDeviceCapability = getCapabilityName(deviceType)
|
||||||
|
if (capabilityCommands[currentDeviceCapability]) {
|
||||||
|
return command in capabilityCommands[currentDeviceCapability] ? true : false
|
||||||
|
} else {
|
||||||
|
// Handling other device types here, which don't accept commands
|
||||||
|
httpError(400, "Bad request.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Need to get the attribute name to do the lookup. Only
|
||||||
|
* doing it for the device types which accept commands
|
||||||
|
* @return attribute name of the device type
|
||||||
|
*/
|
||||||
|
def getCapabilityName(type) {
|
||||||
|
switch(type) {
|
||||||
|
case "switches":
|
||||||
|
return "Switch"
|
||||||
|
case "locks":
|
||||||
|
return "Lock"
|
||||||
|
default:
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructing the map over here of
|
||||||
|
* supported commands by device capability
|
||||||
|
* @return a map of device capability -> supported commands
|
||||||
|
*/
|
||||||
|
def getDeviceCapabilityCommands(deviceCapabilities) {
|
||||||
|
def map = [:]
|
||||||
|
deviceCapabilities.collect {
|
||||||
|
map[it.name] = it.commands.collect{ it.name.toString() }
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and executes the command
|
||||||
|
* on the device or devices
|
||||||
|
*/
|
||||||
|
def executeCommand(device, type, command) {
|
||||||
|
if (validateCommand(device, type, command)) {
|
||||||
|
device."$command"()
|
||||||
|
} else {
|
||||||
|
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private show(devices, type) {
|
private show(devices, type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user