mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3faca7c84 | ||
|
|
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 | ||
|
|
e56086aac3 | ||
|
|
6b142622db | ||
|
|
8d07e81b80 | ||
|
|
0fcff53eba |
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
Commercial Electric Smart LED Recessed Downlight (Model # 53166161)
|
||||
|
||||
Modified Osram device handler for Commercial Electric Smart LED Recessed Downlight available at Home Depot (Model # 53166161).
|
||||
This is a Home Depot house brand manufactured by ETI Solid State Lighting, a subsidiary of Elec-Tech International, HK
|
||||
Only modification required was the range of Color Temperature - warm white (2700K) to daylight (5000K).
|
||||
|
||||
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Commercial Electric Smart LED Recessed Downlight", namespace: "ericcreighton", author: "Eric Creighton") {
|
||||
|
||||
capability "Color Temperature"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "colorName", "string"
|
||||
|
||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||
attribute "heartbeat", "string"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "ETI", model: "531661XX"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..5000)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorName", label: '${currentValue}'
|
||||
}
|
||||
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.trace description
|
||||
|
||||
// save heartbeat (i.e. last time we got a message from device)
|
||||
state.heartbeat = Calendar.getInstance().getTimeInMillis()
|
||||
|
||||
if (description?.startsWith("catchall:")) {
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.trace "descMap : $descMap"
|
||||
|
||||
if (descMap.cluster == "0300") {
|
||||
log.debug descMap.value
|
||||
def tempInMired = convertHexToInt(descMap.value)
|
||||
def tempInKelvin = Math.round(1000000/tempInMired)
|
||||
log.trace "temp in kelvin: $tempInKelvin"
|
||||
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
|
||||
}
|
||||
else if(descMap.cluster == "0008"){
|
||||
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||
log.debug "dimmer value is $dimmerValue"
|
||||
sendEvent(name: "level", value: dimmerValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevel(state?.levelValue)
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
sendEvent(name: "heartbeat", value: "alive", displayed:false)
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7"
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
state.levelValue = 100
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return onOffConfig() + levelConfig() + configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 300 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||
//min level change is 01
|
||||
def levelConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
if(value<101){
|
||||
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
|
||||
}
|
||||
|
||||
def tempInMired = Math.round(1000000/value)
|
||||
def finalHex = swapEndianHex(hex(tempInMired, 4))
|
||||
def genericName = getGenericName(value)
|
||||
log.debug "generic name is : $genericName"
|
||||
|
||||
def cmds = []
|
||||
sendEvent(name: "colorTemperature", value: value, displayed:false)
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
|
||||
|
||||
cmds
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
state.levelValue = (value==null) ? 100 : value
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: state.levelValue)
|
||||
def level = hex(state.levelValue * 254 / 100)
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
private getGenericName(value){
|
||||
def genericName = "White"
|
||||
if(value < 3300){
|
||||
genericName = "Soft White"
|
||||
} else if(value < 4150){
|
||||
genericName = "Moonlight"
|
||||
} else if(value < 5000){
|
||||
genericName = "Cool White"
|
||||
} else if(value <= 6500){
|
||||
genericName = "Daylight"
|
||||
}
|
||||
|
||||
genericName
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -64,23 +64,21 @@
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
/*
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
|
||||
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
standardTile("status", "device.contact", width: 2, height: 2) {
|
||||
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
@@ -112,8 +110,8 @@
|
||||
}
|
||||
|
||||
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,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"
|
||||
|
||||
]
|
||||
@@ -442,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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -64,10 +64,13 @@ 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
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -94,11 +97,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,18 +118,20 @@ 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
|
||||
|
||||
state.inBulbDiscovery = true
|
||||
state.bridgeRefreshCount = 0
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
@@ -129,7 +143,7 @@ def bulbDiscovery()
|
||||
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
|
||||
}
|
||||
section {
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||
|
||||
@@ -194,21 +208,22 @@ 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
|
||||
bulbmap
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
@@ -231,24 +246,16 @@ 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"
|
||||
state.inBulbDiscovery = false
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
addBridge()
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
@@ -263,22 +270,27 @@ 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."
|
||||
log.trace "Here: $hub, $data"
|
||||
if (state.inBulbDiscovery) {
|
||||
def bulbs = [:]
|
||||
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]
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
msg = "${bulbs.size()} bulbs found. $state.bulbs"
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
@@ -294,7 +306,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 +334,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 +344,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 +372,6 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -454,17 +470,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 +485,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 +576,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 +615,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 +659,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,9 +691,13 @@ ${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
|
||||
@@ -681,9 +708,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
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def authPage() {
|
||||
|
||||
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") {
|
||||
@@ -372,9 +373,9 @@ def updateDevices() {
|
||||
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("lifx", "LIFX Color Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
|
||||
} else {
|
||||
childDevice = addChildDevice("lifx", "LIFX White Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,4 +391,4 @@ def refreshDevices() {
|
||||
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>
|
||||
"""
|
||||
}
|
||||
Reference in New Issue
Block a user