mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-28 05:13:07 +00:00
Compare commits
99 Commits
MSA-1255-1
...
MSA-1354-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e32676fe5 | ||
|
|
ab61db3699 | ||
|
|
11e047e31d | ||
|
|
8cc87f3858 | ||
|
|
e818695947 | ||
|
|
bbba20288e | ||
|
|
ff9dd3f6e2 | ||
|
|
7adff88d0f | ||
|
|
ad1f1b2dc9 | ||
|
|
c875547942 | ||
|
|
1676a9c381 | ||
|
|
49d293e749 | ||
|
|
5d1b033486 | ||
|
|
31f77513da | ||
|
|
98d7829d1a | ||
|
|
c6f706e47a | ||
|
|
532afd7336 | ||
|
|
ac7f1a0c66 | ||
|
|
cb26f055d7 | ||
|
|
34107f935e | ||
|
|
cc2d19e951 | ||
|
|
031a15ec86 | ||
|
|
fc2db2575d | ||
|
|
fd549631e6 | ||
|
|
417c246d61 | ||
|
|
038d770691 | ||
|
|
ce28ec2039 | ||
|
|
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 | ||
|
|
feba6643a6 | ||
|
|
49c893771d | ||
|
|
d73f4c2ded | ||
|
|
13a056ec8d | ||
|
|
bd0ccd0c21 | ||
|
|
d34508c19d | ||
|
|
a8e118fe83 | ||
|
|
072cc066b6 | ||
|
|
17562c96ae | ||
|
|
f55452d1c6 | ||
|
|
14b5fd41b8 | ||
|
|
0f4d7bd520 | ||
|
|
a95fbf2612 | ||
|
|
0f60ca61cb | ||
|
|
293a73136e | ||
|
|
3b56fb4a2f | ||
|
|
26a0f6f939 | ||
|
|
8de3276ce6 | ||
|
|
f77c5810c6 | ||
|
|
9fc5f14dd7 | ||
|
|
55d7a4a263 | ||
|
|
cca1eccce6 | ||
|
|
56eef9cf22 | ||
|
|
b33d621696 | ||
|
|
45b78eff8d | ||
|
|
a133406b6e | ||
|
|
bd62962ee1 | ||
|
|
f1e54c8a5c | ||
|
|
a5da182bf4 | ||
|
|
a4a48fddd2 | ||
|
|
2bd18859b9 | ||
|
|
32f8d2d944 | ||
|
|
e1de599668 | ||
|
|
0e01cbed06 | ||
|
|
566425c531 | ||
|
|
bf476940e9 | ||
|
|
8821c68e9c | ||
|
|
27c05f4e5b | ||
|
|
69ae9973da | ||
|
|
e7448e7908 | ||
|
|
973c16f088 | ||
|
|
e278a3b57d | ||
|
|
b05d956d95 | ||
|
|
d17cadc4c7 | ||
|
|
91c01dc643 |
126
devicetypes/com-lametric/lametric.src/lametric.groovy
Normal file
126
devicetypes/com-lametric/lametric.src/lametric.groovy
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* LaMetric
|
||||||
|
*
|
||||||
|
* Copyright 2016 Smart Atoms Ltd.
|
||||||
|
* Author: Mykola Kirichuk
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "LaMetric", namespace: "com.lametric", author: "Mykola Kirichuk") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Notification"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
attribute "currentIP", "string"
|
||||||
|
attribute "serialNumber", "string"
|
||||||
|
attribute "volume", "string"
|
||||||
|
attribute "mode", "enum", ["offline","online"]
|
||||||
|
|
||||||
|
command "setOffline"
|
||||||
|
command "setOnline"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles (scale: 2){
|
||||||
|
// TODO: define your main and details tiles here
|
||||||
|
tiles(scale: 2) {
|
||||||
|
multiAttributeTile(name:"rich-control"){
|
||||||
|
tileAttribute ("mode", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "online", label: "LaMetric", action: "", icon: "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3C200"
|
||||||
|
attributeState "offline", label: "LaMetric", action: "", icon: "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3F3F3"
|
||||||
|
}
|
||||||
|
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "default", label:'SN: ${currentValue}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "default", label:'SN: ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("networkAddress", "device.currentIP", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["rich-control"])
|
||||||
|
details(["rich-control","networkAddress"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
if (description)
|
||||||
|
{
|
||||||
|
unschedule("setOffline")
|
||||||
|
}
|
||||||
|
// TODO: handle 'battery' attribute
|
||||||
|
// TODO: handle 'button' attribute
|
||||||
|
// TODO: handle 'status' attribute
|
||||||
|
// TODO: handle 'level' attribute
|
||||||
|
// TODO: handle 'level' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def setOnline()
|
||||||
|
{
|
||||||
|
log.debug("set online");
|
||||||
|
sendEvent(name:"mode", value:"online")
|
||||||
|
unschedule("setOffline")
|
||||||
|
}
|
||||||
|
def setOffline(){
|
||||||
|
log.debug("set offline");
|
||||||
|
sendEvent(name:"mode", value:"offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level) {
|
||||||
|
log.debug "Executing 'setLevel' ${level}"
|
||||||
|
// TODO: handle 'setLevel' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def deviceNotification(notif) {
|
||||||
|
log.debug "Executing 'deviceNotification' ${notif}"
|
||||||
|
// TODO: handle 'deviceNotification' command
|
||||||
|
def result = parent.sendNotificationMessageToDevice(device.deviceNetworkId, notif);
|
||||||
|
log.debug ("result ${result}");
|
||||||
|
log.debug parent;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
// TODO: handle 'poll' command
|
||||||
|
log.debug "Executing 'poll'"
|
||||||
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
|
{
|
||||||
|
runIn(30, setOffline)
|
||||||
|
}
|
||||||
|
parent.poll(device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
// log.debug "${device?.currentIP}"
|
||||||
|
log.debug "${device?.currentValue("currentIP")}"
|
||||||
|
log.debug "${device?.currentValue("serialNumber")}"
|
||||||
|
log.debug "${device?.currentValue("volume")}"
|
||||||
|
// poll()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*def setLevel() {
|
||||||
|
log.debug "Executing 'setLevel'"
|
||||||
|
// TODO: handle 'setLevel' command
|
||||||
|
}*/
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//@Deprecated: Moved to ZLL Dimmer Bulb
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
//fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
|
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
|
|||||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
updateDataValue("MSR", msr)
|
updateDataValue("MSR", msr)
|
||||||
|
updateDataValue("manufacturer", cmd.manufacturerName)
|
||||||
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
|
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ metadata {
|
|||||||
attribute "tamper", "enum", ["detected", "clear"]
|
attribute "tamper", "enum", ["detected", "clear"]
|
||||||
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
|
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 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 {
|
simulator {
|
||||||
//battery
|
//battery
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ metadata {
|
|||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "level", label: 'Level ${currentValue}%'
|
|
||||||
}
|
|
||||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||||
attributeState "color", action:"setAdjustedColor"
|
attributeState "color", action:"setAdjustedColor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ metadata {
|
|||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "level", label: 'Level ${currentValue}%'
|
|
||||||
}
|
|
||||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||||
attributeState "color", action:"setAdjustedColor"
|
attributeState "color", action:"setAdjustedColor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ metadata {
|
|||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "level", label: 'Level ${currentValue}%'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,11 +19,6 @@ metadata {
|
|||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
attribute "colorName", "string"
|
attribute "colorName", "string"
|
||||||
|
|
||||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
|
||||||
attribute "heartbeat", "string"
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
@@ -75,9 +70,6 @@ metadata {
|
|||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
//log.trace description
|
//log.trace description
|
||||||
|
|
||||||
// save heartbeat (i.e. last time we got a message from device)
|
|
||||||
state.heartbeat = Calendar.getInstance().getTimeInMillis()
|
|
||||||
|
|
||||||
if (description?.startsWith("catchall:")) {
|
if (description?.startsWith("catchall:")) {
|
||||||
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||||
{
|
{
|
||||||
@@ -132,7 +124,6 @@ def off() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
sendEvent(name: "heartbeat", value: "alive", displayed:false)
|
|
||||||
[
|
[
|
||||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") {
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Power Meter"
|
capability "Power Meter"
|
||||||
@@ -25,9 +25,6 @@ metadata {
|
|||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Health Check"
|
capability "Health Check"
|
||||||
|
|
||||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
|
||||||
attribute "heartbeat", "string"
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet"
|
||||||
@@ -81,9 +78,6 @@ metadata {
|
|||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
|
||||||
// save heartbeat (i.e. last time we got a message from device)
|
|
||||||
state.heartbeat = Calendar.getInstance().getTimeInMillis()
|
|
||||||
|
|
||||||
def finalResult = zigbee.getKnownDescription(description)
|
def finalResult = zigbee.getKnownDescription(description)
|
||||||
|
|
||||||
//TODO: Remove this after getKnownDescription can parse it automatically
|
//TODO: Remove this after getKnownDescription can parse it automatically
|
||||||
@@ -124,7 +118,6 @@ def on() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
sendEvent(name: "heartbeat", value: "alive", displayed:false)
|
|
||||||
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
|
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
capability "Motion Sensor"
|
capability "Motion Sensor"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
|
|
||||||
capability "Three Axis"
|
capability "Three Axis"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
@@ -403,39 +403,21 @@ def refresh() {
|
|||||||
|
|
||||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||||
refreshCmds = refreshCmds + [
|
/* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276)
|
||||||
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
||||||
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
Separating these out in a separate if-else because I do not want to touch Centralite part
|
||||||
Separating these out in a separate if-else because I do not want to touch Centralite part
|
as of now.
|
||||||
as of now.
|
*/
|
||||||
*/
|
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode])
|
||||||
|
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode])
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
|
||||||
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
refreshCmds = refreshCmds + [
|
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode])
|
||||||
/* sensitivity - default value (8) */
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Common refresh commands
|
//Common refresh commands
|
||||||
refreshCmds = refreshCmds + [
|
refreshCmds += zigbee.readAttribute(0x0402, 0x0000) +
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
zigbee.readAttribute(0x0001, 0x0020) +
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])
|
||||||
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global read 0xFC02 0x0010",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
|
||||||
]
|
|
||||||
|
|
||||||
return refreshCmds + enrollResponse()
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
@@ -443,38 +425,15 @@ def refresh() {
|
|||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
|
||||||
log.debug "Configuring Reporting"
|
log.debug "Configuring Reporting"
|
||||||
|
|
||||||
def configCmds = [
|
def configCmds = enrollResponse() +
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
zigbee.batteryConfig() +
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
zigbee.temperatureConfig() +
|
||||||
|
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs
|
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {0100}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {0100}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {0100}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
|
||||||
]
|
|
||||||
|
|
||||||
return configCmds + refresh()
|
return configCmds + refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
|||||||
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.
|
||||||
@@ -22,7 +22,7 @@ metadata {
|
|||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
valueTile("currentColor", "device.color") {
|
valueTile("currentColor", "device.color") {
|
||||||
state "default", label: '${currentValue}'
|
state "color", label: '${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) {
|
controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) {
|
||||||
@@ -41,6 +41,13 @@ def parse(String description) {
|
|||||||
log.debug "Parsing '${description}'"
|
log.debug "Parsing '${description}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setColor(value) {
|
||||||
|
log.debug "setting color: $value"
|
||||||
|
if (value.hex) { sendEvent(name: "color", value: value.hex) }
|
||||||
|
if (value.hue) { sendEvent(name: "hue", value: value.hue) }
|
||||||
|
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation) }
|
||||||
|
}
|
||||||
|
|
||||||
def setSaturation(percent) {
|
def setSaturation(percent) {
|
||||||
log.debug "Executing 'setSaturation'"
|
log.debug "Executing 'setSaturation'"
|
||||||
sendEvent(name: "saturation", value: percent)
|
sendEvent(name: "saturation", value: percent)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) {
|
valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "range", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") {
|
controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") {
|
||||||
|
|||||||
@@ -41,17 +41,17 @@ metadata {
|
|||||||
|
|
||||||
// standard flat tile with only a label
|
// standard flat tile with only a label
|
||||||
standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
|
standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
|
||||||
state "default", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff"
|
state "label", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// standard flat tile with icon and label
|
// standard flat tile with icon and label
|
||||||
standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
|
standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
|
||||||
state "default", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff"
|
state "iconLabel", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// standard flat tile with only icon (Refreh text is IN the icon file)
|
// standard flat tile with only icon (Refreh text is IN the icon file)
|
||||||
standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") {
|
standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") {
|
||||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "icon", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// standard with defaultState = true
|
// standard with defaultState = true
|
||||||
@@ -74,19 +74,19 @@ metadata {
|
|||||||
|
|
||||||
// utility tiles to fill the spaces
|
// utility tiles to fill the spaces
|
||||||
standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") {
|
standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") {
|
||||||
state "default", label:''
|
state "emptySmall", label:'', defaultState: true
|
||||||
}
|
}
|
||||||
standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") {
|
standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") {
|
||||||
state "default", label:''
|
state "emptyBigger", label:'', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// multi-line text (explicit newlines)
|
// multi-line text (explicit newlines)
|
||||||
standardTile("multiLine", "device.multiLine", width: 2, height: 2) {
|
standardTile("multiLine", "device.multiLine", width: 2, height: 2) {
|
||||||
state "default", label: '${currentValue}'
|
state "multiLine", label: '${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) {
|
standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) {
|
||||||
state "default", label: '${currentValue}', icon: "st.switches.switch.off"
|
state "multiLineIcon", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
main("actionRings")
|
main("actionRings")
|
||||||
|
|||||||
@@ -22,68 +22,68 @@ metadata {
|
|||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
valueTile("text", "device.text", width: 2, height: 2) {
|
valueTile("text", "device.text", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("longText", "device.longText", width: 2, height: 2) {
|
valueTile("longText", "device.longText", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("integer", "device.integer", width: 2, height: 2) {
|
valueTile("integer", "device.integer", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) {
|
valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("pi", "device.pi", width: 2, height: 2) {
|
valueTile("pi", "device.pi", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) {
|
valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "val", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("bgColor", "device.integer", width: 2, height: 2) {
|
valueTile("bgColor", "device.integer", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}', backgroundColor: "#e86d13"
|
state "val", label:'${currentValue}', backgroundColor: "#e86d13", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("bgColorRange", "device.integer", width: 2, height: 2) {
|
valueTile("bgColorRange", "device.integer", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}', backgroundColors: [
|
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||||
[value: 10, color: "#ff0000"],
|
[value: 10, color: "#ff0000"],
|
||||||
[value: 90, color: "#0000ff"]
|
[value: 90, color: "#0000ff"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) {
|
valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}', backgroundColors: [
|
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||||
[value: 10, color: "#333333"]
|
[value: 10, color: "#333333"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) {
|
valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) {
|
||||||
state "default", label:'${currentValue}', backgroundColors: [
|
state "valWithConflict", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||||
[value: 10, color: "#990000"],
|
[value: 10, color: "#990000"],
|
||||||
[value: 10, color: "#000099"]
|
[value: 10, color: "#000099"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("noValue", "device.nada", width: 4, height: 2) {
|
valueTile("noValue", "device.nada", width: 4, height: 2) {
|
||||||
state "default", label:'${currentValue}'
|
state "noval", label:'${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("multiLine", "device.multiLine", width: 3, height: 2) {
|
valueTile("multiLine", "device.multiLine", width: 3, height: 2) {
|
||||||
state "default", label: '${currentValue}'
|
state "val", label: '${currentValue}', defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) {
|
valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) {
|
||||||
state "default", label: '${currentValue}', icon: "st.switches.switch.off"
|
state "val", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
main("text")
|
main("text")
|
||||||
details([
|
details([
|
||||||
"text", "longText", "integer",
|
"text", "longText", "integer",
|
||||||
"integerFloat", "pi", "floatAsText",
|
"integerFloat", "pi", "floatAsText",
|
||||||
"bgColor", "bgColorRange", "bgColorRangeSingleItem",
|
"bgColor", "bgColorRange", "bgColorRangeSingleItem",
|
||||||
"bgColorRangeConflict", "noValue",
|
"bgColorRangeConflict", "noValue",
|
||||||
|
|||||||
@@ -39,15 +39,15 @@ metadata {
|
|||||||
attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
}
|
}
|
||||||
tileAttribute("device.level", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.level", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", icon: 'st.Weather.weather1', action:"randomizeLevel"
|
attributeState "level", icon: 'st.Weather.weather1', action:"randomizeLevel", defaultState: true
|
||||||
}
|
}
|
||||||
tileAttribute("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "default", action:"switch level.setLevel"
|
attributeState "level", action:"switch level.setLevel", defaultState: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) {
|
multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) {
|
||||||
tileAttribute("device.level", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.level", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label:'${currentValue}', backgroundColors:[
|
attributeState "level", label:'${currentValue}', defaultState: true, backgroundColors:[
|
||||||
[value: 0, color: "#ff0000"],
|
[value: 0, color: "#ff0000"],
|
||||||
[value: 20, color: "#ffff00"],
|
[value: 20, color: "#ffff00"],
|
||||||
[value: 40, color: "#00ff00"],
|
[value: 40, color: "#00ff00"],
|
||||||
@@ -69,34 +69,34 @@ metadata {
|
|||||||
}
|
}
|
||||||
multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) {
|
multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) {
|
||||||
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821"
|
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true
|
||||||
}
|
}
|
||||||
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821"
|
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"multilineTile", type:"generic", width:6, height:4) {
|
multiAttributeTile(name:"multilineTile", type:"generic", width:6, height:4) {
|
||||||
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821"
|
attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true
|
||||||
}
|
}
|
||||||
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821"
|
attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"lengthyTileWithIcon", type:"generic", width:6, height:4) {
|
multiAttributeTile(name:"lengthyTileWithIcon", type:"generic", width:6, height:4) {
|
||||||
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on"
|
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
|
||||||
}
|
}
|
||||||
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on"
|
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"multilineTileWithIcon", type:"generic", width:6, height:4) {
|
multiAttributeTile(name:"multilineTileWithIcon", type:"generic", width:6, height:4) {
|
||||||
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on"
|
attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
|
||||||
}
|
}
|
||||||
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on"
|
attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ def installed() {
|
|||||||
sendEvent(name: "multilineText", value: "Line 1 YES\nLine 2 YES\nLine 3 NO")
|
sendEvent(name: "multilineText", value: "Line 1 YES\nLine 2 YES\nLine 3 NO")
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse() {
|
def parse(String description) {
|
||||||
// This is a simulated device. No incoming data to parse.
|
// This is a simulated device. No incoming data to parse.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
state "reset", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single", defaultState: true
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "refresh", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
@@ -173,7 +173,6 @@ def setColor(value) {
|
|||||||
def reset() {
|
def reset() {
|
||||||
log.debug "Executing 'reset'"
|
log.debug "Executing 'reset'"
|
||||||
setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23])
|
setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23])
|
||||||
//parent.poll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setAdjustedColor(value) {
|
def setAdjustedColor(value) {
|
||||||
@@ -189,7 +188,6 @@ def setAdjustedColor(value) {
|
|||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
//parent.manualRefresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def adjustOutgoingHue(percent) {
|
def adjustOutgoingHue(percent) {
|
||||||
@@ -208,4 +206,3 @@ def adjustOutgoingHue(percent) {
|
|||||||
log.info "percent: $percent, adjusted: $adjusted"
|
log.info "percent: $percent, adjusted: $adjusted"
|
||||||
adjusted
|
adjusted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ metadata {
|
|||||||
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing")
|
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing")
|
||||||
}
|
}
|
||||||
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
|
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
|
||||||
attributeState("default", action:"music Player.previousTrack")
|
attributeState("status", action:"music Player.previousTrack", defaultState: true)
|
||||||
}
|
}
|
||||||
tileAttribute("device.status", key: "NEXT_TRACK") {
|
tileAttribute("device.status", key: "NEXT_TRACK") {
|
||||||
attributeState("default", action:"music Player.nextTrack")
|
attributeState("status", action:"music Player.nextTrack", defaultState: true)
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState("level", action:"music Player.setLevel")
|
attributeState("level", action:"music Player.setLevel")
|
||||||
@@ -50,7 +50,7 @@ metadata {
|
|||||||
attributeState("muted", action:"music Player.unmute", nextState: "unmuted")
|
attributeState("muted", action:"music Player.unmute", nextState: "unmuted")
|
||||||
}
|
}
|
||||||
tileAttribute("device.trackDescription", key: "MARQUEE") {
|
tileAttribute("device.trackDescription", key: "MARQUEE") {
|
||||||
attributeState("default", label:"${currentValue}")
|
attributeState("trackDescription", label:"${currentValue}", defaultState: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ metadata {
|
|||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) {
|
multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) {
|
||||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
}
|
}
|
||||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||||
attributeState("VALUE_UP", action: "tempUp")
|
attributeState("VALUE_UP", action: "tempUp")
|
||||||
attributeState("VALUE_DOWN", action: "tempDown")
|
attributeState("VALUE_DOWN", action: "tempDown")
|
||||||
}
|
}
|
||||||
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
||||||
attributeState("default", label:'${currentValue}%', unit:"%")
|
attributeState("humidity", label:'${currentValue}%', unit:"%", defaultState: true)
|
||||||
}
|
}
|
||||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||||
attributeState("idle", backgroundColor:"#44b621")
|
attributeState("idle", backgroundColor:"#44b621")
|
||||||
@@ -53,15 +53,16 @@ metadata {
|
|||||||
attributeState("auto", label:'${name}')
|
attributeState("auto", label:'${name}')
|
||||||
}
|
}
|
||||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
}
|
}
|
||||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) {
|
multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) {
|
||||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
|
attributeState("temp", label:'${currentValue}', unit:"dF")
|
||||||
}
|
}
|
||||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||||
attributeState("VALUE_UP", action: "tempUp")
|
attributeState("VALUE_UP", action: "tempUp")
|
||||||
@@ -79,15 +80,16 @@ metadata {
|
|||||||
attributeState("auto", label:'${name}')
|
attributeState("auto", label:'${name}')
|
||||||
}
|
}
|
||||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
|
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF")
|
||||||
}
|
}
|
||||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) {
|
multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) {
|
||||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||||
attributeState("default", label:'${currentValue}', unit:"dF",
|
attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true,
|
||||||
backgroundColors:[
|
backgroundColors:[
|
||||||
[value: 31, color: "#153591"],
|
[value: 31, color: "#153591"],
|
||||||
[value: 44, color: "#1e9cbb"],
|
[value: 44, color: "#1e9cbb"],
|
||||||
@@ -118,30 +120,30 @@ metadata {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"tempDown"
|
state "tempDown", label:'down', action:"tempDown", defaultState: true
|
||||||
}
|
}
|
||||||
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"tempUp"
|
state "tempUp", label:'up', action:"tempUp", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"heatDown"
|
state "heatDown", label:'down', action:"heatDown", defaultState: true
|
||||||
}
|
}
|
||||||
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"heatUp"
|
state "heatUp", label:'up', action:"heatUp", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"coolDown"
|
state "coolDown", label:'down', action:"coolDown", defaultState: true
|
||||||
}
|
}
|
||||||
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"coolUp"
|
state "coolUp", label:'up', action:"coolUp", defaultState: true
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019"
|
//fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019"
|
||||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019", manufacturer: "CREE", model: "Connected A-19 60W Equivalent", deviceJoinName: "Cree Connected Bulb"
|
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019"
|
||||||
|
//fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019", manufacturer: "CREE", model: "Connected A-19 60W Equivalent", deviceJoinName: "Cree Connected Bulb"
|
||||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB006", deviceJoinName: "Philips Hue White"
|
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB006", deviceJoinName: "Philips Hue White"
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ metadata {
|
|||||||
capability "Motion Sensor"
|
capability "Motion Sensor"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion
|
||||||
|
fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion
|
||||||
|
fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion +
|
||||||
|
fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814
|
||||||
|
fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02
|
||||||
|
fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
@@ -125,9 +132,9 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
|||||||
}
|
}
|
||||||
if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) {
|
if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) {
|
||||||
result << response(zwave.batteryV1.batteryGet())
|
result << response(zwave.batteryV1.batteryGet())
|
||||||
result << response("delay 1200")
|
} else {
|
||||||
|
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
}
|
}
|
||||||
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,11 +95,17 @@ def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
if (state.manufacturer != cmd.manufacturerName) {
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
updateDataValue("manufacturer", cmd.manufacturerName)
|
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||||
}
|
log.debug "productId: ${cmd.productId}"
|
||||||
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
updateDataValue("MSR", msr)
|
||||||
|
updateDataValue("manufacturer", cmd.manufacturerName)
|
||||||
|
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
// Handles all Z-Wave commands we aren't interested in
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
[:]
|
[:]
|
||||||
|
|||||||
@@ -0,0 +1,839 @@
|
|||||||
|
/**
|
||||||
|
* LaMetric (Connect)
|
||||||
|
*
|
||||||
|
* Copyright 2016 Smart Atoms Ltd.
|
||||||
|
* Author: Mykola Kirichuk
|
||||||
|
*
|
||||||
|
* 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: "LaMetric (Connect)",
|
||||||
|
namespace: "com.lametric",
|
||||||
|
author: "Mykola Kirichuk",
|
||||||
|
description: "Control your LaMetric Time smart display",
|
||||||
|
category: "Family",
|
||||||
|
iconUrl: "https://developer.lametric.com/assets/smart_things/smart_things_60.png",
|
||||||
|
iconX2Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
|
||||||
|
iconX3Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
|
||||||
|
singleInstance: true)
|
||||||
|
{
|
||||||
|
appSetting "clientId"
|
||||||
|
appSetting "clientSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
page(name: "auth", title: "LaMetric", nextPage:"", content:"authPage", uninstall: true, install:true)
|
||||||
|
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5);
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||||
|
path("/oauth/callback") {action: [GET: "callback"]}
|
||||||
|
}
|
||||||
|
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
|
||||||
|
def getEventNameListOfUserDeviceParsed(){ "EventListOfUserRemoteDevicesParsed" }
|
||||||
|
def getEventNameTokenRefreshed(){ "EventAuthTokenRefreshed" }
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
sendEvent(name:"Updated", value:true)
|
||||||
|
unsubscribe()
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
// TODO: subscribe to attributes, devices, locations, etc.
|
||||||
|
log.debug("initialize");
|
||||||
|
state.subscribe = false;
|
||||||
|
if (selecteddevice) {
|
||||||
|
addDevice()
|
||||||
|
subscribeNetworkEvents(true)
|
||||||
|
refreshDevices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the new device to instantiate in the user's smartapps
|
||||||
|
* This must be an app owned by the namespace (see #getNameSpace).
|
||||||
|
*
|
||||||
|
* @return name
|
||||||
|
*/
|
||||||
|
|
||||||
|
def getDeviceName() {
|
||||||
|
return "LaMetric"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the namespace this app and siblings use
|
||||||
|
*
|
||||||
|
* @return namespace
|
||||||
|
*/
|
||||||
|
def getNameSpace() {
|
||||||
|
return "com.lametric"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all discovered devices or an empty array if none
|
||||||
|
*
|
||||||
|
* @return array of devices
|
||||||
|
*/
|
||||||
|
def getDevices() {
|
||||||
|
state.remoteDevices = state.remoteDevices ?: [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of devices which have been verified
|
||||||
|
*
|
||||||
|
* @return array of verified devices
|
||||||
|
*/
|
||||||
|
def getVerifiedDevices() {
|
||||||
|
getDevices().findAll{ it?.value?.verified == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a Map object which can be used with a preference page
|
||||||
|
* to represent a list of devices detected and verified.
|
||||||
|
*
|
||||||
|
* @return Map with zero or more devices
|
||||||
|
*/
|
||||||
|
Map getSelectableDevice() {
|
||||||
|
def devices = getVerifiedDevices()
|
||||||
|
def map = [:]
|
||||||
|
devices.each {
|
||||||
|
def value = "${it.value.name}"
|
||||||
|
def key = it.value.id
|
||||||
|
map["${key}"] = value
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the refresh loop, making sure to keep us up-to-date with changes
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private refreshDevices(){
|
||||||
|
log.debug "refresh device list"
|
||||||
|
listOfUserRemoteDevices()
|
||||||
|
//every 30 min
|
||||||
|
runIn(1800, "refreshDevices")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The deviceDiscovery page used by preferences. Will automatically
|
||||||
|
* make calls to the underlying discovery mechanisms as well as update
|
||||||
|
* whenever new devices are discovered AND verified.
|
||||||
|
*
|
||||||
|
* @return a dynamicPage() object
|
||||||
|
*/
|
||||||
|
/******************************************************************************************************************
|
||||||
|
DEVICE DISCOVERY AND VALIDATION
|
||||||
|
******************************************************************************************************************/
|
||||||
|
def deviceDiscovery()
|
||||||
|
{
|
||||||
|
// if(canInstallLabs())
|
||||||
|
if (1)
|
||||||
|
{
|
||||||
|
// userDeviceList();
|
||||||
|
log.debug("deviceDiscovery")
|
||||||
|
def refreshInterval = 3 // Number of seconds between refresh
|
||||||
|
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||||
|
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
|
||||||
|
|
||||||
|
def devices = getSelectableDevice()
|
||||||
|
def numFound = devices.size() ?: 0
|
||||||
|
|
||||||
|
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
|
||||||
|
subscribeNetworkEvents()
|
||||||
|
|
||||||
|
//device discovery request every 15s
|
||||||
|
// if((deviceRefreshCount % 15) == 0) {
|
||||||
|
// discoverLaMetrics()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Verify request every 3 seconds except on discoveries
|
||||||
|
if(((deviceRefreshCount % 5) == 0)) {
|
||||||
|
verifyDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace "Discovered devices: ${devices}"
|
||||||
|
|
||||||
|
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||||
|
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||||
|
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||||
|
|
||||||
|
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||||
|
|
||||||
|
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
|
||||||
|
section("Upgrade") {
|
||||||
|
paragraph "$upgradeNeeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a subscription for network events
|
||||||
|
*
|
||||||
|
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
|
||||||
|
*/
|
||||||
|
private subscribeNetworkEvents(force=false) {
|
||||||
|
if (force) {
|
||||||
|
unsubscribe()
|
||||||
|
state.subscribe = false
|
||||||
|
}
|
||||||
|
if(!state.subscribe) {
|
||||||
|
log.debug("subscribe on network events")
|
||||||
|
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||||
|
// subscribe(app, appHandler)
|
||||||
|
state.subscribe = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private verifyDevices()
|
||||||
|
{
|
||||||
|
log.debug "verify.devices"
|
||||||
|
def devices = getDevices();
|
||||||
|
for (it in devices) {
|
||||||
|
log.trace ("verify device ${it.value}")
|
||||||
|
def localIp = it?.value?.ipv4_internal;
|
||||||
|
def apiKey = it?.value?.api_key;
|
||||||
|
getAllInfoFromDevice(localIp, apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def appHandler(evt)
|
||||||
|
{
|
||||||
|
log.debug("application event handler ${evt.name}")
|
||||||
|
if (evt.name == eventNameListOfUserDeviceParsed)
|
||||||
|
{
|
||||||
|
log.debug ("new account device list received ${evt.value}")
|
||||||
|
def newRemoteDeviceList
|
||||||
|
try {
|
||||||
|
newRemoteDeviceList = parseJson(evt.value)
|
||||||
|
} catch (e)
|
||||||
|
{
|
||||||
|
log.debug "Wrong value ${e}"
|
||||||
|
}
|
||||||
|
if (newRemoteDeviceList)
|
||||||
|
{
|
||||||
|
def remoteDevices = getDevices();
|
||||||
|
newRemoteDeviceList.each{deviceInfo ->
|
||||||
|
if (deviceInfo) {
|
||||||
|
def device = remoteDevices[deviceInfo.id]?:[:];
|
||||||
|
log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
|
||||||
|
deviceInfo.each() {
|
||||||
|
device[it.key] = it.value;
|
||||||
|
}
|
||||||
|
remoteDevices[deviceInfo.id] = device;
|
||||||
|
} else {
|
||||||
|
log.debug ("empty device info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verifyDevices();
|
||||||
|
} else {
|
||||||
|
log.debug "wrong value ${newRemoteDeviceList}"
|
||||||
|
}
|
||||||
|
} else if (evt.name == getEventNameTokenRefreshed())
|
||||||
|
{
|
||||||
|
log.debug "token refreshed"
|
||||||
|
state.refreshToken = evt.refreshToken
|
||||||
|
state.authToken = evt.access_token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def locationHandler(evt)
|
||||||
|
{
|
||||||
|
log.debug("network event handler ${evt.name}")
|
||||||
|
if (evt.name == "ssdpTerm")
|
||||||
|
{
|
||||||
|
log.debug "ignore ssdp"
|
||||||
|
} else {
|
||||||
|
def lanEvent = parseLanMessage(evt.description, true)
|
||||||
|
log.debug lanEvent.headers;
|
||||||
|
if (lanEvent.body)
|
||||||
|
{
|
||||||
|
log.trace "lan event ${lanEvent}";
|
||||||
|
def parsedJsonBody;
|
||||||
|
try {
|
||||||
|
parsedJsonBody = parseJson(lanEvent.body);
|
||||||
|
} catch (e)
|
||||||
|
{
|
||||||
|
log.debug ("not json responce ignore $e");
|
||||||
|
}
|
||||||
|
if (parsedJsonBody)
|
||||||
|
{
|
||||||
|
log.trace (parsedJsonBody)
|
||||||
|
log.debug("responce for device ${parsedJsonBody?.id}")
|
||||||
|
//put or post response
|
||||||
|
if (parsedJsonBody.success)
|
||||||
|
{
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//poll response
|
||||||
|
log.debug "poll responce"
|
||||||
|
log.debug ("poll responce ${parsedJsonBody}")
|
||||||
|
def deviceId = parsedJsonBody?.id;
|
||||||
|
if (deviceId)
|
||||||
|
{
|
||||||
|
def devices = getDevices();
|
||||||
|
def device = devices."${deviceId}";
|
||||||
|
|
||||||
|
device.verified = true;
|
||||||
|
device.dni = [device.serial_number, device.id].join('.')
|
||||||
|
device.hub = evt?.hubId;
|
||||||
|
device.volume = parsedJsonBody?.audio?.volume;
|
||||||
|
log.debug "verified device ${deviceId}"
|
||||||
|
def childDevice = getChildDevice(device.dni)
|
||||||
|
//update device info
|
||||||
|
if (childDevice)
|
||||||
|
{
|
||||||
|
log.debug("send event to ${childDevice}")
|
||||||
|
childDevice.sendEvent(name:"currentIP",value:device?.ipv4_internal);
|
||||||
|
childDevice.sendEvent(name:"volume",value:device?.volume);
|
||||||
|
childDevice.setOnline();
|
||||||
|
}
|
||||||
|
log.trace device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the child devices based on the user's selection
|
||||||
|
*
|
||||||
|
* Uses selecteddevice defined in the deviceDiscovery() page
|
||||||
|
*/
|
||||||
|
def addDevice() {
|
||||||
|
def devices = getVerifiedDevices()
|
||||||
|
def devlist
|
||||||
|
log.trace "Adding childs"
|
||||||
|
|
||||||
|
// If only one device is selected, we don't get a list (when using simulator)
|
||||||
|
if (!(selecteddevice instanceof List)) {
|
||||||
|
devlist = [selecteddevice]
|
||||||
|
} else {
|
||||||
|
devlist = selecteddevice
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace "These are being installed: ${devlist}"
|
||||||
|
log.debug ("devlist" + devlist)
|
||||||
|
devlist.each { dni ->
|
||||||
|
def newDevice = devices[dni];
|
||||||
|
if (newDevice)
|
||||||
|
{
|
||||||
|
def d = getChildDevice(newDevice.dni)
|
||||||
|
if(!d) {
|
||||||
|
log.debug ("get child devices" + getChildDevices())
|
||||||
|
log.trace "concrete device ${newDevice}"
|
||||||
|
def deviceName = newDevice.name
|
||||||
|
d = addChildDevice(getNameSpace(), getDeviceName(), newDevice.dni, newDevice.hub, [label:"${deviceName}"])
|
||||||
|
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||||
|
childDevice.sendEvent(name:"serialNumber", value:newDevice.serial_number)
|
||||||
|
log.trace "Created ${d.displayName} with id $dni"
|
||||||
|
} else {
|
||||||
|
log.trace "${d.displayName} with id $dni already exists"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//******************************************************************************************************************
|
||||||
|
// OAUTH
|
||||||
|
//******************************************************************************************************************
|
||||||
|
|
||||||
|
def getServerUrl() { "https://graph.api.smartthings.com" }
|
||||||
|
def getShardUrl() { getApiServerUrl() }
|
||||||
|
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||||
|
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
|
||||||
|
def getApiEndpoint() { "https://developer.lametric.com" }
|
||||||
|
def getTokenUrl() { "${apiEndpoint}${apiTokenPath}" }
|
||||||
|
def getAuthScope() { [ "basic", "devices_read" ] }
|
||||||
|
def getSmartThingsClientId() { appSettings.clientId }
|
||||||
|
def getSmartThingsClientSecret() { appSettings.clientSecret }
|
||||||
|
def getApiTokenPath() { "/api/v2/oauth2/token" }
|
||||||
|
def getApiUserMeDevicesList() { "/api/v2/users/me/devices" }
|
||||||
|
|
||||||
|
def toQueryString(Map m) {
|
||||||
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||||
|
}
|
||||||
|
def composeScope(List scopes)
|
||||||
|
{
|
||||||
|
def result = "";
|
||||||
|
scopes.each(){ scope ->
|
||||||
|
result += "${scope} "
|
||||||
|
}
|
||||||
|
if (result.length())
|
||||||
|
return result.substring(0, result.length() - 1);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
def authPage() {
|
||||||
|
log.debug "authPage()"
|
||||||
|
|
||||||
|
if(!state.accessToken) { //this is to access token for 3rd party to make a call to connect app
|
||||||
|
state.accessToken = createAccessToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
def description
|
||||||
|
def uninstallAllowed = false
|
||||||
|
def oauthTokenProvided = false
|
||||||
|
|
||||||
|
if(state.authToken) {
|
||||||
|
description = "You are connected."
|
||||||
|
uninstallAllowed = true
|
||||||
|
oauthTokenProvided = true
|
||||||
|
} else {
|
||||||
|
description = "Click to enter LaMetric Credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
def redirectUrl = buildRedirectUrl
|
||||||
|
log.debug "RedirectUrl = ${redirectUrl}"
|
||||||
|
// get rid of next button until the user is actually auth'd
|
||||||
|
if (!oauthTokenProvided) {
|
||||||
|
return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
|
||||||
|
section(){
|
||||||
|
paragraph "Tap below to log in to the LaMatric service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
|
||||||
|
href url:redirectUrl, style:"embedded", required:true, title:"LaMetric", description:description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subscribeNetworkEvents()
|
||||||
|
listOfUserRemoteDevices()
|
||||||
|
return deviceDiscovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private refreshAuthToken() {
|
||||||
|
log.debug "refreshing auth token"
|
||||||
|
|
||||||
|
if(!state.refreshToken) {
|
||||||
|
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||||
|
} else {
|
||||||
|
def refreshParams = [
|
||||||
|
method: 'POST',
|
||||||
|
uri : apiEndpoint,
|
||||||
|
path : apiTokenPath,
|
||||||
|
body : [grant_type: 'refresh_token',
|
||||||
|
refresh_token: "${state.refreshToken}",
|
||||||
|
client_id : smartThingsClientId,
|
||||||
|
client_secret: smartThingsClientSecret,
|
||||||
|
redirect_uri: callbackUrl],
|
||||||
|
]
|
||||||
|
|
||||||
|
log.debug refreshParams
|
||||||
|
|
||||||
|
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the LaMetric (Connect) SmartApp and re-enter your account login credentials."
|
||||||
|
//changed to httpPost
|
||||||
|
try {
|
||||||
|
def jsonMap
|
||||||
|
httpPost(refreshParams) { resp ->
|
||||||
|
if(resp.status == 200) {
|
||||||
|
log.debug "Token refreshed...calling saved RestAction now! $resp.data"
|
||||||
|
jsonMap = resp.data
|
||||||
|
if(resp.data) {
|
||||||
|
state.refreshToken = resp?.data?.refresh_token
|
||||||
|
state.authToken = resp?.data?.access_token
|
||||||
|
if(state.action && state.action != "") {
|
||||||
|
log.debug "Executing next action: ${state.action}"
|
||||||
|
|
||||||
|
"${state.action}"()
|
||||||
|
|
||||||
|
state.action = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.warn ("No data in refresh token!");
|
||||||
|
}
|
||||||
|
state.action = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
|
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||||
|
log.debug e.response.data;
|
||||||
|
def reAttemptPeriod = 300 // in sec
|
||||||
|
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
|
||||||
|
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||||
|
} else if (e.statusCode == 401) { // unauthorized
|
||||||
|
state.reAttempt = state.reAttempt + 1
|
||||||
|
log.warn "reAttempt refreshAuthToken to try = ${state.reAttempt}"
|
||||||
|
if (state.reAttempt <= 3) {
|
||||||
|
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||||
|
} else {
|
||||||
|
sendPushAndFeeds(notificationMessage)
|
||||||
|
state.reAttempt = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def callback() {
|
||||||
|
log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||||
|
|
||||||
|
def code = params.code
|
||||||
|
def oauthState = params.state
|
||||||
|
|
||||||
|
if (oauthState == state.oauthInitState){
|
||||||
|
|
||||||
|
def tokenParams = [
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code : code,
|
||||||
|
client_id : smartThingsClientId,
|
||||||
|
client_secret: smartThingsClientSecret,
|
||||||
|
redirect_uri: callbackUrl
|
||||||
|
]
|
||||||
|
log.trace tokenParams
|
||||||
|
log.trace tokenUrl
|
||||||
|
try {
|
||||||
|
httpPost(uri: tokenUrl, body: tokenParams) { resp ->
|
||||||
|
log.debug "swapped token: $resp.data"
|
||||||
|
state.refreshToken = resp.data.refresh_token
|
||||||
|
state.authToken = resp.data.access_token
|
||||||
|
}
|
||||||
|
} catch (e)
|
||||||
|
{
|
||||||
|
log.debug "fail ${e}";
|
||||||
|
}
|
||||||
|
if (state.authToken) {
|
||||||
|
success()
|
||||||
|
} else {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error "callback() failed oauthState != state.oauthInitState"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def oauthInitUrl() {
|
||||||
|
log.debug "oauthInitUrl with callback: ${callbackUrl}"
|
||||||
|
|
||||||
|
state.oauthInitState = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
def oauthParams = [
|
||||||
|
response_type: "code",
|
||||||
|
scope: composeScope(authScope),
|
||||||
|
client_id: smartThingsClientId,
|
||||||
|
state: state.oauthInitState,
|
||||||
|
redirect_uri: callbackUrl
|
||||||
|
]
|
||||||
|
log.debug oauthParams
|
||||||
|
log.debug "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}"
|
||||||
|
|
||||||
|
redirect(location: "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def success() {
|
||||||
|
def message = """
|
||||||
|
<p>Your LaMetric Account is now connected to SmartThings!</p>
|
||||||
|
<p>Click 'Done' to finish setup.</p>
|
||||||
|
"""
|
||||||
|
connectionStatus(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
def fail() {
|
||||||
|
def message = """
|
||||||
|
<p>The connection could not be established!</p>
|
||||||
|
<p>Click 'Done' to return to the menu.</p>
|
||||||
|
"""
|
||||||
|
connectionStatus(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
def connectionStatus(message, redirectUrl = null) {
|
||||||
|
def redirectHtml = ""
|
||||||
|
if (redirectUrl) {
|
||||||
|
redirectHtml = """
|
||||||
|
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
def html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width" id="viewport" name="viewport">
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'latoRegular';
|
||||||
|
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot");
|
||||||
|
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot?#iefix") format("embedded-opentype"),
|
||||||
|
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.woff") format("woff"),
|
||||||
|
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.ttf") format("truetype"),
|
||||||
|
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.svg#latoRegular") format("svg");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal; }
|
||||||
|
.clearfix:after, .mobile .connect:after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
display: table; }
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
transition: all .3s ease 0s; }
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #5c5c5c;
|
||||||
|
min-width: 1149px;
|
||||||
|
font-family: 'latoRegular', 'Lato';
|
||||||
|
}
|
||||||
|
.fixed-page #page {
|
||||||
|
min-height: 100%;
|
||||||
|
background: url(https://developer.lametric.com/assets/smart_things/page-bg.png) 50% 0 repeat-y;
|
||||||
|
}
|
||||||
|
.mobile {
|
||||||
|
min-width: 100%;
|
||||||
|
color: #757575; }
|
||||||
|
.mobile .wrap {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 640px;
|
||||||
|
min-width: inherit; }
|
||||||
|
.mobile .connect {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 230px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
text-align: center; }
|
||||||
|
.mobile .connect img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 2%;
|
||||||
|
border-radius: 15px; }
|
||||||
|
.mobile .connect img:first-child {
|
||||||
|
margin-left: 0; }
|
||||||
|
.mobile .info {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-bottom: 50px; }
|
||||||
|
.mobile .info p {
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 50px;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 50px;
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
@media screen and (max-width: 639px) {
|
||||||
|
.mobile .connect{
|
||||||
|
padding-top: 100px; }
|
||||||
|
.mobile .wrap {
|
||||||
|
margin: 0 20px; }
|
||||||
|
.mobile .connect img {
|
||||||
|
width: 16%; }
|
||||||
|
.mobile .connect img:first-child, .mobile .connect img:last-child {
|
||||||
|
width: 40%; }
|
||||||
|
.mobile .info p{
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-top: 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="fixed-page mobile">
|
||||||
|
|
||||||
|
<div id="page">
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<div class="connect">
|
||||||
|
<img src="https://developer.lametric.com/assets/smart_things/product.png" width="190" height="190"><img src="https://developer.lametric.com/assets/smart_things/connected.png" width="87" height="19"><img src="https://developer.lametric.com/assets/smart_things/product-1.png" width="192" height="192">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
${message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
"""
|
||||||
|
render contentType: 'text/html', data: html
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//******************************************************************************************************************
|
||||||
|
// LOCAL API
|
||||||
|
//******************************************************************************************************************
|
||||||
|
|
||||||
|
def getLocalApiDeviceInfoPath() { "/api/v2/info" }
|
||||||
|
def getLocalApiSendNotificationPath() { "/api/v2/device/notifications" }
|
||||||
|
def getLocalApiIndexPath() { "/api/v2/device" }
|
||||||
|
def getLocalApiUser() { "dev" }
|
||||||
|
|
||||||
|
|
||||||
|
void requestDeviceInfo(localIp, apiKey)
|
||||||
|
{
|
||||||
|
if (localIp && apiKey)
|
||||||
|
{
|
||||||
|
log.debug("request info ${localIp}");
|
||||||
|
def command = new physicalgraph.device.HubAction([
|
||||||
|
method: "GET",
|
||||||
|
path: localApiDeviceInfoPath,
|
||||||
|
headers: [
|
||||||
|
HOST: "${localIp}:8080",
|
||||||
|
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
|
||||||
|
]])
|
||||||
|
log.debug command
|
||||||
|
sendHubCommand(command)
|
||||||
|
command;
|
||||||
|
} else {
|
||||||
|
log.debug ("Unknown api key or ip address ${localIp} ${apiKey}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendNotificationMessageToDevice(dni, data)
|
||||||
|
{
|
||||||
|
log.debug "send something"
|
||||||
|
def device = resolveDNI2Device(dni);
|
||||||
|
def localIp = device?.ipv4_internal;
|
||||||
|
def apiKey = device?.api_key;
|
||||||
|
if (localIp && apiKey)
|
||||||
|
{
|
||||||
|
log.debug "send notification message to device ${localIp}:8080 ${data}"
|
||||||
|
sendHubCommand(new physicalgraph.device.HubAction([
|
||||||
|
method: "POST",
|
||||||
|
path: localApiSendNotificationPath,
|
||||||
|
body: data,
|
||||||
|
headers: [
|
||||||
|
HOST: "${localIp}:8080",
|
||||||
|
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}",
|
||||||
|
"Content-type":"application/json",
|
||||||
|
"Accept":"application/json"
|
||||||
|
]]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAllInfoFromDevice(localIp, apiKey)
|
||||||
|
{
|
||||||
|
log.debug "send something"
|
||||||
|
if (localIp && apiKey)
|
||||||
|
{
|
||||||
|
def hubCommand = new physicalgraph.device.HubAction([
|
||||||
|
method: "GET",
|
||||||
|
path: localApiIndexPath+"?fields=info,wifi,volume,bluetooth,id,name,mode,model,serial_number,os_version",
|
||||||
|
headers: [
|
||||||
|
HOST: "${localIp}:8080",
|
||||||
|
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
|
||||||
|
]])
|
||||||
|
log.debug "sending request ${hubCommand}"
|
||||||
|
sendHubCommand(hubCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//******************************************************************************************************************
|
||||||
|
// DEVICE HANDLER COMMANDs API
|
||||||
|
//******************************************************************************************************************
|
||||||
|
|
||||||
|
def resolveDNI2Device(dni)
|
||||||
|
{
|
||||||
|
getDevices().find { it?.value?.dni == dni }?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
def requestRefreshDeviceInfo (dni)
|
||||||
|
{
|
||||||
|
log.debug "device ${dni} request refresh";
|
||||||
|
// def devices = getDevices();
|
||||||
|
// def concreteDevice = devices[dni];
|
||||||
|
// requestDeviceInfo(conreteDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private poll(dni) {
|
||||||
|
def device = resolveDNI2Device(dni);
|
||||||
|
def localIp = device?.ipv4_internal;
|
||||||
|
def apiKey = device?.api_key;
|
||||||
|
getAllInfoFromDevice(localIp, apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
//******************************************************************************************************************
|
||||||
|
// CLOUD METHODS
|
||||||
|
//******************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
void listOfUserRemoteDevices()
|
||||||
|
{
|
||||||
|
log.debug "get user device list"
|
||||||
|
def deviceList = []
|
||||||
|
if (state.accessToken)
|
||||||
|
{
|
||||||
|
def deviceListParams = [
|
||||||
|
uri: apiEndpoint,
|
||||||
|
path: apiUserMeDevicesList,
|
||||||
|
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${state.authToken}"]
|
||||||
|
]
|
||||||
|
log.debug "making request ${deviceListParams}"
|
||||||
|
def result;
|
||||||
|
try {
|
||||||
|
httpGet(deviceListParams){ resp ->
|
||||||
|
if (resp.status == 200)
|
||||||
|
{
|
||||||
|
deviceList = resp.data
|
||||||
|
|
||||||
|
def remoteDevices = getDevices();
|
||||||
|
for (deviceInfo in deviceList) {
|
||||||
|
if (deviceInfo)
|
||||||
|
{
|
||||||
|
def device = remoteDevices."${deviceInfo.id}"?:[:];
|
||||||
|
log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
|
||||||
|
for (it in deviceInfo ) {
|
||||||
|
device."${it.key}" = it.value;
|
||||||
|
}
|
||||||
|
remoteDevices."${deviceInfo.id}" = device;
|
||||||
|
} else {
|
||||||
|
log.debug ("empty device info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verifyDevices();
|
||||||
|
} else {
|
||||||
|
log.debug "http status: ${resp.status}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (groovyx.net.http.HttpResponseException e)
|
||||||
|
{
|
||||||
|
log.debug("failed to get device list ${e}")
|
||||||
|
def status = e.response.status
|
||||||
|
if (status == 401) {
|
||||||
|
state.action = "refreshDevices"
|
||||||
|
log.debug "Refreshing your auth_token!"
|
||||||
|
refreshAuthToken()
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug ("no access token to fetch user device list");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
/**
|
||||||
|
* Lametric Notifier
|
||||||
|
*
|
||||||
|
* Copyright 2016 Smart Atoms Ltd.
|
||||||
|
* Author: Mykola Kirichuk
|
||||||
|
*
|
||||||
|
* 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 groovy.json.JsonOutput
|
||||||
|
|
||||||
|
definition(
|
||||||
|
name: "LaMetric Notifier",
|
||||||
|
namespace: "com.lametric",
|
||||||
|
author: "Mykola Kirichuk",
|
||||||
|
description: "Allows you to send notifications to your LaMetric Time when something happens in your home to notify the whole family.",
|
||||||
|
category: "Family",
|
||||||
|
iconUrl: "https://developer.lametric.com/assets/smart_things/weather_60.png",
|
||||||
|
iconX2Url: "https://developer.lametric.com/assets/smart_things/weather_120.png",
|
||||||
|
iconX3Url: "https://developer.lametric.com/assets/smart_things/weather_120.png")
|
||||||
|
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
page(name: "mainPage", title: "Show a message on your LaMetric when something happens", install: true, uninstall: true)
|
||||||
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
|
section {
|
||||||
|
input "starting", "time", title: "Starting", required: false
|
||||||
|
input "ending", "time", title: "Ending", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getSoundList() {
|
||||||
|
[
|
||||||
|
"none":"No Sound",
|
||||||
|
"car" : "Car",
|
||||||
|
"cash" : "Cash Register",
|
||||||
|
"cat" : "Cat Meow",
|
||||||
|
"dog" : "Dog Bark",
|
||||||
|
"dog2" : "Dog Bark 2",
|
||||||
|
"letter_email" : "The mail has arrived",
|
||||||
|
"knock-knock" : "Knocking Sound",
|
||||||
|
"bicycle" : "Bicycle",
|
||||||
|
"negative1" : "Negative 1",
|
||||||
|
"negative2" : "Negative 2",
|
||||||
|
"negative3" : "Negative 3",
|
||||||
|
"negative4" : "Negative 4",
|
||||||
|
"negative5" : "Negative 5",
|
||||||
|
"lose1" : "Lose 1",
|
||||||
|
"lose2" : "Lose 2",
|
||||||
|
"energy" : "Energy",
|
||||||
|
"water1" : "Water 1",
|
||||||
|
"water2" : "Water 2",
|
||||||
|
"notification" : "Notification 1",
|
||||||
|
"notification2" : "Notification 2",
|
||||||
|
"notification3" : "Notification 3",
|
||||||
|
"notification4" : "Notification 4",
|
||||||
|
"open_door" : "Door unlocked",
|
||||||
|
"win" : "Win",
|
||||||
|
"win2" : "Win 2",
|
||||||
|
"positive1" : "Positive 1",
|
||||||
|
"positive2" : "Positive 2",
|
||||||
|
"positive3" : "Positive 3",
|
||||||
|
"positive4" : "Positive 4",
|
||||||
|
"positive5" : "Positive 5",
|
||||||
|
"positive6" : "Positive 6",
|
||||||
|
"statistic" : "Page turning",
|
||||||
|
"wind" : "Wind",
|
||||||
|
"wind_short" : "Small Wind",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getControlToAttributeMap(){
|
||||||
|
[
|
||||||
|
"motion": "motion.active",
|
||||||
|
"contact": "contact.open",
|
||||||
|
"contactClosed": "contact.close",
|
||||||
|
"acceleration": "acceleration.active",
|
||||||
|
"mySwitch": "switch.on",
|
||||||
|
"mySwitchOff": "switch.off",
|
||||||
|
"arrivalPresence": "presence.present",
|
||||||
|
"departurePresence": "presence.not present",
|
||||||
|
"smoke": "smoke.detected",
|
||||||
|
"smoke1": "smoke.tested",
|
||||||
|
"water": "water.wet",
|
||||||
|
"button1": "button.pushed",
|
||||||
|
"triggerModes": "mode",
|
||||||
|
"timeOfDay": "time",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPriorityList(){
|
||||||
|
[
|
||||||
|
"warning":"Not So Important (may be ignored at night)",
|
||||||
|
"critical": "Very Important"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getIconsList(){
|
||||||
|
state.icons = state.icons?:["1":"default"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def getIconLabels() {
|
||||||
|
state.iconLabels = state.iconLabels?:["1":"Default Icon"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getSortedIconLabels() {
|
||||||
|
state.iconLabels = state.iconLabels?:["1":"Default Icon"]
|
||||||
|
state.iconLabels.sort {a,b -> a.key.toInteger() <=> b.key.toInteger()};
|
||||||
|
}
|
||||||
|
def getLametricHost() { "https://developer.lametric.com" }
|
||||||
|
def getDefaultIconData() { """data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAe0lEQVQYlWNUVFBgYGBgYNi6bdt/BiTg7eXFyMDAwMCELBmz7z9DzL7/KBoYr127BpeEgbV64QzfRVYxMDAwMLAgSy5xYoSoeMPAwPkmjOG7yCqIgjf8WVC90xnQAdwKj7OZcMGD8m/hVjDBXLvDGKEbJunt5cXISMibAF0FMibYF7nMAAAAAElFTkSuQmCC""" }
|
||||||
|
|
||||||
|
def mainPage() {
|
||||||
|
def iconRequestOptions = [headers: ["Accept": "application/json"],
|
||||||
|
uri: "${lametricHost}/api/v2/icons", query:["fields":"id,title,type,code", "order":"title"]]
|
||||||
|
|
||||||
|
def icons = getIconsList();
|
||||||
|
def iconLabels = getIconLabels();
|
||||||
|
if (icons?.size() <= 2)
|
||||||
|
{
|
||||||
|
log.debug iconRequestOptions
|
||||||
|
try {
|
||||||
|
httpGet(iconRequestOptions) { resp ->
|
||||||
|
int i = 2;
|
||||||
|
resp.data.data.each(){
|
||||||
|
def iconId = it?.id
|
||||||
|
def iconType = it?.type
|
||||||
|
def prefix = "i"
|
||||||
|
if (iconId)
|
||||||
|
{
|
||||||
|
if (iconType == "movie")
|
||||||
|
{
|
||||||
|
prefix = "a"
|
||||||
|
}
|
||||||
|
def iconurl = "${lametricHost}/content/apps/icon_thumbs/${prefix}${iconId}_icon_thumb_big.png";
|
||||||
|
icons["$i"] = it.code
|
||||||
|
iconLabels["$i"] = it.title
|
||||||
|
} else {
|
||||||
|
log.debug "wrong id"
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e)
|
||||||
|
{
|
||||||
|
log.debug "fail ${e}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dynamicPage(name: "mainPage") {
|
||||||
|
def anythingSet = anythingSet()
|
||||||
|
def notificationMessage = defaultNotificationMessage();
|
||||||
|
log.debug "set $anythingSet"
|
||||||
|
if (anythingSet) {
|
||||||
|
section("Show message when"){
|
||||||
|
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true , submitOnChange:true
|
||||||
|
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||||
|
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def hideable = anythingSet || app.installationState == "COMPLETE"
|
||||||
|
def sectionTitle = anythingSet ? "Select additional triggers" : "Show message when..."
|
||||||
|
|
||||||
|
section(sectionTitle, hideable: hideable, hidden: true){
|
||||||
|
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||||
|
ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true, submitOnChange:true
|
||||||
|
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
|
||||||
|
}
|
||||||
|
|
||||||
|
section (title:"Select LaMetrics"){
|
||||||
|
input "selectedDevices", "capability.notification", required: true, multiple:true
|
||||||
|
}
|
||||||
|
section (title: "Configure message"){
|
||||||
|
input "defaultMessage", "bool", title: "Use Default Text:\n\"$notificationMessage\"", required: false, defaultValue: true, submitOnChange:true
|
||||||
|
def showMessageInput = (settings["defaultMessage"] == null || settings["defaultMessage"] == true) ? false : true;
|
||||||
|
if (showMessageInput)
|
||||||
|
{
|
||||||
|
input "customMessage","text",title:"Use Custom Text", defaultValue:"", required:false, multiple: false
|
||||||
|
}
|
||||||
|
input "selectedIcon", "enum", title: "With Icon", required: false, multiple: false, defaultValue:"1", options: getSortedIconLabels()
|
||||||
|
input "selectedSound", "enum", title: "With Sound", required: true, defaultValue:"none" , options: soundList
|
||||||
|
input "showPriority", "enum", title: "Is This Notification Very Important?", required: true, multiple:false, defaultValue: "warning", options: priorityList
|
||||||
|
}
|
||||||
|
section("More options", hideable: true, hidden: true) {
|
||||||
|
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||||
|
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||||
|
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
if (settings.modes) {
|
||||||
|
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section([mobileOnly:true]) {
|
||||||
|
label title: "Assign a name", required: false
|
||||||
|
mode title: "Set for specific mode(s)", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private songOptions() {
|
||||||
|
log.trace "song option"
|
||||||
|
// Make sure current selection is in the set
|
||||||
|
|
||||||
|
def options = new LinkedHashSet()
|
||||||
|
if (state.selectedSong?.station) {
|
||||||
|
options << state.selectedSong.station
|
||||||
|
}
|
||||||
|
else if (state.selectedSong?.description) {
|
||||||
|
// TODO - Remove eventually? 'description' for backward compatibility
|
||||||
|
options << state.selectedSong.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for recent tracks
|
||||||
|
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||||
|
def dataMaps = states.collect{it.jsonValue}
|
||||||
|
options.addAll(dataMaps.collect{it.station})
|
||||||
|
|
||||||
|
log.trace "${options.size()} songs in list"
|
||||||
|
options.take(20) as List
|
||||||
|
}
|
||||||
|
|
||||||
|
private anythingSet() {
|
||||||
|
for (it in controlToAttributeMap) {
|
||||||
|
log.debug ("key ${it.key} value ${settings[it.key]} ${settings[it.key]?true:false}")
|
||||||
|
if (settings[it.key]) {
|
||||||
|
log.debug constructMessageFor(it.value, settings[it.key])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
def defaultNotificationMessage(){
|
||||||
|
def message = "";
|
||||||
|
for (it in controlToAttributeMap) {
|
||||||
|
if (settings[it.key]) {
|
||||||
|
message = constructMessageFor(it.value, settings[it.key])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
def constructMessageFor(group, device)
|
||||||
|
{
|
||||||
|
log.debug ("$group $device")
|
||||||
|
def message;
|
||||||
|
def firstDevice;
|
||||||
|
if (device instanceof List)
|
||||||
|
{
|
||||||
|
firstDevice = device[0];
|
||||||
|
} else {
|
||||||
|
firstDevice = device;
|
||||||
|
}
|
||||||
|
switch(group)
|
||||||
|
{
|
||||||
|
case "motion.active":
|
||||||
|
message = "Motion detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "contact.open":
|
||||||
|
message = "Openning detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "contact.closed":
|
||||||
|
message = "Closing detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "acceleration.active":
|
||||||
|
message = "Acceleration detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "switch.on":
|
||||||
|
message = "$firstDevice.displayName turned on at $location.name"
|
||||||
|
break;
|
||||||
|
case "switch.off":
|
||||||
|
message = "$firstDevice.displayName turned off at $location.name"
|
||||||
|
break;
|
||||||
|
case "presence.present":
|
||||||
|
message = "$firstDevice.displayName detected arrival at $location.name"
|
||||||
|
break;
|
||||||
|
case "presence.not present":
|
||||||
|
message = "$firstDevice.displayName detected departure at $location.name"
|
||||||
|
break;
|
||||||
|
case "smoke.detected":
|
||||||
|
message = "Smoke detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "smoke.tested":
|
||||||
|
message = "Smoke tested by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "water.wet":
|
||||||
|
message = "Dampness detected by $firstDevice.displayName at $location.name"
|
||||||
|
break;
|
||||||
|
case "button.pushed":
|
||||||
|
message = "$firstDevice.displayName pushed at $location.name"
|
||||||
|
break;
|
||||||
|
case "time":
|
||||||
|
break;
|
||||||
|
// case "mode":
|
||||||
|
// message = "Mode changed to ??? at $location.name"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ifUnset(Map options, String name, String capability) {
|
||||||
|
if (!settings[name]) {
|
||||||
|
input(options, name, capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ifSet(Map options, String name, String capability) {
|
||||||
|
if (settings[name]) {
|
||||||
|
input(options, name, capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
subscribeToEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
unsubscribe()
|
||||||
|
unschedule()
|
||||||
|
subscribeToEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
def subscribeToEvents() {
|
||||||
|
log.trace "subscribe to events"
|
||||||
|
log.debug "${contact} ${contactClosed} ${mySwitch} ${mySwitchOff} ${acceleration}${arrivalPresence} ${button1}"
|
||||||
|
// subscribe(app, appTouchHandler)
|
||||||
|
subscribe(contact, "contact.open", eventHandler)
|
||||||
|
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||||
|
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||||
|
subscribe(motion, "motion.active", eventHandler)
|
||||||
|
subscribe(mySwitch, "switch.on", eventHandler)
|
||||||
|
subscribe(mySwitchOff, "switch.off", eventHandler)
|
||||||
|
subscribe(arrivalPresence, "presence.present", eventHandler)
|
||||||
|
subscribe(departurePresence, "presence.not present", eventHandler)
|
||||||
|
subscribe(smoke, "smoke.detected", eventHandler)
|
||||||
|
subscribe(smoke, "smoke.tested", eventHandler)
|
||||||
|
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
|
||||||
|
subscribe(water, "water.wet", eventHandler)
|
||||||
|
subscribe(button1, "button.pushed", eventHandler)
|
||||||
|
|
||||||
|
if (triggerModes) {
|
||||||
|
subscribe(location, modeChangeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOfDay) {
|
||||||
|
schedule(timeOfDay, scheduledTimeHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def eventHandler(evt) {
|
||||||
|
log.trace "eventHandler(${evt?.name}: ${evt?.value})"
|
||||||
|
def name = evt?.name;
|
||||||
|
def value = evt?.value;
|
||||||
|
|
||||||
|
if (allOk) {
|
||||||
|
log.trace "allOk"
|
||||||
|
takeAction(evt)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "Not taking action because it was already taken today"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def modeChangeHandler(evt) {
|
||||||
|
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
|
||||||
|
if (evt?.value in triggerModes) {
|
||||||
|
eventHandler(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def scheduledTimeHandler() {
|
||||||
|
eventHandler(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def appTouchHandler(evt) {
|
||||||
|
takeAction(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private takeAction(evt) {
|
||||||
|
|
||||||
|
log.trace "takeAction()"
|
||||||
|
def messageToShow
|
||||||
|
if (defaultMessage)
|
||||||
|
{
|
||||||
|
messageToShow = constructMessageFor("${evt.name}.${evt.value}", evt.device);
|
||||||
|
} else {
|
||||||
|
messageToShow = customMessage;
|
||||||
|
}
|
||||||
|
if (messageToShow)
|
||||||
|
{
|
||||||
|
log.debug "text ${messageToShow}"
|
||||||
|
def notification = [:];
|
||||||
|
def frame1 = [:];
|
||||||
|
frame1.text = messageToShow;
|
||||||
|
if (selectedIcon != "1")
|
||||||
|
{
|
||||||
|
frame1.icon = state.icons[selectedIcon];
|
||||||
|
} else {
|
||||||
|
frame1.icon = defaultIconData;
|
||||||
|
}
|
||||||
|
def soundId = sound;
|
||||||
|
def sound = [:];
|
||||||
|
sound.id = selectedSound;
|
||||||
|
sound.category = "notifications";
|
||||||
|
def frames = [];
|
||||||
|
frames << frame1;
|
||||||
|
def model = [:];
|
||||||
|
model.frames = frames;
|
||||||
|
if (selectedSound != "none")
|
||||||
|
{
|
||||||
|
model.sound = sound;
|
||||||
|
}
|
||||||
|
notification.model = model;
|
||||||
|
notification.priority = showPriority;
|
||||||
|
def serializedData = new JsonOutput().toJson(notification);
|
||||||
|
|
||||||
|
selectedDevices.each { lametricDevice ->
|
||||||
|
log.trace "send notification to ${lametricDevice} ${serializedData}"
|
||||||
|
lametricDevice.deviceNotification(serializedData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug "No message to show"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace "Exiting takeAction()"
|
||||||
|
}
|
||||||
|
|
||||||
|
private frequencyKey(evt) {
|
||||||
|
"lastActionTimeStamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
private dayString(Date date) {
|
||||||
|
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
if (location.timeZone) {
|
||||||
|
df.setTimeZone(location.timeZone)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||||
|
}
|
||||||
|
df.format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private oncePerDayOk(Long lastTime) {
|
||||||
|
def result = true
|
||||||
|
if (oncePerDay) {
|
||||||
|
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
||||||
|
log.trace "oncePerDayOk = $result"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - centralize somehow
|
||||||
|
private getAllOk() {
|
||||||
|
modeOk && daysOk && timeOk
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModeOk() {
|
||||||
|
def result = !modes || modes.contains(location.mode)
|
||||||
|
log.trace "modeOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDaysOk() {
|
||||||
|
def result = true
|
||||||
|
if (days) {
|
||||||
|
def df = new java.text.SimpleDateFormat("EEEE")
|
||||||
|
if (location.timeZone) {
|
||||||
|
df.setTimeZone(location.timeZone)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||||
|
}
|
||||||
|
def day = df.format(new Date())
|
||||||
|
result = days.contains(day)
|
||||||
|
}
|
||||||
|
log.trace "daysOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTimeOk() {
|
||||||
|
def result = true
|
||||||
|
if (starting && ending) {
|
||||||
|
def currTime = now()
|
||||||
|
def start = timeToday(starting, location?.timeZone).time
|
||||||
|
def stop = timeToday(ending, location?.timeZone).time
|
||||||
|
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||||
|
}
|
||||||
|
log.trace "timeOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private hhmm(time, fmt = "h:mm a")
|
||||||
|
{
|
||||||
|
def t = timeToday(time, location.timeZone)
|
||||||
|
def f = new java.text.SimpleDateFormat(fmt)
|
||||||
|
f.setTimeZone(location.timeZone ?: timeZone(time))
|
||||||
|
f.format(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTimeLabel()
|
||||||
|
{
|
||||||
|
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||||
|
}
|
||||||
@@ -4,29 +4,33 @@
|
|||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
|
||||||
private apiUrl() { "https://api.netatmo.com" }
|
private getApiUrl() { "https://api.netatmo.com" }
|
||||||
private getVendorName() { "netatmo" }
|
private getVendorName() { "netatmo" }
|
||||||
private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" }
|
private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" }
|
||||||
private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" }
|
private getVendorTokenPath(){ "${apiUrl}/oauth2/token" }
|
||||||
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
|
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
|
||||||
private getClientId() { appSettings.clientId }
|
private getClientId() { appSettings.clientId }
|
||||||
private getClientSecret() { appSettings.clientSecret }
|
private getClientSecret() { appSettings.clientSecret }
|
||||||
private getServerUrl() { "https://graph.api.smartthings.com" }
|
private getServerUrl() { appSettings.serverUrl }
|
||||||
|
private getShardUrl() { return getApiServerUrl() }
|
||||||
|
private getCallbackUrl() { "${serverUrl}/oauth/callback" }
|
||||||
|
private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
|
||||||
|
|
||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition(
|
definition(
|
||||||
name: "Netatmo (Connect)",
|
name: "Netatmo (Connect)",
|
||||||
namespace: "dianoga",
|
namespace: "dianoga",
|
||||||
author: "Brian Steere",
|
author: "Brian Steere",
|
||||||
description: "Netatmo Integration",
|
description: "Netatmo Integration",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
||||||
oauth: true,
|
oauth: true,
|
||||||
singleInstance: true
|
singleInstance: true
|
||||||
){
|
){
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
|
appSetting "serverUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -35,35 +39,52 @@ preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mappings {
|
mappings {
|
||||||
path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]}
|
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||||
path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]}
|
path("/oauth/callback") {action: [GET: "callback"]}
|
||||||
path("/auth"){action: [GET: "auth"]}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def authPage() {
|
def authPage() {
|
||||||
log.debug "In authPage"
|
log.debug "In authPage"
|
||||||
if(canInstallLabs()) {
|
|
||||||
def description = null
|
|
||||||
|
|
||||||
if (state.vendorAccessToken == null) {
|
def description
|
||||||
log.debug "About to create access token."
|
def uninstallAllowed = false
|
||||||
|
def oauthTokenProvided = false
|
||||||
|
|
||||||
createAccessToken()
|
if (!state.accessToken) {
|
||||||
description = "Tap to enter Credentials."
|
log.debug "About to create access token."
|
||||||
|
state.accessToken = createAccessToken()
|
||||||
|
}
|
||||||
|
|
||||||
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) {
|
if (canInstallLabs()) {
|
||||||
section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description }
|
|
||||||
|
def redirectUrl = getBuildRedirectUrl()
|
||||||
|
log.debug "Redirect url = ${redirectUrl}"
|
||||||
|
|
||||||
|
if (state.authToken) {
|
||||||
|
description = "Tap 'Next' to proceed"
|
||||||
|
uninstallAllowed = true
|
||||||
|
oauthTokenProvided = true
|
||||||
|
} else {
|
||||||
|
description = "Click to enter Credentials."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oauthTokenProvided) {
|
||||||
|
log.debug "Show the login page"
|
||||||
|
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
|
||||||
|
section() {
|
||||||
|
paragraph "Tap below to log in to the netatmo and authorize SmartThings access."
|
||||||
|
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
description = "Tap 'Next' to proceed"
|
log.debug "Show the devices page"
|
||||||
|
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
|
||||||
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) {
|
section() {
|
||||||
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description }
|
input(name:"Devices", style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||||
|
|
||||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||||
@@ -78,229 +99,175 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def auth() {
|
|
||||||
redirect location: oauthInitUrl()
|
|
||||||
}
|
|
||||||
|
|
||||||
def oauthInitUrl() {
|
def oauthInitUrl() {
|
||||||
log.debug "In oauthInitUrl"
|
log.debug "In oauthInitUrl"
|
||||||
|
|
||||||
/* OAuth Step 1: Request access code with our client ID */
|
|
||||||
|
|
||||||
state.oauthInitState = UUID.randomUUID().toString()
|
state.oauthInitState = UUID.randomUUID().toString()
|
||||||
|
|
||||||
def oauthParams = [ response_type: "code",
|
|
||||||
client_id: getClientId(),
|
|
||||||
state: state.oauthInitState,
|
|
||||||
redirect_uri: buildRedirectUrl("receiveToken") ,
|
|
||||||
scope: "read_station"
|
|
||||||
]
|
|
||||||
|
|
||||||
return getVendorAuthPath() + toQueryString(oauthParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
def buildRedirectUrl(endPoint) {
|
|
||||||
log.debug "In buildRedirectUrl"
|
|
||||||
|
|
||||||
return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def receiveToken() {
|
|
||||||
log.debug "In receiveToken"
|
|
||||||
|
|
||||||
def oauthParams = [
|
def oauthParams = [
|
||||||
client_secret: getClientSecret(),
|
response_type: "code",
|
||||||
client_id: getClientId(),
|
client_id: getClientId(),
|
||||||
grant_type: "authorization_code",
|
client_secret: getClientSecret(),
|
||||||
redirect_uri: buildRedirectUrl('receiveToken'),
|
state: state.oauthInitState,
|
||||||
code: params.code,
|
redirect_uri: getCallbackUrl(),
|
||||||
scope: "read_station"
|
scope: "read_station"
|
||||||
]
|
|
||||||
|
|
||||||
def tokenUrl = getVendorTokenPath()
|
|
||||||
def params = [
|
|
||||||
uri: tokenUrl,
|
|
||||||
contentType: 'application/x-www-form-urlencoded',
|
|
||||||
body: oauthParams,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
log.debug params
|
log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
||||||
|
|
||||||
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
|
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
|
||||||
try {
|
}
|
||||||
httpPost(params) { response ->
|
|
||||||
log.debug response.data
|
|
||||||
def slurper = new JsonSlurper();
|
|
||||||
|
|
||||||
response.data.each {key, value ->
|
def callback() {
|
||||||
def data = slurper.parseText(key);
|
log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||||
log.debug "Data: $data"
|
|
||||||
|
|
||||||
state.vendorRefreshToken = data.refresh_token
|
def code = params.code
|
||||||
state.vendorAccessToken = data.access_token
|
def oauthState = params.state
|
||||||
state.vendorTokenExpires = now() + (data.expires_in * 1000)
|
|
||||||
return
|
if (oauthState == state.oauthInitState) {
|
||||||
|
|
||||||
|
def tokenParams = [
|
||||||
|
client_secret: getClientSecret(),
|
||||||
|
client_id : getClientId(),
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
redirect_uri: getCallbackUrl(),
|
||||||
|
code: code,
|
||||||
|
scope: "read_station"
|
||||||
|
]
|
||||||
|
|
||||||
|
log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
||||||
|
|
||||||
|
def tokenUrl = getVendorTokenPath()
|
||||||
|
def params = [
|
||||||
|
uri: tokenUrl,
|
||||||
|
contentType: 'application/x-www-form-urlencoded',
|
||||||
|
body: tokenParams
|
||||||
|
]
|
||||||
|
|
||||||
|
log.debug "PARAMS: ${params}"
|
||||||
|
|
||||||
|
httpPost(params) { resp ->
|
||||||
|
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
|
||||||
|
resp.data.each { key, value ->
|
||||||
|
def data = slurper.parseText(key)
|
||||||
|
|
||||||
|
state.refreshToken = data.refresh_token
|
||||||
|
state.authToken = data.access_token
|
||||||
|
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||||
|
log.debug "swapped token: $resp.data"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug "Error: $e"
|
// Handle success and failure here, and render stuff accordingly
|
||||||
|
if (state.authToken) {
|
||||||
|
success()
|
||||||
|
} else {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.error "callback() failed oauthState != state.oauthInitState"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.debug "State: $state"
|
def success() {
|
||||||
|
log.debug "in success"
|
||||||
|
def message = """
|
||||||
|
<p>We have located your """ + getVendorName() + """ account.</p>
|
||||||
|
<p>Tap 'Done' to continue to Devices.</p>
|
||||||
|
"""
|
||||||
|
connectionStatus(message)
|
||||||
|
}
|
||||||
|
|
||||||
if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install
|
def fail() {
|
||||||
return
|
log.debug "in fail"
|
||||||
|
def message = """
|
||||||
|
<p>The connection could not be established!</p>
|
||||||
|
<p>Click 'Done' to return to the menu.</p>
|
||||||
|
"""
|
||||||
|
connectionStatus(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
def connectionStatus(message, redirectUrl = null) {
|
||||||
|
def redirectHtml = ""
|
||||||
|
if (redirectUrl) {
|
||||||
|
redirectHtml = """
|
||||||
|
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
|
|
||||||
|
|
||||||
def html = """
|
def html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>${getVendorName()} Connection</title>
|
<title>${getVendorName()} Connection</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Swiss 721 W01 Light';
|
font-family: 'Swiss 721 W01 Light';
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
/*background: #eee;*/
|
/*background: #eee;*/
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
img:nth-child(2) {
|
img:nth-child(2) {
|
||||||
margin: 0 30px;
|
margin: 0 30px;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
font-size: 2.2em;
|
font-size: 2.2em;
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
p:last-child {
|
p:last-child {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
span {
|
span {
|
||||||
font-family: 'Swiss 721 W01 Light';
|
font-family: 'Swiss 721 W01 Light';
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
|
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||||
<p>We have located your """ + getVendorName() + """ account.</p>
|
${message}
|
||||||
<p>Tap 'Done' to process your credentials.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
render contentType: 'text/html', data: html
|
render contentType: 'text/html', data: html
|
||||||
}
|
}
|
||||||
|
|
||||||
def receivedToken() {
|
|
||||||
log.debug "In receivedToken"
|
|
||||||
|
|
||||||
def html = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Withings Connection</title>
|
|
||||||
<style type="text/css">
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Swiss 721 W01 Light';
|
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
width: 560px;
|
|
||||||
padding: 40px;
|
|
||||||
/*background: #eee;*/
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
img:nth-child(2) {
|
|
||||||
margin: 0 30px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 2.2em;
|
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
|
||||||
text-align: center;
|
|
||||||
color: #666666;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
p:last-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
span {
|
|
||||||
font-family: 'Swiss 721 W01 Light';
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
|
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
|
||||||
<p>Tap 'Done' to continue to Devices.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
render contentType: 'text/html', data: html
|
|
||||||
}
|
|
||||||
|
|
||||||
// "
|
|
||||||
|
|
||||||
def refreshToken() {
|
def refreshToken() {
|
||||||
log.debug "In refreshToken"
|
log.debug "In refreshToken"
|
||||||
|
|
||||||
@@ -308,8 +275,8 @@ def refreshToken() {
|
|||||||
client_secret: getClientSecret(),
|
client_secret: getClientSecret(),
|
||||||
client_id: getClientId(),
|
client_id: getClientId(),
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: state.vendorRefreshToken
|
refresh_token: state.refreshToken
|
||||||
]
|
]
|
||||||
|
|
||||||
def tokenUrl = getVendorTokenPath()
|
def tokenUrl = getVendorTokenPath()
|
||||||
def params = [
|
def params = [
|
||||||
@@ -318,7 +285,7 @@ def refreshToken() {
|
|||||||
body: oauthParams,
|
body: oauthParams,
|
||||||
]
|
]
|
||||||
|
|
||||||
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
|
// OAuth Step 2: Request access token with our client Secret and OAuth "Code"
|
||||||
try {
|
try {
|
||||||
httpPost(params) { response ->
|
httpPost(params) { response ->
|
||||||
def slurper = new JsonSlurper();
|
def slurper = new JsonSlurper();
|
||||||
@@ -327,9 +294,9 @@ def refreshToken() {
|
|||||||
def data = slurper.parseText(key);
|
def data = slurper.parseText(key);
|
||||||
log.debug "Data: $data"
|
log.debug "Data: $data"
|
||||||
|
|
||||||
state.vendorRefreshToken = data.refresh_token
|
state.refreshToken = data.refresh_token
|
||||||
state.vendorAccessToken = data.access_token
|
state.accessToken = data.access_token
|
||||||
state.vendorTokenExpires = now() + (data.expires_in * 1000)
|
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,9 +305,8 @@ def refreshToken() {
|
|||||||
log.debug "Error: $e"
|
log.debug "Error: $e"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "State: $state"
|
// We didn't get an access token
|
||||||
|
if ( !state.accessToken ) {
|
||||||
if ( !state.vendorAccessToken ) { //We didn't get an access token
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,13 +448,13 @@ def listDevices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def apiGet(String path, Map query, Closure callback) {
|
def apiGet(String path, Map query, Closure callback) {
|
||||||
if(now() >= state.vendorTokenExpires) {
|
if(now() >= state.tokenExpires) {
|
||||||
refreshToken();
|
refreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
query['access_token'] = state.vendorAccessToken
|
query['access_token'] = state.accessToken
|
||||||
def params = [
|
def params = [
|
||||||
uri: apiUrl(),
|
uri: getApiUrl(),
|
||||||
path: path,
|
path: path,
|
||||||
'query': query
|
'query': query
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -370,9 +370,7 @@ def parse_api_response(resp, message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() {
|
def getServerUrl() { return getApiServerUrl() }
|
||||||
return "https://graph.api.smartthings.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
def debugEvent(message, displayEvent) {
|
def debugEvent(message, displayEvent) {
|
||||||
def results = [
|
def results = [
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ preferences()
|
|||||||
section("Allow Simple Control to Monitor and Control These Things...")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
|
page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
|
||||||
@@ -31,12 +37,17 @@ preferences()
|
|||||||
|
|
||||||
mappings {
|
mappings {
|
||||||
path("/devices") {
|
path("/devices") {
|
||||||
|
action: [
|
||||||
|
GET: "getDevices"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/:deviceType/devices") {
|
||||||
action: [
|
action: [
|
||||||
GET: "getDevices",
|
GET: "getDevices",
|
||||||
POST: "handleDevicesWithIDs"
|
POST: "handleDevicesWithIDs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
path("/device/:id") {
|
path("/device/:deviceType/:id") {
|
||||||
action: [
|
action: [
|
||||||
GET: "getDevice",
|
GET: "getDevice",
|
||||||
POST: "updateDevice"
|
POST: "updateDevice"
|
||||||
@@ -93,33 +104,40 @@ def handleDevicesWithIDs()
|
|||||||
//log.debug("ids: ${ids}")
|
//log.debug("ids: ${ids}")
|
||||||
def command = data?.command
|
def command = data?.command
|
||||||
def arguments = data?.arguments
|
def arguments = data?.arguments
|
||||||
|
def type = params?.deviceType
|
||||||
|
//log.debug("device type: ${type}")
|
||||||
if (command)
|
if (command)
|
||||||
{
|
{
|
||||||
def success = false
|
def statusCode = 404
|
||||||
//log.debug("command ${command}, arguments ${arguments}")
|
//log.debug("command ${command}, arguments ${arguments}")
|
||||||
for (devId in ids)
|
for (devId in ids)
|
||||||
{
|
{
|
||||||
def device = allDevices.find { it.id == devId }
|
def device = allDevices.find { it.id == devId }
|
||||||
if (device) {
|
//log.debug("device: ${device}")
|
||||||
if (arguments) {
|
// Check if we have a device that responds to the specified command
|
||||||
|
if (validateCommand(device, type, command)) {
|
||||||
|
if (arguments) {
|
||||||
device."$command"(*arguments)
|
device."$command"(*arguments)
|
||||||
} else {
|
}
|
||||||
device."$command"()
|
else {
|
||||||
}
|
device."$command"()
|
||||||
success = true
|
}
|
||||||
|
statusCode = 200
|
||||||
} else {
|
} else {
|
||||||
//log.debug("device not found ${devId}")
|
statusCode = 403
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
def responseData = "{}"
|
||||||
if (success)
|
switch (statusCode)
|
||||||
{
|
{
|
||||||
render status: 200, data: "{}"
|
case 403:
|
||||||
}
|
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
|
||||||
else
|
break
|
||||||
{
|
case 404:
|
||||||
render status: 404, data: '{"msg": "Device not found"}'
|
responseData = '{"msg": "Device not found"}'
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
render status: statusCode, data: responseData
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -164,25 +182,101 @@ def updateDevice()
|
|||||||
def data = request.JSON
|
def data = request.JSON
|
||||||
def command = data?.command
|
def command = data?.command
|
||||||
def arguments = data?.arguments
|
def arguments = data?.arguments
|
||||||
|
def type = params?.deviceType
|
||||||
|
//log.debug("device type: ${type}")
|
||||||
|
|
||||||
//log.debug("updateDevice, params: ${params}, request: ${data}")
|
//log.debug("updateDevice, params: ${params}, request: ${data}")
|
||||||
if (!command) {
|
if (!command) {
|
||||||
render status: 400, data: '{"msg": "command is required"}'
|
render status: 400, data: '{"msg": "command is required"}'
|
||||||
} else {
|
} else {
|
||||||
|
def statusCode = 404
|
||||||
def device = allDevices.find { it.id == params.id }
|
def device = allDevices.find { it.id == params.id }
|
||||||
if (device) {
|
if (device) {
|
||||||
if (arguments) {
|
// Check if we have a device that responds to the specified command
|
||||||
device."$command"(*arguments)
|
if (validateCommand(device, type, command)) {
|
||||||
|
if (arguments) {
|
||||||
|
device."$command"(*arguments)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
device."$command"()
|
||||||
|
}
|
||||||
|
statusCode = 200
|
||||||
} else {
|
} 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()
|
def listSubscriptions()
|
||||||
{
|
{
|
||||||
//log.debug "listSubscriptions()"
|
//log.debug "listSubscriptions()"
|
||||||
@@ -361,7 +455,13 @@ def agentDiscovery(params=[:])
|
|||||||
}
|
}
|
||||||
section("Allow Simple Control to Monitor and Control These Things...")
|
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()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -297,8 +297,8 @@ private getTimeOk() {
|
|||||||
def result = true
|
def result = true
|
||||||
if (starting && ending) {
|
if (starting && ending) {
|
||||||
def currTime = now()
|
def currTime = now()
|
||||||
def start = timeToday(starting).time
|
def start = timeToday(starting, location.timeZone).time
|
||||||
def stop = timeToday(ending).time
|
def stop = timeToday(ending, location.timeZone).time
|
||||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||||
}
|
}
|
||||||
log.trace "timeOk = $result"
|
log.trace "timeOk = $result"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -323,6 +335,8 @@ private getDeviceType(hueType) {
|
|||||||
return "Hue Bulb"
|
return "Hue Bulb"
|
||||||
else if (hueType?.equalsIgnoreCase("Color Light"))
|
else if (hueType?.equalsIgnoreCase("Color Light"))
|
||||||
return "Hue Bloom"
|
return "Hue Bloom"
|
||||||
|
else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
|
||||||
|
return "Hue White Ambiance Bulb"
|
||||||
else
|
else
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -346,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,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)
|
||||||
@@ -484,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
|
|||||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||||
} else {
|
} else {
|
||||||
log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch"
|
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()
|
switch1?.on()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
|
|||||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||||
} else {
|
} else {
|
||||||
log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
|
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()
|
switch1?.on()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ definition(
|
|||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
appSetting "serverUrl"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -192,7 +191,7 @@ def getSmartThingsClientId() {
|
|||||||
return "pREqugabRetre4EstetherufrePumamExucrEHuc"
|
return "pREqugabRetre4EstetherufrePumamExucrEHuc"
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() { appSettings.serverUrl }
|
def getServerUrl() { getApiServerUrl() }
|
||||||
|
|
||||||
def buildRedirectUrl()
|
def buildRedirectUrl()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ def initialize() {
|
|||||||
state.aux = 0
|
state.aux = 0
|
||||||
if (selectedhubs || selectedactivities) {
|
if (selectedhubs || selectedactivities) {
|
||||||
addDevice()
|
addDevice()
|
||||||
runEvery5Minutes("discovery")
|
runEvery5Minutes("poll")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +394,9 @@ def discovery() {
|
|||||||
}
|
}
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||||
state.resethub = true
|
state.resethub = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
log.info "Logitech Harmony - Error: $e"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ def activity(dni,mode) {
|
|||||||
def poll() {
|
def poll() {
|
||||||
// GET THE LIST OF ACTIVITIES
|
// GET THE LIST OF ACTIVITIES
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
getActivityList()
|
getActivityList()
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||||
try {
|
try {
|
||||||
@@ -520,14 +520,17 @@ def poll() {
|
|||||||
return "Poll completed $map - $state.hubs"
|
return "Poll completed $map - $state.hubs"
|
||||||
}
|
}
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
if (e.statusCode == 401) { // token is expired
|
if (e.statusCode == 401) { // token is expired
|
||||||
state.remove("HarmonyAccessToken")
|
state.remove("HarmonyAccessToken")
|
||||||
return "Harmony Access token has expired"
|
log.warn "Harmony Access token has expired"
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.trace 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") {
|
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) {
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ def scheduleCheck(evt) {
|
|||||||
else {
|
else {
|
||||||
if(people){
|
if(people){
|
||||||
//don't turn off lights if anyone is home
|
//don't turn off lights if anyone is home
|
||||||
if(someoneIsHome()){
|
if(someoneIsHome){
|
||||||
log.debug("Stopping Check for Light")
|
log.debug("Stopping Check for Light")
|
||||||
unschedule()
|
unschedule()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,22 +92,87 @@ void updateLock() {
|
|||||||
|
|
||||||
private void updateAll(devices) {
|
private void updateAll(devices) {
|
||||||
def command = request.JSON?.command
|
def command = request.JSON?.command
|
||||||
if (command) {
|
def type = params.param1
|
||||||
devices."$command"()
|
if (!devices) {
|
||||||
|
httpError(404, "Devices not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 command = request.JSON?.command
|
||||||
if (command) {
|
def type = params.param1
|
||||||
def device = devices.find { it.id == params.id }
|
def device = devices?.find { it.id == params.id }
|
||||||
if (!device) {
|
|
||||||
httpError(404, "Device not found")
|
if (!device) {
|
||||||
} else {
|
httpError(404, "Device not found")
|
||||||
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, name) {
|
private show(devices, name) {
|
||||||
|
|||||||
Reference in New Issue
Block a user