mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-10 13:21:52 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1241802315 | ||
|
|
0414ae0460 | ||
|
|
67006204da | ||
|
|
32e3916a85 | ||
|
|
dc1071d132 | ||
|
|
94f8b4ecfc | ||
|
|
239f771ac1 | ||
|
|
87b6715a00 | ||
|
|
d6a96317bf | ||
|
|
6d64212c93 | ||
|
|
088e746f99 | ||
|
|
c26701383e | ||
|
|
0b8f1d0168 | ||
|
|
b78337c96b | ||
|
|
9fcd327da2 | ||
|
|
c3ce69994e | ||
|
|
5bd03d1914 | ||
|
|
950780d30c | ||
|
|
8040ddd6f7 | ||
|
|
4863b2345e | ||
|
|
32b4914ba0 | ||
|
|
c76a2e807b | ||
|
|
617d53da43 | ||
|
|
29f315ac33 | ||
|
|
2baac34ba3 | ||
|
|
811a1af4bf | ||
|
|
b765b46c50 | ||
|
|
6eac6affcf | ||
|
|
d2cecf6908 | ||
|
|
61d2aac45a | ||
|
|
b5d0a5e74b | ||
|
|
1c2189b63c | ||
|
|
1adb4000a6 | ||
|
|
bad978afbd | ||
|
|
80b4d6a665 | ||
|
|
f51d6542b8 | ||
|
|
7baad1c35e | ||
|
|
621bcfadd2 | ||
|
|
83bbaebef2 | ||
|
|
bde5abcdb5 | ||
|
|
99e48dbeed | ||
|
|
2ee1b26a7f | ||
|
|
a7ed8f4afe | ||
|
|
148ee3521d | ||
|
|
d7490a086a | ||
|
|
189bec58db | ||
|
|
7e26fd1040 | ||
|
|
a244808073 | ||
|
|
33f1209c80 | ||
|
|
d977c4d46b | ||
|
|
01b2f57d7a | ||
|
|
ab00d703bf | ||
|
|
6c3a7886ed | ||
|
|
5719bbcaac | ||
|
|
324d9bf780 | ||
|
|
eb8d861c6c | ||
|
|
99a4d75e4b | ||
|
|
d601484398 | ||
|
|
a3fbc8ebd8 | ||
|
|
bfc14ffd9e | ||
|
|
1c327d1433 | ||
|
|
deee914573 | ||
|
|
f6b541c30f | ||
|
|
c00fbd3652 | ||
|
|
76f056180d | ||
|
|
c39b63b944 | ||
|
|
40bf47ec0b | ||
|
|
dc2ac4bedc | ||
|
|
0cf90064ec | ||
|
|
0497660ab5 | ||
|
|
6005f7266e | ||
|
|
5d38cabe75 | ||
|
|
29f94ee6ac | ||
|
|
8929673ff0 | ||
|
|
94228c258a | ||
|
|
1b424a8ea8 | ||
|
|
35a7a79073 | ||
|
|
8dd3b2396f | ||
|
|
39d2def035 | ||
|
|
e1a9f2f761 | ||
|
|
37eb8cc0a1 | ||
|
|
402b0be80b | ||
|
|
a761590322 | ||
|
|
8d4a73cd30 | ||
|
|
4bf2b55c26 | ||
|
|
13fcbfcdc7 | ||
|
|
288a4e99fa | ||
|
|
55a3842b00 | ||
|
|
a25fb6d84d | ||
|
|
d9a2d8109e | ||
|
|
0376a5823f | ||
|
|
78e25c6958 | ||
|
|
7e5d6e99d1 | ||
|
|
f5e3a16ad1 | ||
|
|
c361387bf8 | ||
|
|
f0c6e085eb | ||
|
|
67a1bd1256 | ||
|
|
32182ae13b | ||
|
|
e56086aac3 | ||
|
|
6b142622db | ||
|
|
8d07e81b80 | ||
|
|
0fcff53eba |
@@ -56,7 +56,7 @@ metadata {
|
||||
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
|
||||
}
|
||||
|
||||
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
|
||||
graphTile(name: "powerGraph", attribute: "device.power")
|
||||
|
||||
main(["power", "energy"])
|
||||
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||
@@ -68,16 +68,8 @@ metadata {
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
|
||||
}
|
||||
|
||||
def selectedGraphPrecision() {
|
||||
graphPrecision ?: "Daily"
|
||||
}
|
||||
|
||||
def selectedGraphType() {
|
||||
graphType ?: "line"
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -91,22 +83,6 @@ mappings {
|
||||
GET: "renderGraph"
|
||||
]
|
||||
}
|
||||
path("/graphDataSizes") { // for testing. remove before publishing
|
||||
action:
|
||||
[
|
||||
GET: "graphDataSizes"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def graphDataSizes() { // for testing. remove before publishing
|
||||
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
|
||||
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
|
||||
dateTypes[dateTypeData.key] = dateTypeData.value.size()
|
||||
dateTypes
|
||||
}
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -121,8 +97,7 @@ def parse(String description) {
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
|
||||
PLATFORM_migrateGraphDataIfNeeded()
|
||||
PLATFORM_storeData(result.name, result.value)
|
||||
storeGraphData(result.name, result.value)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -176,535 +151,15 @@ def configure() {
|
||||
|
||||
def renderGraph() {
|
||||
|
||||
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||
def data = fetchGraphData(params.attribute)
|
||||
|
||||
def totalData = data*.runningSum
|
||||
|
||||
def xValues = data*.unixTime
|
||||
|
||||
def yValues = [
|
||||
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
|
||||
Total: [color: "#49a201", data: totalData]
|
||||
]
|
||||
|
||||
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
// TODO: // ========================================================
|
||||
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
|
||||
// TODO: // ========================================================
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM TILES
|
||||
// ========================================================
|
||||
|
||||
def PLATFORM_graphTile(Map tileParams) {
|
||||
def cleanAttribute = tileParams.attribute - "device." - "capability."
|
||||
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH RENDERING
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphTypeOptions() {
|
||||
[
|
||||
"line", // DEFAULT
|
||||
"spline",
|
||||
"step",
|
||||
"area",
|
||||
"area-spline",
|
||||
"area-step",
|
||||
"bar",
|
||||
"scatter",
|
||||
"pie",
|
||||
"donut",
|
||||
"gauge",
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_renderGraph(graphParams) {
|
||||
|
||||
String attribute = graphParams.attribute
|
||||
List xValues = graphParams.xValues
|
||||
Map yValues = graphParams.yValues
|
||||
String focus = graphParams.focus ?: ""
|
||||
String label = graphParams.label ?: ""
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
|
||||
Available type values:
|
||||
line // DEFAULT
|
||||
spline
|
||||
step
|
||||
area
|
||||
area-spline
|
||||
area-step
|
||||
bar
|
||||
scatter
|
||||
pie
|
||||
donut
|
||||
gauge
|
||||
|
||||
*/
|
||||
|
||||
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
|
||||
|
||||
def legendData = yValues*.key
|
||||
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
|
||||
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
|
||||
|
||||
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
|
||||
renderHTML(htmlTitle) { html ->
|
||||
html.head {
|
||||
"""
|
||||
<!-- Load c3.css -->
|
||||
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Load d3.js and c3.js -->
|
||||
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
|
||||
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
|
||||
|
||||
<script>
|
||||
function getDocumentHeight() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientHeight;
|
||||
}
|
||||
function getDocumentWidth() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientWidth;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.legend {
|
||||
position: absolute;
|
||||
width: 80%;
|
||||
padding-left: 15%;
|
||||
z-index: 999;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.legend span {
|
||||
width: ${100 / yValues.size()}%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
html.body {
|
||||
"""
|
||||
<div class="legend"></div>
|
||||
<div id="chart" style="max-height: 120px; position: relative;"></div>
|
||||
|
||||
<script>
|
||||
|
||||
// Generate the chart
|
||||
var chart = c3.generate(${graphData as grails.converters.JSON});
|
||||
|
||||
// Resize the chart to the size of the device tile
|
||||
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
|
||||
|
||||
// Focus data if specified
|
||||
${focusJS}
|
||||
|
||||
// Update the chart when ${attribute} events are received
|
||||
function ${attribute}(evt) {
|
||||
var newValue = ['${flowColumn}'];
|
||||
newValue.push(evt.value);
|
||||
|
||||
var newX = ['x'];
|
||||
newX.push(evt.unixTime);
|
||||
|
||||
chart.flow({
|
||||
columns: [
|
||||
newX,
|
||||
newValue
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Build the custom legend
|
||||
d3.select('.legend').selectAll('span')
|
||||
.data(${legendData as grails.converters.JSON})
|
||||
.enter().append('span')
|
||||
.attr('data-id', function (id) { return id; })
|
||||
.html(function (id) { return id; })
|
||||
.each(function (id) {
|
||||
d3.select(this).style('background-color', chart.color(id));
|
||||
})
|
||||
.on('mouseover', function (id) {
|
||||
chart.focus(id);
|
||||
})
|
||||
.on('mouseout', function (id) {
|
||||
chart.revert();
|
||||
})
|
||||
.on('click', function (id) {
|
||||
chart.toggle(id);
|
||||
});
|
||||
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
*/
|
||||
|
||||
[
|
||||
interaction: [
|
||||
enabled: false
|
||||
],
|
||||
bindto : '#chart',
|
||||
padding : [
|
||||
left : 30,
|
||||
right : 30,
|
||||
bottom: 0,
|
||||
top : 0
|
||||
],
|
||||
legend : [
|
||||
show: false,
|
||||
// hide : false,//(yValues.keySet().size() < 2),
|
||||
// position: 'inset',
|
||||
// inset: [
|
||||
// anchor: "top-right"
|
||||
// ],
|
||||
// item: [
|
||||
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
|
||||
// ]
|
||||
],
|
||||
data : [
|
||||
x : "x",
|
||||
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
|
||||
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
|
||||
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
|
||||
],
|
||||
axis : [
|
||||
x: [
|
||||
type: 'timeseries',
|
||||
tick: [
|
||||
centered: true,
|
||||
culling : [max: 7],
|
||||
fit : true,
|
||||
format : PLATFORM_getGraphDateFormat()
|
||||
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
|
||||
]
|
||||
],
|
||||
y: [
|
||||
label : label,
|
||||
padding: [
|
||||
top: 50
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
|
||||
// https://github.com/mbostock/d3/wiki/Time-Formatting
|
||||
def graphDateFormat
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Hourly":
|
||||
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFormat = "%a" // abbreviated weekday name
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFormat = "%b" // abbreviated month name
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFormat = "%y" // year without century as a decimal number [00,99]
|
||||
break
|
||||
}
|
||||
graphDateFormat
|
||||
}
|
||||
|
||||
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
|
||||
def graphDateFunction = "function(date) { return date; }"
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getMinutes();
|
||||
}
|
||||
"""
|
||||
break;
|
||||
case "Hourly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var hour = date.getHours();
|
||||
if (hour == 0) {
|
||||
return String(/12 am/).substring(1).slice(0,-1);
|
||||
} else if (hour > 12) {
|
||||
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
|
||||
} else {
|
||||
return hour + String(/ am/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFunction = """ function(date) {
|
||||
var day = date.getDay();
|
||||
switch(day) {
|
||||
case 0: return String(/Sun/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Mon/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Tue/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Wed/).substring(1).slice(0,-1);
|
||||
case 4: return String(/Thu/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Fri/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Sat/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var month = date.getMonth();
|
||||
switch(month) {
|
||||
case 0: return String(/Jan/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Feb/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Mar/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Apr/).substring(1).slice(0,-1);
|
||||
case 4: return String(/May/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Jun/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Jul/).substring(1).slice(0,-1);
|
||||
case 7: return String(/Aug/).substring(1).slice(0,-1);
|
||||
case 8: return String(/Sep/).substring(1).slice(0,-1);
|
||||
case 9: return String(/Oct/).substring(1).slice(0,-1);
|
||||
case 10: return String(/Nov/).substring(1).slice(0,-1);
|
||||
case 11: return String(/Dec/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getFullYear();
|
||||
}
|
||||
"""
|
||||
break
|
||||
}
|
||||
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
|
||||
}
|
||||
|
||||
private jsEscapeString(str = "") {
|
||||
"String(/${str}/).substring(1).slice(0,-1);"
|
||||
}
|
||||
|
||||
private PLATFORM_fetchGraphData(attribute) {
|
||||
|
||||
log.debug "PLATFORM_fetchGraphData(${attribute})"
|
||||
|
||||
/*
|
||||
[
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
def attributeBucket = state["measure.${attribute}"] ?: [:]
|
||||
def dateType = selectedGraphPrecision()
|
||||
attributeBucket[dateType]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM DATA STORAGE
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
|
||||
|
||||
private PLATFORM_storeData(attribute, value) {
|
||||
PLATFORM_graphPrecisionOptions().each { dateType ->
|
||||
PLATFORM_addDataToBucket(attribute, value, dateType)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[
|
||||
Hourly: [
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
private PLATFORM_addDataToBucket(attribute, value, dateType) {
|
||||
|
||||
def numberValue = value.toBigDecimal()
|
||||
|
||||
def attributeKey = "measure.${attribute}"
|
||||
def attributeBucket = state[attributeKey] ?: [:]
|
||||
|
||||
def dateTypeBucket = attributeBucket[dateType] ?: []
|
||||
|
||||
def now = new Date()
|
||||
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
|
||||
def item = dateTypeBucket.find { it.dateString == itemDateString }
|
||||
|
||||
if (!item) {
|
||||
// no entry for this data point yet, fill with initial values
|
||||
item = [:]
|
||||
item.average = numberValue
|
||||
item.runningSum = numberValue
|
||||
item.runningCount = 1
|
||||
item.min = numberValue
|
||||
item.max = numberValue
|
||||
item.unixTime = now.getTime()
|
||||
item.dateString = itemDateString
|
||||
|
||||
// add the new data point
|
||||
dateTypeBucket << item
|
||||
|
||||
// clear out old data points
|
||||
def old = PLATFORM_getOldDateString(dateType)
|
||||
if (old) { // annual data never gets cleared
|
||||
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
|
||||
}
|
||||
|
||||
// limit the size of the bucket. Live data can stack up fast
|
||||
def sizeLimit = 25
|
||||
if (dateTypeBucket.size() > sizeLimit) {
|
||||
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
|
||||
}
|
||||
|
||||
} else {
|
||||
//re-calculate average/min/max for this bucket
|
||||
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
|
||||
item.runningCount = item.runningCount.toInteger() + 1
|
||||
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
|
||||
|
||||
if (item.min == null) {
|
||||
item.min = numberValue
|
||||
} else if (numberValue < item.min.toBigDecimal()) {
|
||||
item.min = numberValue
|
||||
}
|
||||
if (item.max == null) {
|
||||
item.max = numberValue
|
||||
} else if (numberValue > item.max.toBigDecimal()) {
|
||||
item.max = numberValue
|
||||
}
|
||||
}
|
||||
|
||||
attributeBucket[dateType] = dateTypeBucket
|
||||
state[attributeKey] = attributeBucket
|
||||
}
|
||||
|
||||
private PLATFORM_getOldDateString(dateType) {
|
||||
def now = new Date()
|
||||
def date
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
|
||||
break
|
||||
case "Hourly":
|
||||
date = (now - 1).getTime()
|
||||
break
|
||||
case "Daily":
|
||||
date = (now - 10).getTime()
|
||||
break
|
||||
case "Monthly":
|
||||
date = (now - 30).getTime()
|
||||
break
|
||||
case "Annually":
|
||||
break
|
||||
}
|
||||
date
|
||||
}
|
||||
|
||||
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
|
||||
|
||||
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
|
||||
|
||||
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
|
||||
|
||||
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
|
||||
|
||||
private PLATFORM_getAnnuallyFormat() { "yyyy" }
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH DATA MIGRATION
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_migrateGraphDataIfNeeded() {
|
||||
if (!state.hasMigratedOldGraphData) {
|
||||
def acceptableKeys = PLATFORM_graphPrecisionOptions()
|
||||
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
|
||||
needsMigration.each { PLATFORM_migrateGraphData(it) }
|
||||
state.hasMigratedOldGraphData = true
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_migrateGraphData(attribute) {
|
||||
|
||||
log.trace "about to migrate ${attribute}"
|
||||
|
||||
def attributeBucket = state[attribute] ?: [:]
|
||||
def migratedAttributeBucket = [:]
|
||||
|
||||
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
|
||||
|
||||
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
|
||||
"PLATFORM_get${it}Format"().size() == oldDateString.size()
|
||||
}
|
||||
|
||||
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
|
||||
|
||||
def newBucket = attributeBucket[dateType] ?: []
|
||||
/*
|
||||
def existingNewItem = newBucket.find { it.dateString == oldDateString }
|
||||
if (existingNewItem) {
|
||||
newBucket.remove(existingNewItem)
|
||||
}
|
||||
*/
|
||||
|
||||
def newItem = [
|
||||
min : oldItem.min,
|
||||
max : oldItem.max,
|
||||
average : oldItem.average,
|
||||
runningSum : oldItem.runningSum,
|
||||
runningCount: oldItem.runningCount,
|
||||
dateString : oldDateString,
|
||||
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
|
||||
]
|
||||
|
||||
newBucket << newItem
|
||||
migratedAttributeBucket[dateType] = newBucket
|
||||
}
|
||||
|
||||
state[attribute] = migratedAttributeBucket
|
||||
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Signal Strength"
|
||||
@@ -34,8 +34,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
/**
|
||||
* Bose SoundTouch
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Needed to be able to serialize the XmlSlurper data back to XML
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
|
||||
/**
|
||||
* List our capabilties. Doing so adds predefined command(s) which
|
||||
* belong to the capability.
|
||||
*/
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Music Player"
|
||||
capability "Polling"
|
||||
|
||||
/**
|
||||
* Define all commands, ie, if you have a custom action not
|
||||
* covered by a capability, you NEED to define it here or
|
||||
* the call will not be made.
|
||||
*
|
||||
* To call a capability function, just prefix it with the name
|
||||
* of the capability, for example, refresh would be "refresh.refresh"
|
||||
*/
|
||||
command "preset1"
|
||||
command "preset2"
|
||||
command "preset3"
|
||||
command "preset4"
|
||||
command "preset5"
|
||||
command "preset6"
|
||||
command "aux"
|
||||
|
||||
command "everywhereJoin"
|
||||
command "everywhereLeave"
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the various tiles and the states that they can be in.
|
||||
* The 2nd parameter defines an event which the tile listens to,
|
||||
* if received, it tries to map it to a state.
|
||||
*
|
||||
* You can also use ${currentValue} for the value of the event
|
||||
* or ${name} for the name of the event. Just make SURE to use
|
||||
* single quotes, otherwise it will only be interpreted at time of
|
||||
* launch, instead of every time the event triggers.
|
||||
*/
|
||||
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
|
||||
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff"
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821"
|
||||
}
|
||||
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
|
||||
state "station1", label:'${currentValue}', action:"preset1"
|
||||
}
|
||||
valueTile("2", "device.station2", decoration: "flat", canChangeIcon: false) {
|
||||
state "station2", label:'${currentValue}', action:"preset2"
|
||||
}
|
||||
valueTile("3", "device.station3", decoration: "flat", canChangeIcon: false) {
|
||||
state "station3", label:'${currentValue}', action:"preset3"
|
||||
}
|
||||
valueTile("4", "device.station4", decoration: "flat", canChangeIcon: false) {
|
||||
state "station4", label:'${currentValue}', action:"preset4"
|
||||
}
|
||||
valueTile("5", "device.station5", decoration: "flat", canChangeIcon: false) {
|
||||
state "station5", label:'${currentValue}', action:"preset5"
|
||||
}
|
||||
valueTile("6", "device.station6", decoration: "flat", canChangeIcon: false) {
|
||||
state "station6", label:'${currentValue}', action:"preset6"
|
||||
}
|
||||
valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'Auxillary\nInput', action:"aux"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") {
|
||||
state "volume", action:"music Player.setLevel"
|
||||
}
|
||||
|
||||
standardTile("playpause", "device.playpause", decoration: "flat") {
|
||||
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
|
||||
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
|
||||
}
|
||||
|
||||
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn"
|
||||
}
|
||||
standardTile("next", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn"
|
||||
}
|
||||
|
||||
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
|
||||
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
|
||||
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
|
||||
// Final state is used if the device is in a state where joining is not possible
|
||||
state "unavailable", label:"Not Available"
|
||||
}
|
||||
|
||||
// Defines which tile to show in the overview
|
||||
main "switch"
|
||||
|
||||
// Defines which tile(s) to show when user opens the detailed view
|
||||
details ([
|
||||
"nowplaying", "refresh", // Row 1 (112)
|
||||
"prev", "playpause", "next", // Row 2 (123)
|
||||
"volume", // Row 3 (111)
|
||||
"1", "2", "3", // Row 4 (123)
|
||||
"4", "5", "6", // Row 5 (123)
|
||||
"aux", "everywhere"]) // Row 6 (122)
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* The following section simply maps the actions as defined in
|
||||
* the metadata into onAction() calls.
|
||||
*
|
||||
* This is preferred since some actions can be dealt with more
|
||||
* efficiently this way. Also keeps all user interaction code in
|
||||
* one place.
|
||||
*
|
||||
*/
|
||||
def off() { onAction("off") }
|
||||
def on() { onAction("on") }
|
||||
def volup() { onAction("volup") }
|
||||
def voldown() { onAction("voldown") }
|
||||
def preset1() { onAction("1") }
|
||||
def preset2() { onAction("2") }
|
||||
def preset3() { onAction("3") }
|
||||
def preset4() { onAction("4") }
|
||||
def preset5() { onAction("5") }
|
||||
def preset6() { onAction("6") }
|
||||
def aux() { onAction("aux") }
|
||||
def refresh() { onAction("refresh") }
|
||||
def setLevel(level) { onAction("volume", level) }
|
||||
def play() { onAction("play") }
|
||||
def pause() { onAction("pause") }
|
||||
def mute() { onAction("mute") }
|
||||
def unmute() { onAction("unmute") }
|
||||
def previousTrack() { onAction("previous") }
|
||||
def nextTrack() { onAction("next") }
|
||||
def everywhereJoin() { onAction("ejoin") }
|
||||
def everywhereLeave() { onAction("eleave") }
|
||||
/**************************************************************************/
|
||||
|
||||
/**
|
||||
* Main point of interaction with things.
|
||||
* This function is called by SmartThings Cloud with the resulting data from
|
||||
* any action (see HubAction()).
|
||||
*
|
||||
* Conversely, to execute any actions, you need to return them as a single
|
||||
* item or a list (flattened).
|
||||
*
|
||||
* @param data Data provided by the cloud
|
||||
* @return an action or a list() of actions. Can also return null if no further
|
||||
* action is desired at this point.
|
||||
*/
|
||||
def parse(String event) {
|
||||
def data = parseLanMessage(event)
|
||||
def actions = []
|
||||
|
||||
// List of permanent root node handlers
|
||||
def handlers = [
|
||||
"nowPlaying" : "boseParseNowPlaying",
|
||||
"volume" : "boseParseVolume",
|
||||
"presets" : "boseParsePresets",
|
||||
"zone" : "boseParseEverywhere",
|
||||
]
|
||||
|
||||
// No need to deal with non-XML data
|
||||
if (!data.headers || !data.headers?."content-type".contains("xml"))
|
||||
return null
|
||||
|
||||
// Move any pending callbacks into ready state
|
||||
prepareCallbacks()
|
||||
|
||||
def xml = new XmlSlurper().parseText(data.body)
|
||||
// Let each parser take a stab at it
|
||||
handlers.each { node,func ->
|
||||
if (xml.name() == node)
|
||||
actions << "$func"(xml)
|
||||
}
|
||||
// If we have callbacks waiting for this...
|
||||
actions << processCallbacks(xml)
|
||||
|
||||
// Be nice and helpful
|
||||
if (actions.size() == 0) {
|
||||
log.warn "parse(): Unhandled data = " + lan
|
||||
return null
|
||||
}
|
||||
|
||||
// Issue new actions
|
||||
return actions.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the devicetype is first installed.
|
||||
*
|
||||
* @return action(s) to take or null
|
||||
*/
|
||||
def installed() {
|
||||
onAction("refresh")
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for dealing with user input and taking the
|
||||
* appropiate action.
|
||||
*
|
||||
* @param user The user interaction
|
||||
* @param data Additional data (optional)
|
||||
* @return action(s) to take (or null if none)
|
||||
*/
|
||||
def onAction(String user, data=null) {
|
||||
log.info "onAction(${user})"
|
||||
|
||||
// Keep IP address current (since device may have changed)
|
||||
state.address = parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
|
||||
// Process action
|
||||
def actions = null
|
||||
switch (user) {
|
||||
case "on":
|
||||
actions = boseSetPowerState(true)
|
||||
break
|
||||
case "off":
|
||||
boseSetNowPlaying(null, "STANDBY")
|
||||
actions = boseSetPowerState(false)
|
||||
break
|
||||
case "volume":
|
||||
actions = boseSetVolume(data)
|
||||
break
|
||||
case "aux":
|
||||
boseSetNowPlaying(null, "AUX")
|
||||
boseZoneReset()
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
actions = boseSetInput(user)
|
||||
break
|
||||
case "refresh":
|
||||
boseSetNowPlaying(null, "REFRESH")
|
||||
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
|
||||
break
|
||||
case "play":
|
||||
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "pause":
|
||||
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "previous":
|
||||
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "next":
|
||||
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "mute":
|
||||
actions = boseSetMute(true)
|
||||
break
|
||||
case "unmute":
|
||||
actions = boseSetMute(false)
|
||||
break
|
||||
case "ejoin":
|
||||
actions = boseZoneJoin()
|
||||
break
|
||||
case "eleave":
|
||||
actions = boseZoneLeave()
|
||||
break
|
||||
default:
|
||||
log.error "Unhandled action: " + user
|
||||
}
|
||||
|
||||
// Make sure we don't have nested lists
|
||||
if (actions instanceof List)
|
||||
return actions.flatten()
|
||||
return actions
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every so often (every 5 minutes actually) to refresh the
|
||||
* tiles so the user gets the correct information.
|
||||
*/
|
||||
def poll() {
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins this speaker into the everywhere zone
|
||||
*/
|
||||
def boseZoneJoin() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneJoin(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker from the everywhere zone
|
||||
*/
|
||||
def boseZoneLeave() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneLeave(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker and any children WITHOUT
|
||||
* signaling the speakers themselves. This is needed
|
||||
* in certain cases where we know the user action will
|
||||
* cause the zone to collapse (for example, AUX)
|
||||
*/
|
||||
def boseZoneReset() {
|
||||
parent.boseZoneReset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles <nowPlaying></nowPlaying> information and can also
|
||||
* perform addtional actions if there is a pending command
|
||||
* stored in the state variable. For example, the power is
|
||||
* handled this way.
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseNowPlaying(xmlData) {
|
||||
def result = []
|
||||
|
||||
// Perform display update, allow it to add additional commands
|
||||
if (boseSetNowPlaying(xmlData)) {
|
||||
result << boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses volume data
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseVolume(xmlData) {
|
||||
def result = []
|
||||
|
||||
sendEvent(name:"volume", value:xmlData.actualvolume.text())
|
||||
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the result of the boseGetEverywhereState() call
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
def boseParseEverywhere(xmlData) {
|
||||
// No good way of detecting the correct state right now
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses presets and updates the buttons
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParsePresets(xmlData) {
|
||||
def result = []
|
||||
|
||||
state.preset = [:]
|
||||
|
||||
def missing = ["1", "2", "3", "4", "5", "6"]
|
||||
for (preset in xmlData.preset) {
|
||||
def id = preset.attributes()['id']
|
||||
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
|
||||
if (name == "##TRANS_SONGS##")
|
||||
name = "Local\nPlaylist"
|
||||
sendEvent(name:"station${id}", value:name)
|
||||
missing = missing.findAll { it -> it != id }
|
||||
|
||||
// Store the presets into the state for recall later
|
||||
state.preset["$id"] = XmlUtil.serialize(preset.ContentItem)
|
||||
}
|
||||
|
||||
for (id in missing) {
|
||||
state.preset["$id"] = null
|
||||
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on <nowPlaying></nowPlaying>, updates the visual
|
||||
* representation of the speaker
|
||||
*
|
||||
* @param xmlData The nowPlaying info
|
||||
* @param override Provide the source type manually (optional)
|
||||
*
|
||||
* @return true if it would prefer a refresh soon
|
||||
*/
|
||||
def boseSetNowPlaying(xmlData, override=null) {
|
||||
def needrefresh = false
|
||||
def nowplaying = null
|
||||
|
||||
if (xmlData && xmlData.playStatus) {
|
||||
switch(xmlData.playStatus) {
|
||||
case "BUFFERING_STATE":
|
||||
nowplaying = "Please wait\nBuffering..."
|
||||
needrefresh = true
|
||||
break
|
||||
case "PLAY_STATE":
|
||||
sendEvent(name:"playpause", value:"play")
|
||||
break
|
||||
case "PAUSE_STATE":
|
||||
case "STOP_STATE":
|
||||
sendEvent(name:"playpause", value:"pause")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the previous section didn't handle this, take another stab at it
|
||||
if (!nowplaying) {
|
||||
nowplaying = ""
|
||||
switch (override ? override : xmlData.attributes()['source']) {
|
||||
case "AUX":
|
||||
nowplaying = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
nowplaying = "Air Play"
|
||||
break
|
||||
case "STANDBY":
|
||||
nowplaying = "Standby"
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
nowplaying = "${xmlData.stationName.text()}\n\n${xmlData.description.text()}"
|
||||
break
|
||||
case "REFRESH":
|
||||
nowplaying = "Please wait"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
if (xmlData.ContentItem.itemName[0])
|
||||
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
|
||||
case "STORED_MUSIC":
|
||||
nowplaying += "${xmlData.track.text()}"
|
||||
if (xmlData.artist)
|
||||
nowplaying += "\nby\n${xmlData.artist.text()}"
|
||||
if (xmlData.album)
|
||||
nowplaying += "\n\n(${xmlData.album.text()})"
|
||||
break
|
||||
default:
|
||||
if (xmlData != null)
|
||||
nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
|
||||
else
|
||||
nowplaying = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Some last parsing which only deals with actual data from device
|
||||
if (xmlData) {
|
||||
if (xmlData.attributes()['source'] == "STANDBY") {
|
||||
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
|
||||
sendEvent(name:"switch", value:"off")
|
||||
} else {
|
||||
sendEvent(name:"switch", value:"on")
|
||||
}
|
||||
boseSetPlayerAttributes(xmlData)
|
||||
}
|
||||
|
||||
// Do not allow a standby device or AUX to be master
|
||||
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if ((override ? override : xmlData.attributes()['source']) == "AUX")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if (boseGetZone()) {
|
||||
log.info "We're in the zone: " + boseGetZone()
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
|
||||
sendEvent(name:"nowplaying", value:nowplaying)
|
||||
|
||||
return needrefresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the attributes exposed by the music Player capability
|
||||
*
|
||||
* @param xmlData The NowPlaying XML data
|
||||
*/
|
||||
def boseSetPlayerAttributes(xmlData) {
|
||||
// Refresh attributes
|
||||
def trackText = ""
|
||||
def trackDesc = ""
|
||||
def trackData = [:]
|
||||
|
||||
switch (xmlData.attributes()['source']) {
|
||||
case "STANDBY":
|
||||
trackData["station"] = trackText = trackDesc = "Standby"
|
||||
break
|
||||
case "AUX":
|
||||
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
trackData["station"] = trackText = trackDesc = "Air Play"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
case "STORED_MUSIC":
|
||||
trackText = trackDesc = "${xmlData.track.text()}"
|
||||
trackData["name"] = xmlData.track.text()
|
||||
if (xmlData.artist) {
|
||||
trackText += " by ${xmlData.artist.text()}"
|
||||
trackDesc += " - ${xmlData.artist.text()}"
|
||||
trackData["artist"] = xmlData.artist.text()
|
||||
}
|
||||
if (xmlData.album) {
|
||||
trackText += " (${xmlData.album.text()})"
|
||||
trackData["album"] = xmlData.album.text()
|
||||
}
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
trackDesc = xmlData.stationName.text()
|
||||
trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
|
||||
trackData["station"] = xmlData.stationName.text()
|
||||
break
|
||||
default:
|
||||
trackText = trackDesc = xmlData.ContentItem.itemName[0].text()
|
||||
}
|
||||
|
||||
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the state of the "play everywhere" mode
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetEverywhereState() {
|
||||
return boseGET("/getZone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a remote key event
|
||||
*
|
||||
* @param key The name of the key
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note It's VITAL that it's done as two requests, or it will ignore the
|
||||
* the second key info.
|
||||
*/
|
||||
def boseKeypress(key) {
|
||||
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
|
||||
def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
|
||||
|
||||
return [bosePOST("/key", press), bosePOST("/key", release)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses or plays current preset
|
||||
*
|
||||
* @param play If true, plays, else it pauses (depending on preset, may stop)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetPlayMode(boolean play) {
|
||||
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
|
||||
return boseKeypress(play ? "PLAY" : "PAUSE")
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume in a deterministic way.
|
||||
*
|
||||
* @param New volume level, ranging from 0 to 100
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetVolume(int level) {
|
||||
def result = []
|
||||
int vol = Math.min(100, Math.max(level, 0))
|
||||
|
||||
sendEvent(name:"volume", value:"${vol}")
|
||||
|
||||
return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mute state, unfortunately, for now, we need to query current
|
||||
* state before taking action (no discrete mute/unmute)
|
||||
*
|
||||
* @param mute If true, mutes the system
|
||||
* @return command
|
||||
*/
|
||||
def boseSetMute(boolean mute) {
|
||||
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
|
||||
return boseGetVolume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for boseSetMute(), checks current state and changes it
|
||||
* if it doesn't match the requested state.
|
||||
*
|
||||
* @param xml The volume XML data
|
||||
* @param mute The new state of mute
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetMute(xml, mute) {
|
||||
def result = []
|
||||
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
|
||||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
|
||||
{
|
||||
result << boseKeypress("MUTE")
|
||||
}
|
||||
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "mute"))
|
||||
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of the volume
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetVolume() {
|
||||
return boseGET("/volume")
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the track to either the previous or next
|
||||
*
|
||||
* @param direction > 0 = next track, < 0 = previous track, 0 = no action
|
||||
* @return command
|
||||
*/
|
||||
def boseChangeTrack(int direction) {
|
||||
if (direction < 0) {
|
||||
return boseKeypress("PREV_TRACK")
|
||||
} else if (direction > 0) {
|
||||
return boseKeypress("NEXT_TRACK")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to preset 1-6 or AUX
|
||||
*
|
||||
* @param input The input (one of 1,2,3,4,5,6,aux)
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note If no presets have been loaded, it will first refresh the presets.
|
||||
*/
|
||||
def boseSetInput(input) {
|
||||
log.info "boseSetInput(${input})"
|
||||
def result = []
|
||||
|
||||
if (!state.preset) {
|
||||
result << boseGetPresets()
|
||||
queueCallback('presets', 'cb_boseSetInput', input)
|
||||
} else {
|
||||
result << cb_boseSetInput(null, input)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by boseSetInput(), either called directly by
|
||||
* boseSetInput() if we already have presets, or called after
|
||||
* retreiving the presets for the first time.
|
||||
*
|
||||
* @param xml The presets XML data
|
||||
* @param input Desired input
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Uses KEY commands for AUX, otherwise /select endpoint.
|
||||
* Reason for this is latency. Since keypresses are done
|
||||
* in pairs (press + release), you could accidentally change
|
||||
* the preset if there is a long delay between the two.
|
||||
*/
|
||||
def cb_boseSetInput(xml, input) {
|
||||
def result = []
|
||||
|
||||
if (input >= "1" && input <= "6" && state.preset["$input"])
|
||||
result << bosePOST("/select", state.preset["$input"])
|
||||
else if (input.toLowerCase() == "aux") {
|
||||
result << boseKeypress("AUX_INPUT")
|
||||
}
|
||||
|
||||
// Horrible workaround... but we need to delay
|
||||
// the update by at least a few seconds...
|
||||
result << boseRefreshNowPlaying(3000)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the power state of the bose unit
|
||||
*
|
||||
* @param device The device in-question
|
||||
* @param enable True to power on, false to power off
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Will first query state before acting since there
|
||||
* is no discreete call.
|
||||
*/
|
||||
def boseSetPowerState(boolean enable) {
|
||||
log.info "boseSetPowerState(${enable})"
|
||||
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used by boseSetPowerState(), is used
|
||||
* to handle the fact that we only have a toggle for power.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param state The requested state
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetPowerState(xml, state) {
|
||||
def result = []
|
||||
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
|
||||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
|
||||
{
|
||||
result << boseKeypress("POWER")
|
||||
if (state == "POWERON") {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
|
||||
}
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* We're sometimes too quick on the draw and get a refreshed nowPlaying
|
||||
* which shows standby (essentially, the device has yet to completely
|
||||
* transition to awake state), so we need to poll a few times extra
|
||||
* to make sure we get it right.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param tries A counter which will decrease, once it reaches zero,
|
||||
* we give up and assume that whatever we got was correct.
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseConfirmPowerOn(xml, tries) {
|
||||
def result = []
|
||||
log.warn "boseConfirmPowerOn() attempt #" + tries
|
||||
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an update on currently playing item(s)
|
||||
*
|
||||
* @param delay If set to non-zero, delays x ms before issuing
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseRefreshNowPlaying(delay=0) {
|
||||
if (delay > 0) {
|
||||
return ["delay ${delay}", boseGET("/now_playing")]
|
||||
}
|
||||
return boseGET("/now_playing")
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the list of presets
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetPresets() {
|
||||
return boseGET("/presets")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes GET requests to BOSE device
|
||||
*
|
||||
* @param path What endpoint
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGET(String path) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: path,
|
||||
headers: [
|
||||
HOST: state.address + ":8090",
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes a POST request to the BOSE device with
|
||||
* the provided data.
|
||||
*
|
||||
* @param path What endpoint
|
||||
* @param data What data
|
||||
* @param address Specific ip and port (optional)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def bosePOST(String path, String data, String address=null) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "POST",
|
||||
path: path,
|
||||
body: data,
|
||||
headers: [
|
||||
HOST: address ?: (state.address + ":8090"),
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a callback function for when a specific XML root is received
|
||||
* Will execute on subsequent parse() call(s), never on the current
|
||||
* parse() call.
|
||||
*
|
||||
* @param root The root node that this callback should react to
|
||||
* @param func Name of the function
|
||||
* @param param Parameters for function (optional)
|
||||
*/
|
||||
def queueCallback(String root, String func, param=null) {
|
||||
if (!state.pending)
|
||||
state.pending = [:]
|
||||
if (!state.pending[root])
|
||||
state.pending[root] = []
|
||||
state.pending[root] << ["$func":"$param"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers the pending callbacks into readiness state
|
||||
* so they can be executed by processCallbacks()
|
||||
*
|
||||
* This is needed to avoid reacting to queueCallbacks() within
|
||||
* the same loop.
|
||||
*/
|
||||
def prepareCallbacks() {
|
||||
if (!state.pending)
|
||||
return
|
||||
if (!state.ready)
|
||||
state.ready = [:]
|
||||
state.ready << state.pending
|
||||
state.pending = [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes any ready callback for a specific root node
|
||||
* with associated parameter and then clears that entry.
|
||||
*
|
||||
* If a callback returns data, it's added to a list of
|
||||
* commands which is returned to the caller of this function
|
||||
*
|
||||
* Once a callback has been used, it's removed from the list
|
||||
* of queued callbacks (ie, it executes only once!)
|
||||
*
|
||||
* @param xml The XML data to be examined and delegated
|
||||
* @return list of commands
|
||||
*/
|
||||
def processCallbacks(xml) {
|
||||
def result = []
|
||||
|
||||
if (!state.ready)
|
||||
return result
|
||||
|
||||
if (state.ready[xml.name()]) {
|
||||
state.ready[xml.name()].each { callback ->
|
||||
callback.each { func, param ->
|
||||
if (func != "func") {
|
||||
if (param)
|
||||
result << "$func"(xml, param)
|
||||
else
|
||||
result << "$func"(xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ready.remove(xml.name())
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* State managament for the Play Everywhere zone.
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* A device is either:
|
||||
*
|
||||
* null = Not participating
|
||||
* server = running the show
|
||||
* client = under the control of the server
|
||||
*
|
||||
* @param newstate (see above for types)
|
||||
*/
|
||||
def boseSetZone(String newstate) {
|
||||
log.debug "boseSetZone($newstate)"
|
||||
state.zone = newstate
|
||||
|
||||
// Refresh our state
|
||||
if (newstate) {
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else {
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the Everywhere zone, returns the current state
|
||||
* of zone membership (null, server, client)
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* @return state
|
||||
*/
|
||||
def boseGetZone() {
|
||||
return state.zone
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DeviceID of this particular device.
|
||||
*
|
||||
* Needs to be done this way since DNI is not always
|
||||
* the same as DeviceID which is used internally by
|
||||
* BOSE.
|
||||
*
|
||||
* @param devID The DeviceID
|
||||
*/
|
||||
def boseSetDeviceID(String devID) {
|
||||
state.deviceID = devID
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the DeviceID for this device
|
||||
*
|
||||
* @return deviceID
|
||||
*/
|
||||
def boseGetDeviceID() {
|
||||
return state.deviceID
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IP of this device
|
||||
*
|
||||
* @return IP address
|
||||
*/
|
||||
def getDeviceIP() {
|
||||
return parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
}
|
||||
@@ -0,0 +1,887 @@
|
||||
/**
|
||||
* Fidure Thermostat, Based on ZigBee thermostat (SmartThings)
|
||||
*
|
||||
* Author: Fidure
|
||||
* Date: 2014-12-13
|
||||
* Updated: 2015-08-26
|
||||
*/
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Polling"
|
||||
|
||||
attribute "displayTemperature","number"
|
||||
attribute "displaySetpoint", "string"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
attribute "upButtonState", "string"
|
||||
attribute "downButtonState", "string"
|
||||
|
||||
attribute "runningMode", "string"
|
||||
attribute "lockLevel", "string"
|
||||
|
||||
command "setThermostatTime"
|
||||
command "lock"
|
||||
|
||||
attribute "prorgammingOperation", "number"
|
||||
attribute "prorgammingOperationDisplay", "string"
|
||||
command "Program"
|
||||
|
||||
attribute "setpointHold", "string"
|
||||
attribute "setpointHoldDisplay", "string"
|
||||
command "Hold"
|
||||
attribute "holdExpiary", "string"
|
||||
|
||||
attribute "lastTimeSync", "string"
|
||||
|
||||
attribute "thermostatOperatingState", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator { }
|
||||
// pref
|
||||
preferences {
|
||||
|
||||
input ("hold_time", "enum", title: "Default Hold Time in Hours",
|
||||
description: "Default Hold Duration in hours",
|
||||
range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"],
|
||||
displayDuringSetup: false)
|
||||
input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"])
|
||||
input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"])
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "displayTemperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 29, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
[value: 36, color: "#bc2323"],
|
||||
// fahrenheit range
|
||||
[value: 37, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off"
|
||||
state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool"
|
||||
state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat"
|
||||
state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto"
|
||||
}
|
||||
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
}
|
||||
|
||||
standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
|
||||
state "Resting", label: 'Resting'
|
||||
state "Heating", icon:"st.thermostat.heating"
|
||||
state "Cooling", icon:"st.thermostat.cooling"
|
||||
}
|
||||
|
||||
|
||||
standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") {
|
||||
state "Unlocked", action:"lock", label:'${name}'
|
||||
state "Mode Only", action:"lock", label:'${name}'
|
||||
state "Setpoint", action:"lock", label:'${name}'
|
||||
state "Full", action:"lock", label:'${name}'
|
||||
}
|
||||
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||
}
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||
}
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) {
|
||||
state "default", label: 'Schedule'
|
||||
}
|
||||
valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"Program", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) {
|
||||
state "setpointHold", action:"Hold", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("setpoint", "displaySetpoint", width: 2, height: 2)
|
||||
{
|
||||
state("displaySetpoint", label: '${currentValue}°',
|
||||
backgroundColor: "#919191")
|
||||
}
|
||||
|
||||
standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up"
|
||||
state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down"
|
||||
state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
|
||||
|
||||
main "temperature"
|
||||
details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold",
|
||||
"heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def getMin() {
|
||||
try
|
||||
{
|
||||
if (getTemperatureScale() == "C")
|
||||
return 10
|
||||
else
|
||||
return 50
|
||||
} catch (all)
|
||||
{
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
def getMax() {
|
||||
try {
|
||||
if (getTemperatureScale() == "C")
|
||||
return 30
|
||||
else
|
||||
return 86
|
||||
} catch (all)
|
||||
{
|
||||
return 86
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def result = []
|
||||
|
||||
if (description?.startsWith("read attr -")) {
|
||||
|
||||
//TODO: Parse RAW strings for multiple attributes
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map: $descMap"
|
||||
for ( atMap in descMap.attrs)
|
||||
{
|
||||
def map = [:]
|
||||
|
||||
if (descMap.cluster == "0201")
|
||||
{
|
||||
//log.trace "attribute: ${atMap.attrId} "
|
||||
switch(atMap.attrId.toLowerCase())
|
||||
{
|
||||
case "0000":
|
||||
map.name = "temperature"
|
||||
map.value = getTemperature(atMap.value)
|
||||
result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value))
|
||||
break;
|
||||
case "0005":
|
||||
//log.debug "hex time: ${descMap.value}"
|
||||
if (atMap.encoding == "23")
|
||||
{
|
||||
map.name = "holdExpiary"
|
||||
map.value = "${convertToTime(atMap.value).getTime()}"
|
||||
//log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}"
|
||||
updateHoldLabel("HoldExp", "${map.value}")
|
||||
}
|
||||
break;
|
||||
case "0011":
|
||||
map.name = "coolingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0012":
|
||||
map.name = "heatingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001c":
|
||||
map.name = "thermostatMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001e": //running mode enum8
|
||||
map.name = "runningMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0023": // setpoint hold enum8
|
||||
map.name = "setpointHold"
|
||||
map.value = getHoldMap()[atMap.value]
|
||||
updateHoldLabel("Hold", map.value)
|
||||
break;
|
||||
case "0024": // hold duration int16u
|
||||
map.name = "setpointHoldDuration"
|
||||
map.value = Integer.parseInt("${atMap.value}", 16)
|
||||
|
||||
break;
|
||||
case "0025": // thermostat programming operation bitmap8
|
||||
map.name = "prorgammingOperation"
|
||||
def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01]
|
||||
result += createEvent("name":"prorgammingOperationDisplay", "value": val)
|
||||
map.value = atMap.value
|
||||
break;
|
||||
case "0029":
|
||||
// relay state
|
||||
map.name = "thermostatOperatingState"
|
||||
map.value = getThermostatOperatingState(atMap.value)
|
||||
break;
|
||||
}
|
||||
} else if (descMap.cluster == "0204")
|
||||
{
|
||||
if (atMap.attrId == "0001")
|
||||
{
|
||||
map.name = "lockLevel"
|
||||
map.value = getLockMap()[atMap.value]
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
result += createEvent(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "Parse returned $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
def map = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
|
||||
def attrId = map.get('attrId')
|
||||
def encoding = map.get('encoding')
|
||||
def value = map.get('value')
|
||||
def result = map.get('result')
|
||||
def list = [];
|
||||
|
||||
if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) {
|
||||
def raw = map.get('raw')
|
||||
|
||||
def size = Long.parseLong(''+ map.get('size'), 16)
|
||||
def index = 12;
|
||||
def len
|
||||
|
||||
//log.trace "processing multi attributes"
|
||||
while((index-12) < size) {
|
||||
attrId = flipHexStringEndianness(raw[index..(index+3)])
|
||||
index+= 4;
|
||||
if (result == "success")
|
||||
index+=2;
|
||||
encoding = raw[index..(index+1)]
|
||||
index+= 2;
|
||||
len =getDataLengthByType(encoding)
|
||||
value = flipHexStringEndianness(raw[index..(index+len-1)])
|
||||
index+=len;
|
||||
list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"]
|
||||
}
|
||||
}
|
||||
else
|
||||
list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"]
|
||||
|
||||
map.remove('value')
|
||||
map.remove('encoding')
|
||||
map.remove('attrId')
|
||||
map += ['attrs' : list ]
|
||||
}
|
||||
|
||||
def flipHexStringEndianness(s)
|
||||
{
|
||||
s = s.reverse()
|
||||
def sb = new StringBuilder()
|
||||
for (int i=0; i < s.length() -1; i+=2)
|
||||
sb.append(s.charAt(i+1)).append(s.charAt(i))
|
||||
sb
|
||||
}
|
||||
|
||||
def getDataLengthByType(t)
|
||||
{
|
||||
// number of bytes in each static data type
|
||||
def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4,
|
||||
"1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2,
|
||||
"2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4,
|
||||
"e8":2, "e9":2, "ea":4, "f0":8, "f1":16]
|
||||
|
||||
// return number of hex chars
|
||||
return map.get(t) * 2
|
||||
}
|
||||
|
||||
|
||||
def getProgrammingMap() { [
|
||||
0:"Off",
|
||||
1:"On"
|
||||
]}
|
||||
|
||||
def getModeMap() { [
|
||||
"00":"off",
|
||||
"01":"auto",
|
||||
"03":"cool",
|
||||
"04":"heat"
|
||||
]}
|
||||
|
||||
def getFanModeMap() { [
|
||||
"04":"fanOn",
|
||||
"05":"fanAuto"
|
||||
]}
|
||||
|
||||
def getHoldMap()
|
||||
{[
|
||||
"00":"Off",
|
||||
"01":"On"
|
||||
]}
|
||||
|
||||
|
||||
def updateSetpoint(attrib, val)
|
||||
{
|
||||
def cool = device.currentState("coolingSetpoint")?.value
|
||||
def heat = device.currentState("heatingSetpoint")?.value
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
def value = '--';
|
||||
|
||||
|
||||
if ("heat" == mode && heat != null)
|
||||
value = heat;
|
||||
else if ("cool" == mode && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "cool" && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "heat" && heat != null)
|
||||
value = heat;
|
||||
|
||||
sendEvent("name":"displaySetpoint", "value": value)
|
||||
}
|
||||
|
||||
def raiseSetpoint()
|
||||
{
|
||||
sendEvent("name":"upButtonState", "value": "pressed")
|
||||
sendEvent("name":"upButtonState", "value": "normal")
|
||||
adjustSetpoint(5)
|
||||
}
|
||||
|
||||
def lowerSetpoint()
|
||||
{
|
||||
sendEvent("name":"downButtonState", "value": "pressed")
|
||||
sendEvent("name":"downButtonState", "value": "normal")
|
||||
adjustSetpoint(-5)
|
||||
}
|
||||
|
||||
def adjustSetpoint(value)
|
||||
{
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
//default to both heat and cool
|
||||
def modeData = 0x02
|
||||
|
||||
if ("heat" == mode || "heat" == runningMode)
|
||||
modeData = "00"
|
||||
else if ("cool" == mode || "cool" == runningMode)
|
||||
modeData = "01"
|
||||
|
||||
def amountData = String.format("%02X", value)[-2..-1]
|
||||
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getDisplayTemperature(value)
|
||||
{
|
||||
def t = Integer.parseInt("$value", 16);
|
||||
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
t = (((t + 4) / 10) as Integer) / 10;
|
||||
} else {
|
||||
t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10;
|
||||
}
|
||||
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
def updateHoldLabel(attr, value)
|
||||
{
|
||||
def currentHold = (device?.currentState("setpointHold")?.value)?: "..."
|
||||
|
||||
def holdExp = device?.currentState("holdExpiary")?.value
|
||||
holdExp = holdExp?: "${(new Date()).getTime()}"
|
||||
|
||||
if ("Hold" == attr)
|
||||
{
|
||||
currentHold = value
|
||||
}
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
holdExp = value
|
||||
}
|
||||
boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime()))
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
if (!past)
|
||||
currentHold = "On"
|
||||
else
|
||||
currentHold = "Off"
|
||||
}
|
||||
|
||||
def holdString = (currentHold == "On")?
|
||||
( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") :
|
||||
((currentHold == "Off")? " is Off" : " ...")
|
||||
|
||||
sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}")
|
||||
}
|
||||
|
||||
def getSetPointHoldDuration()
|
||||
{
|
||||
def holdTime = 0
|
||||
|
||||
if (settings.hold_time?.contains("Hours"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim())
|
||||
}
|
||||
else if (settings.hold_time?.contains("Day"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24
|
||||
}
|
||||
|
||||
def currentHoldDuration = device.currentState("setpointHoldDuration")?.value
|
||||
|
||||
|
||||
if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60))
|
||||
{
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" +
|
||||
String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian
|
||||
|
||||
+ "}", "delay 100",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200",
|
||||
]
|
||||
|
||||
} else
|
||||
{
|
||||
[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def Hold()
|
||||
{
|
||||
def currentHold = device.currentState("setpointHold")?.value
|
||||
|
||||
def next = (currentHold == "On") ? "00" : "01"
|
||||
def nextHold = getHoldMap()[next]
|
||||
|
||||
sendEvent("name":"setpointHold", "value":nextHold)
|
||||
|
||||
// set the duration first if it's changed
|
||||
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" ,
|
||||
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
] + getSetPointHoldDuration()
|
||||
}
|
||||
|
||||
def compareWithNow(d)
|
||||
{
|
||||
long mins = (new Date(d)).getTime() - (new Date()).getTime()
|
||||
|
||||
mins /= 1000 * 60;
|
||||
|
||||
log.trace "mins: ${mins}"
|
||||
|
||||
boolean past = (mins < 0)
|
||||
def ret = (past)? "" : "in "
|
||||
|
||||
if (past)
|
||||
mins *= -1;
|
||||
|
||||
float t = 0;
|
||||
// minutes
|
||||
if (mins < 60)
|
||||
{
|
||||
ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '')
|
||||
}else if (mins < 1440)
|
||||
{
|
||||
t = ( Math.round((14 + mins)/30) as Integer) / 2
|
||||
ret += t + " hr" + ((t > 1)? 's' : '')
|
||||
} else
|
||||
{
|
||||
t = (Math.round((359 + mins)/720) as Integer) / 2
|
||||
ret += t + " day" + ((t > 1)? 's' : '')
|
||||
}
|
||||
ret += (past)? " ago": ""
|
||||
|
||||
log.trace "ret: ${ret}"
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
def convertToTime(data)
|
||||
{
|
||||
def time = Integer.parseInt("$data", 16) as long;
|
||||
time *= 1000;
|
||||
time += 946684800000; // 481418694
|
||||
time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
|
||||
def d = new Date(time);
|
||||
|
||||
//log.trace "converted $data to Time $d"
|
||||
return d;
|
||||
}
|
||||
|
||||
def Program()
|
||||
{
|
||||
def currentSched = device.currentState("prorgammingOperation")?.value
|
||||
|
||||
def next = Integer.parseInt(currentSched?: "00", 16);
|
||||
if ( (next & 0x01) == 0x01)
|
||||
next = next & 0xfe;
|
||||
else
|
||||
next = next | 0x01;
|
||||
|
||||
def nextSched = getProgrammingMap()[next & 0x01]
|
||||
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getThermostatOperatingState(value)
|
||||
{
|
||||
String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"]
|
||||
String desc = 'idle'
|
||||
value = Integer.parseInt(''+value, 16)
|
||||
|
||||
// only check for 1-stage for A1730
|
||||
for ( i in 0..2 ) {
|
||||
if (value & 1 << i)
|
||||
desc = m[i]
|
||||
}
|
||||
|
||||
desc
|
||||
}
|
||||
|
||||
def checkLastTimeSync(delay)
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if (!lastSync)
|
||||
lastSync = "${new Date(0)}"
|
||||
|
||||
if (settings.sync_clock ?: false && lastSync != new Date(0))
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
long duration = (new Date()).getTime() - (new Date(lastSync)).getTime()
|
||||
|
||||
// log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}"
|
||||
if (duration > 86400000)
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date()}")
|
||||
return setThermostatTime()
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def readAttributesCommand(cluster, attribList)
|
||||
{
|
||||
def attrString = ''
|
||||
|
||||
for ( val in attribList ) {
|
||||
attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff)
|
||||
}
|
||||
|
||||
//log.trace "list: " + attrString
|
||||
|
||||
["raw "+ cluster + " {00 00 00 $attrString}","delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 100",
|
||||
]
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
// log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23])
|
||||
|
||||
readAttributesCommand(0x201, [0x00,0x11,0x12]) +
|
||||
readAttributesCommand(0x201, [0x1C,0x1E,0x23]) +
|
||||
readAttributesCommand(0x201, [0x24,0x25,0x29]) +
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary
|
||||
"send 0x${device.deviceNetworkId} 1 1" , "delay 1500",
|
||||
] + checkLastTimeSync(2000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def poll() {
|
||||
log.trace "poll called"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt("$value", 16) / 100
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius as Integer
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(degrees) {
|
||||
def temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(degrees) {
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def modes() {
|
||||
["off", "heat", "cool"]
|
||||
}
|
||||
|
||||
def setThermostatFanMode() {
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
//log.debug "switching fan from current mode: $currentFanMode"
|
||||
def returnCommand
|
||||
|
||||
switch (currentFanMode) {
|
||||
case "fanAuto":
|
||||
returnCommand = fanOn()
|
||||
break
|
||||
case "fanOn":
|
||||
returnCommand = fanAuto()
|
||||
break
|
||||
}
|
||||
if(!currentFanMode) { returnCommand = fanAuto() }
|
||||
returnCommand
|
||||
}
|
||||
|
||||
def setThermostatMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def modeOrder = modes()
|
||||
def index = modeOrder.indexOf(currentMode)
|
||||
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
|
||||
|
||||
setThermostatMode(next)
|
||||
}
|
||||
|
||||
def setThermostatMode(String next) {
|
||||
def val = (getModeMap().find { it.value == next }?.key)?: "00"
|
||||
|
||||
// log.trace "mode changing to $next sending value: $val"
|
||||
|
||||
sendEvent("name":"thermostatMode", "value":"$next")
|
||||
["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] +
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
"$value"()
|
||||
}
|
||||
|
||||
def off() {
|
||||
setThermostatMode("off")
|
||||
}
|
||||
|
||||
def cool() {
|
||||
setThermostatMode("cool")}
|
||||
|
||||
def heat() {
|
||||
setThermostatMode("heat")
|
||||
}
|
||||
|
||||
def auto() {
|
||||
setThermostatMode("auto")
|
||||
}
|
||||
|
||||
def on() {
|
||||
fanOn()
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanOn")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
|
||||
}
|
||||
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if ((settings.sync_clock ?: false) == false)
|
||||
{
|
||||
log.debug "resetting last sync time. Used to be: $lastSync"
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getLockMap()
|
||||
{[
|
||||
"00":"Unlocked",
|
||||
"01":"Mode Only",
|
||||
"02":"Setpoint",
|
||||
"03":"Full",
|
||||
"04":"Full",
|
||||
"05":"Full",
|
||||
|
||||
]}
|
||||
def lock()
|
||||
{
|
||||
|
||||
def currentLock = device.currentState("lockLevel")?.value
|
||||
def val = getLockMap().find { it.value == currentLock }?.key
|
||||
|
||||
|
||||
|
||||
//log.debug "current lock is: ${val}"
|
||||
|
||||
if (val == "00")
|
||||
val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key
|
||||
else
|
||||
val = "00"
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def setThermostatTime()
|
||||
{
|
||||
|
||||
if ((settings.sync_clock ?: false))
|
||||
{
|
||||
log.debug "sync time is disabled, leaving"
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Date date = new Date();
|
||||
String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings();
|
||||
|
||||
long millis = date.getTime(); // Millis since Unix epoch
|
||||
millis -= 946684800000; // adjust for ZigBee EPOCH
|
||||
// adjust for time zone and DST offset
|
||||
millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
//convert to seconds
|
||||
millis /= 1000;
|
||||
|
||||
// print to a string for hex capture
|
||||
String s = String.format("%08X", millis);
|
||||
// hex capture for message format
|
||||
String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2);
|
||||
|
||||
[
|
||||
"raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500",
|
||||
|
||||
] + refresh()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private getEndpointId()
|
||||
{
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
@@ -19,7 +19,6 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -317,7 +317,7 @@ def setLevel(value) {
|
||||
state.trigger = "setLevel"
|
||||
state.lvl = "${level}"
|
||||
|
||||
if (dimRate) {
|
||||
if (dimRate && (state?.rate != null)) {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -15,19 +15,27 @@ metadata {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control"){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
||||
}
|
||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||
attributeState "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
}
|
||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
||||
}
|
||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["icon"])
|
||||
details(["networkAddress","serialNumber"])
|
||||
details(["rich-control", "networkAddress"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +44,6 @@ def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def results = []
|
||||
def result = parent.parse(this, description)
|
||||
|
||||
if (result instanceof physicalgraph.device.HubAction){
|
||||
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
||||
results << result
|
||||
@@ -44,32 +51,30 @@ def parse(description) {
|
||||
//do nothing
|
||||
log.trace "HUE BRIDGE was updated"
|
||||
} else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
map = stringToMap(description)
|
||||
}
|
||||
if (map?.name && map?.value) {
|
||||
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
results << createEvent(name: "${map.name}", value: "${map.value}")
|
||||
} else {
|
||||
log.trace "Parsing description"
|
||||
def msg = parseLanMessage(description)
|
||||
if (msg.body) {
|
||||
def contentType = msg.headers["Content-Type"]
|
||||
if (contentType?.contains("json")) {
|
||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||
if (bulbs.state) {
|
||||
log.warn "NOT PROCESSED: $msg.body"
|
||||
}
|
||||
else {
|
||||
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
|
||||
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
|
||||
log.info "Bridge response: $msg.body"
|
||||
} else {
|
||||
// Sending Bulbs List to parent"
|
||||
if (parent.state.inBulbDiscovery)
|
||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||
}
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* Hue Bulb
|
||||
*
|
||||
@@ -15,8 +16,8 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -49,7 +50,6 @@ metadata {
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
@@ -68,13 +68,13 @@ def parse(description) {
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on(transition = "4") {
|
||||
log.trace parent.on(this,transition)
|
||||
def on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off(transition = "4") {
|
||||
log.trace parent.off(this,transition)
|
||||
def off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ def setHue(percent) {
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
|
||||
def setColor(value,alert = "none",transition = 4) {
|
||||
def setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
parent.setColor(this, value, alert, transition)
|
||||
parent.setColor(this, value)
|
||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||
|
||||
@@ -19,24 +19,41 @@ metadata {
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "lighting", 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)"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
||||
attributeState "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "refresh"])
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* LIFX Color Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setColor"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
||||
state "level", label: '${currentValue}%'
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setHue(percentage) {
|
||||
log.debug "setHue ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "hue", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setHue result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setSaturation(percentage) {
|
||||
log.debug "setSaturation ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "saturation", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setSaturation result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColor(Map color) {
|
||||
log.debug "setColor ${color}"
|
||||
def attrs = []
|
||||
def events = []
|
||||
color.each { key, value ->
|
||||
switch (key) {
|
||||
case "hue":
|
||||
attrs << "hue:${value * 3.6}"
|
||||
events << createEvent(name: "hue", value: value)
|
||||
break
|
||||
case "saturation":
|
||||
attrs << "saturation:${value / 100}"
|
||||
events << createEvent(name: "saturation", value: value)
|
||||
break
|
||||
case "colorTemperature":
|
||||
attrs << "kelvin:${value}"
|
||||
events << createEvent(name: "colorTemperature", value: value)
|
||||
break
|
||||
}
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "color", value: color.hex)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
events.each { sendEvent(it) }
|
||||
} else {
|
||||
log.error("Bad setColor result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
||||
if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data
|
||||
|
||||
sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
|
||||
sendEvent(name: "hue", value: data.color.hue / 3.6)
|
||||
sendEvent(name: "saturation", value: data.color.saturation * 100)
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* LIFX White Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
||||
state "level", label: '${currentValue}%'
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
||||
if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data
|
||||
|
||||
sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
@@ -24,8 +24,8 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
|
||||
}
|
||||
|
||||
tiles {
|
||||
|
||||
@@ -24,10 +24,10 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -22,10 +22,8 @@ metadata {
|
||||
|
||||
command "setAdjustedColor"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -28,6 +28,9 @@ metadata {
|
||||
// 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-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"
|
||||
}
|
||||
|
||||
@@ -45,8 +48,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||
|
||||
}
|
||||
|
||||
@@ -280,16 +280,14 @@ def configure() {
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -299,7 +297,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -25,7 +25,7 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
@@ -39,9 +39,9 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
@@ -301,20 +301,18 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -322,12 +320,12 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
|
||||
@@ -292,18 +292,16 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -313,7 +311,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
command "enrollResponse"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
@@ -54,10 +54,10 @@
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
@@ -395,35 +395,34 @@ def getTemperature(value) {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
def configCmds = [
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
|
||||
]
|
||||
@@ -440,7 +439,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -297,29 +297,27 @@ def getTemperature(value) {
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -327,7 +325,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -275,22 +275,16 @@ def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -300,7 +294,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -253,22 +253,19 @@ def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Valve"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
status "open": "command: 2503, payload: FF"
|
||||
status "close": "command: 2503, payload: 00"
|
||||
|
||||
// reply messages
|
||||
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||
}
|
||||
|
||||
// tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
|
||||
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "valve"
|
||||
details(["valve","refresh"])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse description : $description"
|
||||
def result = null
|
||||
def cmd = zwave.parse(description, [0x20: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(zwaveEvent(cmd))
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
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)
|
||||
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
[:] // Handles all Z-Wave commands we aren't interested in
|
||||
}
|
||||
|
||||
def open() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely opened
|
||||
}
|
||||
|
||||
def close() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely closed
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
|
||||
if (getDataValue("MSR") == null) {
|
||||
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
}
|
||||
delayBetween(commands,100)
|
||||
}
|
||||
@@ -11,8 +11,13 @@
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
|
||||
* devices for which the user has authorized. Likewise, Initial State's services call this
|
||||
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
|
||||
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Initial State Event Streamer",
|
||||
namespace: "initialstate.events",
|
||||
@@ -28,32 +33,31 @@ import groovy.json.JsonSlurper
|
||||
|
||||
preferences {
|
||||
section("Choose which devices to monitor...") {
|
||||
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
|
||||
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
|
||||
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
|
||||
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
|
||||
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
|
||||
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
|
||||
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
|
||||
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
@@ -74,77 +78,71 @@ mappings {
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
/*if (accelerometers != null) {
|
||||
if (accelerometers != null) {
|
||||
subscribe(accelerometers, "acceleration", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (alarms != null) {
|
||||
subscribe(alarms, "alarm", genericHandler)
|
||||
}
|
||||
/*if (batteries != null) {
|
||||
if (batteries != null) {
|
||||
subscribe(batteries, "battery", genericHandler)
|
||||
}*/
|
||||
/*if (beacons != null) {
|
||||
}
|
||||
if (beacons != null) {
|
||||
subscribe(beacons, "presence", genericHandler)
|
||||
}*/
|
||||
/*
|
||||
if (buttons != null) {
|
||||
subscribe(buttons, "button", genericHandler)
|
||||
}*/
|
||||
/*if (cos != null) {
|
||||
}
|
||||
|
||||
if (cos != null) {
|
||||
subscribe(cos, "carbonMonoxide", genericHandler)
|
||||
}*/
|
||||
/*if (colors != null) {
|
||||
}
|
||||
if (colors != null) {
|
||||
subscribe(colors, "hue", genericHandler)
|
||||
subscribe(colors, "saturation", genericHandler)
|
||||
subscribe(colors, "color", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (contacts != null) {
|
||||
subscribe(contacts, "contact", genericHandler)
|
||||
}
|
||||
/*if (doorsControllers != null) {
|
||||
subscribe(doorsControllers, "door", genericHandler)
|
||||
}*/
|
||||
/*if (energyMeters != null) {
|
||||
if (energyMeters != null) {
|
||||
subscribe(energyMeters, "energy", genericHandler)
|
||||
}*/
|
||||
/*if (illuminances != null) {
|
||||
}
|
||||
if (illuminances != null) {
|
||||
subscribe(illuminances, "illuminance", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (locks != null) {
|
||||
subscribe(locks, "lock", genericHandler)
|
||||
}
|
||||
if (motions != null) {
|
||||
subscribe(motions, "motion", genericHandler)
|
||||
}
|
||||
/*if (musicPlayers != null) {
|
||||
if (musicPlayers != null) {
|
||||
subscribe(musicPlayers, "status", genericHandler)
|
||||
subscribe(musicPlayers, "level", genericHandler)
|
||||
subscribe(musicPlayers, "trackDescription", genericHandler)
|
||||
subscribe(musicPlayers, "trackData", genericHandler)
|
||||
subscribe(musicPlayers, "mute", genericHandler)
|
||||
}*/
|
||||
/*if (powerMeters != null) {
|
||||
}
|
||||
if (powerMeters != null) {
|
||||
subscribe(powerMeters, "power", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (presences != null) {
|
||||
subscribe(presences, "presence", genericHandler)
|
||||
}
|
||||
if (humidities != null) {
|
||||
subscribe(humidities, "humidity", genericHandler)
|
||||
}
|
||||
/*if (relaySwitches != null) {
|
||||
if (relaySwitches != null) {
|
||||
subscribe(relaySwitches, "switch", genericHandler)
|
||||
}*/
|
||||
/*if (sleepSensors != null) {
|
||||
}
|
||||
if (sleepSensors != null) {
|
||||
subscribe(sleepSensors, "sleeping", genericHandler)
|
||||
}*/
|
||||
/*if (smokeDetectors != null) {
|
||||
}
|
||||
if (smokeDetectors != null) {
|
||||
subscribe(smokeDetectors, "smoke", genericHandler)
|
||||
}*/
|
||||
/*if (peds != null) {
|
||||
}
|
||||
if (peds != null) {
|
||||
subscribe(peds, "steps", genericHandler)
|
||||
subscribe(peds, "goal", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (switches != null) {
|
||||
subscribe(switches, "switch", genericHandler)
|
||||
}
|
||||
@@ -163,9 +161,9 @@ def subscribeToEvents() {
|
||||
subscribe(thermostats, "thermostatFanMode", genericHandler)
|
||||
subscribe(thermostats, "thermostatOperatingState", genericHandler)
|
||||
}
|
||||
/*if (valves != null) {
|
||||
if (valves != null) {
|
||||
subscribe(valves, "contact", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (waterSensors != null) {
|
||||
subscribe(waterSensors, "water", genericHandler)
|
||||
}
|
||||
@@ -173,23 +171,23 @@ def subscribeToEvents() {
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (state.accessKey == null) {
|
||||
if (atomicState.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: state.accessKey
|
||||
accessKey: atomicState.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (state.bucketKey == null) {
|
||||
if (atomicState.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: state.bucketKey,
|
||||
bucketName: state.bucketName
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -202,53 +200,94 @@ def setBucketKey() {
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
|
||||
state.bucketKey = "$newBucketKey"
|
||||
state.bucketName = "$newBucketName"
|
||||
state.isBucketCreated = false
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newAccessKey && newAccessKey != state.accessKey) {
|
||||
state.accessKey = "$newAccessKey"
|
||||
state.isBucketCreated = false
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
|
||||
atomicState.version = "1.0.18"
|
||||
subscribeToEvents()
|
||||
|
||||
state.isBucketCreated = false
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
atomicState.eventBuffer = []
|
||||
|
||||
runEvery15Minutes(flushBuffer)
|
||||
|
||||
log.debug "installed (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
atomicState.version = "1.0.18"
|
||||
unsubscribe()
|
||||
|
||||
if (state.bucketKey != null && state.accessKey != null) {
|
||||
state.isBucketCreated = false
|
||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (atomicState.eventBuffer == null) {
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
}
|
||||
|
||||
subscribeToEvents()
|
||||
|
||||
log.debug "updated (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def createBucket() {
|
||||
def uninstalled() {
|
||||
log.debug "uninstalled (version $atomicState.version)"
|
||||
}
|
||||
|
||||
if (!state.bucketName) {
|
||||
state.bucketName = state.bucketKey
|
||||
def tryCreateBucket() {
|
||||
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
|
||||
// if the bucket has already been created, no need to continue
|
||||
if (atomicState.isBucketCreated) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!atomicState.bucketName) {
|
||||
atomicState.bucketName = atomicState.bucketKey
|
||||
}
|
||||
def bucketName = "${state.bucketName}"
|
||||
def bucketKey = "${state.bucketKey}"
|
||||
def accessKey = "${state.accessKey}"
|
||||
if (!atomicState.accessKey) {
|
||||
return
|
||||
}
|
||||
def bucketName = "${atomicState.bucketName}"
|
||||
def bucketKey = "${atomicState.bucketKey}"
|
||||
def accessKey = "${atomicState.accessKey}"
|
||||
|
||||
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
||||
|
||||
def bucketCreatePost = [
|
||||
uri: 'https://groker.initialstate.com/api/buckets',
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-AccessKey": accessKey
|
||||
@@ -258,10 +297,20 @@ def createBucket() {
|
||||
|
||||
log.debug bucketCreatePost
|
||||
|
||||
httpPostJson(bucketCreatePost) {
|
||||
log.debug "bucket posted"
|
||||
state.isBucketCreated = true
|
||||
try {
|
||||
// Create a bucket on Initial State so the data has a logical grouping
|
||||
httpPostJson(bucketCreatePost) { resp ->
|
||||
log.debug "bucket posted"
|
||||
if (resp.status >= 400) {
|
||||
log.error "bucket not created successfully"
|
||||
} else {
|
||||
atomicState.isBucketCreated = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "bucket creation error: $e"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def genericHandler(evt) {
|
||||
@@ -273,33 +322,73 @@ def genericHandler(evt) {
|
||||
}
|
||||
def value = "$evt.value"
|
||||
|
||||
tryCreateBucket()
|
||||
|
||||
eventHandler(key, value)
|
||||
}
|
||||
|
||||
def eventHandler(name, value) {
|
||||
// This is a handler function for flushing the event buffer
|
||||
// after a specified amount of time to reduce the load on ST servers
|
||||
def flushBuffer() {
|
||||
log.trace "About to flush the buffer on schedule"
|
||||
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
|
||||
tryShipEvents()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.accessKey == null || state.bucketKey == null) {
|
||||
def eventHandler(name, value) {
|
||||
log.debug atomicState.eventBuffer
|
||||
|
||||
def eventBuffer = atomicState.eventBuffer
|
||||
def epoch = now() / 1000
|
||||
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
||||
|
||||
log.debug eventBuffer
|
||||
|
||||
atomicState.eventBuffer = eventBuffer
|
||||
|
||||
if (eventBuffer.size() >= 10) {
|
||||
tryShipEvents()
|
||||
}
|
||||
}
|
||||
|
||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
||||
def tryShipEvents() {
|
||||
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
// can't ship if access key and bucket key are null, so finish trying
|
||||
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.isBucketCreated) {
|
||||
createBucket()
|
||||
}
|
||||
|
||||
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
|
||||
def eventPost = [
|
||||
uri: 'https://groker.initialstate.com/api/events',
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-BucketKey": "${state.bucketKey}",
|
||||
"X-IS-AccessKey": "${state.accessKey}"
|
||||
"X-IS-BucketKey": "${atomicState.bucketKey}",
|
||||
"X-IS-AccessKey": "${atomicState.accessKey}",
|
||||
"Accept-Version": "0.0.2"
|
||||
],
|
||||
body: eventBody
|
||||
body: atomicState.eventBuffer
|
||||
]
|
||||
|
||||
log.debug eventPost
|
||||
|
||||
httpPostJson(eventPost) {
|
||||
log.debug "event data posted"
|
||||
try {
|
||||
// post the events to initial state
|
||||
httpPostJson(eventPost) { resp ->
|
||||
log.debug "shipped events and got ${resp.status}"
|
||||
if (resp.status >= 400) {
|
||||
log.error "shipping failed... ${resp.data}"
|
||||
} else {
|
||||
// clear the buffer
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "shipping events failed: $e"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* Bose SoundTouch (Connect)
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Bose SoundTouch (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control your Bose SoundTouch speakers",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the urn that we're looking for
|
||||
*
|
||||
* @return URN which we are looking for
|
||||
*
|
||||
* @todo This + getUSNQualifier should be one and should use regular expressions
|
||||
*/
|
||||
def getDeviceType() {
|
||||
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
|
||||
}
|
||||
|
||||
/**
|
||||
* If not null, returns an additional qualifier for ssdUSN
|
||||
* to avoid spamming the network
|
||||
*
|
||||
* @return Additional qualifier OR null if not needed
|
||||
*/
|
||||
def getUSNQualifier() {
|
||||
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "Bose SoundTouch"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace this app and siblings use
|
||||
*
|
||||
* @return namespace
|
||||
*/
|
||||
def getNameSpace() {
|
||||
return "smartthings"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
def deviceDiscovery()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
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) {
|
||||
discoverDevices()
|
||||
}
|
||||
|
||||
// Verify request every 3 seconds except on discoveries
|
||||
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user has selected device(s) and
|
||||
* pressed "Install".
|
||||
*/
|
||||
def installed() {
|
||||
log.trace "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when app has been updated
|
||||
*/
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user uninstalls the app
|
||||
*
|
||||
* We don't need to manually do anything here because any children
|
||||
* are automatically removed upon the removal of the parent.
|
||||
*
|
||||
* Only time to do anything here is when you need to notify
|
||||
* the remote end. And even then you're discouraged from removing
|
||||
* the children manually.
|
||||
*/
|
||||
def uninstalled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* If user has selected devices, will start monitoring devices
|
||||
* for changes (new address, port, etc...)
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize()"
|
||||
state.subscribe = false
|
||||
if (selecteddevice) {
|
||||
addDevice()
|
||||
refreshDevices()
|
||||
subscribeNetworkEvents(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}"
|
||||
|
||||
devlist.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newDevice = devices.find { (it.value.mac) == dni }
|
||||
def deviceName = newDevice?.value.name
|
||||
if (!deviceName)
|
||||
deviceName = getDeviceName() + "[${newDevice?.value.name}]"
|
||||
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
|
||||
d.boseSetDeviceID(newDevice.value.deviceID)
|
||||
log.trace "Created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.trace "${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a DeviceNetworkId to an address. Primarily used by children
|
||||
*
|
||||
* @param dni Device Network id
|
||||
* @return address or null
|
||||
*/
|
||||
def resolveDNI2Address(dni) {
|
||||
def device = getVerifiedDevices().find { (it.value.mac) == dni }
|
||||
if (device) {
|
||||
return convertHexToIP(device.value.networkAddress)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a child to the "Play Everywhere" zone
|
||||
*
|
||||
* @param child The speaker joining the zone
|
||||
* @return A list of maps with POST data
|
||||
*/
|
||||
def boseZoneJoin(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server) {
|
||||
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
|
||||
child.boseSetZone("client")
|
||||
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
getChildDevices().each{ it ->
|
||||
log.trace "child: " + child
|
||||
log.trace "zone : " + it.boseGetZone()
|
||||
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
} else {
|
||||
log.debug "boseJoinZone() No server, add it!"
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
child.boseSetZone("server")
|
||||
}
|
||||
results << result
|
||||
return results
|
||||
}
|
||||
|
||||
def boseZoneReset() {
|
||||
getChildDevices().each{ it.boseSetZone(null) }
|
||||
}
|
||||
|
||||
def boseZoneHasMaster() {
|
||||
return getChildDevices().find{ it.boseGetZone() == "server" } != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a speaker from the play everywhere zone.
|
||||
*
|
||||
* @param child Which speaker is leaving
|
||||
* @return a list of maps with POST data
|
||||
*/
|
||||
def boseZoneLeave(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// First, tag us as a non-member
|
||||
child.boseSetZone(null)
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
} else {
|
||||
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
|
||||
// Dismantle the entire thing, first send this to master
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
|
||||
// Also issue this to each individual client
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.trace "Additional device: " + dev
|
||||
result['host'] = dev.getDeviceIP() + ":8090"
|
||||
results << result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Define our XML parsers
|
||||
*
|
||||
* @return mapping of root-node <-> parser function
|
||||
*/
|
||||
def getParsers() {
|
||||
[
|
||||
"root" : "parseDESC",
|
||||
"info" : "parseINFO"
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when location has changed, contains information from
|
||||
* network transactions. See deviceDiscovery() for where it is
|
||||
* registered.
|
||||
*
|
||||
* @param evt Holds event information
|
||||
*/
|
||||
def onLocation(evt) {
|
||||
// Convert the event into something we can use
|
||||
def lanEvent = parseLanMessage(evt.description, true)
|
||||
lanEvent << ["hub":evt?.hubId]
|
||||
|
||||
// Determine what we need to do...
|
||||
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
|
||||
(getUSNQualifier() == null ||
|
||||
lanEvent?.ssdpUSN?.contains(getUSNQualifier())
|
||||
)
|
||||
)
|
||||
{
|
||||
parseSSDP(lanEvent)
|
||||
}
|
||||
else if (
|
||||
lanEvent.headers && lanEvent.body &&
|
||||
lanEvent.headers."content-type".contains("xml")
|
||||
)
|
||||
{
|
||||
def parsers = getParsers()
|
||||
def xmlData = new XmlSlurper().parseText(lanEvent.body)
|
||||
|
||||
// Let each parser take a stab at it
|
||||
parsers.each { node,func ->
|
||||
if (xmlData.name() == node)
|
||||
"$func"(xmlData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP description file.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseDESC(xmlData) {
|
||||
log.info "parseDESC()"
|
||||
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
def devicetxml = body.device.deviceType.text().toLowerCase()
|
||||
|
||||
// Make sure it's the type we want
|
||||
if (devicetxml == devicetype) {
|
||||
def devices = getDevices()
|
||||
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
|
||||
if (device && !device.value?.verified) {
|
||||
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
|
||||
device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
|
||||
} else {
|
||||
log.error "parseDESC(): The xml file returned a device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle BOSE <info></info> result. This is an alternative to
|
||||
* using the SSDP description standard. Some of the speakers do
|
||||
* not support SSDP description, so we need this as well.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseINFO(xmlData) {
|
||||
log.info "parseINFO()"
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
|
||||
def deviceID = xmlData.attributes()['deviceID']
|
||||
def device = getDevices().find {it?.key?.contains(deviceID)}
|
||||
if (device && !device.value?.verified) {
|
||||
device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP discovery messages and adds them to the list
|
||||
* of discovered devices. If it already exists, it will update
|
||||
* the port and location (in case it was moved).
|
||||
*
|
||||
* @param lanEvent
|
||||
*/
|
||||
def parseSSDP(lanEvent) {
|
||||
//SSDP DISCOVERY EVENTS
|
||||
def USN = lanEvent.ssdpUSN.toString()
|
||||
def devices = getDevices()
|
||||
|
||||
if (!(devices."${USN}")) {
|
||||
//device does not exist
|
||||
log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
|
||||
devices << ["${USN}":lanEvent]
|
||||
} else {
|
||||
// update the values
|
||||
def d = devices."${USN}"
|
||||
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
|
||||
log.trace "parseSSDP() Updating device location (ip & port)"
|
||||
d.networkAddress = lanEvent.networkAddress
|
||||
d.deviceAddress = lanEvent.deviceAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the refresh loop, making sure to keep us up-to-date with changes
|
||||
*
|
||||
*/
|
||||
private refreshDevices() {
|
||||
discoverDevices()
|
||||
verifyDevices()
|
||||
runIn(300, "refreshDevices")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
subscribe(location, null, onLocation, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType())
|
||||
*/
|
||||
private discoverDevices() {
|
||||
log.trace "discoverDevice() Issuing SSDP request"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through the list of unverified devices and issues a verification
|
||||
* request for each of them (basically calling verifyDevice() per unverified)
|
||||
*/
|
||||
private verifyDevices() {
|
||||
def devices = getDevices().findAll { it?.value?.verified != true }
|
||||
|
||||
devices.each {
|
||||
verifyDevice(
|
||||
it?.value?.mac,
|
||||
convertHexToIP(it?.value?.networkAddress),
|
||||
convertHexToInt(it?.value?.deviceAddress),
|
||||
it?.value?.ssdpPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the device, in this case, we need to obtain the info block which
|
||||
* holds information such as the actual mac to use in certain scenarios.
|
||||
*
|
||||
* Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
|
||||
* functions.
|
||||
*
|
||||
* @param deviceNetworkId The DNI of the device
|
||||
* @param ip The address of the device on the network (not the same as DNI)
|
||||
* @param port The port to use (0 will be treated as invalid and will use 80)
|
||||
* @param devicessdpPath The URL path (for example, /desc)
|
||||
*
|
||||
* @note Result is captured in locationHandler()
|
||||
*/
|
||||
private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) {
|
||||
if(ip) {
|
||||
def address = ip + ":8090"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/info",
|
||||
headers: [
|
||||
HOST: address,
|
||||
]]))
|
||||
} else {
|
||||
log.warn("verifyDevice() IP address was empty")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of devices which have been verified
|
||||
*
|
||||
* @return array of verified devices
|
||||
*/
|
||||
def getVerifiedDevices() {
|
||||
getDevices().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all discovered devices or an empty array if none
|
||||
*
|
||||
* @return array of devices
|
||||
*/
|
||||
def getDevices() {
|
||||
state.devices = state.devices ?: [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal string to an integer
|
||||
*
|
||||
* @param hex The string with a hexadecimal value
|
||||
* @return An integer
|
||||
*/
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD
|
||||
*
|
||||
* @param hex Address represented in hex
|
||||
* @return String containing normal IPv4 dot notation
|
||||
*/
|
||||
private String convertHexToIP(hex) {
|
||||
if (hex)
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
else
|
||||
hex
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this setup can support SmarthThing Labs items
|
||||
*
|
||||
* @return true if it supports it.
|
||||
*/
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the firmwares on all hubs owned by user match or exceed the
|
||||
* provided version number.
|
||||
*
|
||||
* @param desiredFirmware The version that must match or exceed
|
||||
* @return true if hub has same or newer
|
||||
*/
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of firmware version for every hub the user has
|
||||
*
|
||||
* @return List of firmwares
|
||||
*/
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -64,10 +64,14 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -94,11 +98,20 @@ def bridgeLinking()
|
||||
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||
def paragraphText
|
||||
def hueimage = null
|
||||
if (selectedHue) {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
hueimage = null
|
||||
}
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success! - click 'Next'"
|
||||
title = "Success!"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
hueimage = null
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
@@ -106,30 +119,39 @@ def bridgeLinking()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("Button Press") {
|
||||
section("") {
|
||||
paragraph """${paragraphText}"""
|
||||
if (hueimage != null)
|
||||
image "${hueimage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bulbDiscovery()
|
||||
{
|
||||
def bulbDiscovery() {
|
||||
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
state.inBulbDiscovery = true
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
state.bridgeRefreshCount = 0
|
||||
def bulboptions = bulbsDiscovered() ?: [:]
|
||||
def numFound = bulboptions.size() ?: 0
|
||||
if (numFound == 0)
|
||||
app.updateSetting("selectedBulbs", "")
|
||||
|
||||
if((bulbRefreshCount % 3) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
||||
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]
|
||||
|
||||
@@ -194,24 +216,29 @@ Map bridgesDiscovered() {
|
||||
|
||||
Map bulbsDiscovered() {
|
||||
def bulbs = getHueBulbs()
|
||||
def map = [:]
|
||||
def bulbmap = [:]
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
bulbs.each {
|
||||
def value = "${it?.value?.name}"
|
||||
def key = app.id +"/"+ it?.value?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.value.name}"
|
||||
def key = app.id +"/"+ it.value.id
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
bulbs.each {
|
||||
def value = "${it?.name}"
|
||||
def key = app.id +"/"+ it?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.name}"
|
||||
def key = app.id +"/"+ it.id
|
||||
logg += "$value - $key, "
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
return bulbmap
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
@@ -231,24 +258,19 @@ def installed() {
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
state.subscribe = false
|
||||
state.bridgeSelectedOverride = false
|
||||
def bridge = null
|
||||
|
||||
log.debug "Initializing"
|
||||
unsubscribe(bridge)
|
||||
state.inBulbDiscovery = false
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
addBridge()
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
@@ -263,27 +285,34 @@ def manualRefresh() {
|
||||
|
||||
def uninstalled(){
|
||||
state.bridges = [:]
|
||||
state.subscribe = false
|
||||
state.username = null
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(evt) {
|
||||
def bulbs = [:]
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
evt.jsonData.each { k,v ->
|
||||
log.trace "$k: $v"
|
||||
if (v instanceof Map) {
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
|
||||
}
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
log.info "${bulbs.size()} bulbs found"
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs.each { dni ->
|
||||
selectedBulbs?.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
@@ -294,7 +323,7 @@ def addBulbs() {
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
@@ -322,7 +351,7 @@ def addBridge() {
|
||||
def d = getChildDevice(selectedHue)
|
||||
if(!d) {
|
||||
// compatibility with old devices
|
||||
def newbridge = true
|
||||
def newbridge = true
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
@@ -332,22 +361,27 @@ def addBridge() {
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
newbridge = false
|
||||
newbridge = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
if (vbridge.value.ip && vbridge.value.port) {
|
||||
if (vbridge.value.ip.contains("."))
|
||||
if (vbridge.value.ip.contains(".")) {
|
||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||
else
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
} else
|
||||
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
}
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
||||
@@ -355,7 +389,6 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -397,8 +430,11 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
@@ -406,7 +442,8 @@ def locationHandler(evt) {
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
}
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,17 +491,13 @@ def locationHandler(evt) {
|
||||
|
||||
def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
|
||||
//shrink the large bulb lists
|
||||
convertBulbListToMap()
|
||||
|
||||
poll()
|
||||
|
||||
if(!state.subscribe) {
|
||||
try {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
} catch (all) {
|
||||
log.trace "Subscription already exist"
|
||||
}
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
@@ -473,44 +506,49 @@ def doDeviceSync(){
|
||||
/////////////////////////////////////
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body instanceof java.util.HashMap)
|
||||
{ //poll response
|
||||
def bodyString = parsedEvent.body.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body
|
||||
try {
|
||||
body = new groovy.json.JsonSlurper().parseText(bodyString)
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
poll()
|
||||
}
|
||||
if (body instanceof java.util.HashMap) {
|
||||
//poll response
|
||||
def bulbs = getChildDevices()
|
||||
//for each bulb
|
||||
for (bulb in body) {
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
if (d) {
|
||||
if (bulb.value.state?.reachable) {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{ //put response
|
||||
@@ -559,25 +597,25 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition = 4) {
|
||||
def on(childDevice, transition_deprecated = 0) {
|
||||
log.debug "Executing 'on'"
|
||||
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
|
||||
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
|
||||
def percent = childDevice.device?.currentValue("level") as Integer
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
||||
return "level: $percent"
|
||||
}
|
||||
|
||||
def off(childDevice, transition = 4) {
|
||||
def off(childDevice, transition_deprecated = 0) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
return "level: 0"
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
@@ -598,19 +636,21 @@ def setHue(childDevice, percent) {
|
||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||
}
|
||||
|
||||
def setColor(childDevice, color, alert = "none", transition = 4) {
|
||||
log.debug "Executing 'setColor($color)'"
|
||||
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
|
||||
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
||||
def transition = huesettings.transition ? huesettings.transition : 4
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (color.level != null) {
|
||||
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||
if (huesettings.level != null) {
|
||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
|
||||
if (color.switch) {
|
||||
value.on = color.switch == "on"
|
||||
if (huesettings.switch) {
|
||||
value.on = huesettings.switch == "on"
|
||||
}
|
||||
|
||||
log.debug "sending command $value"
|
||||
@@ -640,15 +680,19 @@ private getId(childDevice) {
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
try {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
def host = getBridgeIP()
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/$path"
|
||||
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
||||
def length = bodyJSON.getBytes().size().toString()
|
||||
@@ -668,12 +712,21 @@ ${bodyJSON}
|
||||
private getBridgeIP() {
|
||||
def host = null
|
||||
if (selectedHue) {
|
||||
def d = getChildDevice(dni)
|
||||
if (d)
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
def d = getChildDevice(selectedHue)
|
||||
if (d) {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
host = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (!bridge) {
|
||||
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
|
||||
//in this case we will look up the bridge by comparing the incorrect mac addresses
|
||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||
}
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
@@ -681,9 +734,9 @@ private getBridgeIP() {
|
||||
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
||||
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
||||
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
||||
}
|
||||
}
|
||||
log.trace "Bridge: $selectedHue - Host: $host"
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
|
||||
394
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
394
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* LIFX
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "LIFX (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "LIFX",
|
||||
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
oauth: true) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
}
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "Credentials", title: "LIFX", content: "authPage", install: false)
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/callback") { action: [ GET: "oauthCallback" ] }
|
||||
path("/oauth/initialize") { action: [ GET: "oauthInit"] }
|
||||
path("/test") { action: [ GET: "oauthSuccess" ] }
|
||||
}
|
||||
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" }
|
||||
def buildRedirectUrl(page) {
|
||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage"
|
||||
if (!state.lifxAccessToken) {
|
||||
log.debug "no LIFX access token"
|
||||
// This is the SmartThings access token
|
||||
if (!state.accessToken) {
|
||||
log.debug "no access token, create access token"
|
||||
createAccessToken() // predefined method
|
||||
}
|
||||
def description = "Tap to enter LIFX credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below
|
||||
log.debug "app id: ${app.id}"
|
||||
log.debug "redirect url: ${redirectUrl}"
|
||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) {
|
||||
section {
|
||||
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
||||
// href(url:buildRedirectUrl("test"), title: "Message test")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "have LIFX access token"
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size()
|
||||
def refreshInterval = 3
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// OAuth
|
||||
|
||||
def oauthInit() {
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ]
|
||||
log.info("Redirecting user to OAuth setup")
|
||||
redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def oauthCallback() {
|
||||
def redirectUrl = null
|
||||
if (params.authQueryString) {
|
||||
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
success()
|
||||
} else {
|
||||
def code = params.code
|
||||
if (code) {
|
||||
if (code.size() > 6) {
|
||||
// LIFX code
|
||||
log.debug "Exchanging code for access token"
|
||||
oauthReceiveToken(redirectUrl)
|
||||
} else {
|
||||
// Initiate the LIFX OAuth flow.
|
||||
oauthInit()
|
||||
}
|
||||
} else {
|
||||
log.debug "This code should be unreachable"
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def oauthReceiveToken(redirectUrl = null) {
|
||||
|
||||
log.debug "receiveToken - params: ${params}"
|
||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
|
||||
def params = [
|
||||
uri: "https://cloud.lifx.com/oauth/token",
|
||||
body: oauthParams,
|
||||
headers: [
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
]
|
||||
httpPost(params) { response ->
|
||||
state.lifxAccessToken = response.data.access_token
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
oauthSuccess()
|
||||
} else {
|
||||
oauthFailure()
|
||||
}
|
||||
}
|
||||
|
||||
def oauthSuccess() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthFailure() {
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthReceivedToken() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is already connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthConnectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>SmartThings Connection</title>
|
||||
<style type="text/css">
|
||||
@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: 280;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 15px;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
${redirectHtml}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
|
||||
<p>
|
||||
${message}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
// App lifecycle hooks
|
||||
|
||||
def installed() {
|
||||
enableCallback() // wtf does this do?
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
// Check for new devices and remove old ones every 3 hours
|
||||
runEvery3Hours('updateDevices')
|
||||
}
|
||||
|
||||
// called after settings are changed
|
||||
def updated() {
|
||||
enableCallback() // not sure what this does
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.info("Uninstalling, removing child devices...")
|
||||
unschedule('updateDevices')
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
private removeChildDevices(devices) {
|
||||
devices.each {
|
||||
deleteChildDevice(it.deviceNetworkId) // 'it' is default
|
||||
}
|
||||
}
|
||||
|
||||
// called after Done is hit after selecting a Location
|
||||
def initialize() {
|
||||
log.debug "initialize"
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
// Misc
|
||||
|
||||
Map apiRequestHeaders() {
|
||||
return ["Authorization": "Bearer ${state.lifxAccessToken}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
}
|
||||
|
||||
// Requests
|
||||
|
||||
def logResponse(response) {
|
||||
log.info("Status: ${response.status}")
|
||||
log.info("Body: ${response.data}")
|
||||
}
|
||||
|
||||
// API Requests
|
||||
// logObject is because log doesn't work if this method is being called from a Device
|
||||
def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
||||
try {
|
||||
return c()
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}")
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("lifxAccessToken")
|
||||
options.logObject.warn "Access token is not valid"
|
||||
}
|
||||
return options.errerReturn
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
options.logObject.warn "Connection timed out, not much we can do here"
|
||||
return options.errerReturn
|
||||
}
|
||||
}
|
||||
|
||||
def apiGET(path) {
|
||||
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
def apiPUT(path, body = [:]) {
|
||||
log.debug("Beginning API PUT: ${path}, ${body}")
|
||||
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
def devicesList(selector = '') {
|
||||
logErrors([]) {
|
||||
def resp = apiGET("/lights/${selector}")
|
||||
if (resp.status == 200) {
|
||||
return resp.data
|
||||
} else {
|
||||
log.error("Non-200 from device list call. ${resp.status} ${resp.data}")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map locationOptions() {
|
||||
|
||||
def options = [:]
|
||||
def devices = devicesList()
|
||||
devices.each { device ->
|
||||
options[device.location.id] = device.location.name
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
def devicesInLocation() {
|
||||
return devicesList("location_id:${settings.selectedLocationId}")
|
||||
}
|
||||
|
||||
// ensures the devices list is up to date
|
||||
def updateDevices() {
|
||||
if (!state.devices) {
|
||||
state.devices = [:]
|
||||
}
|
||||
def devices = devicesInLocation()
|
||||
def deviceIds = devices*.id
|
||||
devices.each { device ->
|
||||
def childDevice = getChildDevice(device.id)
|
||||
if (!childDevice) {
|
||||
log.info("Adding device ${device.id}: ${device.capabilities}")
|
||||
def data = [
|
||||
label: device.label,
|
||||
level: sprintf("%f", (device.brightness ?: 1) * 100),
|
||||
switch: device.connected ? device.power : "unreachable",
|
||||
colorTemperature: device.color.kelvin
|
||||
]
|
||||
if (device.capabilities.has_color) {
|
||||
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
||||
data["hue"] = device.color.hue / 3.6
|
||||
data["saturation"] = device.color.saturation * 100
|
||||
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
|
||||
} else {
|
||||
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each {
|
||||
log.info("Deleting ${it.deviceNetworkId}")
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block
|
||||
}
|
||||
|
||||
def refreshDevices() {
|
||||
log.info("Refreshing all devices...")
|
||||
getChildDevices().each { device ->
|
||||
device.refresh()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "Juan Pablo Risso",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||
@@ -394,7 +394,9 @@ def discovery() {
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||
state.resethub = true
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -459,7 +461,9 @@ def activity(dni,mode) {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
}
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
msg = ex
|
||||
}
|
||||
runIn(10, "poll", [overwrite: true])
|
||||
return msg
|
||||
}
|
||||
@@ -517,7 +521,9 @@ def poll() {
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +556,9 @@ def getActivityList() {
|
||||
log.trace e
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.trace e
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
@@ -565,9 +573,9 @@ def getActivityName(activity,hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
@@ -585,9 +593,9 @@ def getActivityId(activity,hubId) {
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
@@ -602,9 +610,9 @@ def getHubName(hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
@@ -79,12 +79,9 @@ def scenePage(params=[:]) {
|
||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||
}
|
||||
|
||||
if (sceneId == currentSceneId) {
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +222,7 @@ private restoreStates(sceneId) {
|
||||
if (type == "level") {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(value)
|
||||
light.setLevel(level)
|
||||
}
|
||||
}
|
||||
else if (type == "color") {
|
||||
|
||||
@@ -346,18 +346,20 @@ private getSensorJSON(id, key) {
|
||||
|
||||
def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
|
||||
|
||||
httpGet(uri: sensorUrl) { response ->
|
||||
httpGet(uri: sensorUrl) { response ->
|
||||
|
||||
def json = new org.json.JSONObject(response.data)
|
||||
def sensors = [:]
|
||||
|
||||
state.sensors = json
|
||||
|
||||
json.each { sensorId, sensorName ->
|
||||
response.data.each { sensorId, sensorName ->
|
||||
sensors[sensorId] = sensorName
|
||||
createChild(sensorId, sensorName)
|
||||
}
|
||||
}
|
||||
|
||||
state.sensors = sensors
|
||||
|
||||
return "success"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def createChild(sensorId, sensorName) {
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
/**
|
||||
* Title: Withings Service Manager
|
||||
* Description: Connect Your Withings Devices
|
||||
*
|
||||
* Author: steve
|
||||
* Date: 1/9/15
|
||||
*
|
||||
*
|
||||
* Copyright 2015 steve
|
||||
*
|
||||
* 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: "Withings Manager",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Connect With Withings",
|
||||
category: "",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
oauth: true
|
||||
) {
|
||||
appSetting "consumerKey"
|
||||
appSetting "consumerSecret"
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PAGES
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
page(name: "authPage")
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
|
||||
def installOptions = false
|
||||
def description = "Required (tap to set)"
|
||||
def authState
|
||||
|
||||
if (oauth_token()) {
|
||||
// TODO: Check if it's valid
|
||||
if (true) {
|
||||
description = "Saved (tap to change)"
|
||||
installOptions = true
|
||||
authState = "complete"
|
||||
} else {
|
||||
// Worth differentiating here? (no longer valid vs. non-existent state.externalAuthToken?)
|
||||
description = "Required (tap to set)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dynamicPage(name: "authPage", install: installOptions, uninstall: true) {
|
||||
section {
|
||||
|
||||
if (installOptions) {
|
||||
input(name: "withingsLabel", type: "text", title: "Add a name", description: null, required: true)
|
||||
}
|
||||
|
||||
href url: shortUrl("authenticate"), style: "embedded", required: false, title: "Authenticate with Withings", description: description, state: authState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// MAPPINGS
|
||||
// ========================================================
|
||||
|
||||
mappings {
|
||||
path("/authenticate") {
|
||||
action:
|
||||
[
|
||||
GET: "authenticate"
|
||||
]
|
||||
}
|
||||
path("/x") {
|
||||
action:
|
||||
[
|
||||
GET: "exchangeTokenFromWithings"
|
||||
]
|
||||
}
|
||||
path("/n") {
|
||||
action:
|
||||
[POST: "notificationReceived"]
|
||||
}
|
||||
|
||||
path("/test/:action") {
|
||||
action:
|
||||
[GET: "test"]
|
||||
}
|
||||
}
|
||||
|
||||
def test() {
|
||||
"${params.action}"()
|
||||
}
|
||||
|
||||
def authenticate() {
|
||||
// do not hit userAuthorizationUrl when the page is executed. It will replace oauth_tokens
|
||||
// instead, redirect through here so we know for sure that the user wants to authenticate
|
||||
// plus, the short-lived tokens that are used during authentication are only valid for 2 minutes
|
||||
// so make sure we give the user as much of that 2 minutes as possible to enter their credentials and deal with network latency
|
||||
log.trace "starting Withings authentication flow"
|
||||
redirect location: userAuthorizationUrl()
|
||||
}
|
||||
|
||||
def exchangeTokenFromWithings() {
|
||||
// Withings hits us here during the oAuth flow
|
||||
// log.trace "exchangeTokenFromWithings ${params}"
|
||||
atomicState.userid = params.userid // TODO: restructure this for multi-user access
|
||||
exchangeToken()
|
||||
}
|
||||
|
||||
def notificationReceived() {
|
||||
// log.trace "notificationReceived params: ${params}"
|
||||
|
||||
def notificationParams = [
|
||||
startdate: params.startdate,
|
||||
userid : params.userid,
|
||||
enddate : params.enddate,
|
||||
]
|
||||
|
||||
def measures = wGetMeasures(notificationParams)
|
||||
sendMeasureEvents(measures)
|
||||
return [status: 0]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// HANDLERS
|
||||
// ========================================================
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
// wRevokeAllNotifications()
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
if (!getChild()) { createChild() }
|
||||
app.updateLabel(withingsLabel)
|
||||
wCreateNotification()
|
||||
backfillMeasures()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// CHILD DEVICE
|
||||
// ========================================================
|
||||
|
||||
private getChild() {
|
||||
def children = childDevices
|
||||
children.size() ? children.first() : null
|
||||
}
|
||||
|
||||
private void createChild() {
|
||||
def child = addChildDevice("smartthings", "Withings User", userid(), null, [name: app.label, label: withingsLabel])
|
||||
atomicState.child = [dni: child.deviceNetworkId]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// URL HELPERS
|
||||
// ========================================================
|
||||
|
||||
def stBaseUrl() {
|
||||
if (!atomicState.serverUrl) {
|
||||
stToken()
|
||||
atomicState.serverUrl = buildActionUrl("").split(/api\//).first()
|
||||
}
|
||||
return atomicState.serverUrl
|
||||
}
|
||||
|
||||
def stToken() {
|
||||
atomicState.accessToken ?: createAccessToken()
|
||||
}
|
||||
|
||||
def shortUrl(path = "", urlParams = [:]) {
|
||||
attachParams("${stBaseUrl()}api/t/${stToken()}/s/${app.id}/${path}", urlParams)
|
||||
}
|
||||
|
||||
def noTokenUrl(path = "", urlParams = [:]) {
|
||||
attachParams("${stBaseUrl()}api/smartapps/installations/${app.id}/${path}", urlParams)
|
||||
}
|
||||
|
||||
def attachParams(url, urlParams = [:]) {
|
||||
[url, toQueryString(urlParams)].findAll().join("?")
|
||||
}
|
||||
|
||||
String toQueryString(Map m = [:]) {
|
||||
// log.trace "toQueryString. URLEncoder will be used on ${m}"
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS MEASURES
|
||||
// ========================================================
|
||||
|
||||
def unixTime(date = new Date()) {
|
||||
def unixTime = date.time / 1000 as int
|
||||
// log.debug "converting ${date.time} to ${unixTime}"
|
||||
unixTime
|
||||
}
|
||||
|
||||
def backfillMeasures() {
|
||||
// log.trace "backfillMeasures"
|
||||
def measureParams = [startdate: unixTime(new Date() - 10)]
|
||||
def measures = wGetMeasures(measureParams)
|
||||
sendMeasureEvents(measures)
|
||||
}
|
||||
|
||||
// this is body measures. // TODO: get activity and others too
|
||||
def wGetMeasures(measureParams = [:]) {
|
||||
def baseUrl = "https://wbsapi.withings.net/measure"
|
||||
def urlParams = [
|
||||
action : "getmeas",
|
||||
userid : userid(),
|
||||
startdate : unixTime(new Date() - 5),
|
||||
enddate : unixTime(),
|
||||
oauth_token: oauth_token()
|
||||
] + measureParams
|
||||
def measureData = fetchDataFromWithings(baseUrl, urlParams)
|
||||
// log.debug "measureData: ${measureData}"
|
||||
measureData.body.measuregrps.collect { parseMeasureGroup(it) }.flatten()
|
||||
}
|
||||
/*
|
||||
[
|
||||
body:[
|
||||
measuregrps:[
|
||||
[
|
||||
category:1, // 1 for real measurements, 2 for user objectives.
|
||||
grpid:310040317,
|
||||
measures:[
|
||||
[
|
||||
unit:0, // Power of ten the "value" parameter should be multiplied to to get the real value. Eg : value = 20 and unit=-1 means the value really is 2.0
|
||||
value:60, // Value for the measure in S.I units (kilogram, meters, etc.). Value should be multiplied by 10 to the power of "unit" (see below) to get the real value.
|
||||
type:11 // 1 : Weight (kg), 4 : Height (meter), 5 : Fat Free Mass (kg), 6 : Fat Ratio (%), 8 : Fat Mass Weight (kg), 9 : Diastolic Blood Pressure (mmHg), 10 : Systolic Blood Pressure (mmHg), 11 : Heart Pulse (bpm), 54 : SP02(%)
|
||||
],
|
||||
[
|
||||
unit:-3,
|
||||
value:-1000,
|
||||
type:18
|
||||
]
|
||||
],
|
||||
date:1422750210,
|
||||
attrib:2
|
||||
]
|
||||
],
|
||||
updatetime:1422750227
|
||||
],
|
||||
status:0
|
||||
]
|
||||
*/
|
||||
|
||||
def sendMeasureEvents(measures) {
|
||||
// log.debug "measures: ${measures}"
|
||||
measures.each {
|
||||
if (it.name && it.value) {
|
||||
sendEvent(userid(), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def parseMeasureGroup(measureGroup) {
|
||||
long time = measureGroup.date // must be long. INT_MAX is too small
|
||||
time *= 1000
|
||||
measureGroup.measures.collect { parseMeasure(it) + [date: new Date(time)] }
|
||||
}
|
||||
|
||||
def parseMeasure(measure) {
|
||||
// log.debug "parseMeasure($measure)"
|
||||
[
|
||||
name : measureAttribute(measure),
|
||||
value: measureValue(measure)
|
||||
]
|
||||
}
|
||||
|
||||
def measureValue(measure) {
|
||||
def value = measure.value * 10.power(measure.unit)
|
||||
if (measure.type == 1) { // Weight (kg)
|
||||
value *= 2.20462262 // kg to lbs
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
String measureAttribute(measure) {
|
||||
def attribute = ""
|
||||
switch (measure.type) {
|
||||
case 1: attribute = "weight"; break;
|
||||
case 4: attribute = "height"; break;
|
||||
case 5: attribute = "leanMass"; break;
|
||||
case 6: attribute = "fatRatio"; break;
|
||||
case 8: attribute = "fatMass"; break;
|
||||
case 9: attribute = "diastolicPressure"; break;
|
||||
case 10: attribute = "systolicPressure"; break;
|
||||
case 11: attribute = "heartPulse"; break;
|
||||
case 54: attribute = "SP02"; break;
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
String measureDescription(measure) {
|
||||
def description = ""
|
||||
switch (measure.type) {
|
||||
case 1: description = "Weight (kg)"; break;
|
||||
case 4: description = "Height (meter)"; break;
|
||||
case 5: description = "Fat Free Mass (kg)"; break;
|
||||
case 6: description = "Fat Ratio (%)"; break;
|
||||
case 8: description = "Fat Mass Weight (kg)"; break;
|
||||
case 9: description = "Diastolic Blood Pressure (mmHg)"; break;
|
||||
case 10: description = "Systolic Blood Pressure (mmHg)"; break;
|
||||
case 11: description = "Heart Pulse (bpm)"; break;
|
||||
case 54: description = "SP02(%)"; break;
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS NOTIFICATIONS
|
||||
// ========================================================
|
||||
|
||||
def wNotificationBaseUrl() { "https://wbsapi.withings.net/notify" }
|
||||
|
||||
def wNotificationCallbackUrl() { shortUrl("n") }
|
||||
|
||||
def wGetNotification() {
|
||||
def userId = userid()
|
||||
def url = wNotificationBaseUrl()
|
||||
def params = [
|
||||
action: "subscribe"
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
// TODO: keep track of notification expiration
|
||||
def wCreateNotification() {
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "subscribe",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token(),
|
||||
comment : "hmm" // TODO: figure out what to do here. spaces seem to break the request
|
||||
]
|
||||
|
||||
fetchDataFromWithings(baseUrl, urlParams)
|
||||
}
|
||||
|
||||
def wRevokeAllNotifications() {
|
||||
def notifications = wListNotifications()
|
||||
notifications.each {
|
||||
wRevokeNotification([callbackurl: it.callbackurl]) // use the callbackurl Withings has on file
|
||||
}
|
||||
}
|
||||
|
||||
def wRevokeNotification(notificationParams = [:]) {
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "revoke",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token()
|
||||
] + notificationParams
|
||||
|
||||
fetchDataFromWithings(baseUrl, urlParams)
|
||||
}
|
||||
|
||||
def wListNotifications() {
|
||||
|
||||
/*
|
||||
{
|
||||
body: {
|
||||
profiles: [
|
||||
{
|
||||
appli: 1,
|
||||
expires: 2147483647,
|
||||
callbackurl: "https://graph.api.smartthings.com/api/t/72ab3e57-5839-4cca-9562-dcc818f83bc9/s/537757a0-c4c8-40ea-8cea-aa283915bbd9/n",
|
||||
comment: "hmm"
|
||||
}
|
||||
]
|
||||
},
|
||||
status: 0
|
||||
}*/
|
||||
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "list",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token()
|
||||
]
|
||||
|
||||
def notificationData = fetchDataFromWithings(baseUrl, urlParams)
|
||||
notificationData.body.profiles
|
||||
}
|
||||
|
||||
def defaultOauthParams() {
|
||||
defaultParameterKeys().inject([:]) { keyMap, currentKey ->
|
||||
keyMap[currentKey] = "${currentKey}"()
|
||||
keyMap
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS DATA FETCHING
|
||||
// ========================================================
|
||||
|
||||
def fetchDataFromWithings(baseUrl, urlParams) {
|
||||
|
||||
// log.debug "fetchDataFromWithings(${baseUrl}, ${urlParams})"
|
||||
|
||||
def defaultParams = defaultOauthParams()
|
||||
def paramStrings = buildOauthParams(urlParams + defaultParams)
|
||||
// log.debug "paramStrings: $paramStrings"
|
||||
def url = buildOauthUrl(baseUrl, paramStrings, oauth_token_secret())
|
||||
def json
|
||||
// log.debug "about to make request to ${url}"
|
||||
httpGet(uri: url, headers: ["Content-Type": "application/json"]) { response ->
|
||||
json = new groovy.json.JsonSlurper().parse(response.data)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH LOGGING
|
||||
// ========================================================
|
||||
|
||||
def wLogEnabled() { false } // For troubleshooting Oauth flow
|
||||
|
||||
void wLog(message = "") {
|
||||
if (!wLogEnabled()) { return }
|
||||
def wLogMessage = atomicState.wLogMessage
|
||||
if (wLogMessage.length()) {
|
||||
wLogMessage += "\n|"
|
||||
}
|
||||
wLogMessage += message
|
||||
atomicState.wLogMessage = wLogMessage
|
||||
}
|
||||
|
||||
void wLogNew(seedMessage = "") {
|
||||
if (!wLogEnabled()) { return }
|
||||
def olMessage = atomicState.wLogMessage
|
||||
if (oldMessage) {
|
||||
log.debug "purging old wLogMessage: ${olMessage}"
|
||||
}
|
||||
atomicState.wLogMessage = seedMessage
|
||||
}
|
||||
|
||||
String wLogMessage() {
|
||||
if (!wLogEnabled()) { return }
|
||||
def wLogMessage = atomicState.wLogMessage
|
||||
atomicState.wLogMessage = ""
|
||||
wLogMessage
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH DESCRIPTION
|
||||
// >>>>>> The user opens the authPage for this SmartApp
|
||||
// STEP 1 get a token to be used in the url the user taps
|
||||
// STEP 2 generate the url to be tapped by the user
|
||||
// >>>>>> The user taps the url and logs in to Withings
|
||||
// STEP 3 generate a token to be used for accessing user data
|
||||
// STEP 4 access user data
|
||||
// ========================================================
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 1: get an oAuth "request token"
|
||||
// ========================================================
|
||||
|
||||
def requestTokenUrl() {
|
||||
wLogNew "WITHINGS OAUTH STEP 1: get an oAuth 'request token'"
|
||||
|
||||
def keys = defaultParameterKeys() + "oauth_callback"
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/request_token", paramStrings, "")
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 2: End-user authorization
|
||||
// ========================================================
|
||||
|
||||
def userAuthorizationUrl() {
|
||||
|
||||
// get url from Step 1
|
||||
def tokenUrl = requestTokenUrl()
|
||||
|
||||
// collect token from Withings
|
||||
collectTokenFromWithings(tokenUrl)
|
||||
|
||||
wLogNew "WITHINGS OAUTH STEP 2: End-user authorization"
|
||||
|
||||
def keys = defaultParameterKeys() + "oauth_token"
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/authorize", paramStrings, oauth_token_secret())
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 3: Generating access token
|
||||
// ========================================================
|
||||
|
||||
def exchangeTokenUrl() {
|
||||
wLogNew "WITHINGS OAUTH STEP 3: Generating access token"
|
||||
|
||||
def keys = defaultParameterKeys() + ["oauth_token", "userid"]
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/access_token", paramStrings, oauth_token_secret())
|
||||
}
|
||||
|
||||
def exchangeToken() {
|
||||
|
||||
def tokenUrl = exchangeTokenUrl()
|
||||
// log.debug "about to hit ${tokenUrl}"
|
||||
|
||||
try {
|
||||
// replace old token with a long-lived token
|
||||
def token = collectTokenFromWithings(tokenUrl)
|
||||
// log.debug "collected token from Withings: ${token}"
|
||||
renderAction("authorized", "Withings Connection")
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error e
|
||||
renderAction("notAuthorized", "Withings Connection Failed")
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// OAUTH 1.0
|
||||
// ========================================================
|
||||
|
||||
def defaultParameterKeys() {
|
||||
[
|
||||
"oauth_consumer_key",
|
||||
"oauth_nonce",
|
||||
"oauth_signature_method",
|
||||
"oauth_timestamp",
|
||||
"oauth_version"
|
||||
]
|
||||
}
|
||||
|
||||
def oauth_consumer_key() { consumerKey }
|
||||
|
||||
def oauth_nonce() { nonce() }
|
||||
|
||||
def nonce() { UUID.randomUUID().toString().replaceAll("-", "") }
|
||||
|
||||
def oauth_signature_method() { "HMAC-SHA1" }
|
||||
|
||||
def oauth_timestamp() { (int) (new Date().time / 1000) }
|
||||
|
||||
def oauth_version() { 1.0 }
|
||||
|
||||
def oauth_callback() { shortUrl("x") }
|
||||
|
||||
def oauth_token() { atomicState.wToken?.oauth_token }
|
||||
|
||||
def oauth_token_secret() { atomicState.wToken?.oauth_token_secret }
|
||||
|
||||
def userid() { atomicState.userid }
|
||||
|
||||
String hmac(String oAuthSignatureBaseString, String oAuthSecret) throws java.security.SignatureException {
|
||||
if (!oAuthSecret.contains("&")) { log.warn "Withings requires \"&\" to be included no matter what" }
|
||||
// get an hmac_sha1 key from the raw key bytes
|
||||
def signingKey = new javax.crypto.spec.SecretKeySpec(oAuthSecret.getBytes(), "HmacSHA1")
|
||||
// get an hmac_sha1 Mac instance and initialize with the signing key
|
||||
def mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||
mac.init(signingKey)
|
||||
// compute the hmac on input data bytes
|
||||
byte[] rawHmac = mac.doFinal(oAuthSignatureBaseString.getBytes())
|
||||
return org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac)
|
||||
}
|
||||
|
||||
Map parseResponseString(String responseString) {
|
||||
// log.debug "parseResponseString: ${responseString}"
|
||||
responseString.split("&").inject([:]) { c, it ->
|
||||
def parts = it.split('=')
|
||||
def k = parts[0]
|
||||
def v = parts[1]
|
||||
c[k] = v
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
String applyParams(endpoint, oauthParams) { endpoint + "?" + oauthParams.sort().join("&") }
|
||||
|
||||
String buildSignature(endpoint, oAuthParams, oAuthSecret) {
|
||||
def oAuthSignatureBaseParts = ["GET", endpoint, oAuthParams.join("&")]
|
||||
def oAuthSignatureBaseString = oAuthSignatureBaseParts.collect { URLEncoder.encode(it) }.join("&")
|
||||
wLog " ==> oAuth signature base string : \n${oAuthSignatureBaseString}"
|
||||
wLog " .. applying hmac-sha1 to base string, with secret : ${oAuthSecret} (notice the \"&\")"
|
||||
wLog " .. base64 encode then url-encode the hmac-sha1 hash"
|
||||
String hmacResult = hmac(oAuthSignatureBaseString, oAuthSecret)
|
||||
def signature = URLEncoder.encode(hmacResult)
|
||||
wLog " ==> oauth_signature = ${signature}"
|
||||
return signature
|
||||
}
|
||||
|
||||
List buildOauthParams(List parameterKeys) {
|
||||
wLog " .. adding oAuth parameters : "
|
||||
def oauthParams = []
|
||||
parameterKeys.each { key ->
|
||||
def value = "${key}"()
|
||||
wLog " ${key} = ${value}"
|
||||
oauthParams << "${key}=${URLEncoder.encode(value.toString())}"
|
||||
}
|
||||
|
||||
wLog " .. sorting all request parameters alphabetically "
|
||||
oauthParams.sort()
|
||||
}
|
||||
|
||||
List buildOauthParams(Map parameters) {
|
||||
wLog " .. adding oAuth parameters : "
|
||||
def oauthParams = []
|
||||
parameters.each { k, v ->
|
||||
wLog " ${k} = ${v}"
|
||||
oauthParams << "${k}=${URLEncoder.encode(v.toString())}"
|
||||
}
|
||||
|
||||
wLog " .. sorting all request parameters alphabetically "
|
||||
oauthParams.sort()
|
||||
}
|
||||
|
||||
String buildOauthUrl(String endpoint, List parameterStrings, String oAuthTokenSecret) {
|
||||
wLog "Api endpoint : ${endpoint}"
|
||||
|
||||
wLog "Signing request :"
|
||||
def oAuthSecret = "${consumerSecret}&${oAuthTokenSecret}"
|
||||
def signature = buildSignature(endpoint, parameterStrings, oAuthSecret)
|
||||
|
||||
parameterStrings << "oauth_signature=${signature}"
|
||||
|
||||
def finalUrl = applyParams(endpoint, parameterStrings)
|
||||
wLog "Result: ${finalUrl}"
|
||||
if (wLogEnabled()) {
|
||||
log.debug wLogMessage()
|
||||
}
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
def collectTokenFromWithings(tokenUrl) {
|
||||
// get token from Withings using the url generated in Step 1
|
||||
def tokenString
|
||||
httpGet(uri: tokenUrl) { resp -> // oauth_token=<token_key>&oauth_token_secret=<token_secret>
|
||||
tokenString = resp.data.toString()
|
||||
// log.debug "collectTokenFromWithings: ${tokenString}"
|
||||
}
|
||||
def token = parseResponseString(tokenString)
|
||||
atomicState.wToken = token
|
||||
return token
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// APP SETTINGS
|
||||
// ========================================================
|
||||
|
||||
def getConsumerKey() { appSettings.consumerKey }
|
||||
|
||||
def getConsumerSecret() { appSettings.consumerSecret }
|
||||
|
||||
// figure out how to put this in settings
|
||||
def getUserId() { atomicState.wToken?.userid }
|
||||
|
||||
// ========================================================
|
||||
// HTML rendering
|
||||
// ========================================================
|
||||
|
||||
def renderAction(action, title = "") {
|
||||
log.debug "renderAction: $action"
|
||||
renderHTML(title) {
|
||||
head { "${action}HtmlHead"() }
|
||||
body { "${action}HtmlBody"() }
|
||||
}
|
||||
}
|
||||
|
||||
def authorizedHtmlHead() {
|
||||
log.trace "authorizedHtmlHead"
|
||||
"""
|
||||
<style type="text/css">
|
||||
@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;
|
||||
max-width:20%;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
/*font-size: 1.2em;*/
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
|
||||
def authorizedHtmlBody() {
|
||||
"""
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/withings@2x.png" alt="withings 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>Your Withings scale is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
def notAuthorizedHtmlHead() {
|
||||
log.trace "notAuthorizedHtmlHead"
|
||||
authorizedHtmlHead()
|
||||
}
|
||||
|
||||
def notAuthorizedHtmlBody() {
|
||||
"""
|
||||
<div class="container">
|
||||
<p>There was an error connecting to SmartThings!</p>
|
||||
<p>Click 'Done' to try again.</p>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
/**
|
||||
* Home Remote
|
||||
*
|
||||
* Copyright 2015 The Home Remote
|
||||
*
|
||||
* 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: "Home Remote",
|
||||
namespace: "thehomeremote.homeremote",
|
||||
author: "The Home Remote",
|
||||
description: "Web service that enables communication between the Home Remote app and a SmartThings hub.",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
oauth: [displayName: "The Home Remote", displayLink: "http://thehomeremote.com/"])
|
||||
|
||||
|
||||
preferences {
|
||||
section() {
|
||||
input "accelerationSensors", "capability.accelerationSensor",title: "Acceleration Sensors", multiple: true, required: false
|
||||
input "alarms", "capability.alarm",title: "Alarms", multiple: true, required: false
|
||||
input "batteries", "capability.battery",title: "Batteries", multiple: true, required: false
|
||||
input "beacons", "capability.beacon",title: "Beacons", multiple: true, required: false
|
||||
input "buttonGroup", "capability.button",title: "Buttons", multiple: true, required: false
|
||||
input "carbonMonoxideDetectors", "capability.carbonMonoxideDetector",title: "CO Detectors", multiple: true, required: false
|
||||
input "colorControls", "capability.colorControl",title: "Color Lights", multiple: true, required: false
|
||||
input "contactSensors", "capability.contactSensor",title: "Contact Sensors", multiple: true, required: false
|
||||
input "doorControls", "capability.doorControl",title: "Door Controllers", multiple: true, required: false
|
||||
input "energyMeters", "capability.energyMeter",title: "Energy Meters", multiple: true, required: false
|
||||
input "illuminanceMeasurements", "capability.illuminanceMeasurement",title: "Illuminance Sensors", multiple: true, required: false
|
||||
input "imageCaptures", "capability.imageCapture",title: "Cameras", multiple: true, required: false
|
||||
input "locks", "capability.lock",title: "Locks", multiple: true, required: false
|
||||
input "mediaControllers", "capability.mediaController",title: "Media Controllers", multiple: true, required: false
|
||||
input "momentaries", "capability.momentary",title: "Momentary Buttons", multiple: true, required: false
|
||||
input "motionSensors", "capability.motionSensor",title: "Motion Sensors", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer",title: "Music Players", multiple: true, required: false
|
||||
input "powerMeters", "capability.powerMeter",title: "Power Meters", multiple: true, required: false
|
||||
input "presenceSensors", "capability.presenceSensor",title: "Presence Sensors", multiple: true, required: false
|
||||
input "relativeHumidityMeasurements", "capability.relativeHumidityMeasurement",title: "Humidity Sensors", multiple: true, required: false
|
||||
input "relaySwitches", "capability.relaySwitch",title: "Relays", multiple: true, required: false
|
||||
input "signalStrengths", "capability.signalStrength",title: "Signal Strengths", multiple: true, required: false
|
||||
input "sleepSensors", "capability.sleepSensor",title: "Sleep Sensors", multiple: true, required: false
|
||||
input "smokeDetectors", "capability.smokeDetector",title: "Smoke Detectors", multiple: true, required: false
|
||||
input "speechSyntheses", "capability.speechSynthesis",title: "Speech Syntheses", multiple: true, required: false
|
||||
input "stepSensors", "capability.stepSensor",title: "Step Sensors", multiple: true, required: false
|
||||
input "switches", "capability.switch",title: "Switches", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel",title: "Dimmers", multiple: true, required: false
|
||||
input "temperatureMeasurements", "capability.temperatureMeasurement",title: "Temperature Sensors", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat",title: "Thermostats", multiple: true, required: false
|
||||
input "threeAxes", "capability.threeAxis",title: "Three axis Sensors", multiple: true, required: false
|
||||
input "tones", "capability.tone",title: "Tones", multiple: true, required: false
|
||||
input "touchSensors", "capability.touchSensor",title: "Touch Sensors", multiple: true, required: false
|
||||
input "valves", "capability.valve",title: "Valves", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor",title: "Water Sensors", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/GetCurrentValues") {
|
||||
action: [
|
||||
GET: "getCurrentValues"
|
||||
]
|
||||
}
|
||||
path("/GetCurrentValuesWithDisplayName") {
|
||||
action: [
|
||||
GET: "getCurrentValuesWithDisplayName"
|
||||
]
|
||||
}
|
||||
path("/ExecuteCommand") {
|
||||
action: [
|
||||
PUT: "executeCommand"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getCurrentValues() {
|
||||
def resp = []
|
||||
|
||||
accelerationSensors.each {
|
||||
resp << [id: it.id, capability: "AccelerationSensor", attribute: "acceleration", value: it.currentValue("acceleration")]
|
||||
}
|
||||
|
||||
alarms.each {
|
||||
resp << [id: it.id, capability: "Alarm", attribute: "alarm", value: it.currentValue("alarm")]
|
||||
}
|
||||
|
||||
batteries.each {
|
||||
resp << [id: it.id, capability: "Battery", attribute: "battery", value: it.currentValue("battery")]
|
||||
}
|
||||
|
||||
beacons.each {
|
||||
resp << [id: it.id, capability: "Beacon", attribute: "presence", value: it.currentValue("presence")]
|
||||
}
|
||||
|
||||
buttonGroup.each {
|
||||
resp << [id: it.id, capability: "Button", attribute: "button", value: it.currentValue("button")]
|
||||
}
|
||||
|
||||
carbonMonoxideDetectors.each {
|
||||
resp << [id: it.id, capability: "CarbonMonoxideDetector", attribute: "carbonMonoxide", value: it.currentValue("carbonMonoxide")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, capability: "ColorControl", attribute: "hue", value: it.currentValue("hue")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, capability: "ColorControl", attribute: "saturation", value: it.currentValue("saturation")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, capability: "ColorControl", attribute: "color", value: it.currentValue("color")]
|
||||
}
|
||||
|
||||
contactSensors.each {
|
||||
resp << [id: it.id, capability: "ContactSensor", attribute: "contact", value: it.currentValue("contact")]
|
||||
}
|
||||
|
||||
doorControls.each {
|
||||
resp << [id: it.id, capability: "DoorControl", attribute: "door", value: it.currentValue("door")]
|
||||
}
|
||||
|
||||
energyMeters.each {
|
||||
resp << [id: it.id, capability: "EnergyMeter", attribute: "energy", value: it.currentValue("energy")]
|
||||
}
|
||||
|
||||
illuminanceMeasurements.each {
|
||||
resp << [id: it.id, capability: "IlluminanceMeasurement", attribute: "illuminance", value: it.currentValue("illuminance")]
|
||||
}
|
||||
|
||||
imageCaptures.each {
|
||||
resp << [id: it.id, capability: "ImageCapture", attribute: "image", value: it.currentValue("image")]
|
||||
}
|
||||
|
||||
locks.each {
|
||||
resp << [id: it.id, capability: "Lock", attribute: "lock", value: it.currentValue("lock")]
|
||||
}
|
||||
|
||||
mediaControllers.each {
|
||||
resp << [id: it.id, capability: "MediaController", attribute: "activities", value: it.currentValue("activities")]
|
||||
}
|
||||
|
||||
mediaControllers.each {
|
||||
resp << [id: it.id, capability: "MediaController", attribute: "currentActivity", value: it.currentValue("currentActivity")]
|
||||
}
|
||||
|
||||
motionSensors.each {
|
||||
resp << [id: it.id, capability: "MotionSensor", attribute: "motion", value: it.currentValue("motion")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, capability: "MusicPlayer", attribute: "status", value: it.currentValue("status")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, capability: "MusicPlayer", attribute: "level", value: it.currentValue("level")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, capability: "MusicPlayer", attribute: "trackDescription", value: it.currentValue("trackDescription")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, capability: "MusicPlayer", attribute: "trackData", value: it.currentValue("trackData")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, capability: "MusicPlayer", attribute: "mute", value: it.currentValue("mute")]
|
||||
}
|
||||
|
||||
powerMeters.each {
|
||||
resp << [id: it.id, capability: "PowerMeter", attribute: "power", value: it.currentValue("power")]
|
||||
}
|
||||
|
||||
presenceSensors.each {
|
||||
resp << [id: it.id, capability: "PresenceSensor", attribute: "presence", value: it.currentValue("presence")]
|
||||
}
|
||||
|
||||
relativeHumidityMeasurements.each {
|
||||
resp << [id: it.id, capability: "RelativeHumidityMeasurement", attribute: "humidity", value: it.currentValue("humidity")]
|
||||
}
|
||||
|
||||
relaySwitches.each {
|
||||
resp << [id: it.id, capability: "RelaySwitch", attribute: "switch", value: it.currentValue("switch")]
|
||||
}
|
||||
|
||||
signalStrengths.each {
|
||||
resp << [id: it.id, capability: "SignalStrength", attribute: "lqi", value: it.currentValue("lqi")]
|
||||
}
|
||||
|
||||
signalStrengths.each {
|
||||
resp << [id: it.id, capability: "SignalStrength", attribute: "rssi", value: it.currentValue("rssi")]
|
||||
}
|
||||
|
||||
sleepSensors.each {
|
||||
resp << [id: it.id, capability: "SleepSensor", attribute: "sleeping", value: it.currentValue("sleeping")]
|
||||
}
|
||||
|
||||
smokeDetectors.each {
|
||||
resp << [id: it.id, capability: "SmokeDetector", attribute: "smoke", value: it.currentValue("smoke")]
|
||||
}
|
||||
|
||||
stepSensors.each {
|
||||
resp << [id: it.id, capability: "StepSensor", attribute: "steps", value: it.currentValue("steps")]
|
||||
}
|
||||
|
||||
stepSensors.each {
|
||||
resp << [id: it.id, capability: "StepSensor", attribute: "goal", value: it.currentValue("goal")]
|
||||
}
|
||||
|
||||
switches.each {
|
||||
resp << [id: it.id, capability: "Switch", attribute: "switch", value: it.currentValue("switch")]
|
||||
}
|
||||
|
||||
switchLevels.each {
|
||||
resp << [id: it.id, capability: "SwitchLevel", attribute: "level", value: it.currentValue("level")]
|
||||
}
|
||||
|
||||
temperatureMeasurements.each {
|
||||
resp << [id: it.id, capability: "TemperatureMeasurement", attribute: "temperature", value: it.currentValue("temperature")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "temperature", value: it.currentValue("temperature")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "heatingSetpoint", value: it.currentValue("heatingSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "coolingSetpoint", value: it.currentValue("coolingSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "thermostatSetpoint", value: it.currentValue("thermostatSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "thermostatMode", value: it.currentValue("thermostatMode")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "thermostatFanMode", value: it.currentValue("thermostatFanMode")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, capability: "Thermostat", attribute: "thermostatOperatingState", value: it.currentValue("thermostatOperatingState")]
|
||||
}
|
||||
|
||||
threeAxes.each {
|
||||
resp << [id: it.id, capability: "ThreeAxis", attribute: "threeAxis", value: it.currentValue("threeAxis")]
|
||||
}
|
||||
|
||||
touchSensors.each {
|
||||
resp << [id: it.id, capability: "TouchSensor", attribute: "touch", value: it.currentValue("touch")]
|
||||
}
|
||||
|
||||
valves.each {
|
||||
resp << [id: it.id, capability: "Valve", attribute: "contact", value: it.currentValue("contact")]
|
||||
}
|
||||
|
||||
waterSensors.each {
|
||||
resp << [id: it.id, capability: "WaterSensor", attribute: "water", value: it.currentValue("water")]
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
def getCurrentValuesWithDisplayName() {
|
||||
def resp = []
|
||||
|
||||
accelerationSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "AccelerationSensor", attribute: "acceleration", value: it.currentValue("acceleration")]
|
||||
}
|
||||
|
||||
alarms.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Alarm", attribute: "alarm", value: it.currentValue("alarm")]
|
||||
}
|
||||
|
||||
batteries.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Battery", attribute: "battery", value: it.currentValue("battery")]
|
||||
}
|
||||
|
||||
beacons.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Beacon", attribute: "presence", value: it.currentValue("presence")]
|
||||
}
|
||||
|
||||
buttonGroup.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Button", attribute: "button", value: it.currentValue("button")]
|
||||
}
|
||||
|
||||
carbonMonoxideDetectors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "CarbonMonoxideDetector", attribute: "carbonMonoxide", value: it.currentValue("carbonMonoxide")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ColorControl", attribute: "hue", value: it.currentValue("hue")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ColorControl", attribute: "saturation", value: it.currentValue("saturation")]
|
||||
}
|
||||
|
||||
colorControls.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ColorControl", attribute: "color", value: it.currentValue("color")]
|
||||
}
|
||||
|
||||
contactSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ContactSensor", attribute: "contact", value: it.currentValue("contact")]
|
||||
}
|
||||
|
||||
doorControls.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "DoorControl", attribute: "door", value: it.currentValue("door")]
|
||||
}
|
||||
|
||||
energyMeters.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "EnergyMeter", attribute: "energy", value: it.currentValue("energy")]
|
||||
}
|
||||
|
||||
illuminanceMeasurements.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "IlluminanceMeasurement", attribute: "illuminance", value: it.currentValue("illuminance")]
|
||||
}
|
||||
|
||||
imageCaptures.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ImageCapture", attribute: "image", value: it.currentValue("image")]
|
||||
}
|
||||
|
||||
locks.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Lock", attribute: "lock", value: it.currentValue("lock")]
|
||||
}
|
||||
|
||||
mediaControllers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MediaController", attribute: "activities", value: it.currentValue("activities")]
|
||||
}
|
||||
|
||||
mediaControllers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MediaController", attribute: "currentActivity", value: it.currentValue("currentActivity")]
|
||||
}
|
||||
|
||||
motionSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MotionSensor", attribute: "motion", value: it.currentValue("motion")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MusicPlayer", attribute: "status", value: it.currentValue("status")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MusicPlayer", attribute: "level", value: it.currentValue("level")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MusicPlayer", attribute: "trackDescription", value: it.currentValue("trackDescription")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MusicPlayer", attribute: "trackData", value: it.currentValue("trackData")]
|
||||
}
|
||||
|
||||
musicPlayers.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "MusicPlayer", attribute: "mute", value: it.currentValue("mute")]
|
||||
}
|
||||
|
||||
powerMeters.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "PowerMeter", attribute: "power", value: it.currentValue("power")]
|
||||
}
|
||||
|
||||
presenceSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "PresenceSensor", attribute: "presence", value: it.currentValue("presence")]
|
||||
}
|
||||
|
||||
relativeHumidityMeasurements.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "RelativeHumidityMeasurement", attribute: "humidity", value: it.currentValue("humidity")]
|
||||
}
|
||||
|
||||
relaySwitches.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "RelaySwitch", attribute: "switch", value: it.currentValue("switch")]
|
||||
}
|
||||
|
||||
signalStrengths.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "SignalStrength", attribute: "lqi", value: it.currentValue("lqi")]
|
||||
}
|
||||
|
||||
signalStrengths.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "SignalStrength", attribute: "rssi", value: it.currentValue("rssi")]
|
||||
}
|
||||
|
||||
sleepSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "SleepSensor", attribute: "sleeping", value: it.currentValue("sleeping")]
|
||||
}
|
||||
|
||||
smokeDetectors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "SmokeDetector", attribute: "smoke", value: it.currentValue("smoke")]
|
||||
}
|
||||
|
||||
stepSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "StepSensor", attribute: "steps", value: it.currentValue("steps")]
|
||||
}
|
||||
|
||||
stepSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "StepSensor", attribute: "goal", value: it.currentValue("goal")]
|
||||
}
|
||||
|
||||
switches.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Switch", attribute: "switch", value: it.currentValue("switch")]
|
||||
}
|
||||
|
||||
switchLevels.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "SwitchLevel", attribute: "level", value: it.currentValue("level")]
|
||||
}
|
||||
|
||||
temperatureMeasurements.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "TemperatureMeasurement", attribute: "temperature", value: it.currentValue("temperature")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "temperature", value: it.currentValue("temperature")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "heatingSetpoint", value: it.currentValue("heatingSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "coolingSetpoint", value: it.currentValue("coolingSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "thermostatSetpoint", value: it.currentValue("thermostatSetpoint")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "thermostatMode", value: it.currentValue("thermostatMode")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "thermostatFanMode", value: it.currentValue("thermostatFanMode")]
|
||||
}
|
||||
|
||||
thermostats.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Thermostat", attribute: "thermostatOperatingState", value: it.currentValue("thermostatOperatingState")]
|
||||
}
|
||||
|
||||
threeAxes.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "ThreeAxis", attribute: "threeAxis", value: it.currentValue("threeAxis")]
|
||||
}
|
||||
|
||||
touchSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "TouchSensor", attribute: "touch", value: it.currentValue("touch")]
|
||||
}
|
||||
|
||||
valves.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "Valve", attribute: "contact", value: it.currentValue("contact")]
|
||||
}
|
||||
|
||||
waterSensors.each {
|
||||
resp << [id: it.id, displayName: it.displayName, capability: "WaterSensor", attribute: "water", value: it.currentValue("water")]
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
def getDevices(capability){
|
||||
|
||||
def result
|
||||
|
||||
switch (capability) {
|
||||
case "Alarm":
|
||||
result = alarms
|
||||
break
|
||||
case "ColorControl":
|
||||
result = colorControls
|
||||
break
|
||||
case "DoorControl":
|
||||
result = doorControls
|
||||
break
|
||||
case "ImageCapture":
|
||||
result = imageCaptures
|
||||
break
|
||||
case "Lock":
|
||||
result = locks
|
||||
break
|
||||
case "MediaController":
|
||||
result = mediaControllers
|
||||
break
|
||||
case "Momentary":
|
||||
result = momentaries
|
||||
break
|
||||
case "MusicPlayer":
|
||||
result = musicPlayers
|
||||
break
|
||||
case "RelaySwitch":
|
||||
result = relaySwitches
|
||||
break
|
||||
case "SpeechSynthesis":
|
||||
result = speechSyntheses
|
||||
break
|
||||
case "Switch":
|
||||
result = switches
|
||||
break
|
||||
case "SwitchLevel":
|
||||
result = switchLevels
|
||||
break
|
||||
case "Thermostat":
|
||||
result = thermostats
|
||||
break
|
||||
case "ThermostatCoolingSetpoint":
|
||||
result = thermostatCoolingSetpoints
|
||||
break
|
||||
case "ThermostatFanMode":
|
||||
result = thermostatFanModes
|
||||
break
|
||||
case "ThermostatHeatingSetpoint":
|
||||
result = thermostatHeatingSetpoints
|
||||
break
|
||||
case "ThermostatMode":
|
||||
result = thermostatModes
|
||||
break
|
||||
case "Tone":
|
||||
result = tones
|
||||
break
|
||||
case "Valve":
|
||||
result = valves
|
||||
break
|
||||
default:
|
||||
result = valves
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getDoorControlCommand(value){
|
||||
def result
|
||||
switch (value) {
|
||||
case "closed":
|
||||
result = "close"
|
||||
break
|
||||
case "open":
|
||||
result = "open"
|
||||
break
|
||||
default:
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getLockCommand(value){
|
||||
def result
|
||||
switch (value) {
|
||||
case "locked":
|
||||
result = "lock"
|
||||
break
|
||||
case "unlocked":
|
||||
result = "unlock"
|
||||
break
|
||||
default:
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getMuteCommand(value){
|
||||
def result
|
||||
switch (value) {
|
||||
case "muted":
|
||||
result = "mute"
|
||||
break
|
||||
case "unmuted":
|
||||
result = "unmute"
|
||||
break
|
||||
default:
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getContactCommand(value){
|
||||
def result
|
||||
switch (value) {
|
||||
case "closed":
|
||||
result = "close"
|
||||
break
|
||||
case "open":
|
||||
result = "open"
|
||||
break
|
||||
default:
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getThermostatFanModeCommand(value){
|
||||
def result
|
||||
switch (value) {
|
||||
case "on":
|
||||
result = "fanOn"
|
||||
break
|
||||
case "auto":
|
||||
result = "fanAuto"
|
||||
break
|
||||
case "circulate":
|
||||
result = "fanCirculate"
|
||||
break
|
||||
default:
|
||||
result = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
void executeCommand() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def deviceId = request.JSON?.deviceId
|
||||
def capability = request.JSON?.capability
|
||||
def attribute = request.JSON?.attribute
|
||||
def value = request.JSON?.value
|
||||
if (deviceId) {
|
||||
def devices = getDevices(capability)
|
||||
def command
|
||||
def valueIsParameter = false
|
||||
switch (attribute) {
|
||||
case "hue":
|
||||
command = "setHue"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "saturation":
|
||||
command = "setSaturation"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "color":
|
||||
command = "setColor"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "level":
|
||||
command = "setLevel"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "heatingSetpoint":
|
||||
command = "setHeatingSetpoint"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "coolingSetpoint":
|
||||
command = "setCoolingSetpoint"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "currentActivity":
|
||||
command = "startActivity"
|
||||
valueIsParameter = true
|
||||
break
|
||||
case "door":
|
||||
command = getDoorControlCommand(value)
|
||||
break
|
||||
case "lock":
|
||||
command = getLockCommand(value)
|
||||
break
|
||||
case "mute":
|
||||
command = getMuteCommand(value)
|
||||
break
|
||||
case "thermostatFanMode":
|
||||
command = getThermostatFanModeCommand(value)
|
||||
break
|
||||
case "thermostatMode":
|
||||
if (value == "emergency heat") {
|
||||
command = "emergencyHeat"
|
||||
}
|
||||
break
|
||||
case "contact":
|
||||
command = getContactCommand(value)
|
||||
break
|
||||
default:
|
||||
command = value
|
||||
}
|
||||
// check that the switch supports the specified command
|
||||
// If not, return an error using httpError, providing a HTTP status code.
|
||||
devices.each {
|
||||
if (it.id == deviceId) {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(501, "$command is not a valid command for all devices specified")
|
||||
}
|
||||
if(valueIsParameter){
|
||||
it."$command"(value)
|
||||
}
|
||||
else{
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {}
|
||||
|
||||
def updated() {}
|
||||
307
smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy
Normal file
307
smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* WeatherBug Home
|
||||
*
|
||||
* Copyright 2015 WeatherBug
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "WeatherBug Home",
|
||||
namespace: "WeatherBug",
|
||||
author: "WeatherBug Home",
|
||||
description: "WeatherBug Home",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png",
|
||||
oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"])
|
||||
|
||||
|
||||
preferences {
|
||||
section("Select thermostats") {
|
||||
input "thermostatDevice", "capability.thermostat", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/appInfo") { action: [ GET: "getAppInfo" ] }
|
||||
path("/getLocation") { action: [ GET: "getLoc" ] }
|
||||
path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] }
|
||||
path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* data from the installed SmartApp, including the location data, and
|
||||
* a list of the devices that were authorized to be accessed. The WeatherBug
|
||||
* Home Service will leverage this data to represent the connected devices as well as their
|
||||
* location and associated the data with a WeatherBug user account.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
* @return Location, including id, latitude, longitude, zip code, and name, and the list of devices
|
||||
*/
|
||||
def getAppInfo() {
|
||||
def devices = thermostatDevice
|
||||
def lat = location.latitude
|
||||
def lon = location.longitude
|
||||
if(!(devices instanceof Collection))
|
||||
{
|
||||
devices = [devices]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [ "Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":lat,
|
||||
"Longitude":lon,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* location data from the installed SmartApp. The WeatherBug
|
||||
* Home Service will leverage this data to associate the location to a WeatherBug Home account
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @return Location, including id, latitude, longitude, zip code, and name
|
||||
*/
|
||||
def getLoc() {
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* thermostat data and store it for display to a WeatherBug user.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to get data for
|
||||
* @return Thermostat data including temperature, set points, running modes, and operating states
|
||||
*/
|
||||
def getCurrentReport() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
DeviceId: device.id,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to set
|
||||
* the thermostat setpoint.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to set
|
||||
* @return Indication of whether the operation succeeded or failed
|
||||
|
||||
def setTemperature() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
if(device != null)
|
||||
{
|
||||
def mode = device.latestState('thermostatMode').stringValue
|
||||
def value = params.temp as Integer
|
||||
log.trace "Suggested temperature: $value, $mode"
|
||||
if ( mode == "cool")
|
||||
device.setCoolingSetpoint(value)
|
||||
else if ( mode == "heat")
|
||||
device.setHeatingSetpoint(value)
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code : 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Updated with state: ${state}"
|
||||
log.debug "Updated with location ${location} ${location.id} ${location.name}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscribe to changes on the thermostat attributes
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize enter"
|
||||
subscribe(thermostatDevice, "heatingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "coolingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatFanMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatOperatingState", pushLatest)
|
||||
subscribe(thermostatDevice, "temperature", pushLatest)
|
||||
}
|
||||
|
||||
/**
|
||||
* The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def uninstalled() {
|
||||
log.trace "uninstall entered"
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will push the latest thermostat data to the WeatherBug Home Service so it can store
|
||||
* and display the data to the WeatherBug user. Data pushed includes the thermostat data as well
|
||||
* as location id.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def pushLatest(evt) {
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == evt.deviceId}
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive',
|
||||
body: [
|
||||
DeviceId: evt.deviceId,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
log.debug postParams
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* This method attempts to get the value of a device attribute, but if an error occurs null is returned
|
||||
* @return The device attribute value, or null
|
||||
*/
|
||||
def GetOrDefault(device, attrib)
|
||||
{
|
||||
def val
|
||||
try{
|
||||
val = device.latestValue(attrib)
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Failed to get attribute " + attrib + " from device " + device
|
||||
val = null
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
/*
|
||||
* Convenience method that sends data to WeatherBug, logging any exceptions that may occur
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def sendToWeatherBug(postParams)
|
||||
{
|
||||
try{
|
||||
log.debug postParams
|
||||
httpPostJson(postParams) { resp ->
|
||||
resp.headers.each {
|
||||
log.debug "${it.name} : ${it.value}"
|
||||
}
|
||||
log.debug "response contentType: ${resp.contentType}"
|
||||
log.debug "response data: ${resp.data}"
|
||||
}
|
||||
log.debug "Communication with WeatherBug succeeded";
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Communication with WeatherBug failed.\n${ex}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user