mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
47 Commits
MSA-1282-1
...
MSA-1338-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d787c1b41e | ||
|
|
5d1b033486 | ||
|
|
31f77513da | ||
|
|
c6f706e47a | ||
|
|
532afd7336 | ||
|
|
ac7f1a0c66 | ||
|
|
cb26f055d7 | ||
|
|
cc2d19e951 | ||
|
|
031a15ec86 | ||
|
|
fc2db2575d | ||
|
|
fd549631e6 | ||
|
|
417c246d61 | ||
|
|
0f1781c02e | ||
|
|
4ce6ee0890 | ||
|
|
f8050a5cd5 | ||
|
|
d44dac448b | ||
|
|
67c20abcba | ||
|
|
d56e132896 | ||
|
|
009ec2539d | ||
|
|
ff0860cbe1 | ||
|
|
ecfb99974b | ||
|
|
aa3a18f421 | ||
|
|
c2f18a91be | ||
|
|
fc6b14b85e | ||
|
|
0c1208928f | ||
|
|
02d9963fab | ||
|
|
f131fb71cf | ||
|
|
9c27ed6cb7 | ||
|
|
35edaa19c7 | ||
|
|
dc09201866 | ||
|
|
6afcbf8f70 | ||
|
|
2894d52efa | ||
|
|
a21f9f177c | ||
|
|
02f968b8cb | ||
|
|
32f0385e30 | ||
|
|
b105d9d80e | ||
|
|
9bfad5d6a4 | ||
|
|
85a335d365 | ||
|
|
40e6778e31 | ||
|
|
bd0ccd0c21 | ||
|
|
26a0f6f939 | ||
|
|
56eef9cf22 | ||
|
|
566425c531 | ||
|
|
ab2ba8104d | ||
|
|
973c16f088 | ||
|
|
b05d956d95 | ||
|
|
d17cadc4c7 |
@@ -94,11 +94,11 @@ def parse(String description) {
|
||||
def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])
|
||||
if (cmd) {
|
||||
result = createEvent(zwaveEvent(cmd))
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
storeGraphData(result.name, result.value)
|
||||
} else {
|
||||
log.debug "zwave.parse returned null command. Cannot create event"
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
|
||||
storeGraphData(result.name, result.value)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ metadata {
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
|
||||
fingerprint mfr:"010F", prod:"0C02", model:"1002"
|
||||
}
|
||||
simulator {
|
||||
//battery
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Hue White Ambiance Bulb
|
||||
*
|
||||
* Philips Hue Type "Color Temperature Light"
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Hue White Ambiance Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"rich-control", 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", range:"(0..100)"
|
||||
}
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["rich-control"])
|
||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
def results = []
|
||||
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
log.debug "Hue Ambience Bulb stringToMap - ${map}"
|
||||
map = stringToMap(description)
|
||||
}
|
||||
|
||||
if (map?.name && map?.value) {
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// handle commands
|
||||
void on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
void off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
void setLevel(percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
if (percent != null && percent >= 0 && percent <= 100) {
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "$percent is not 0-100"
|
||||
}
|
||||
}
|
||||
|
||||
void setColorTemperature(value) {
|
||||
if (value) {
|
||||
log.trace "setColorTemperature: ${value}k"
|
||||
parent.setColorTemperature(this, value)
|
||||
sendEvent(name: "colorTemperature", value: value)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "Invalid color temperature"
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,30 @@
|
||||
# Smartsense Motion Sensor
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [Samsung SmartThings Motion Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-motion-sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health]($health)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Motion Sensor** - can detect motion
|
||||
* **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 motion sensor that has 120min check-in interval
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
42
devicetypes/smartthings/tile-ux/README.md
Normal file
42
devicetypes/smartthings/tile-ux/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Device Tiles Examples and Reference
|
||||
|
||||
This package contains examples of Device tiles, organized by tile type.
|
||||
|
||||
## Purpose
|
||||
|
||||
Each Device Handler shows example usages of a specific tile, and is meant to represent the variety of permutations that a tile can be configured.
|
||||
|
||||
The various tiles can be used by QA to test tiles on all supported mobile devices, and by developers as a reference implementation.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Self-publish the Device Handlers in this package.
|
||||
2. Self-publish the Device Tile Controller SmartApp. The SmartApp can be found [here](https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy).
|
||||
3. Install the SmartApp from the Marketplace, under "My Apps".
|
||||
4. Select the simulated devices you want to install and press "Done".
|
||||
|
||||
The simulated devices can then be found in the "Things" view of "My Home" in the mobile app.
|
||||
You may wish to create a new room for these simulated devices for easy access.
|
||||
|
||||
## Usage
|
||||
|
||||
Each simulated device can be interacted with like other devices.
|
||||
You can use the mobile app to interact with the tiles to see how they look and behave.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you get an error when installing the simulated devices using the controller SmartApp, ensure that you have published all the Device Handlers for yourself.
|
||||
Also check live logging to see if there is a specific tile that is causing installation issues.
|
||||
|
||||
## FAQ
|
||||
|
||||
*Question: A tile isn't behaving as expected. What should I do?*
|
||||
|
||||
QA should create a JIRA ticket for any issues or inconsistencies of tiles across devices.
|
||||
|
||||
Developers may file a support ticket, and reference the specific tile and issue observed.
|
||||
|
||||
*Question: I'd like to contribute an example tile usage that would be helpful for testing and reference purposes. Can I do that?*
|
||||
|
||||
We recommend that you open an issue in the SmartThingsPublic repository describing the example tile and usage.
|
||||
That way we can discuss with you the proposed change, and then if appropriate you can create a PR associated to the issue.
|
||||
@@ -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,28 +52,26 @@ 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:"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"
|
||||
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"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
@@ -84,15 +82,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" ])
|
||||
@@ -103,10 +101,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}")
|
||||
@@ -116,73 +114,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 )
|
||||
|
||||
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))
|
||||
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() {
|
||||
@@ -200,29 +198,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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Gidjit Hub
|
||||
*
|
||||
* Copyright 2016 Matthew Page
|
||||
*
|
||||
* 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: "Gidjit Hub",
|
||||
namespace: "com.gidjit.smartthings.hub",
|
||||
author: "Matthew Page",
|
||||
description: "Act as an endpoint so user's of Gidjit can quickly access and control their devices and execute routines. Users can do this quickly as Gidjit filters these actions based on their environment",
|
||||
category: "Convenience",
|
||||
iconUrl: "http://www.gidjit.com/appicon.png",
|
||||
iconX2Url: "http://www.gidjit.com/appicon@2x.png",
|
||||
iconX3Url: "http://www.gidjit.com/appicon@3x.png",
|
||||
oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"])
|
||||
|
||||
|
||||
|
||||
preferences {
|
||||
section ("Allow Gidjit to have access, there by allowing you to quickly control and monitor the following devices") {
|
||||
input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false
|
||||
input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade
|
||||
//input "bulbs", "capability.colorControl", title: "Control your lights", multiple: true, required: false //windowShade
|
||||
|
||||
}
|
||||
}
|
||||
mappings {
|
||||
path("/structureinfo") {
|
||||
action: [
|
||||
GET: "structureInfo"
|
||||
]
|
||||
}
|
||||
path("/helloactions") {
|
||||
action: [
|
||||
GET: "helloActions"
|
||||
]
|
||||
}
|
||||
path("/helloactions/:label") {
|
||||
action: [
|
||||
PUT: "executeAction"
|
||||
]
|
||||
}
|
||||
|
||||
path("/switch/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitch"
|
||||
]
|
||||
}
|
||||
|
||||
path("/thermostat/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateThermostat"
|
||||
]
|
||||
}
|
||||
|
||||
path("/windowshade/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateWindowShade"
|
||||
]
|
||||
}
|
||||
path("/acquiredata/:id") {
|
||||
action: [
|
||||
GET: "acquiredata"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
def helloActions() {
|
||||
def actions = location.helloHome?.getPhrases()*.label
|
||||
if(!actions) {
|
||||
return []
|
||||
}
|
||||
return actions
|
||||
}
|
||||
def executeAction() {
|
||||
def actions = location.helloHome?.getPhrases()*.label
|
||||
def a = actions?.find() { it == params.label }
|
||||
if (!a) {
|
||||
httpError(400, "invalid label $params.label")
|
||||
return
|
||||
}
|
||||
location.helloHome?.execute(params.label)
|
||||
}
|
||||
/* this is the primary function called to query at the structure and its devices */
|
||||
def structureInfo() { //list all devices
|
||||
def list = [:]
|
||||
def currId = location.id
|
||||
list[currId] = [:]
|
||||
list[currId].name = location.name
|
||||
list[currId].id = location.id
|
||||
list[currId].temperatureScale = location.temperatureScale
|
||||
list[currId].devices = [:]
|
||||
|
||||
def setValues = {
|
||||
if (params.brief) {
|
||||
return [id: it.id, name: it.displayName]
|
||||
}
|
||||
def newList = [id: it.id, name: it.displayName, suppCapab: it.capabilities.collect {
|
||||
"$it.name"
|
||||
}, suppAttributes: it.supportedAttributes.collect {
|
||||
"$it.name"
|
||||
}, suppCommands: it.supportedCommands.collect {
|
||||
"$it.name"
|
||||
}]
|
||||
|
||||
return newList
|
||||
}
|
||||
switches?.each {
|
||||
list[currId].devices[it.id] = setValues(it)
|
||||
}
|
||||
thermostats?.each {
|
||||
list[currId].devices[it.id] = setValues(it)
|
||||
}
|
||||
windowShades?.each {
|
||||
list[currId].devices[it.id] = setValues(it)
|
||||
}
|
||||
|
||||
return list
|
||||
|
||||
}
|
||||
/* This function returns all of the current values of the specified Devices attributes */
|
||||
def acquiredata() {
|
||||
def resp = [:]
|
||||
if (!params.id) {
|
||||
httpError(400, "invalid id $params.id")
|
||||
return
|
||||
}
|
||||
def dev = switches.find() { it.id == params.id } ?: windowShades.find() { it.id == params.id } ?:
|
||||
thermostats.find() { it.id == params.id }
|
||||
|
||||
if (!dev) {
|
||||
httpError(400, "invalid id $params.id")
|
||||
return
|
||||
}
|
||||
def att = dev.supportedAttributes
|
||||
att.each {
|
||||
resp[it.name] = dev.currentValue("$it.name")
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
void updateSwitch() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
def sw = switches.find() { it.id == params.id }
|
||||
if (!sw) {
|
||||
httpError(400, "invalid id $params.id")
|
||||
return
|
||||
}
|
||||
switch(command) {
|
||||
case "on":
|
||||
if ( sw.currentSwitch != "on" ) {
|
||||
sw.on()
|
||||
}
|
||||
break
|
||||
case "off":
|
||||
if ( sw.currentSwitch != "off" ) {
|
||||
sw.off()
|
||||
}
|
||||
break
|
||||
default:
|
||||
httpError(400, "$command is not a valid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void updateThermostat() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
def therm = thermostats.find() { it.id == params.id }
|
||||
if (!therm || !command) {
|
||||
httpError(400, "invalid id $params.id")
|
||||
return
|
||||
}
|
||||
def passComm = [
|
||||
"off",
|
||||
"heat",
|
||||
"emergencyHeat",
|
||||
"cool",
|
||||
"fanOn",
|
||||
"fanAuto",
|
||||
"fanCirculate",
|
||||
"auto"
|
||||
|
||||
]
|
||||
def passNumParamComm = [
|
||||
"setHeatingSetpoint",
|
||||
"setCoolingSetpoint",
|
||||
]
|
||||
def passStringParamComm = [
|
||||
"setThermostatMode",
|
||||
"setThermostatFanMode",
|
||||
]
|
||||
if (command in passComm) {
|
||||
therm."$command"()
|
||||
} else if (command in passNumParamComm && params.p1 && params.p1.isFloat()) {
|
||||
therm."$command"(Float.parseFloat(params.p1))
|
||||
} else if (command in passStringParamComm && params.p1) {
|
||||
therm."$command"(params.p1)
|
||||
} else {
|
||||
httpError(400, "$command is not a valid command")
|
||||
}
|
||||
}
|
||||
|
||||
void updateWindowShade() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
def ws = windowShades.find() { it.id == params.id }
|
||||
if (!ws || !command) {
|
||||
httpError(400, "invalid id $params.id")
|
||||
return
|
||||
}
|
||||
def passComm = [
|
||||
"open",
|
||||
"close",
|
||||
"presetPosition",
|
||||
]
|
||||
if (command in passComm) {
|
||||
ws."$command"()
|
||||
} else {
|
||||
httpError(400, "$command is not a valid command")
|
||||
}
|
||||
}
|
||||
// TODO: implement event handlers
|
||||
@@ -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] )
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -21,6 +21,12 @@ preferences()
|
||||
section("Allow Simple Control to Monitor and Control These Things...")
|
||||
{
|
||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
|
||||
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
|
||||
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
|
||||
}
|
||||
|
||||
page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
|
||||
@@ -31,12 +37,17 @@ preferences()
|
||||
|
||||
mappings {
|
||||
path("/devices") {
|
||||
action: [
|
||||
GET: "getDevices"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/devices") {
|
||||
action: [
|
||||
GET: "getDevices",
|
||||
POST: "handleDevicesWithIDs"
|
||||
]
|
||||
}
|
||||
path("/device/:id") {
|
||||
}
|
||||
path("/device/:deviceType/:id") {
|
||||
action: [
|
||||
GET: "getDevice",
|
||||
POST: "updateDevice"
|
||||
@@ -93,33 +104,40 @@ def handleDevicesWithIDs()
|
||||
//log.debug("ids: ${ids}")
|
||||
def command = data?.command
|
||||
def arguments = data?.arguments
|
||||
def type = params?.deviceType
|
||||
//log.debug("device type: ${type}")
|
||||
if (command)
|
||||
{
|
||||
def success = false
|
||||
def statusCode = 404
|
||||
//log.debug("command ${command}, arguments ${arguments}")
|
||||
for (devId in ids)
|
||||
{
|
||||
def device = allDevices.find { it.id == devId }
|
||||
if (device) {
|
||||
if (arguments) {
|
||||
//log.debug("device: ${device}")
|
||||
// Check if we have a device that responds to the specified command
|
||||
if (validateCommand(device, type, command)) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
success = true
|
||||
}
|
||||
else {
|
||||
device."$command"()
|
||||
}
|
||||
statusCode = 200
|
||||
} else {
|
||||
//log.debug("device not found ${devId}")
|
||||
statusCode = 403
|
||||
}
|
||||
}
|
||||
|
||||
if (success)
|
||||
def responseData = "{}"
|
||||
switch (statusCode)
|
||||
{
|
||||
render status: 200, data: "{}"
|
||||
}
|
||||
else
|
||||
{
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
case 403:
|
||||
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
|
||||
break
|
||||
case 404:
|
||||
responseData = '{"msg": "Device not found"}'
|
||||
break
|
||||
}
|
||||
render status: statusCode, data: responseData
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -164,25 +182,101 @@ def updateDevice()
|
||||
def data = request.JSON
|
||||
def command = data?.command
|
||||
def arguments = data?.arguments
|
||||
def type = params?.deviceType
|
||||
//log.debug("device type: ${type}")
|
||||
|
||||
//log.debug("updateDevice, params: ${params}, request: ${data}")
|
||||
if (!command) {
|
||||
render status: 400, data: '{"msg": "command is required"}'
|
||||
} else {
|
||||
def statusCode = 404
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (device) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
// Check if we have a device that responds to the specified command
|
||||
if (validateCommand(device, type, command)) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
}
|
||||
else {
|
||||
device."$command"()
|
||||
}
|
||||
statusCode = 200
|
||||
} else {
|
||||
device."$command"()
|
||||
statusCode = 403
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
|
||||
def responseData = "{}"
|
||||
switch (statusCode)
|
||||
{
|
||||
case 403:
|
||||
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
|
||||
break
|
||||
case 404:
|
||||
responseData = '{"msg": "Device not found"}'
|
||||
break
|
||||
}
|
||||
render status: statusCode, data: responseData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validating the command passed by the user based on capability.
|
||||
* @return boolean
|
||||
*/
|
||||
def validateCommand(device, deviceType, command) {
|
||||
//log.debug("validateCommand ${command}")
|
||||
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
|
||||
//log.debug("capabilityCommands: ${capabilityCommands}")
|
||||
def currentDeviceCapability = getCapabilityName(deviceType)
|
||||
//log.debug("currentDeviceCapability: ${currentDeviceCapability}")
|
||||
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"
|
||||
case "thermostats":
|
||||
return "Thermostat"
|
||||
case "doorControls":
|
||||
return "Door Control"
|
||||
case "colorControls":
|
||||
return "Color Control"
|
||||
case "musicPlayers":
|
||||
return "Music Player"
|
||||
case "switchLevels":
|
||||
return "Switch Level"
|
||||
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
|
||||
}
|
||||
|
||||
def listSubscriptions()
|
||||
{
|
||||
//log.debug "listSubscriptions()"
|
||||
@@ -361,7 +455,13 @@ def agentDiscovery(params=[:])
|
||||
}
|
||||
section("Allow Simple Control to Monitor and 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
|
||||
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
|
||||
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
|
||||
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,5 +772,3 @@ def List getRealHubFirmwareVersions()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ 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)
|
||||
}
|
||||
@@ -53,12 +54,21 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||
@@ -88,19 +105,15 @@ 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) {
|
||||
@@ -110,8 +123,6 @@ def bridgeLinking()
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("") {
|
||||
paragraph """${paragraphText}"""
|
||||
if (hueimage != null)
|
||||
image "${hueimage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,13 +146,14 @@ 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]
|
||||
|
||||
}
|
||||
@@ -323,6 +335,8 @@ private getDeviceType(hueType) {
|
||||
return "Hue Bulb"
|
||||
else if (hueType?.equalsIgnoreCase("Color Light"))
|
||||
return "Hue Bloom"
|
||||
else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
|
||||
return "Hue White Ambiance Bulb"
|
||||
else
|
||||
return null
|
||||
}
|
||||
@@ -346,26 +360,29 @@ 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
|
||||
d?.refresh()
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -397,6 +414,7 @@ def addBridge() {
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
d?.completedSetup = true
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
@@ -484,7 +502,21 @@ void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
def bridges = getHueBridges()
|
||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
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 {
|
||||
log.error "/description.xml returned a bridge that didn't exist"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
|
||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||
} else {
|
||||
log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch"
|
||||
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
|
||||
def tempScale = location.temperatureScale ?: "F"
|
||||
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
||||
switch1?.on()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
|
||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||
} else {
|
||||
log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
|
||||
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
|
||||
def tempScale = location.temperatureScale ?: "F"
|
||||
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
||||
switch1?.on()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ def initialize() {
|
||||
state.aux = 0
|
||||
if (selectedhubs || selectedactivities) {
|
||||
addDevice()
|
||||
runEvery5Minutes("discovery")
|
||||
runEvery5Minutes("poll")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,9 +394,9 @@ def discovery() {
|
||||
}
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||
state.resethub = true
|
||||
state.resethub = true
|
||||
} catch (e) {
|
||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||
log.info "Logitech Harmony - Error: $e"
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -474,7 +474,7 @@ def activity(dni,mode) {
|
||||
def poll() {
|
||||
// GET THE LIST OF ACTIVITIES
|
||||
if (state.HarmonyAccessToken) {
|
||||
getActivityList()
|
||||
getActivityList()
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||
try {
|
||||
@@ -520,14 +520,17 @@ def poll() {
|
||||
return "Poll completed $map - $state.hubs"
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -107,8 +107,8 @@ mappings {
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks",
|
||||
PUT: "updateLock",
|
||||
POST: "updateLock"
|
||||
PUT: "updateLocks",
|
||||
POST: "updateLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
@@ -442,31 +442,87 @@ def executePhrase() {
|
||||
}
|
||||
|
||||
private void updateAll(devices) {
|
||||
def type = params.param1
|
||||
def command = request.JSON?.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
devices."$command"()
|
||||
if (!devices) {
|
||||
httpError(404, "Devices not found")
|
||||
}
|
||||
if (command){
|
||||
devices.each { device ->
|
||||
executeCommand(device, type, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void update(devices) {
|
||||
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
|
||||
//def command = request.JSON?.command
|
||||
def command = params.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
def device = devices.find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
def type = params.param1
|
||||
def command = request.JSON?.command
|
||||
def device = devices?.find { it.id == params.id }
|
||||
|
||||
if (!device) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user