mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-19 21:03:46 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e0995e43 | ||
|
|
bf7a7830cc | ||
|
|
7d90555006 | ||
|
|
04c19990cf |
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"
|
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"])
|
main(["power", "energy"])
|
||||||
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||||
@@ -68,8 +68,16 @@ metadata {
|
|||||||
// ========================================================
|
// ========================================================
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
|
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: "line", required: false, options: graphTypeOptions()
|
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"
|
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}"
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
|
||||||
storeGraphData(result.name, result.value)
|
PLATFORM_migrateGraphDataIfNeeded()
|
||||||
|
PLATFORM_storeData(result.name, result.value)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -151,15 +176,535 @@ def configure() {
|
|||||||
|
|
||||||
def renderGraph() {
|
def renderGraph() {
|
||||||
|
|
||||||
def data = fetchGraphData(params.attribute)
|
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||||
|
|
||||||
def totalData = data*.runningSum
|
def totalData = data*.runningSum
|
||||||
|
|
||||||
def xValues = data*.unixTime
|
def xValues = data*.unixTime
|
||||||
|
|
||||||
def yValues = [
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -15,27 +15,19 @@ metadata {
|
|||||||
// TODO: define status and reply messages here
|
// TODO: define status and reply messages here
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles {
|
||||||
multiAttributeTile(name:"rich-control"){
|
|
||||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
|
||||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
|
||||||
}
|
|
||||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "default", label:'SN: ${currentValue}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
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"
|
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) {
|
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||||
state "default", label:'SN: ${currentValue}'
|
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
|
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||||
}
|
}
|
||||||
|
|
||||||
main (["icon"])
|
main (["icon"])
|
||||||
details(["rich-control", "networkAddress"])
|
details(["networkAddress","serialNumber"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +36,7 @@ def parse(description) {
|
|||||||
log.debug "Parsing '${description}'"
|
log.debug "Parsing '${description}'"
|
||||||
def results = []
|
def results = []
|
||||||
def result = parent.parse(this, description)
|
def result = parent.parse(this, description)
|
||||||
|
|
||||||
if (result instanceof physicalgraph.device.HubAction){
|
if (result instanceof physicalgraph.device.HubAction){
|
||||||
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
||||||
results << result
|
results << result
|
||||||
@@ -51,30 +44,32 @@ def parse(description) {
|
|||||||
//do nothing
|
//do nothing
|
||||||
log.trace "HUE BRIDGE was updated"
|
log.trace "HUE BRIDGE was updated"
|
||||||
} else {
|
} else {
|
||||||
|
log.trace "HUE BRIDGE, OTHER"
|
||||||
def map = description
|
def map = description
|
||||||
if (description instanceof String) {
|
if (description instanceof String) {
|
||||||
map = stringToMap(description)
|
map = stringToMap(description)
|
||||||
}
|
}
|
||||||
if (map?.name && map?.value) {
|
if (map?.name && map?.value) {
|
||||||
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||||
results << createEvent(name: "${map.name}", value: "${map.value}")
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
} else {
|
}
|
||||||
log.trace "Parsing description"
|
else {
|
||||||
|
log.trace "HUE BRIDGE, OTHER"
|
||||||
def msg = parseLanMessage(description)
|
def msg = parseLanMessage(description)
|
||||||
if (msg.body) {
|
if (msg.body) {
|
||||||
def contentType = msg.headers["Content-Type"]
|
def contentType = msg.headers["Content-Type"]
|
||||||
if (contentType?.contains("json")) {
|
if (contentType?.contains("json")) {
|
||||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||||
if (bulbs.state) {
|
if (bulbs.state) {
|
||||||
log.info "Bridge response: $msg.body"
|
log.warn "NOT PROCESSED: $msg.body"
|
||||||
} else {
|
}
|
||||||
// Sending Bulbs List to parent"
|
else {
|
||||||
if (parent.state.inBulbDiscovery)
|
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
|
||||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (contentType?.contains("xml")) {
|
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
|
* Hue Bulb
|
||||||
*
|
*
|
||||||
@@ -16,8 +15,8 @@ metadata {
|
|||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
command "reset"
|
command "reset"
|
||||||
command "refresh"
|
command "refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
@@ -50,6 +49,7 @@ metadata {
|
|||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
@@ -68,13 +68,13 @@ def parse(description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle commands
|
// handle commands
|
||||||
def on() {
|
def on(transition = "4") {
|
||||||
log.trace parent.on(this)
|
log.trace parent.on(this,transition)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off(transition = "4") {
|
||||||
log.trace parent.off(this)
|
log.trace parent.off(this,transition)
|
||||||
sendEvent(name: "switch", value: "off")
|
sendEvent(name: "switch", value: "off")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +107,9 @@ def setHue(percent) {
|
|||||||
sendEvent(name: "hue", value: percent)
|
sendEvent(name: "hue", value: percent)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor(value) {
|
def setColor(value,alert = "none",transition = 4) {
|
||||||
log.debug "setColor: ${value}, $this"
|
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.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||||
|
|||||||
@@ -19,41 +19,24 @@ metadata {
|
|||||||
simulator {
|
simulator {
|
||||||
// TODO: define status and reply messages here
|
// 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"])
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
details(["rich-control", "refresh"])
|
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
|
// parse events into attributes
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ metadata {
|
|||||||
command "enrollResponse"
|
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: "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: "3043"
|
||||||
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: "3045"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ metadata {
|
|||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
|
|
||||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ metadata {
|
|||||||
|
|
||||||
command "setAdjustedColor"
|
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: "Gardenspot RGB"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY 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
|
// simulator metadata
|
||||||
|
|||||||
@@ -280,14 +280,16 @@ def configure() {
|
|||||||
def configCmds = [
|
def configCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"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 300 0600 {01}", "delay 200",
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"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 300 3600 {6400}", "delay 200",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
"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
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
@@ -297,7 +299,7 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
|
|||||||
@@ -301,18 +301,20 @@ def configure() {
|
|||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
|
|
||||||
def configCmds = [
|
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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"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 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"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}",
|
"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() {
|
def enrollResponse() {
|
||||||
@@ -320,12 +322,12 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointId() {
|
private getEndpointId() {
|
||||||
|
|||||||
@@ -292,16 +292,18 @@ def configure() {
|
|||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
|
|
||||||
def configCmds = [
|
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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"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 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"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 30 3600 {6400}",
|
"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
|
||||||
}
|
}
|
||||||
@@ -311,7 +313,7 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
|
|||||||
@@ -64,21 +64,23 @@
|
|||||||
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 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
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
section {
|
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)
|
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) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
|
||||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
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-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
|
||||||
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
|
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("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
}
|
}
|
||||||
@@ -110,8 +112,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main(["status", "acceleration", "temperature"])
|
main(["contact", "acceleration", "temperature"])
|
||||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,34 +397,35 @@ def getTemperature(value) {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting"
|
log.debug "Configuring Reporting"
|
||||||
|
|
||||||
def configCmds = [
|
def configCmds = [
|
||||||
|
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"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 600 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"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 30 3600 {6400}",
|
"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} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"zcl mfg-code 0x104E",
|
"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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code 0x104E",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code 0x104E",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code 0x104E",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -439,7 +442,7 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
|
|||||||
@@ -297,27 +297,29 @@ def getTemperature(value) {
|
|||||||
return refreshCmds + enrollResponse()
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
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",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"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 600 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"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 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
]
|
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
@@ -325,7 +327,7 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
|
|||||||
@@ -275,16 +275,22 @@ def configure() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
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",
|
"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 600 3600 {01}",
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 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
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
@@ -294,7 +300,7 @@ def enrollResponse() {
|
|||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//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",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
|
|||||||
@@ -253,19 +253,22 @@ def configure() {
|
|||||||
|
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
def configCmds = [
|
def configCmds = [
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
private hex(value) {
|
private hex(value) {
|
||||||
|
|||||||
@@ -11,13 +11,8 @@
|
|||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
* 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
|
* 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.
|
* 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(
|
definition(
|
||||||
name: "Initial State Event Streamer",
|
name: "Initial State Event Streamer",
|
||||||
namespace: "initialstate.events",
|
namespace: "initialstate.events",
|
||||||
@@ -33,31 +28,32 @@ import groovy.json.JsonSlurper
|
|||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
section("Choose which devices to monitor...") {
|
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 "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
|
||||||
input "batteries", "capability.battery", title: "Batteries", 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 "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||||
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
|
||||||
input "colors", "capability.colorControl", title: "Color Controllers", 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 "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||||
input "doorsControllers", "capability.doorControl", title: "Door Controllers", 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 "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||||
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance 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 "locks", "capability.lock", title: "Locks", multiple: true, required: false
|
||||||
input "motions", "capability.motionSensor", title: "Motion Sensors", 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 "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||||
input "powerMeters", "capability.powerMeter", title: "Power Meters", 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 "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
|
||||||
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", 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 "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||||
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", 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 "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||||
input "peds", "capability.stepSensor", title: "Pedometers", 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 "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
||||||
input "switchLevels", "capability.switchLevel", title: "Switch Levels", 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 "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
|
||||||
input "thermostats", "capability.thermostat", title: "Thermostats", 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
|
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,71 +74,77 @@ mappings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def subscribeToEvents() {
|
def subscribeToEvents() {
|
||||||
if (accelerometers != null) {
|
/*if (accelerometers != null) {
|
||||||
subscribe(accelerometers, "acceleration", genericHandler)
|
subscribe(accelerometers, "acceleration", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (alarms != null) {
|
if (alarms != null) {
|
||||||
subscribe(alarms, "alarm", genericHandler)
|
subscribe(alarms, "alarm", genericHandler)
|
||||||
}
|
}
|
||||||
if (batteries != null) {
|
/*if (batteries != null) {
|
||||||
subscribe(batteries, "battery", genericHandler)
|
subscribe(batteries, "battery", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (beacons != null) {
|
/*if (beacons != null) {
|
||||||
subscribe(beacons, "presence", genericHandler)
|
subscribe(beacons, "presence", genericHandler)
|
||||||
}
|
}*/
|
||||||
|
/*
|
||||||
if (cos != null) {
|
if (buttons != null) {
|
||||||
|
subscribe(buttons, "button", genericHandler)
|
||||||
|
}*/
|
||||||
|
/*if (cos != null) {
|
||||||
subscribe(cos, "carbonMonoxide", genericHandler)
|
subscribe(cos, "carbonMonoxide", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (colors != null) {
|
/*if (colors != null) {
|
||||||
subscribe(colors, "hue", genericHandler)
|
subscribe(colors, "hue", genericHandler)
|
||||||
subscribe(colors, "saturation", genericHandler)
|
subscribe(colors, "saturation", genericHandler)
|
||||||
subscribe(colors, "color", genericHandler)
|
subscribe(colors, "color", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (contacts != null) {
|
if (contacts != null) {
|
||||||
subscribe(contacts, "contact", genericHandler)
|
subscribe(contacts, "contact", genericHandler)
|
||||||
}
|
}
|
||||||
if (energyMeters != null) {
|
/*if (doorsControllers != null) {
|
||||||
|
subscribe(doorsControllers, "door", genericHandler)
|
||||||
|
}*/
|
||||||
|
/*if (energyMeters != null) {
|
||||||
subscribe(energyMeters, "energy", genericHandler)
|
subscribe(energyMeters, "energy", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (illuminances != null) {
|
/*if (illuminances != null) {
|
||||||
subscribe(illuminances, "illuminance", genericHandler)
|
subscribe(illuminances, "illuminance", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (locks != null) {
|
if (locks != null) {
|
||||||
subscribe(locks, "lock", genericHandler)
|
subscribe(locks, "lock", genericHandler)
|
||||||
}
|
}
|
||||||
if (motions != null) {
|
if (motions != null) {
|
||||||
subscribe(motions, "motion", genericHandler)
|
subscribe(motions, "motion", genericHandler)
|
||||||
}
|
}
|
||||||
if (musicPlayers != null) {
|
/*if (musicPlayers != null) {
|
||||||
subscribe(musicPlayers, "status", genericHandler)
|
subscribe(musicPlayers, "status", genericHandler)
|
||||||
subscribe(musicPlayers, "level", genericHandler)
|
subscribe(musicPlayers, "level", genericHandler)
|
||||||
subscribe(musicPlayers, "trackDescription", genericHandler)
|
subscribe(musicPlayers, "trackDescription", genericHandler)
|
||||||
subscribe(musicPlayers, "trackData", genericHandler)
|
subscribe(musicPlayers, "trackData", genericHandler)
|
||||||
subscribe(musicPlayers, "mute", genericHandler)
|
subscribe(musicPlayers, "mute", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (powerMeters != null) {
|
/*if (powerMeters != null) {
|
||||||
subscribe(powerMeters, "power", genericHandler)
|
subscribe(powerMeters, "power", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (presences != null) {
|
if (presences != null) {
|
||||||
subscribe(presences, "presence", genericHandler)
|
subscribe(presences, "presence", genericHandler)
|
||||||
}
|
}
|
||||||
if (humidities != null) {
|
if (humidities != null) {
|
||||||
subscribe(humidities, "humidity", genericHandler)
|
subscribe(humidities, "humidity", genericHandler)
|
||||||
}
|
}
|
||||||
if (relaySwitches != null) {
|
/*if (relaySwitches != null) {
|
||||||
subscribe(relaySwitches, "switch", genericHandler)
|
subscribe(relaySwitches, "switch", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (sleepSensors != null) {
|
/*if (sleepSensors != null) {
|
||||||
subscribe(sleepSensors, "sleeping", genericHandler)
|
subscribe(sleepSensors, "sleeping", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (smokeDetectors != null) {
|
/*if (smokeDetectors != null) {
|
||||||
subscribe(smokeDetectors, "smoke", genericHandler)
|
subscribe(smokeDetectors, "smoke", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (peds != null) {
|
/*if (peds != null) {
|
||||||
subscribe(peds, "steps", genericHandler)
|
subscribe(peds, "steps", genericHandler)
|
||||||
subscribe(peds, "goal", genericHandler)
|
subscribe(peds, "goal", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (switches != null) {
|
if (switches != null) {
|
||||||
subscribe(switches, "switch", genericHandler)
|
subscribe(switches, "switch", genericHandler)
|
||||||
}
|
}
|
||||||
@@ -161,9 +163,9 @@ def subscribeToEvents() {
|
|||||||
subscribe(thermostats, "thermostatFanMode", genericHandler)
|
subscribe(thermostats, "thermostatFanMode", genericHandler)
|
||||||
subscribe(thermostats, "thermostatOperatingState", genericHandler)
|
subscribe(thermostats, "thermostatOperatingState", genericHandler)
|
||||||
}
|
}
|
||||||
if (valves != null) {
|
/*if (valves != null) {
|
||||||
subscribe(valves, "contact", genericHandler)
|
subscribe(valves, "contact", genericHandler)
|
||||||
}
|
}*/
|
||||||
if (waterSensors != null) {
|
if (waterSensors != null) {
|
||||||
subscribe(waterSensors, "water", genericHandler)
|
subscribe(waterSensors, "water", genericHandler)
|
||||||
}
|
}
|
||||||
@@ -171,23 +173,23 @@ def subscribeToEvents() {
|
|||||||
|
|
||||||
def getAccessKey() {
|
def getAccessKey() {
|
||||||
log.trace "get access key"
|
log.trace "get access key"
|
||||||
if (atomicState.accessKey == null) {
|
if (state.accessKey == null) {
|
||||||
httpError(404, "Access Key Not Found")
|
httpError(404, "Access Key Not Found")
|
||||||
} else {
|
} else {
|
||||||
[
|
[
|
||||||
accessKey: atomicState.accessKey
|
accessKey: state.accessKey
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getBucketKey() {
|
def getBucketKey() {
|
||||||
log.trace "get bucket key"
|
log.trace "get bucket key"
|
||||||
if (atomicState.bucketKey == null) {
|
if (state.bucketKey == null) {
|
||||||
httpError(404, "Bucket key Not Found")
|
httpError(404, "Bucket key Not Found")
|
||||||
} else {
|
} else {
|
||||||
[
|
[
|
||||||
bucketKey: atomicState.bucketKey,
|
bucketKey: state.bucketKey,
|
||||||
bucketName: atomicState.bucketName
|
bucketName: state.bucketName
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,94 +202,53 @@ def setBucketKey() {
|
|||||||
log.debug "bucket name: $newBucketName"
|
log.debug "bucket name: $newBucketName"
|
||||||
log.debug "bucket key: $newBucketKey"
|
log.debug "bucket key: $newBucketKey"
|
||||||
|
|
||||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
|
||||||
atomicState.bucketKey = "$newBucketKey"
|
state.bucketKey = "$newBucketKey"
|
||||||
atomicState.bucketName = "$newBucketName"
|
state.bucketName = "$newBucketName"
|
||||||
atomicState.isBucketCreated = false
|
state.isBucketCreated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
tryCreateBucket()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setAccessKey() {
|
def setAccessKey() {
|
||||||
log.trace "set access key"
|
log.trace "set access key"
|
||||||
def newAccessKey = request.JSON?.accessKey
|
def newAccessKey = request.JSON?.accessKey
|
||||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
|
||||||
|
|
||||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
if (newAccessKey && newAccessKey != state.accessKey) {
|
||||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
state.accessKey = "$newAccessKey"
|
||||||
atomicState.isBucketCreated = false
|
state.isBucketCreated = false
|
||||||
}
|
|
||||||
|
|
||||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
|
||||||
atomicState.accessKey = "$newAccessKey"
|
|
||||||
atomicState.isBucketCreated = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
atomicState.version = "1.0.18"
|
|
||||||
subscribeToEvents()
|
subscribeToEvents()
|
||||||
|
|
||||||
atomicState.isBucketCreated = false
|
state.isBucketCreated = false
|
||||||
atomicState.grokerSubdomain = "groker"
|
|
||||||
atomicState.eventBuffer = []
|
|
||||||
|
|
||||||
runEvery15Minutes(flushBuffer)
|
|
||||||
|
|
||||||
log.debug "installed (version $atomicState.version)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
atomicState.version = "1.0.18"
|
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
|
|
||||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
if (state.bucketKey != null && state.accessKey != null) {
|
||||||
atomicState.isBucketCreated = false
|
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
|
subscribeToEvents()
|
||||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
}
|
||||||
log.error "streaming url is currently null"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the bucket has already been created, no need to continue
|
def createBucket() {
|
||||||
if (atomicState.isBucketCreated) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!atomicState.bucketName) {
|
if (!state.bucketName) {
|
||||||
atomicState.bucketName = atomicState.bucketKey
|
state.bucketName = state.bucketKey
|
||||||
}
|
}
|
||||||
if (!atomicState.accessKey) {
|
def bucketName = "${state.bucketName}"
|
||||||
return
|
def bucketKey = "${state.bucketKey}"
|
||||||
}
|
def accessKey = "${state.accessKey}"
|
||||||
def bucketName = "${atomicState.bucketName}"
|
|
||||||
def bucketKey = "${atomicState.bucketKey}"
|
|
||||||
def accessKey = "${atomicState.accessKey}"
|
|
||||||
|
|
||||||
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
||||||
|
|
||||||
def bucketCreatePost = [
|
def bucketCreatePost = [
|
||||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
|
uri: 'https://groker.initialstate.com/api/buckets',
|
||||||
headers: [
|
headers: [
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-IS-AccessKey": accessKey
|
"X-IS-AccessKey": accessKey
|
||||||
@@ -297,20 +258,10 @@ def tryCreateBucket() {
|
|||||||
|
|
||||||
log.debug bucketCreatePost
|
log.debug bucketCreatePost
|
||||||
|
|
||||||
try {
|
httpPostJson(bucketCreatePost) {
|
||||||
// Create a bucket on Initial State so the data has a logical grouping
|
log.debug "bucket posted"
|
||||||
httpPostJson(bucketCreatePost) { resp ->
|
state.isBucketCreated = true
|
||||||
log.debug "bucket posted"
|
|
||||||
if (resp.status >= 400) {
|
|
||||||
log.error "bucket not created successfully"
|
|
||||||
} else {
|
|
||||||
atomicState.isBucketCreated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.error "bucket creation error: $e"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def genericHandler(evt) {
|
def genericHandler(evt) {
|
||||||
@@ -322,73 +273,33 @@ def genericHandler(evt) {
|
|||||||
}
|
}
|
||||||
def value = "$evt.value"
|
def value = "$evt.value"
|
||||||
|
|
||||||
tryCreateBucket()
|
|
||||||
|
|
||||||
eventHandler(key, value)
|
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) {
|
def eventHandler(name, value) {
|
||||||
log.debug atomicState.eventBuffer
|
|
||||||
|
|
||||||
def eventBuffer = atomicState.eventBuffer
|
if (state.accessKey == null || state.bucketKey == null) {
|
||||||
def epoch = now() / 1000
|
|
||||||
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
|
||||||
|
|
||||||
log.debug eventBuffer
|
|
||||||
|
|
||||||
atomicState.eventBuffer = eventBuffer
|
|
||||||
|
|
||||||
if (eventBuffer.size() >= 10) {
|
|
||||||
tryShipEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
|
||||||
def tryShipEvents() {
|
|
||||||
|
|
||||||
// can't ship events if there is no grokerSubdomain
|
|
||||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
|
||||||
log.error "streaming url is currently null"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// can't ship if access key and bucket key are null, so finish trying
|
|
||||||
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.isBucketCreated) {
|
||||||
|
createBucket()
|
||||||
|
}
|
||||||
|
|
||||||
|
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
|
||||||
def eventPost = [
|
def eventPost = [
|
||||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
|
uri: 'https://groker.initialstate.com/api/events',
|
||||||
headers: [
|
headers: [
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-IS-BucketKey": "${atomicState.bucketKey}",
|
"X-IS-BucketKey": "${state.bucketKey}",
|
||||||
"X-IS-AccessKey": "${atomicState.accessKey}",
|
"X-IS-AccessKey": "${state.accessKey}"
|
||||||
"Accept-Version": "0.0.2"
|
|
||||||
],
|
],
|
||||||
body: atomicState.eventBuffer
|
body: eventBody
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
log.debug eventPost
|
||||||
// post the events to initial state
|
|
||||||
httpPostJson(eventPost) { resp ->
|
httpPostJson(eventPost) {
|
||||||
log.debug "shipped events and got ${resp.status}"
|
log.debug "event data posted"
|
||||||
if (resp.status >= 400) {
|
|
||||||
log.error "shipping failed... ${resp.data}"
|
|
||||||
} else {
|
|
||||||
// clear the buffer
|
|
||||||
atomicState.eventBuffer = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.error "shipping events failed: $e"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
/**
|
|
||||||
* Hello Home Cube
|
|
||||||
*
|
|
||||||
* Copyright 2015 skp19
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/************
|
|
||||||
* Metadata *
|
|
||||||
************/
|
|
||||||
definition(
|
|
||||||
name: "Hello Home Cube",
|
|
||||||
namespace: "skp19",
|
|
||||||
author: "skp19",
|
|
||||||
description: "Run a Hello Home action by rotating a cube containing a SmartSense Multi",
|
|
||||||
category: "SmartThings Labs",
|
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
|
||||||
)
|
|
||||||
|
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
|
|
||||||
/**********
|
|
||||||
* Setup *
|
|
||||||
**********/
|
|
||||||
preferences {
|
|
||||||
page(name: "mainPage", title: "", nextPage: "scenesPage", uninstall: true) {
|
|
||||||
section("Use the orientation of this cube") {
|
|
||||||
input "cube", "capability.threeAxis", required: false, title: "SmartSense Multi sensor"
|
|
||||||
}
|
|
||||||
section([title: " ", mobileOnly:true]) {
|
|
||||||
label title: "Assign a name", required: false
|
|
||||||
mode title: "Set for specific mode(s)", required: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page(name: "scenesPage", title: "Scenes", install: true, uninstall: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def scenesPage() {
|
|
||||||
log.debug "scenesPage()"
|
|
||||||
def sceneId = getOrientation()
|
|
||||||
dynamicPage(name:"scenesPage") {
|
|
||||||
def phrases = location.helloHome?.getPhrases()*.label
|
|
||||||
section {
|
|
||||||
phrases.sort()
|
|
||||||
input name: "homeAction1", type: "enum", title: "${1}. ${sceneName(1)}${sceneId==1 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
input name: "homeAction2", type: "enum", title: "${2}. ${sceneName(2)}${sceneId==2 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
input name: "homeAction3", type: "enum", title: "${3}. ${sceneName(3)}${sceneId==3 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
input name: "homeAction4", type: "enum", title: "${4}. ${sceneName(4)}${sceneId==4 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
input name: "homeAction5", type: "enum", title: "${5}. ${sceneName(5)}${sceneId==5 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
input name: "homeAction6", type: "enum", title: "${6}. ${sceneName(6)}${sceneId==6 ? ' (current)' : ''}", required: false, options: phrases
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
href "scenesPage", title: "Refresh", description: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*************************
|
|
||||||
* Installation & update *
|
|
||||||
*************************/
|
|
||||||
def installed() {
|
|
||||||
log.debug "Installed with settings: ${settings}"
|
|
||||||
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated() {
|
|
||||||
log.debug "Updated with settings: ${settings}"
|
|
||||||
|
|
||||||
unsubscribe()
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def initialize() {
|
|
||||||
subscribe cube, "threeAxis", positionHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/******************
|
|
||||||
* Event handlers *
|
|
||||||
******************/
|
|
||||||
def positionHandler(evt) {
|
|
||||||
|
|
||||||
def sceneId = getOrientation(evt.xyzValue)
|
|
||||||
log.trace "orientation: $sceneId"
|
|
||||||
|
|
||||||
if (sceneId != state.lastActiveSceneId) {
|
|
||||||
runHomeAction(sceneId)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.trace "No status change"
|
|
||||||
}
|
|
||||||
state.lastActiveSceneId = sceneId
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/******************
|
|
||||||
* Helper methods *
|
|
||||||
******************/
|
|
||||||
private Boolean sceneIsDefined(sceneId) {
|
|
||||||
def tgt = "onoff_${sceneId}".toString()
|
|
||||||
settings.find{it.key.startsWith(tgt)} != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateSetting(name, value) {
|
|
||||||
app.updateSetting(name, value)
|
|
||||||
settings[name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private runHomeAction(sceneId) {
|
|
||||||
log.trace "runHomeAction($sceneId)"
|
|
||||||
|
|
||||||
//RUN HELLO HOME ACTION
|
|
||||||
def homeAction
|
|
||||||
if (sceneId == 1) {
|
|
||||||
homeAction = homeAction1
|
|
||||||
}
|
|
||||||
if (sceneId == 2) {
|
|
||||||
homeAction = homeAction2
|
|
||||||
}
|
|
||||||
if (sceneId == 3) {
|
|
||||||
homeAction = homeAction3
|
|
||||||
}
|
|
||||||
if (sceneId == 4) {
|
|
||||||
homeAction = homeAction4
|
|
||||||
}
|
|
||||||
if (sceneId == 5) {
|
|
||||||
homeAction = homeAction5
|
|
||||||
}
|
|
||||||
if (sceneId == 6) {
|
|
||||||
homeAction = homeAction6
|
|
||||||
}
|
|
||||||
|
|
||||||
if (homeAction) {
|
|
||||||
location.helloHome.execute(homeAction)
|
|
||||||
log.trace "Running Home Action: $homeAction"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.trace "No Home Action Defined for Current State"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOrientation(xyz=null) {
|
|
||||||
final threshold = 250
|
|
||||||
|
|
||||||
def value = xyz ?: cube.currentValue("threeAxis")
|
|
||||||
|
|
||||||
def x = Math.abs(value.x) > threshold ? (value.x > 0 ? 1 : -1) : 0
|
|
||||||
def y = Math.abs(value.y) > threshold ? (value.y > 0 ? 1 : -1) : 0
|
|
||||||
def z = Math.abs(value.z) > threshold ? (value.z > 0 ? 1 : -1) : 0
|
|
||||||
|
|
||||||
def orientation = 0
|
|
||||||
if (z > 0) {
|
|
||||||
if (x == 0 && y == 0) {
|
|
||||||
orientation = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (z < 0) {
|
|
||||||
if (x == 0 && y == 0) {
|
|
||||||
orientation = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (x > 0) {
|
|
||||||
if (y == 0) {
|
|
||||||
orientation = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (x < 0) {
|
|
||||||
if (y == 0) {
|
|
||||||
orientation = 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (y > 0) {
|
|
||||||
orientation = 5
|
|
||||||
}
|
|
||||||
else if (y < 0) {
|
|
||||||
orientation = 6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orientation
|
|
||||||
}
|
|
||||||
|
|
||||||
private sceneName(num) {
|
|
||||||
final names = ["UNDEFINED","One","Two","Three","Four","Five","Six"]
|
|
||||||
settings."sceneName${num}" ?: "Scene ${names[num]}"
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Hue (Connect)",
|
name: "Hue (Connect)",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
@@ -64,13 +64,10 @@ def bridgeDiscovery(params=[:])
|
|||||||
def options = bridges ?: []
|
def options = bridges ?: []
|
||||||
def numFound = options.size() ?: 0
|
def numFound = options.size() ?: 0
|
||||||
|
|
||||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
if(!state.subscribe) {
|
||||||
log.trace "Cleaning old bridges memory"
|
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||||
state.bridges = [:]
|
state.subscribe = true
|
||||||
state.bridgeRefreshCount = 0
|
}
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
|
||||||
|
|
||||||
//bridge discovery request every 15 //25 seconds
|
//bridge discovery request every 15 //25 seconds
|
||||||
if((bridgeRefreshCount % 5) == 0) {
|
if((bridgeRefreshCount % 5) == 0) {
|
||||||
@@ -97,20 +94,11 @@ def bridgeLinking()
|
|||||||
|
|
||||||
def nextPage = ""
|
def nextPage = ""
|
||||||
def title = "Linking with your Hue"
|
def title = "Linking with your Hue"
|
||||||
def paragraphText
|
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||||
def hueimage = null
|
|
||||||
if (selectedHue) {
|
|
||||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
|
||||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
|
||||||
} else {
|
|
||||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
|
||||||
hueimage = null
|
|
||||||
}
|
|
||||||
if (state.username) { //if discovery worked
|
if (state.username) { //if discovery worked
|
||||||
nextPage = "bulbDiscovery"
|
nextPage = "bulbDiscovery"
|
||||||
title = "Success!"
|
title = "Success! - click 'Next'"
|
||||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||||
hueimage = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||||
@@ -118,20 +106,18 @@ def bridgeLinking()
|
|||||||
}
|
}
|
||||||
|
|
||||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||||
section("") {
|
section("Button Press") {
|
||||||
paragraph """${paragraphText}"""
|
paragraph """${paragraphText}"""
|
||||||
if (hueimage != null)
|
|
||||||
image "${hueimage}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def bulbDiscovery() {
|
def bulbDiscovery()
|
||||||
|
{
|
||||||
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
||||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||||
def refreshInterval = 3
|
def refreshInterval = 3
|
||||||
state.inBulbDiscovery = true
|
|
||||||
state.bridgeRefreshCount = 0
|
|
||||||
def options = bulbsDiscovered() ?: []
|
def options = bulbsDiscovered() ?: []
|
||||||
def numFound = options.size() ?: 0
|
def numFound = options.size() ?: 0
|
||||||
|
|
||||||
@@ -143,7 +129,7 @@ def bulbDiscovery() {
|
|||||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||||
|
|
||||||
@@ -208,22 +194,21 @@ Map bridgesDiscovered() {
|
|||||||
|
|
||||||
Map bulbsDiscovered() {
|
Map bulbsDiscovered() {
|
||||||
def bulbs = getHueBulbs()
|
def bulbs = getHueBulbs()
|
||||||
def bulbmap = [:]
|
def map = [:]
|
||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
bulbs.each {
|
bulbs.each {
|
||||||
def value = "${it.value.name}"
|
def value = "${it?.value?.name}"
|
||||||
def key = app.id +"/"+ it.value.id
|
def key = app.id +"/"+ it?.value?.id
|
||||||
bulbmap["${key}"] = value
|
map["${key}"] = value
|
||||||
}
|
}
|
||||||
} else { //backwards compatable
|
} else { //backwards compatable
|
||||||
bulbs.each {
|
bulbs.each {
|
||||||
def value = "${it.name}"
|
def value = "${it?.name}"
|
||||||
def key = app.id +"/"+ it.id
|
def key = app.id +"/"+ it?.id
|
||||||
logg += "$value - $key, "
|
map["${key}"] = value
|
||||||
bulbmap["${key}"] = value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bulbmap
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
def getHueBulbs() {
|
def getHueBulbs() {
|
||||||
@@ -246,16 +231,24 @@ def installed() {
|
|||||||
def updated() {
|
def updated() {
|
||||||
log.trace "Updated with settings: ${settings}"
|
log.trace "Updated with settings: ${settings}"
|
||||||
unschedule()
|
unschedule()
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
log.debug "Initializing"
|
log.debug "Initializing"
|
||||||
state.inBulbDiscovery = false
|
state.subscribe = false
|
||||||
|
state.bridgeSelectedOverride = false
|
||||||
|
def bridge = null
|
||||||
|
|
||||||
if (selectedHue) {
|
if (selectedHue) {
|
||||||
addBridge()
|
addBridge()
|
||||||
addBulbs()
|
bridge = getChildDevice(selectedHue)
|
||||||
|
subscribe(bridge, "bulbList", bulbListHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedBulbs) {
|
||||||
|
addBulbs()
|
||||||
doDeviceSync()
|
doDeviceSync()
|
||||||
runEvery5Minutes("doDeviceSync")
|
runEvery5Minutes("doDeviceSync")
|
||||||
}
|
}
|
||||||
@@ -270,27 +263,22 @@ def manualRefresh() {
|
|||||||
|
|
||||||
def uninstalled(){
|
def uninstalled(){
|
||||||
state.bridges = [:]
|
state.bridges = [:]
|
||||||
state.username = null
|
state.subscribe = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles events to add new bulbs
|
// Handles events to add new bulbs
|
||||||
def bulbListHandler(hub, data = "") {
|
def bulbListHandler(evt) {
|
||||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
def bulbs = [:]
|
||||||
log.trace "Here: $hub, $data"
|
log.trace "Adding bulbs to state..."
|
||||||
if (state.inBulbDiscovery) {
|
state.bridgeProcessedLightList = true
|
||||||
def bulbs = [:]
|
evt.jsonData.each { k,v ->
|
||||||
def logg = ""
|
log.trace "$k: $v"
|
||||||
log.trace "Adding bulbs to state..."
|
if (v instanceof Map) {
|
||||||
state.bridgeProcessedLightList = true
|
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
|
||||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
}
|
||||||
object.each { k,v ->
|
}
|
||||||
if (v instanceof Map)
|
state.bulbs = bulbs
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
log.info "${bulbs.size()} bulbs found"
|
||||||
}
|
|
||||||
state.bulbs = bulbs
|
|
||||||
msg = "${bulbs.size()} bulbs found. $state.bulbs"
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def addBulbs() {
|
def addBulbs() {
|
||||||
@@ -306,7 +294,7 @@ def addBulbs() {
|
|||||||
} else {
|
} else {
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//backwards compatable
|
//backwards compatable
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||||
@@ -334,7 +322,7 @@ def addBridge() {
|
|||||||
def d = getChildDevice(selectedHue)
|
def d = getChildDevice(selectedHue)
|
||||||
if(!d) {
|
if(!d) {
|
||||||
// compatibility with old devices
|
// compatibility with old devices
|
||||||
def newbridge = true
|
def newbridge = true
|
||||||
childDevices.each {
|
childDevices.each {
|
||||||
if (it.getDeviceDataByName("mac")) {
|
if (it.getDeviceDataByName("mac")) {
|
||||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||||
@@ -344,27 +332,22 @@ def addBridge() {
|
|||||||
it.setDeviceNetworkId("${newDNI}")
|
it.setDeviceNetworkId("${newDNI}")
|
||||||
if (oldDNI == selectedHue)
|
if (oldDNI == selectedHue)
|
||||||
app.updateSetting("selectedHue", newDNI)
|
app.updateSetting("selectedHue", newDNI)
|
||||||
newbridge = false
|
newbridge = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newbridge) {
|
if (newbridge) {
|
||||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||||
if (vbridge.value.ip && vbridge.value.port) {
|
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.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||||
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
|
else
|
||||||
} else {
|
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
} else
|
||||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
||||||
@@ -372,6 +355,7 @@ def addBridge() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def locationHandler(evt) {
|
def locationHandler(evt) {
|
||||||
def description = evt.description
|
def description = evt.description
|
||||||
log.trace "Location: $description"
|
log.trace "Location: $description"
|
||||||
@@ -470,13 +454,17 @@ def locationHandler(evt) {
|
|||||||
|
|
||||||
def doDeviceSync(){
|
def doDeviceSync(){
|
||||||
log.trace "Doing Hue Device Sync!"
|
log.trace "Doing Hue Device Sync!"
|
||||||
|
|
||||||
|
//shrink the large bulb lists
|
||||||
convertBulbListToMap()
|
convertBulbListToMap()
|
||||||
|
|
||||||
poll()
|
poll()
|
||||||
try {
|
|
||||||
|
if(!state.subscribe) {
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||||
} catch (all) {
|
state.subscribe = true
|
||||||
log.trace "Subscription already exist"
|
}
|
||||||
}
|
|
||||||
discoverBridges()
|
discoverBridges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,49 +473,44 @@ def doDeviceSync(){
|
|||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
def parse(childDevice, description) {
|
def parse(childDevice, description) {
|
||||||
def parsedEvent = parseLanMessage(description)
|
def parsedEvent = parseLanMessage(description)
|
||||||
if (parsedEvent.headers && parsedEvent.body) {
|
if (parsedEvent.headers && parsedEvent.body) {
|
||||||
def headerString = parsedEvent.headers.toString()
|
def headerString = parsedEvent.headers.toString()
|
||||||
def bodyString = parsedEvent.body.toString()
|
if (headerString?.contains("json")) {
|
||||||
if (headerString?.contains("json")) {
|
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||||
def body
|
if (body instanceof java.util.HashMap)
|
||||||
try {
|
{ //poll response
|
||||||
body = new groovy.json.JsonSlurper().parseText(bodyString)
|
|
||||||
} catch (all) {
|
|
||||||
log.warn "Parsing Body failed - trying again..."
|
|
||||||
poll()
|
|
||||||
}
|
|
||||||
if (body instanceof java.util.HashMap) {
|
|
||||||
//poll response
|
|
||||||
def bulbs = getChildDevices()
|
def bulbs = getChildDevices()
|
||||||
|
//for each bulb
|
||||||
for (bulb in body) {
|
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 (d) {
|
||||||
if (bulb.value.state?.reachable) {
|
if (bulb.value.state?.reachable) {
|
||||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
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)])
|
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||||
if (bulb.value.state.sat) {
|
if (bulb.value.state.sat) {
|
||||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
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 sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||||
def hex = colorUtil.hslToHex(hue, sat)
|
def hex = colorUtil.hslToHex(hue, sat)
|
||||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||||
if (bulb.value.state.sat) {
|
if (bulb.value.state.sat) {
|
||||||
def hue = 23
|
def hue = 23
|
||||||
def sat = 56
|
def sat = 56
|
||||||
def hex = colorUtil.hslToHex(23, 56)
|
def hex = colorUtil.hslToHex(23, 56)
|
||||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{ //put response
|
{ //put response
|
||||||
@@ -576,25 +559,25 @@ def parse(childDevice, description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "parse - got something other than headers,body..."
|
log.debug "parse - got something other than headers,body..."
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def on(childDevice, transition_deprecated = 0) {
|
def on(childDevice, transition = 4) {
|
||||||
log.debug "Executing 'on'"
|
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)
|
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"
|
return "level: $percent"
|
||||||
}
|
}
|
||||||
|
|
||||||
def off(childDevice, transition_deprecated = 0) {
|
def off(childDevice, transition = 4) {
|
||||||
log.debug "Executing 'off'"
|
log.debug "Executing 'off'"
|
||||||
put("lights/${getId(childDevice)}/state", [on: false])
|
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||||
return "level: 0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(childDevice, percent) {
|
def setLevel(childDevice, percent) {
|
||||||
@@ -615,21 +598,19 @@ def setHue(childDevice, percent) {
|
|||||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
|
def setColor(childDevice, color, alert = "none", transition = 4) {
|
||||||
log.debug "Executing 'setColor($huesettings)'"
|
log.debug "Executing 'setColor($color)'"
|
||||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
|
||||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
|
||||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
|
||||||
def transition = huesettings.transition ? huesettings.transition : 4
|
|
||||||
|
|
||||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||||
if (huesettings.level != null) {
|
if (color.level != null) {
|
||||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||||
value.on = value.bri > 0
|
value.on = value.bri > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (huesettings.switch) {
|
if (color.switch) {
|
||||||
value.on = huesettings.switch == "on"
|
value.on = color.switch == "on"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "sending command $value"
|
log.debug "sending command $value"
|
||||||
@@ -659,19 +640,15 @@ private getId(childDevice) {
|
|||||||
private poll() {
|
private poll() {
|
||||||
def host = getBridgeIP()
|
def host = getBridgeIP()
|
||||||
def uri = "/api/${state.username}/lights/"
|
def uri = "/api/${state.username}/lights/"
|
||||||
try {
|
log.debug "GET: $host$uri"
|
||||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||||
HOST: ${host}
|
HOST: ${host}
|
||||||
|
|
||||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||||
} catch (all) {
|
|
||||||
log.warn "Parsing Body failed - trying again..."
|
|
||||||
doDeviceSync()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private put(path, body) {
|
private put(path, body) {
|
||||||
def host = getBridgeIP()
|
def host = getBridgeIP()
|
||||||
def uri = "/api/${state.username}/$path"
|
def uri = "/api/${state.username}/$path"
|
||||||
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
||||||
def length = bodyJSON.getBytes().size().toString()
|
def length = bodyJSON.getBytes().size().toString()
|
||||||
@@ -691,13 +668,9 @@ ${bodyJSON}
|
|||||||
private getBridgeIP() {
|
private getBridgeIP() {
|
||||||
def host = null
|
def host = null
|
||||||
if (selectedHue) {
|
if (selectedHue) {
|
||||||
def d = getChildDevice(selectedHue)
|
def d = getChildDevice(dni)
|
||||||
if (d) {
|
if (d)
|
||||||
if (d.getDeviceDataByName("networkAddress"))
|
host = d.latestState('networkAddress').stringValue
|
||||||
host = d.getDeviceDataByName("networkAddress")
|
|
||||||
else
|
|
||||||
host = d.latestState('networkAddress').stringValue
|
|
||||||
}
|
|
||||||
if (host == null || host == "") {
|
if (host == null || host == "") {
|
||||||
def serialNumber = selectedHue
|
def serialNumber = selectedHue
|
||||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||||
@@ -708,9 +681,9 @@ private getBridgeIP() {
|
|||||||
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
||||||
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
||||||
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
||||||
}
|
}
|
||||||
log.trace "Bridge: $selectedHue - Host: $host"
|
log.trace "Bridge: $selectedHue - Host: $host"
|
||||||
}
|
}
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ def authPage() {
|
|||||||
|
|
||||||
def options = locationOptions() ?: []
|
def options = locationOptions() ?: []
|
||||||
def count = options.size()
|
def count = options.size()
|
||||||
def refreshInterval = 3
|
|
||||||
|
|
||||||
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||||
section("Select your location") {
|
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["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
||||||
data["hue"] = device.color.hue / 3.6
|
data["hue"] = device.color.hue / 3.6
|
||||||
data["saturation"] = device.color.saturation * 100
|
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 {
|
} 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 ->
|
getChildDevices().each { device ->
|
||||||
device.refresh()
|
device.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
definition(
|
definition(
|
||||||
name: "Logitech Harmony (Connect)",
|
name: "Logitech Harmony (Connect)",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "Juan Pablo Risso",
|
||||||
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||||
@@ -394,9 +394,7 @@ def discovery() {
|
|||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||||
state.resethub = true
|
state.resethub = true
|
||||||
} catch (e) {
|
}
|
||||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,9 +459,7 @@ def activity(dni,mode) {
|
|||||||
msg = ex
|
msg = ex
|
||||||
state.aux = 0
|
state.aux = 0
|
||||||
}
|
}
|
||||||
} catch(Exception ex) {
|
}
|
||||||
msg = ex
|
|
||||||
}
|
|
||||||
runIn(10, "poll", [overwrite: true])
|
runIn(10, "poll", [overwrite: true])
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
@@ -521,9 +517,7 @@ def poll() {
|
|||||||
state.remove("HarmonyAccessToken")
|
state.remove("HarmonyAccessToken")
|
||||||
return "Harmony Access token has expired"
|
return "Harmony Access token has expired"
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
}
|
||||||
log.trace e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,9 +550,7 @@ def getActivityList() {
|
|||||||
log.trace e
|
log.trace e
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
} catch(Exception e) {
|
}
|
||||||
log.trace e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
@@ -573,9 +565,9 @@ def getActivityName(activity,hubId) {
|
|||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
actname = response.data.data.activities[activity].name
|
actname = response.data.data.activities[activity].name
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actname
|
return actname
|
||||||
}
|
}
|
||||||
@@ -593,9 +585,9 @@ def getActivityId(activity,hubId) {
|
|||||||
actid = it.key
|
actid = it.key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actid
|
return actid
|
||||||
}
|
}
|
||||||
@@ -610,9 +602,9 @@ def getHubName(hubId) {
|
|||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
hubname = response.data.data.name
|
hubname = response.data.data.name
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hubname
|
return hubname
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,9 +79,12 @@ def scenePage(params=[:]) {
|
|||||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
if (sceneId == currentSceneId) {
|
||||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
section {
|
||||||
}
|
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +225,7 @@ private restoreStates(sceneId) {
|
|||||||
if (type == "level") {
|
if (type == "level") {
|
||||||
log.debug "${light.displayName} level is '$level'"
|
log.debug "${light.displayName} level is '$level'"
|
||||||
if (level != null) {
|
if (level != null) {
|
||||||
light.setLevel(level)
|
light.setLevel(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (type == "color") {
|
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}"
|
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 ->
|
state.sensors = json
|
||||||
sensors[sensorId] = sensorName
|
|
||||||
|
json.each { sensorId, sensorName ->
|
||||||
createChild(sensorId, sensorName)
|
createChild(sensorId, sensorName)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.sensors = sensors
|
|
||||||
|
|
||||||
return "success"
|
return "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def createChild(sensorId, sensorName) {
|
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>
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user