mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-10 05:11:51 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e0995e43 | ||
|
|
bf7a7830cc | ||
|
|
7d90555006 | ||
|
|
04c19990cf |
@@ -1,506 +0,0 @@
|
||||
/**
|
||||
* Keen Home Smart Vent
|
||||
*
|
||||
* Author: Keen Home
|
||||
* Date: 2015-06-23
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
|
||||
capability "Switch Level"
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Temperature Measurement"
|
||||
capability "Battery"
|
||||
|
||||
command "getLevel"
|
||||
command "getOnOff"
|
||||
command "getPressure"
|
||||
command "getBattery"
|
||||
command "getTemperature"
|
||||
command "setZigBeeIdTile"
|
||||
|
||||
fingerprint endpoint: "1",
|
||||
profileId: "0104",
|
||||
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
|
||||
outClusters: "0019"
|
||||
}
|
||||
|
||||
// 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", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
|
||||
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
|
||||
state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, 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"]
|
||||
]
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
|
||||
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch","refresh","temperature","levelSliderControl","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
/**** PARSE METHODS ****/
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('on/off: ')) {
|
||||
map = parseOnOffMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
return map ? createEvent(map) : null
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
log.debug "parseCatchAllMessage"
|
||||
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug "cluster: ${cluster}"
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
log.debug "processing message"
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
return makeBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = convertTemperatureHex(temp)
|
||||
return makeTemperatureResult(value)
|
||||
break
|
||||
|
||||
case 0x0006:
|
||||
return makeOnOffResult(cluster.data[-1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [:]
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
if (cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.debug "parseReportAttributeMessage"
|
||||
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
return makeOnOffResult(Int.parseInt(descMap.value));
|
||||
}
|
||||
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
|
||||
return makeLevelResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = convertTemperatureHex(descMap.value)
|
||||
return makeTemperatureResult(value)
|
||||
}
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
|
||||
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
|
||||
return makePressureResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
|
||||
return makeSerialResult(new String(descMap.value.decodeHex()))
|
||||
}
|
||||
|
||||
// shouldn't get here
|
||||
return [:]
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
// log.debug "${description}"
|
||||
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||
// log.debug "split: " + description.split(": ")
|
||||
def value = Double.parseDouble(description.split(": ")[1])
|
||||
// log.debug "${value}"
|
||||
resultMap = makeTemperatureResult(convertTemperature(value))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseOnOffMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
def value = Integer.parseInt(description - "on/off: ")
|
||||
resultMap = makeOnOffResult(value)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map makeOnOffResult(rawValue) {
|
||||
log.debug "makeOnOffResult: ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
def value = rawValue == 1 ? "on" : "off"
|
||||
return [
|
||||
name: "switch",
|
||||
value: value,
|
||||
descriptionText: "${linkText} is ${value}"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeLevelResult(rawValue) {
|
||||
def linkText = getLinkText(device)
|
||||
// log.debug "rawValue: ${rawValue}"
|
||||
def value = Integer.parseInt(rawValue, 16)
|
||||
def rangeMax = 254
|
||||
|
||||
if (value == 255) {
|
||||
log.debug "obstructed"
|
||||
// Just return here. Once the vent is power cycled
|
||||
// it will go back to the previous level before obstruction.
|
||||
// Therefore, no need to update level on the display.
|
||||
return [
|
||||
name: "switch",
|
||||
value: "obstructed",
|
||||
descriptionText: "${linkText} is obstructed. Please power cycle."
|
||||
]
|
||||
} else if ( device.currentValue("switch") == "obstructed" &&
|
||||
value == 254) {
|
||||
// When the device is reset after an obstruction, the switch
|
||||
// state will be obstructed and the value coming from the device
|
||||
// will be 254. Since we're not using heating/cooling mode from
|
||||
// the device type handler, we need to bump it down to the lower
|
||||
// (cooling) range
|
||||
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
|
||||
value = rangeMax
|
||||
}
|
||||
// else if (device.currentValue("switch") == "off") {
|
||||
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
|
||||
// }
|
||||
|
||||
|
||||
// log.debug "pre-value: ${value}"
|
||||
value = Math.floor(value / rangeMax * 100)
|
||||
// log.debug "post-value: ${value}"
|
||||
|
||||
return [
|
||||
name: "level",
|
||||
value: value,
|
||||
descriptionText: "${linkText} level is ${value}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makePressureResult(rawValue) {
|
||||
log.debug 'makePressureResut'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def pascals = rawValue / 10
|
||||
def result = [
|
||||
name: 'pressure',
|
||||
descriptionText: "${linkText} pressure is ${pascals}Pa",
|
||||
value: pascals
|
||||
]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map makeBatteryResult(rawValue) {
|
||||
// log.debug 'makeBatteryResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug
|
||||
[
|
||||
name: 'battery',
|
||||
value: rawValue,
|
||||
descriptionText: "${linkText} battery is at ${rawValue}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeTemperatureResult(value) {
|
||||
// log.debug 'makeTemperatureResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug "tempOffset: ${tempOffset}"
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
// log.debug "offset: ${offset}"
|
||||
def v = value as int
|
||||
// log.debug "v: ${v}"
|
||||
value = v + offset
|
||||
// log.debug "value: ${value}"
|
||||
}
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: "" + value,
|
||||
descriptionText: "${linkText} is ${value}°${temperatureScale}",
|
||||
]
|
||||
}
|
||||
|
||||
/**** HELPER METHODS ****/
|
||||
private def convertTemperatureHex(value) {
|
||||
// log.debug "convertTemperatureHex(${value})"
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
// log.debug "celsius: ${celsius}"
|
||||
|
||||
return convertTemperature(celsius)
|
||||
}
|
||||
|
||||
private def convertTemperature(celsius) {
|
||||
// log.debug "convertTemperature()"
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
} else {
|
||||
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
|
||||
// log.debug "converted to F: ${fahrenheit}"
|
||||
return fahrenheit
|
||||
}
|
||||
}
|
||||
|
||||
private def makeSerialResult(serial) {
|
||||
log.debug "makeSerialResult: " + serial
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
sendEvent([
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ])
|
||||
return [
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ]
|
||||
}
|
||||
/**** COMMAND METHODS ****/
|
||||
// def mfgCode() {
|
||||
// ["zcl mfg-code 0x115B", "delay 200"]
|
||||
// }
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(makeOnOffResult(1))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(makeOnOffResult(0))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
// does this work?
|
||||
def toggle() {
|
||||
log.debug "toggle()"
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.debug "setting level: ${value}"
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
if (value > 0) {
|
||||
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
|
||||
}
|
||||
def rangeMax = 254
|
||||
def computedLevel = Math.round(value * rangeMax / 100)
|
||||
log.debug "computedLevel: ${computedLevel}"
|
||||
|
||||
def level = new BigInteger(computedLevel.toString()).toString(16)
|
||||
log.debug "level: ${level}"
|
||||
|
||||
if (level.size() < 2){
|
||||
level = '0' + level
|
||||
}
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
|
||||
}
|
||||
|
||||
|
||||
def getOnOff() {
|
||||
log.debug "getOnOff()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
|
||||
}
|
||||
|
||||
def getPressure() {
|
||||
log.debug "getPressure()"
|
||||
[
|
||||
"zcl mfg-code 0x115B", "delay 200",
|
||||
"zcl global read 0x0403 0x20", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
def getLevel() {
|
||||
log.debug "getLevel()"
|
||||
// rattr = read attribute
|
||||
// 0x${} = device net id
|
||||
// 1 = endpoint
|
||||
// 8 = cluster id (level control, in this case)
|
||||
// 0 = attribute within cluster
|
||||
// sendEvent(name: "level", value: value)
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
|
||||
}
|
||||
|
||||
def getTemperature() {
|
||||
log.debug "getTemperature()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
|
||||
}
|
||||
|
||||
def getBattery() {
|
||||
log.debug "getBattery()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
|
||||
}
|
||||
|
||||
def setZigBeeIdTile() {
|
||||
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
sendEvent([
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
|
||||
return [
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
getOnOff() +
|
||||
getLevel() +
|
||||
getTemperature() +
|
||||
getPressure() +
|
||||
getBattery()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "CONFIGURE"
|
||||
log.debug "zigbeeId: ${device.hub.zigbeeId}"
|
||||
|
||||
setZigBeeIdTile()
|
||||
|
||||
def configCmds = [
|
||||
// binding commands
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
// configure report commands
|
||||
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// vent on/off state - type: boolean, change: 1
|
||||
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// vent level - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// mike 2015/06/22: temp and pressure reports are preconfigured, but
|
||||
// we'd like to override their settings for our own purposes
|
||||
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
||||
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// mike 2015/06/22: use new custom pressure attribute
|
||||
// pressure - type: int32u, change: 1 = 0.1Pa
|
||||
"zcl mfg-code 0x115B", "delay 200",
|
||||
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// battery - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
313
devicetypes/lumivietnam/lumi-dimmer.src/lumi-dimmer.groovy
Normal file
313
devicetypes/lumivietnam/lumi-dimmer.src/lumi-dimmer.groovy
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Lumi Dimmer
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Dimmer", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", endpointId: "9", deviceId: "0101", inClusters: "0000, 0003, 0006, 0008", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-DZ1"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: "Off", action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on"
|
||||
state "on", label: "On", action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off"
|
||||
}
|
||||
standardTile("on", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On', action: "on", icon: "st.switches.light.on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("off", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off', action: "off", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
}
|
||||
controlTile("levelControl", "device.levelControl", "slider", width: 2, height: 1) {
|
||||
state "default", action:"switch level.setLevel", backgroundColor:"#79b821"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "on", "off", "levelControl", "level", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
if (finalResult.type == "switch") {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
else if (finalResult.type == "level") {
|
||||
sendEvent(name: "level", value: finalResult.value)
|
||||
sendEvent(name: "levelControl", value: finalResult.value)
|
||||
if (finalResult.value == 0)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
else
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "0")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "50")
|
||||
sendEvent(name: "levelControl", value: "50")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
value = value as Integer
|
||||
sendEvent(name: "level", value: value)
|
||||
sendEvent(name: "levelControl", value: value)
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevelWithRate(value, "0000")// + on()
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||
isDescriptionLevel(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//@return - false or "success" or level [0-100]
|
||||
def isDescriptionLevel(descMap) {
|
||||
def dimmerValue = -1
|
||||
if (descMap.cluster == "0008"){
|
||||
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||
def value = convertHexToInt(descMap.value)
|
||||
dimmerValue = Math.round(value * 100 / 255)
|
||||
if(dimmerValue==0 && value > 0) {
|
||||
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||
}
|
||||
}
|
||||
else if(descMap.clusterId == "0008") {
|
||||
if(descMap.command=="0B"){
|
||||
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmerValue != -1){
|
||||
return [type: "level", value : dimmerValue]
|
||||
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {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 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setLevelWithRate(level, rate) {
|
||||
if(rate == null){
|
||||
rate = "0000"
|
||||
}
|
||||
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
317
devicetypes/lumivietnam/lumi-shade.src/lumi-shade.groovy
Normal file
317
devicetypes/lumivietnam/lumi-shade.src/lumi-shade.groovy
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Lumi Shade
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Shade", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", endpointId: "9", deviceId: "0200", inClusters: "0000, 0003, 0006, 0008", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-BZ1"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: "Closed", action: "switch.on", icon: "st.doors.garage.garage-closed", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
state "turningOn", label: "Opening", action:" switch.on", icon: "st.doors.garage.garage-opening", backgroundColor: "#79b821"
|
||||
state "on", label: "Opened", action: "switch.off", icon: "st.doors.garage.garage-open", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
state "turningOff", label: "Closing", action:" switch.off", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("on", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'Open', action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("off", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Close', action: "off", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffffff"
|
||||
}
|
||||
controlTile("levelControl", "device.levelControl", "slider", width: 2, height: 1) {
|
||||
state "default", action:"switch level.setLevel", backgroundColor:"#79b821"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Shade ${currentValue}%'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "on", "off", "levelControl", "level", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
if (finalResult.type == "switch") {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
else if (finalResult.type == "level") {
|
||||
sendEvent(name: "level", value: finalResult.value)
|
||||
sendEvent(name: "levelControl", value: finalResult.value)
|
||||
if (finalResult.value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "100")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "100")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
value = value as Integer
|
||||
sendEvent(name: "level", value: value)
|
||||
sendEvent(name: "levelControl", value: value)
|
||||
if (value == 100) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevelWithRate(value, "0000")// + on()
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||
isDescriptionLevel(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//@return - false or "success" or level [0-100]
|
||||
def isDescriptionLevel(descMap) {
|
||||
def dimmerValue = -1
|
||||
if (descMap.cluster == "0008"){
|
||||
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||
def value = convertHexToInt(descMap.value)
|
||||
dimmerValue = Math.round(value * 100 / 255)
|
||||
if(dimmerValue==0 && value > 0) {
|
||||
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||
}
|
||||
}
|
||||
else if(descMap.clusterId == "0008") {
|
||||
if(descMap.command=="0B"){
|
||||
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmerValue != -1){
|
||||
return [type: "level", value : dimmerValue]
|
||||
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {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 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setLevelWithRate(level, rate) {
|
||||
if(rate == null){
|
||||
rate = "0000"
|
||||
}
|
||||
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
195
devicetypes/lumivietnam/lumi-switch-1.src/lumi-switch-1.groovy
Normal file
195
devicetypes/lumivietnam/lumi-switch-1.src/lumi-switch-1.groovy
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Lumi Switch 4
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 1", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ1"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On', action: "on1", icon: "st.switches.light.on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off', action: "off1", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "switch1"
|
||||
details(["switch1", "onAll", "offAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
253
devicetypes/lumivietnam/lumi-switch-2.src/lumi-switch-2.groovy
Normal file
253
devicetypes/lumivietnam/lumi-switch-2.src/lumi-switch-2.groovy
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Lumi Switch 2
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 2", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ2"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2"])
|
||||
details(["switch1", "switch2", "switchAll", "onAll", "offAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
281
devicetypes/lumivietnam/lumi-switch-3.src/lumi-switch-3.groovy
Normal file
281
devicetypes/lumivietnam/lumi-switch-3.src/lumi-switch-3.groovy
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Lumi Switch 3
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 3", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switch3", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "on3"
|
||||
command "off3"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ3"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switch3", "device.switch3", canChangeIcon: true) {
|
||||
state "on3", label: "SW3", action: "off3", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off3"
|
||||
state "off3", label: "SW3", action:"on3", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on3"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2", "switch3"])
|
||||
details(["switch1", "switch2", "switch3", "onAll", "offAll", "switchAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
else if (finalResult.srcEP == "05") {
|
||||
state.sw3 = finalResult.value;
|
||||
sendEvent(name: "switch3", value: finalResult.value=="on"?"on3":"off3")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off" && state.sw3 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on" && state.sw3 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 5 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 5"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 5 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on3() {
|
||||
log.debug "Executing 'on3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off3() {
|
||||
log.debug "Executing 'off3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3 5"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3 5"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
309
devicetypes/lumivietnam/lumi-switch-4.src/lumi-switch-4.groovy
Normal file
309
devicetypes/lumivietnam/lumi-switch-4.src/lumi-switch-4.groovy
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Lumi Switch 4
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 4", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switch3", "string"
|
||||
attribute "switch4", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "on3"
|
||||
command "off3"
|
||||
command "on4"
|
||||
command "off4"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ4"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switch3", "device.switch3", canChangeIcon: true) {
|
||||
state "on3", label: "SW3", action: "off3", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off3"
|
||||
state "off3", label: "SW3", action:"on3", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on3"
|
||||
}
|
||||
standardTile("switch4", "device.switch4", canChangeIcon: true) {
|
||||
state "on4", label: "SW4", action: "off4", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off4"
|
||||
state "off4", label: "SW4", action:"on4", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on4"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2", "switch3", "switch4"])
|
||||
details(["switch1", "switch2", "onAll", "switch3", "switch4", "offAll", "switchAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
else if (finalResult.srcEP == "05") {
|
||||
state.sw3 = finalResult.value;
|
||||
sendEvent(name: "switch3", value: finalResult.value=="on"?"on3":"off3")
|
||||
}
|
||||
else if (finalResult.srcEP == "07") {
|
||||
state.sw4 = finalResult.value;
|
||||
sendEvent(name: "switch4", value: finalResult.value=="on"?"on4":"off4")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off" && state.sw3 == "off" && state.sw4 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on" && state.sw3 == "on" && state.sw4 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 5 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 5", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 7 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 7"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 5 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 7 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on3() {
|
||||
log.debug "Executing 'on3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off3() {
|
||||
log.debug "Executing 'off3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on4() {
|
||||
log.debug "Executing 'on4' 0x${device.deviceNetworkId} endpoint 7"
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off4() {
|
||||
log.debug "Executing 'off4' 0x${device.deviceNetworkId} endpoint 7"
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3 5 7"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3 5 7"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ metadata {
|
||||
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
|
||||
}
|
||||
|
||||
graphTile(name: "powerGraph", attribute: "device.power")
|
||||
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
|
||||
|
||||
main(["power", "energy"])
|
||||
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||
@@ -68,8 +68,16 @@ metadata {
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
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()
|
||||
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"
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -83,6 +91,22 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -97,7 +121,8 @@ def parse(String description) {
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
|
||||
storeGraphData(result.name, result.value)
|
||||
PLATFORM_migrateGraphDataIfNeeded()
|
||||
PLATFORM_storeData(result.name, result.value)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -151,15 +176,535 @@ def configure() {
|
||||
|
||||
def renderGraph() {
|
||||
|
||||
def data = fetchGraphData(params.attribute)
|
||||
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||
|
||||
def totalData = data*.runningSum
|
||||
|
||||
def xValues = data*.unixTime
|
||||
|
||||
def yValues = [
|
||||
Total: [color: "#49a201", data: totalData]
|
||||
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
|
||||
]
|
||||
|
||||
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,887 +0,0 @@
|
||||
/**
|
||||
* Fidure Thermostat, Based on ZigBee thermostat (SmartThings)
|
||||
*
|
||||
* Author: Fidure
|
||||
* Date: 2014-12-13
|
||||
* Updated: 2015-08-26
|
||||
*/
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Polling"
|
||||
|
||||
attribute "displayTemperature","number"
|
||||
attribute "displaySetpoint", "string"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
attribute "upButtonState", "string"
|
||||
attribute "downButtonState", "string"
|
||||
|
||||
attribute "runningMode", "string"
|
||||
attribute "lockLevel", "string"
|
||||
|
||||
command "setThermostatTime"
|
||||
command "lock"
|
||||
|
||||
attribute "prorgammingOperation", "number"
|
||||
attribute "prorgammingOperationDisplay", "string"
|
||||
command "Program"
|
||||
|
||||
attribute "setpointHold", "string"
|
||||
attribute "setpointHoldDisplay", "string"
|
||||
command "Hold"
|
||||
attribute "holdExpiary", "string"
|
||||
|
||||
attribute "lastTimeSync", "string"
|
||||
|
||||
attribute "thermostatOperatingState", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator { }
|
||||
// pref
|
||||
preferences {
|
||||
|
||||
input ("hold_time", "enum", title: "Default Hold Time in Hours",
|
||||
description: "Default Hold Duration in hours",
|
||||
range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"],
|
||||
displayDuringSetup: false)
|
||||
input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"])
|
||||
input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"])
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "displayTemperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 29, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
[value: 36, color: "#bc2323"],
|
||||
// fahrenheit range
|
||||
[value: 37, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off"
|
||||
state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool"
|
||||
state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat"
|
||||
state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto"
|
||||
}
|
||||
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
}
|
||||
|
||||
standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
|
||||
state "Resting", label: 'Resting'
|
||||
state "Heating", icon:"st.thermostat.heating"
|
||||
state "Cooling", icon:"st.thermostat.cooling"
|
||||
}
|
||||
|
||||
|
||||
standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") {
|
||||
state "Unlocked", action:"lock", label:'${name}'
|
||||
state "Mode Only", action:"lock", label:'${name}'
|
||||
state "Setpoint", action:"lock", label:'${name}'
|
||||
state "Full", action:"lock", label:'${name}'
|
||||
}
|
||||
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||
}
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||
}
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) {
|
||||
state "default", label: 'Schedule'
|
||||
}
|
||||
valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"Program", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) {
|
||||
state "setpointHold", action:"Hold", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("setpoint", "displaySetpoint", width: 2, height: 2)
|
||||
{
|
||||
state("displaySetpoint", label: '${currentValue}°',
|
||||
backgroundColor: "#919191")
|
||||
}
|
||||
|
||||
standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up"
|
||||
state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down"
|
||||
state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
|
||||
|
||||
main "temperature"
|
||||
details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold",
|
||||
"heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def getMin() {
|
||||
try
|
||||
{
|
||||
if (getTemperatureScale() == "C")
|
||||
return 10
|
||||
else
|
||||
return 50
|
||||
} catch (all)
|
||||
{
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
def getMax() {
|
||||
try {
|
||||
if (getTemperatureScale() == "C")
|
||||
return 30
|
||||
else
|
||||
return 86
|
||||
} catch (all)
|
||||
{
|
||||
return 86
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def result = []
|
||||
|
||||
if (description?.startsWith("read attr -")) {
|
||||
|
||||
//TODO: Parse RAW strings for multiple attributes
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map: $descMap"
|
||||
for ( atMap in descMap.attrs)
|
||||
{
|
||||
def map = [:]
|
||||
|
||||
if (descMap.cluster == "0201")
|
||||
{
|
||||
//log.trace "attribute: ${atMap.attrId} "
|
||||
switch(atMap.attrId.toLowerCase())
|
||||
{
|
||||
case "0000":
|
||||
map.name = "temperature"
|
||||
map.value = getTemperature(atMap.value)
|
||||
result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value))
|
||||
break;
|
||||
case "0005":
|
||||
//log.debug "hex time: ${descMap.value}"
|
||||
if (atMap.encoding == "23")
|
||||
{
|
||||
map.name = "holdExpiary"
|
||||
map.value = "${convertToTime(atMap.value).getTime()}"
|
||||
//log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}"
|
||||
updateHoldLabel("HoldExp", "${map.value}")
|
||||
}
|
||||
break;
|
||||
case "0011":
|
||||
map.name = "coolingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0012":
|
||||
map.name = "heatingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001c":
|
||||
map.name = "thermostatMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001e": //running mode enum8
|
||||
map.name = "runningMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0023": // setpoint hold enum8
|
||||
map.name = "setpointHold"
|
||||
map.value = getHoldMap()[atMap.value]
|
||||
updateHoldLabel("Hold", map.value)
|
||||
break;
|
||||
case "0024": // hold duration int16u
|
||||
map.name = "setpointHoldDuration"
|
||||
map.value = Integer.parseInt("${atMap.value}", 16)
|
||||
|
||||
break;
|
||||
case "0025": // thermostat programming operation bitmap8
|
||||
map.name = "prorgammingOperation"
|
||||
def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01]
|
||||
result += createEvent("name":"prorgammingOperationDisplay", "value": val)
|
||||
map.value = atMap.value
|
||||
break;
|
||||
case "0029":
|
||||
// relay state
|
||||
map.name = "thermostatOperatingState"
|
||||
map.value = getThermostatOperatingState(atMap.value)
|
||||
break;
|
||||
}
|
||||
} else if (descMap.cluster == "0204")
|
||||
{
|
||||
if (atMap.attrId == "0001")
|
||||
{
|
||||
map.name = "lockLevel"
|
||||
map.value = getLockMap()[atMap.value]
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
result += createEvent(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "Parse returned $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
def map = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
|
||||
def attrId = map.get('attrId')
|
||||
def encoding = map.get('encoding')
|
||||
def value = map.get('value')
|
||||
def result = map.get('result')
|
||||
def list = [];
|
||||
|
||||
if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) {
|
||||
def raw = map.get('raw')
|
||||
|
||||
def size = Long.parseLong(''+ map.get('size'), 16)
|
||||
def index = 12;
|
||||
def len
|
||||
|
||||
//log.trace "processing multi attributes"
|
||||
while((index-12) < size) {
|
||||
attrId = flipHexStringEndianness(raw[index..(index+3)])
|
||||
index+= 4;
|
||||
if (result == "success")
|
||||
index+=2;
|
||||
encoding = raw[index..(index+1)]
|
||||
index+= 2;
|
||||
len =getDataLengthByType(encoding)
|
||||
value = flipHexStringEndianness(raw[index..(index+len-1)])
|
||||
index+=len;
|
||||
list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"]
|
||||
}
|
||||
}
|
||||
else
|
||||
list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"]
|
||||
|
||||
map.remove('value')
|
||||
map.remove('encoding')
|
||||
map.remove('attrId')
|
||||
map += ['attrs' : list ]
|
||||
}
|
||||
|
||||
def flipHexStringEndianness(s)
|
||||
{
|
||||
s = s.reverse()
|
||||
def sb = new StringBuilder()
|
||||
for (int i=0; i < s.length() -1; i+=2)
|
||||
sb.append(s.charAt(i+1)).append(s.charAt(i))
|
||||
sb
|
||||
}
|
||||
|
||||
def getDataLengthByType(t)
|
||||
{
|
||||
// number of bytes in each static data type
|
||||
def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4,
|
||||
"1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2,
|
||||
"2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4,
|
||||
"e8":2, "e9":2, "ea":4, "f0":8, "f1":16]
|
||||
|
||||
// return number of hex chars
|
||||
return map.get(t) * 2
|
||||
}
|
||||
|
||||
|
||||
def getProgrammingMap() { [
|
||||
0:"Off",
|
||||
1:"On"
|
||||
]}
|
||||
|
||||
def getModeMap() { [
|
||||
"00":"off",
|
||||
"01":"auto",
|
||||
"03":"cool",
|
||||
"04":"heat"
|
||||
]}
|
||||
|
||||
def getFanModeMap() { [
|
||||
"04":"fanOn",
|
||||
"05":"fanAuto"
|
||||
]}
|
||||
|
||||
def getHoldMap()
|
||||
{[
|
||||
"00":"Off",
|
||||
"01":"On"
|
||||
]}
|
||||
|
||||
|
||||
def updateSetpoint(attrib, val)
|
||||
{
|
||||
def cool = device.currentState("coolingSetpoint")?.value
|
||||
def heat = device.currentState("heatingSetpoint")?.value
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
def value = '--';
|
||||
|
||||
|
||||
if ("heat" == mode && heat != null)
|
||||
value = heat;
|
||||
else if ("cool" == mode && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "cool" && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "heat" && heat != null)
|
||||
value = heat;
|
||||
|
||||
sendEvent("name":"displaySetpoint", "value": value)
|
||||
}
|
||||
|
||||
def raiseSetpoint()
|
||||
{
|
||||
sendEvent("name":"upButtonState", "value": "pressed")
|
||||
sendEvent("name":"upButtonState", "value": "normal")
|
||||
adjustSetpoint(5)
|
||||
}
|
||||
|
||||
def lowerSetpoint()
|
||||
{
|
||||
sendEvent("name":"downButtonState", "value": "pressed")
|
||||
sendEvent("name":"downButtonState", "value": "normal")
|
||||
adjustSetpoint(-5)
|
||||
}
|
||||
|
||||
def adjustSetpoint(value)
|
||||
{
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
//default to both heat and cool
|
||||
def modeData = 0x02
|
||||
|
||||
if ("heat" == mode || "heat" == runningMode)
|
||||
modeData = "00"
|
||||
else if ("cool" == mode || "cool" == runningMode)
|
||||
modeData = "01"
|
||||
|
||||
def amountData = String.format("%02X", value)[-2..-1]
|
||||
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getDisplayTemperature(value)
|
||||
{
|
||||
def t = Integer.parseInt("$value", 16);
|
||||
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
t = (((t + 4) / 10) as Integer) / 10;
|
||||
} else {
|
||||
t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10;
|
||||
}
|
||||
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
def updateHoldLabel(attr, value)
|
||||
{
|
||||
def currentHold = (device?.currentState("setpointHold")?.value)?: "..."
|
||||
|
||||
def holdExp = device?.currentState("holdExpiary")?.value
|
||||
holdExp = holdExp?: "${(new Date()).getTime()}"
|
||||
|
||||
if ("Hold" == attr)
|
||||
{
|
||||
currentHold = value
|
||||
}
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
holdExp = value
|
||||
}
|
||||
boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime()))
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
if (!past)
|
||||
currentHold = "On"
|
||||
else
|
||||
currentHold = "Off"
|
||||
}
|
||||
|
||||
def holdString = (currentHold == "On")?
|
||||
( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") :
|
||||
((currentHold == "Off")? " is Off" : " ...")
|
||||
|
||||
sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}")
|
||||
}
|
||||
|
||||
def getSetPointHoldDuration()
|
||||
{
|
||||
def holdTime = 0
|
||||
|
||||
if (settings.hold_time?.contains("Hours"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim())
|
||||
}
|
||||
else if (settings.hold_time?.contains("Day"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24
|
||||
}
|
||||
|
||||
def currentHoldDuration = device.currentState("setpointHoldDuration")?.value
|
||||
|
||||
|
||||
if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60))
|
||||
{
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" +
|
||||
String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian
|
||||
|
||||
+ "}", "delay 100",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200",
|
||||
]
|
||||
|
||||
} else
|
||||
{
|
||||
[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def Hold()
|
||||
{
|
||||
def currentHold = device.currentState("setpointHold")?.value
|
||||
|
||||
def next = (currentHold == "On") ? "00" : "01"
|
||||
def nextHold = getHoldMap()[next]
|
||||
|
||||
sendEvent("name":"setpointHold", "value":nextHold)
|
||||
|
||||
// set the duration first if it's changed
|
||||
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" ,
|
||||
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
] + getSetPointHoldDuration()
|
||||
}
|
||||
|
||||
def compareWithNow(d)
|
||||
{
|
||||
long mins = (new Date(d)).getTime() - (new Date()).getTime()
|
||||
|
||||
mins /= 1000 * 60;
|
||||
|
||||
log.trace "mins: ${mins}"
|
||||
|
||||
boolean past = (mins < 0)
|
||||
def ret = (past)? "" : "in "
|
||||
|
||||
if (past)
|
||||
mins *= -1;
|
||||
|
||||
float t = 0;
|
||||
// minutes
|
||||
if (mins < 60)
|
||||
{
|
||||
ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '')
|
||||
}else if (mins < 1440)
|
||||
{
|
||||
t = ( Math.round((14 + mins)/30) as Integer) / 2
|
||||
ret += t + " hr" + ((t > 1)? 's' : '')
|
||||
} else
|
||||
{
|
||||
t = (Math.round((359 + mins)/720) as Integer) / 2
|
||||
ret += t + " day" + ((t > 1)? 's' : '')
|
||||
}
|
||||
ret += (past)? " ago": ""
|
||||
|
||||
log.trace "ret: ${ret}"
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
def convertToTime(data)
|
||||
{
|
||||
def time = Integer.parseInt("$data", 16) as long;
|
||||
time *= 1000;
|
||||
time += 946684800000; // 481418694
|
||||
time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
|
||||
def d = new Date(time);
|
||||
|
||||
//log.trace "converted $data to Time $d"
|
||||
return d;
|
||||
}
|
||||
|
||||
def Program()
|
||||
{
|
||||
def currentSched = device.currentState("prorgammingOperation")?.value
|
||||
|
||||
def next = Integer.parseInt(currentSched?: "00", 16);
|
||||
if ( (next & 0x01) == 0x01)
|
||||
next = next & 0xfe;
|
||||
else
|
||||
next = next | 0x01;
|
||||
|
||||
def nextSched = getProgrammingMap()[next & 0x01]
|
||||
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getThermostatOperatingState(value)
|
||||
{
|
||||
String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"]
|
||||
String desc = 'idle'
|
||||
value = Integer.parseInt(''+value, 16)
|
||||
|
||||
// only check for 1-stage for A1730
|
||||
for ( i in 0..2 ) {
|
||||
if (value & 1 << i)
|
||||
desc = m[i]
|
||||
}
|
||||
|
||||
desc
|
||||
}
|
||||
|
||||
def checkLastTimeSync(delay)
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if (!lastSync)
|
||||
lastSync = "${new Date(0)}"
|
||||
|
||||
if (settings.sync_clock ?: false && lastSync != new Date(0))
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
long duration = (new Date()).getTime() - (new Date(lastSync)).getTime()
|
||||
|
||||
// log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}"
|
||||
if (duration > 86400000)
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date()}")
|
||||
return setThermostatTime()
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def readAttributesCommand(cluster, attribList)
|
||||
{
|
||||
def attrString = ''
|
||||
|
||||
for ( val in attribList ) {
|
||||
attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff)
|
||||
}
|
||||
|
||||
//log.trace "list: " + attrString
|
||||
|
||||
["raw "+ cluster + " {00 00 00 $attrString}","delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 100",
|
||||
]
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
// log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23])
|
||||
|
||||
readAttributesCommand(0x201, [0x00,0x11,0x12]) +
|
||||
readAttributesCommand(0x201, [0x1C,0x1E,0x23]) +
|
||||
readAttributesCommand(0x201, [0x24,0x25,0x29]) +
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary
|
||||
"send 0x${device.deviceNetworkId} 1 1" , "delay 1500",
|
||||
] + checkLastTimeSync(2000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def poll() {
|
||||
log.trace "poll called"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt("$value", 16) / 100
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius as Integer
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(degrees) {
|
||||
def temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(degrees) {
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def modes() {
|
||||
["off", "heat", "cool"]
|
||||
}
|
||||
|
||||
def setThermostatFanMode() {
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
//log.debug "switching fan from current mode: $currentFanMode"
|
||||
def returnCommand
|
||||
|
||||
switch (currentFanMode) {
|
||||
case "fanAuto":
|
||||
returnCommand = fanOn()
|
||||
break
|
||||
case "fanOn":
|
||||
returnCommand = fanAuto()
|
||||
break
|
||||
}
|
||||
if(!currentFanMode) { returnCommand = fanAuto() }
|
||||
returnCommand
|
||||
}
|
||||
|
||||
def setThermostatMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def modeOrder = modes()
|
||||
def index = modeOrder.indexOf(currentMode)
|
||||
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
|
||||
|
||||
setThermostatMode(next)
|
||||
}
|
||||
|
||||
def setThermostatMode(String next) {
|
||||
def val = (getModeMap().find { it.value == next }?.key)?: "00"
|
||||
|
||||
// log.trace "mode changing to $next sending value: $val"
|
||||
|
||||
sendEvent("name":"thermostatMode", "value":"$next")
|
||||
["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] +
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
"$value"()
|
||||
}
|
||||
|
||||
def off() {
|
||||
setThermostatMode("off")
|
||||
}
|
||||
|
||||
def cool() {
|
||||
setThermostatMode("cool")}
|
||||
|
||||
def heat() {
|
||||
setThermostatMode("heat")
|
||||
}
|
||||
|
||||
def auto() {
|
||||
setThermostatMode("auto")
|
||||
}
|
||||
|
||||
def on() {
|
||||
fanOn()
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanOn")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
|
||||
}
|
||||
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if ((settings.sync_clock ?: false) == false)
|
||||
{
|
||||
log.debug "resetting last sync time. Used to be: $lastSync"
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getLockMap()
|
||||
{[
|
||||
"00":"Unlocked",
|
||||
"01":"Mode Only",
|
||||
"02":"Setpoint",
|
||||
"03":"Full",
|
||||
"04":"Full",
|
||||
"05":"Full",
|
||||
|
||||
]}
|
||||
def lock()
|
||||
{
|
||||
|
||||
def currentLock = device.currentState("lockLevel")?.value
|
||||
def val = getLockMap().find { it.value == currentLock }?.key
|
||||
|
||||
|
||||
|
||||
//log.debug "current lock is: ${val}"
|
||||
|
||||
if (val == "00")
|
||||
val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key
|
||||
else
|
||||
val = "00"
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def setThermostatTime()
|
||||
{
|
||||
|
||||
if ((settings.sync_clock ?: false))
|
||||
{
|
||||
log.debug "sync time is disabled, leaving"
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Date date = new Date();
|
||||
String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings();
|
||||
|
||||
long millis = date.getTime(); // Millis since Unix epoch
|
||||
millis -= 946684800000; // adjust for ZigBee EPOCH
|
||||
// adjust for time zone and DST offset
|
||||
millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
//convert to seconds
|
||||
millis /= 1000;
|
||||
|
||||
// print to a string for hex capture
|
||||
String s = String.format("%08X", millis);
|
||||
// hex capture for message format
|
||||
String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2);
|
||||
|
||||
[
|
||||
"raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500",
|
||||
|
||||
] + refresh()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private getEndpointId()
|
||||
{
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
@@ -19,6 +19,7 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -317,7 +317,7 @@ def setLevel(value) {
|
||||
state.trigger = "setLevel"
|
||||
state.lvl = "${level}"
|
||||
|
||||
if (dimRate && (state?.rate != null)) {
|
||||
if (dimRate) {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -15,27 +15,19 @@ metadata {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
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}'
|
||||
}
|
||||
}
|
||||
tiles {
|
||||
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: 2, width: 4, inactiveLabel: false) {
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["icon"])
|
||||
details(["rich-control", "networkAddress"])
|
||||
details(["networkAddress","serialNumber"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +36,7 @@ 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
|
||||
@@ -51,30 +44,32 @@ 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 "Parsing description"
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
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.info "Bridge response: $msg.body"
|
||||
} else {
|
||||
// Sending Bulbs List to parent"
|
||||
if (parent.state.inBulbDiscovery)
|
||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||
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)
|
||||
}
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Hue Bulb
|
||||
*
|
||||
@@ -16,8 +15,8 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -50,6 +49,7 @@ metadata {
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
@@ -68,13 +68,13 @@ def parse(description) {
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
log.trace parent.on(this)
|
||||
def on(transition = "4") {
|
||||
log.trace parent.on(this,transition)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.trace parent.off(this)
|
||||
def off(transition = "4") {
|
||||
log.trace parent.off(this,transition)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ def setHue(percent) {
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
|
||||
def setColor(value) {
|
||||
def setColor(value,alert = "none",transition = 4) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
parent.setColor(this, value)
|
||||
parent.setColor(this, value, alert, transition)
|
||||
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,41 +19,24 @@ 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"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "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"])
|
||||
|
||||
}
|
||||
|
||||
// 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", deviceJoinName: "NYCE Ceiling Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
|
||||
}
|
||||
|
||||
tiles {
|
||||
|
||||
@@ -24,10 +24,10 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
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"
|
||||
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"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -22,8 +22,10 @@ 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
|
||||
|
||||
@@ -48,8 +48,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,14 +280,16 @@ def configure() {
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "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
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||
"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"
|
||||
|
||||
"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"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -297,7 +299,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -39,9 +39,9 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
|
||||
])
|
||||
}
|
||||
section {
|
||||
@@ -301,18 +301,20 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "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
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"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"
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -320,12 +322,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"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,16 +292,18 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "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
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"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} 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"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -311,7 +313,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -54,31 +54,33 @@
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
|
||||
])
|
||||
}
|
||||
section {
|
||||
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:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.contact", 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("contact", "device.contact", width: 2, height: 2) {
|
||||
standardTile("status", "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")
|
||||
}
|
||||
@@ -110,8 +112,8 @@
|
||||
}
|
||||
|
||||
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,8 +348,8 @@ def getTemperature(value) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
//def linkText = getLinkText(device)
|
||||
def descriptionText = "was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
@@ -395,34 +397,35 @@ def getTemperature(value) {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
def configCmds = [
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "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
|
||||
"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 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"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} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
|
||||
]
|
||||
@@ -439,7 +442,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -297,27 +297,29 @@ 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}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "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}}",
|
||||
"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 ${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 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 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 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
|
||||
"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
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -325,7 +327,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -275,16 +275,22 @@ 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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "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"
|
||||
|
||||
"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"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -294,7 +300,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}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -253,19 +253,22 @@ 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",
|
||||
"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"
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Valve"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
status "open": "command: 2503, payload: FF"
|
||||
status "close": "command: 2503, payload: 00"
|
||||
|
||||
// reply messages
|
||||
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||
}
|
||||
|
||||
// tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
|
||||
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "valve"
|
||||
details(["valve","refresh"])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse description : $description"
|
||||
def result = null
|
||||
def cmd = zwave.parse(description, [0x20: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(zwaveEvent(cmd))
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
[:] // Handles all Z-Wave commands we aren't interested in
|
||||
}
|
||||
|
||||
def open() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely opened
|
||||
}
|
||||
|
||||
def close() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely closed
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
|
||||
if (getDataValue("MSR") == null) {
|
||||
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
}
|
||||
delayBetween(commands,100)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ metadata {
|
||||
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
|
||||
capability "Actuator"
|
||||
capability "Thermostat"
|
||||
capability "Temperature Measurement"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
@@ -11,13 +11,8 @@
|
||||
* 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",
|
||||
@@ -33,31 +28,32 @@ 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 "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 "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 "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
|
||||
}
|
||||
}
|
||||
@@ -78,71 +74,77 @@ 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 (cos != null) {
|
||||
}*/
|
||||
/*
|
||||
if (buttons != null) {
|
||||
subscribe(buttons, "button", genericHandler)
|
||||
}*/
|
||||
/*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 (energyMeters != null) {
|
||||
/*if (doorsControllers != null) {
|
||||
subscribe(doorsControllers, "door", genericHandler)
|
||||
}*/
|
||||
/*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)
|
||||
}
|
||||
@@ -161,9 +163,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)
|
||||
}
|
||||
@@ -171,23 +173,23 @@ def subscribeToEvents() {
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (atomicState.accessKey == null) {
|
||||
if (state.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: atomicState.accessKey
|
||||
accessKey: state.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (atomicState.bucketKey == null) {
|
||||
if (state.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
bucketKey: state.bucketKey,
|
||||
bucketName: state.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -200,94 +202,53 @@ def setBucketKey() {
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
|
||||
state.bucketKey = "$newBucketKey"
|
||||
state.bucketName = "$newBucketName"
|
||||
state.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
if (newAccessKey && newAccessKey != state.accessKey) {
|
||||
state.accessKey = "$newAccessKey"
|
||||
state.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
atomicState.version = "1.0.18"
|
||||
|
||||
subscribeToEvents()
|
||||
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
atomicState.eventBuffer = []
|
||||
|
||||
runEvery15Minutes(flushBuffer)
|
||||
|
||||
log.debug "installed (version $atomicState.version)"
|
||||
state.isBucketCreated = false
|
||||
}
|
||||
|
||||
def updated() {
|
||||
atomicState.version = "1.0.18"
|
||||
unsubscribe()
|
||||
|
||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||
atomicState.isBucketCreated = false
|
||||
if (state.bucketKey != null && state.accessKey != null) {
|
||||
state.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 uninstalled() {
|
||||
log.debug "uninstalled (version $atomicState.version)"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
// if the bucket has already been created, no need to continue
|
||||
if (atomicState.isBucketCreated) {
|
||||
return
|
||||
}
|
||||
def createBucket() {
|
||||
|
||||
if (!atomicState.bucketName) {
|
||||
atomicState.bucketName = atomicState.bucketKey
|
||||
if (!state.bucketName) {
|
||||
state.bucketName = state.bucketKey
|
||||
}
|
||||
if (!atomicState.accessKey) {
|
||||
return
|
||||
}
|
||||
def bucketName = "${atomicState.bucketName}"
|
||||
def bucketKey = "${atomicState.bucketKey}"
|
||||
def accessKey = "${atomicState.accessKey}"
|
||||
def bucketName = "${state.bucketName}"
|
||||
def bucketKey = "${state.bucketKey}"
|
||||
def accessKey = "${state.accessKey}"
|
||||
|
||||
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
||||
|
||||
def bucketCreatePost = [
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
|
||||
uri: 'https://groker.initialstate.com/api/buckets',
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-AccessKey": accessKey
|
||||
@@ -297,20 +258,10 @@ def tryCreateBucket() {
|
||||
|
||||
log.debug bucketCreatePost
|
||||
|
||||
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"
|
||||
httpPostJson(bucketCreatePost) {
|
||||
log.debug "bucket posted"
|
||||
state.isBucketCreated = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def genericHandler(evt) {
|
||||
@@ -322,80 +273,33 @@ def genericHandler(evt) {
|
||||
}
|
||||
def value = "$evt.value"
|
||||
|
||||
tryCreateBucket()
|
||||
|
||||
eventHandler(key, 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()
|
||||
}
|
||||
}
|
||||
|
||||
def eventHandler(name, value) {
|
||||
log.debug atomicState.eventBuffer
|
||||
|
||||
def eventBuffer = atomicState.eventBuffer
|
||||
def epoch = now() / 1000
|
||||
|
||||
// if for some reason this code block is being run
|
||||
// but the SmartApp wasn't propery setup during install
|
||||
// we need to set initialize the eventBuffer.
|
||||
if (!atomicState.eventBuffer) {
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
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) {
|
||||
if (state.accessKey == null || state.bucketKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.isBucketCreated) {
|
||||
createBucket()
|
||||
}
|
||||
|
||||
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
|
||||
def eventPost = [
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
|
||||
uri: 'https://groker.initialstate.com/api/events',
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-BucketKey": "${atomicState.bucketKey}",
|
||||
"X-IS-AccessKey": "${atomicState.accessKey}",
|
||||
"Accept-Version": "0.0.2"
|
||||
"X-IS-BucketKey": "${state.bucketKey}",
|
||||
"X-IS-AccessKey": "${state.accessKey}"
|
||||
],
|
||||
body: atomicState.eventBuffer
|
||||
body: eventBody
|
||||
]
|
||||
|
||||
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"
|
||||
log.debug eventPost
|
||||
|
||||
httpPostJson(eventPost) {
|
||||
log.debug "event data posted"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Author: LGKahn kahn-st@lgk.com
|
||||
* version 2 user defineable timeout before checking if door opened or closed correctly. Raised default to 25 secs. can reduce to 15 if you have custom simulated door with < 6 sec wait.
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "LGK Virtual Garage Door",
|
||||
namespace: "lgkapps",
|
||||
author: "lgkahn kahn-st@lgk.com",
|
||||
description: "Sync the Simulated garage door device with 2 actual devices, either a tilt or contact sensor and a switch or relay. The simulated device will then control the actual garage door. In addition, the virtual device will sync when the garage door is opened manually, \n It also attempts to double check the door was actually closed in case the beam was crossed. ",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose the switch/relay that opens closes the garage?"){
|
||||
input "opener", "capability.switch", title: "Physical Garage Opener?", required: true
|
||||
}
|
||||
section("Choose the sensor that senses if the garage is open closed? "){
|
||||
input "sensor", "capability.contactSensor", title: "Physical Garage Door Open/Closed?", required: true
|
||||
}
|
||||
|
||||
section("Choose the Virtual Garage Door Device? "){
|
||||
input "virtualgd", "capability.doorControl", title: "Virtual Garage Door?", required: true
|
||||
}
|
||||
|
||||
section("Choose the Virtual Garage Door Device sensor (same as above device)?"){
|
||||
input "virtualgdbutton", "capability.contactSensor", title: "Virtual Garage Door Open/Close Sensor?", required: true
|
||||
}
|
||||
|
||||
section("Timeout before checking if the door opened or closed correctly?"){
|
||||
input "checkTimeout", "number", title: "Door Operation Check Timeout?", required: true, defaultValue: 25
|
||||
}
|
||||
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phone1", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
def realgdstate = sensor.currentContact
|
||||
def virtualgdstate = virtualgd.currentContact
|
||||
//log.debug "in installed ... current state= $realgdstate"
|
||||
//log.debug "gd state= $virtualgd.currentContact"
|
||||
|
||||
subscribe(sensor, "contact", contactHandler)
|
||||
subscribe(virtualgdbutton, "contact", virtualgdcontactHandler)
|
||||
|
||||
// sync them up if need be set virtual same as actual
|
||||
if (realgdstate != virtualgdstate)
|
||||
{
|
||||
if (realgdstate == "open")
|
||||
{
|
||||
virtualgd.open()
|
||||
}
|
||||
else virtualgd.close()
|
||||
}
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
def realgdstate = sensor.currentContact
|
||||
def virtualgdstate = virtualgd.currentContact
|
||||
//log.debug "in updated ... current state= $realgdstate"
|
||||
//log.debug "in updated ... gd state= $virtualgd.currentContact"
|
||||
|
||||
|
||||
unsubscribe()
|
||||
subscribe(sensor, "contact", contactHandler)
|
||||
subscribe(virtualgdbutton, "contact", virtualgdcontactHandler)
|
||||
|
||||
// sync them up if need be set virtual same as actual
|
||||
if (realgdstate != virtualgdstate)
|
||||
{
|
||||
if (realgdstate == "open")
|
||||
{
|
||||
log.debug "opening virtual door"
|
||||
mysend("Virtual Garage Door Opened!")
|
||||
virtualgd.open()
|
||||
}
|
||||
else {
|
||||
virtualgd.close()
|
||||
log.debug "closing virtual door"
|
||||
mysend("Virtual Garage Door Closed!")
|
||||
}
|
||||
}
|
||||
// for debugging and testing uncomment temperatureHandlerTest()
|
||||
}
|
||||
|
||||
def contactHandler(evt)
|
||||
{
|
||||
def virtualgdstate = virtualgd.currentContact
|
||||
// how to determine which contact
|
||||
//log.debug "in contact handler for actual door open/close event. event = $evt"
|
||||
|
||||
if("open" == evt.value)
|
||||
{
|
||||
// contact was opened, turn on a light maybe?
|
||||
log.debug "Contact is in ${evt.value} state"
|
||||
// reset virtual door if necessary
|
||||
if (virtualgdstate != "open")
|
||||
{
|
||||
mysend("Garage Door Opened Manually syncing with Virtual Garage Door!")
|
||||
virtualgd.open()
|
||||
}
|
||||
}
|
||||
if("closed" == evt.value)
|
||||
{
|
||||
// contact was closed, turn off the light?
|
||||
log.debug "Contact is in ${evt.value} state"
|
||||
//reset virtual door
|
||||
if (virtualgdstate != "closed")
|
||||
{
|
||||
mysend("Garage Door Closed Manually syncing with Virtual Garage Door!")
|
||||
virtualgd.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def virtualgdcontactHandler(evt) {
|
||||
// how to determine which contact
|
||||
def realgdstate = sensor.currentContact
|
||||
//log.debug "in virtual gd contact/button handler event = $evt"
|
||||
//log.debug "in virtualgd contact handler check timeout = $checkTimeout"
|
||||
|
||||
if("open" == evt.value)
|
||||
{
|
||||
// contact was opened, turn on a light maybe?
|
||||
log.debug "Contact is in ${evt.value} state"
|
||||
// check to see if door is not in open state if so open
|
||||
if (realgdstate != "open")
|
||||
{
|
||||
log.debug "opening real gd to correspond with button press"
|
||||
mysend("Virtual Garage Door Opened syncing with Actual Garage Door!")
|
||||
opener.on()
|
||||
runIn(checkTimeout, checkIfActuallyOpened)
|
||||
|
||||
}
|
||||
}
|
||||
if("closed" == evt.value)
|
||||
{
|
||||
// contact was closed, turn off the light?
|
||||
log.debug "Contact is in ${evt.value} state"
|
||||
if (realgdstate != "closed")
|
||||
{
|
||||
log.debug "closing real gd to correspond with button press"
|
||||
mysend("Virtual Garage Door Closed syncing with Actual Garage Door!")
|
||||
opener.on()
|
||||
runIn(checkTimeout, checkIfActuallyClosed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private mysend(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phone1) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phone1, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
|
||||
|
||||
def checkIfActuallyClosed()
|
||||
{
|
||||
def realgdstate = sensor.currentContact
|
||||
def virtualgdstate = virtualgd.currentContact
|
||||
//log.debug "in checkifopen ... current state= $realgdstate"
|
||||
//log.debug "in checkifopen ... gd state= $virtualgd.currentContact"
|
||||
|
||||
|
||||
// sync them up if need be set virtual same as actual
|
||||
if (realgdstate == "open" && virtualgdstate == "closed")
|
||||
{
|
||||
log.debug "opening virtual door as it didnt close.. beam probably crossed"
|
||||
mysend("Resetting Virtual Garage Door to Open as real door didn't close (beam probably crossed)!")
|
||||
virtualgd.open()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
def checkIfActuallyOpened()
|
||||
{
|
||||
def realgdstate = sensor.currentContact
|
||||
def virtualgdstate = virtualgd.currentContact
|
||||
//log.debug "in checkifopen ... current state= $realgdstate"
|
||||
//log.debug "in checkifopen ... gd state= $virtualgd.currentContact"
|
||||
|
||||
|
||||
// sync them up if need be set virtual same as actual
|
||||
if (realgdstate == "closed" && virtualgdstate == "open")
|
||||
{
|
||||
log.debug "opening virtual door as it didnt open.. track blocked?"
|
||||
mysend("Resetting Virtual Garage Door to Closed as real door didn't open! (track blocked?)")
|
||||
virtualgd.close()
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -64,14 +64,10 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -98,20 +94,11 @@ def bridgeLinking()
|
||||
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText
|
||||
def hueimage = null
|
||||
if (selectedHue) {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
hueimage = null
|
||||
}
|
||||
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success!"
|
||||
title = "Success! - click 'Next'"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
hueimage = null
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
@@ -119,39 +106,30 @@ def bridgeLinking()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("") {
|
||||
section("Button Press") {
|
||||
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
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
state.bridgeRefreshCount = 0
|
||||
def bulboptions = bulbsDiscovered() ?: [:]
|
||||
def numFound = bulboptions.size() ?: 0
|
||||
if (numFound == 0)
|
||||
app.updateSetting("selectedBulbs", "")
|
||||
|
||||
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if((bulbRefreshCount % 3) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
|
||||
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]
|
||||
|
||||
@@ -216,29 +194,24 @@ Map bridgesDiscovered() {
|
||||
|
||||
Map bulbsDiscovered() {
|
||||
def bulbs = getHueBulbs()
|
||||
def bulbmap = [:]
|
||||
def map = [:]
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
bulbs.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = app.id +"/"+ it.value.id
|
||||
bulbmap["${key}"] = value
|
||||
def value = "${it?.value?.name}"
|
||||
def key = app.id +"/"+ it?.value?.id
|
||||
map["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
bulbs.each {
|
||||
def value = "${it.name}"
|
||||
def key = app.id +"/"+ it.id
|
||||
logg += "$value - $key, "
|
||||
bulbmap["${key}"] = value
|
||||
def value = "${it?.name}"
|
||||
def key = app.id +"/"+ it?.id
|
||||
map["${key}"] = value
|
||||
}
|
||||
}
|
||||
return bulbmap
|
||||
map
|
||||
}
|
||||
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
def getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
@@ -258,19 +231,24 @@ def installed() {
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
unsubscribe(bridge)
|
||||
state.inBulbDiscovery = false
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
log.debug "Initializing"
|
||||
state.subscribe = false
|
||||
state.bridgeSelectedOverride = false
|
||||
def bridge = null
|
||||
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
addBulbs()
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
@@ -285,34 +263,27 @@ def manualRefresh() {
|
||||
|
||||
def uninstalled(){
|
||||
state.bridges = [:]
|
||||
state.username = null
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
def 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 addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs?.each { dni ->
|
||||
selectedBulbs.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
@@ -323,7 +294,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])
|
||||
@@ -351,7 +322,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")}"
|
||||
@@ -361,27 +332,22 @@ 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)
|
||||
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 {
|
||||
else
|
||||
childDevice.sendEvent(name: "networkAddress", value: 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"
|
||||
@@ -389,6 +355,7 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -430,11 +397,8 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
@@ -442,8 +406,7 @@ def locationHandler(evt) {
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,13 +454,17 @@ def locationHandler(evt) {
|
||||
|
||||
def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
|
||||
//shrink the large bulb lists
|
||||
convertBulbListToMap()
|
||||
|
||||
poll()
|
||||
try {
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
} catch (all) {
|
||||
log.trace "Subscription already exist"
|
||||
}
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
@@ -506,49 +473,44 @@ def doDeviceSync(){
|
||||
/////////////////////////////////////
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
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
|
||||
if (headerString?.contains("json")) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
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
|
||||
@@ -597,25 +559,25 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition_deprecated = 0) {
|
||||
def on(childDevice, transition = 4) {
|
||||
log.debug "Executing 'on'"
|
||||
def percent = childDevice.device?.currentValue("level") as Integer
|
||||
// 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 level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
|
||||
return "level: $percent"
|
||||
}
|
||||
|
||||
def off(childDevice, transition_deprecated = 0) {
|
||||
def off(childDevice, transition = 4) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
return "level: 0"
|
||||
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
@@ -636,21 +598,19 @@ def setHue(childDevice, percent) {
|
||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||
}
|
||||
|
||||
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 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 value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (huesettings.level != null) {
|
||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
if (color.level != null) {
|
||||
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
|
||||
if (huesettings.switch) {
|
||||
value.on = huesettings.switch == "on"
|
||||
if (color.switch) {
|
||||
value.on = color.switch == "on"
|
||||
}
|
||||
|
||||
log.debug "sending command $value"
|
||||
@@ -680,19 +640,15 @@ private getId(childDevice) {
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
try {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
log.debug "GET: $host$uri"
|
||||
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()
|
||||
@@ -712,21 +668,12 @@ ${bodyJSON}
|
||||
private getBridgeIP() {
|
||||
def host = null
|
||||
if (selectedHue) {
|
||||
def d = getChildDevice(selectedHue)
|
||||
if (d) {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
host = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
def d = getChildDevice(dni)
|
||||
if (d)
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (!bridge) {
|
||||
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
|
||||
//in this case we will look up the bridge by comparing the incorrect mac addresses
|
||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||
}
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
@@ -734,9 +681,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,7 +62,6 @@ 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") {
|
||||
@@ -373,9 +372,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("smartthings", "LIFX Color Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("lifx", "LIFX Color Bulb", device.id, null, data)
|
||||
} else {
|
||||
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("lifx", "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,4 +390,4 @@ def refreshDevices() {
|
||||
getChildDevices().each { device ->
|
||||
device.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
author: "Juan Pablo Risso",
|
||||
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,9 +394,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -461,9 +459,7 @@ def activity(dni,mode) {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
msg = ex
|
||||
}
|
||||
}
|
||||
runIn(10, "poll", [overwrite: true])
|
||||
return msg
|
||||
}
|
||||
@@ -521,9 +517,7 @@ def poll() {
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,9 +550,7 @@ def getActivityList() {
|
||||
log.trace e
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.trace e
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
@@ -573,9 +565,9 @@ def getActivityName(activity,hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
@@ -593,9 +585,9 @@ def getActivityId(activity,hubId) {
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
@@ -610,9 +602,9 @@ def getHubName(hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
@@ -79,9 +79,12 @@ def scenePage(params=[:]) {
|
||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||
}
|
||||
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
if (sceneId == currentSceneId) {
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +225,7 @@ private restoreStates(sceneId) {
|
||||
if (type == "level") {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(level)
|
||||
light.setLevel(value)
|
||||
}
|
||||
}
|
||||
else if (type == "color") {
|
||||
|
||||
@@ -346,20 +346,18 @@ 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 sensors = [:]
|
||||
def json = new org.json.JSONObject(response.data)
|
||||
|
||||
response.data.each { sensorId, sensorName ->
|
||||
sensors[sensorId] = sensorName
|
||||
state.sensors = json
|
||||
|
||||
json.each { sensorId, sensorName ->
|
||||
createChild(sensorId, sensorName)
|
||||
}
|
||||
|
||||
state.sensors = sensors
|
||||
}
|
||||
|
||||
return "success"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def createChild(sensorId, sensorName) {
|
||||
|
||||
@@ -1,775 +0,0 @@
|
||||
/**
|
||||
* 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>
|
||||
"""
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/**
|
||||
* WeatherBug Home
|
||||
*
|
||||
* Copyright 2015 WeatherBug
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "WeatherBug Home",
|
||||
namespace: "WeatherBug",
|
||||
author: "WeatherBug Home",
|
||||
description: "WeatherBug Home",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png",
|
||||
oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"])
|
||||
|
||||
|
||||
preferences {
|
||||
section("Select thermostats") {
|
||||
input "thermostatDevice", "capability.thermostat", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/appInfo") { action: [ GET: "getAppInfo" ] }
|
||||
path("/getLocation") { action: [ GET: "getLoc" ] }
|
||||
path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] }
|
||||
path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* data from the installed SmartApp, including the location data, and
|
||||
* a list of the devices that were authorized to be accessed. The WeatherBug
|
||||
* Home Service will leverage this data to represent the connected devices as well as their
|
||||
* location and associated the data with a WeatherBug user account.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
* @return Location, including id, latitude, longitude, zip code, and name, and the list of devices
|
||||
*/
|
||||
def getAppInfo() {
|
||||
def devices = thermostatDevice
|
||||
def lat = location.latitude
|
||||
def lon = location.longitude
|
||||
if(!(devices instanceof Collection))
|
||||
{
|
||||
devices = [devices]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [ "Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":lat,
|
||||
"Longitude":lon,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* location data from the installed SmartApp. The WeatherBug
|
||||
* Home Service will leverage this data to associate the location to a WeatherBug Home account
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @return Location, including id, latitude, longitude, zip code, and name
|
||||
*/
|
||||
def getLoc() {
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* thermostat data and store it for display to a WeatherBug user.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to get data for
|
||||
* @return Thermostat data including temperature, set points, running modes, and operating states
|
||||
*/
|
||||
def getCurrentReport() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
DeviceId: device.id,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to set
|
||||
* the thermostat setpoint.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to set
|
||||
* @return Indication of whether the operation succeeded or failed
|
||||
|
||||
def setTemperature() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
if(device != null)
|
||||
{
|
||||
def mode = device.latestState('thermostatMode').stringValue
|
||||
def value = params.temp as Integer
|
||||
log.trace "Suggested temperature: $value, $mode"
|
||||
if ( mode == "cool")
|
||||
device.setCoolingSetpoint(value)
|
||||
else if ( mode == "heat")
|
||||
device.setHeatingSetpoint(value)
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code : 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Updated with state: ${state}"
|
||||
log.debug "Updated with location ${location} ${location.id} ${location.name}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscribe to changes on the thermostat attributes
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize enter"
|
||||
subscribe(thermostatDevice, "heatingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "coolingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatFanMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatOperatingState", pushLatest)
|
||||
subscribe(thermostatDevice, "temperature", pushLatest)
|
||||
}
|
||||
|
||||
/**
|
||||
* The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def uninstalled() {
|
||||
log.trace "uninstall entered"
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will push the latest thermostat data to the WeatherBug Home Service so it can store
|
||||
* and display the data to the WeatherBug user. Data pushed includes the thermostat data as well
|
||||
* as location id.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def pushLatest(evt) {
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == evt.deviceId}
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive',
|
||||
body: [
|
||||
DeviceId: evt.deviceId,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
log.debug postParams
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* This method attempts to get the value of a device attribute, but if an error occurs null is returned
|
||||
* @return The device attribute value, or null
|
||||
*/
|
||||
def GetOrDefault(device, attrib)
|
||||
{
|
||||
def val
|
||||
try{
|
||||
val = device.latestValue(attrib)
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Failed to get attribute " + attrib + " from device " + device
|
||||
val = null
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
/*
|
||||
* Convenience method that sends data to WeatherBug, logging any exceptions that may occur
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def sendToWeatherBug(postParams)
|
||||
{
|
||||
try{
|
||||
log.debug postParams
|
||||
httpPostJson(postParams) { resp ->
|
||||
resp.headers.each {
|
||||
log.debug "${it.name} : ${it.value}"
|
||||
}
|
||||
log.debug "response contentType: ${resp.contentType}"
|
||||
log.debug "response data: ${resp.data}"
|
||||
}
|
||||
log.debug "Communication with WeatherBug succeeded";
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Communication with WeatherBug failed.\n${ex}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user