Compare commits

..

4 Commits

Author SHA1 Message Date
Ngoc Hoang
96e0995e43 Modifying 'Publication Requests for Lumi Touch Switch, Dimmer and Shade' 2015-11-19 02:33:05 -06:00
Ngoc Hoang
bf7a7830cc Modifying 'Publication Requests for Lumi Touch Switch, Dimmer and Shade' 2015-09-09 02:06:50 -05:00
Ngoc Hoang
7d90555006 Modifying 'Publication Requests for Lumi Touch Switch' 2015-09-08 00:06:37 -05:00
Ngoc Hoang
04c19990cf MSA-65: Hi Smartthings Team,
I'm from Lumi Vietnam, and these are device types for our Touch Switch devices. You can see images and info of our devices here:http://www.lumi.com.vn/Elite-Version-pd-4
Our switches have from 1 to 4 Touch Button with each one on different endpoint: 1, 3, 5, 7.
These Device Types are written based on GE Switch template, and support control, read attribute, config report
Sorry for don't have simulator code because I didn't know how to write. But we have real devices for your testing.  I named it Lumi Switch 3 in my devices list. And our hub will online from 5 a.m to 7 p.m GTM+7. You can try it.

Thank you,
Ngoc
2015-09-04 04:09:36 -05:00
49 changed files with 2897 additions and 4908 deletions

View File

@@ -1,195 +0,0 @@
/**
* Climax PSM-29ZBSR - Zigbee - Power Outlet Switch and Energy Meter
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Thanks to SmartThings and its community!
*
* Product Page: http://www.climax.com.tw/psm29zb-zb.php
* Developer manual https://fccid.io/document.php?id=2553357
*
*/
metadata {
definition (name: "Climax Power Outlet", namespace: "bortuzar", author: "bortuzar") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Power Meter"
fingerprint endpointId: "0x0A", profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0702", outClusters: "0000"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("power", "device.power", inactiveLabel: false, decoration: "flat") {
state "power", label: '${currentValue} W'
}
//standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
// state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
//}
main(["switch", "power"])
details(["switch", "power", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "Parse Method Called"
log.trace description
if (description?.startsWith("catchall:")) {
def msg = zigbee.parse(description)
log.trace "data: $msg.data"
} else if (description?.startsWith("read attr -")) {
def descMap = parseDescriptionAsMap(description)
log.debug "Desc Map:"
log.debug descMap
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
name = "switch"
value = descMap.value.endsWith("01") ? "on" : "off"
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
} else if(descMap.cluster =="0702" && descMap.attrId == "0400") {
def value = convertHexToInt(descMap.value[-4..-1])/10
// Reading the last 4 characters of the string...Maybe 4 are needed. Needs further test.
// Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
log.debug value
def name = "power"
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
}
} else {
def name = description?.startsWith("on/off: ") ? "switch" : null
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
def on() {
log.debug "On Method called"
sendEvent(name: "switch", value: "on")
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
}
def off() {
log.debug "Off Method called"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
}
def refresh() {
log.debug "Refresh Method called";
//read attribute 1024Decimal, translates to 400Hex.
[
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} 1 0x0702 1024", , "delay 500"
]
}
def configure() {
log.debug "Configure Method called"
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
def configCmds = [
//Switch Reporting
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
//bing to cluster 0x006. Switch On-Off
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
//bind to cluster 0x702. Power Consumption
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0702 {${device.zigbeeId}} {}", "delay 500",
]
return configCmds + refresh() // send refresh cmds as part of config
}
def parseDescriptionAsMap(description) {
(description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
private hex(value, width=2) {
def s = new BigInteger(Math.round(value).toString()).toString(16)
while (s.size() < width) {
s = "0" + s
}
s
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}

View File

@@ -1,506 +0,0 @@
/**
* Keen Home Smart Vent
*
* Author: Keen Home
* Date: 2015-06-23
*/
metadata {
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
capability "Switch Level"
capability "Switch"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Temperature Measurement"
capability "Battery"
command "getLevel"
command "getOnOff"
command "getPressure"
command "getBattery"
command "getTemperature"
command "setZigBeeIdTile"
fingerprint endpoint: "1",
profileId: "0104",
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
outClusters: "0019"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
}
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("temperature", "device.temperature", inactiveLabel: false) {
state "temperature", label:'${currentValue}°',
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
}
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
}
main "switch"
details(["switch","refresh","temperature","levelSliderControl","battery"])
}
}
/**** PARSE METHODS ****/
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('on/off: ')) {
map = parseOnOffMessage(description)
}
log.debug "Parse returned $map"
return map ? createEvent(map) : null
}
private Map parseCatchAllMessage(String description) {
log.debug "parseCatchAllMessage"
def cluster = zigbee.parse(description)
log.debug "cluster: ${cluster}"
if (shouldProcessMessage(cluster)) {
log.debug "processing message"
switch(cluster.clusterId) {
case 0x0001:
return makeBatteryResult(cluster.data.last())
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = convertTemperatureHex(temp)
return makeTemperatureResult(value)
break
case 0x0006:
return makeOnOffResult(cluster.data[-1])
break
}
}
return [:]
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
if (cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
return false
}
return true
}
private Map parseReportAttributeMessage(String description) {
log.debug "parseReportAttributeMessage"
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
return makeOnOffResult(Int.parseInt(descMap.value));
}
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
return makeLevelResult(descMap.value)
}
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = convertTemperatureHex(descMap.value)
return makeTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
return makePressureResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
return makeSerialResult(new String(descMap.value.decodeHex()))
}
// shouldn't get here
return [:]
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
// log.debug "${description}"
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
// log.debug "split: " + description.split(": ")
def value = Double.parseDouble(description.split(": ")[1])
// log.debug "${value}"
resultMap = makeTemperatureResult(convertTemperature(value))
}
return resultMap
}
private Map parseOnOffMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('on/off: ')) {
def value = Integer.parseInt(description - "on/off: ")
resultMap = makeOnOffResult(value)
}
return resultMap
}
private Map makeOnOffResult(rawValue) {
log.debug "makeOnOffResult: ${rawValue}"
def linkText = getLinkText(device)
def value = rawValue == 1 ? "on" : "off"
return [
name: "switch",
value: value,
descriptionText: "${linkText} is ${value}"
]
}
private Map makeLevelResult(rawValue) {
def linkText = getLinkText(device)
// log.debug "rawValue: ${rawValue}"
def value = Integer.parseInt(rawValue, 16)
def rangeMax = 254
if (value == 255) {
log.debug "obstructed"
// Just return here. Once the vent is power cycled
// it will go back to the previous level before obstruction.
// Therefore, no need to update level on the display.
return [
name: "switch",
value: "obstructed",
descriptionText: "${linkText} is obstructed. Please power cycle."
]
} else if ( device.currentValue("switch") == "obstructed" &&
value == 254) {
// When the device is reset after an obstruction, the switch
// state will be obstructed and the value coming from the device
// will be 254. Since we're not using heating/cooling mode from
// the device type handler, we need to bump it down to the lower
// (cooling) range
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
value = rangeMax
}
// else if (device.currentValue("switch") == "off") {
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
// }
// log.debug "pre-value: ${value}"
value = Math.floor(value / rangeMax * 100)
// log.debug "post-value: ${value}"
return [
name: "level",
value: value,
descriptionText: "${linkText} level is ${value}%"
]
}
private Map makePressureResult(rawValue) {
log.debug 'makePressureResut'
def linkText = getLinkText(device)
def pascals = rawValue / 10
def result = [
name: 'pressure',
descriptionText: "${linkText} pressure is ${pascals}Pa",
value: pascals
]
return result
}
private Map makeBatteryResult(rawValue) {
// log.debug 'makeBatteryResult'
def linkText = getLinkText(device)
// log.debug
[
name: 'battery',
value: rawValue,
descriptionText: "${linkText} battery is at ${rawValue}%"
]
}
private Map makeTemperatureResult(value) {
// log.debug 'makeTemperatureResult'
def linkText = getLinkText(device)
// log.debug "tempOffset: ${tempOffset}"
if (tempOffset) {
def offset = tempOffset as int
// log.debug "offset: ${offset}"
def v = value as int
// log.debug "v: ${v}"
value = v + offset
// log.debug "value: ${value}"
}
return [
name: 'temperature',
value: "" + value,
descriptionText: "${linkText} is ${value}°${temperatureScale}",
]
}
/**** HELPER METHODS ****/
private def convertTemperatureHex(value) {
// log.debug "convertTemperatureHex(${value})"
def celsius = Integer.parseInt(value, 16).shortValue() / 100
// log.debug "celsius: ${celsius}"
return convertTemperature(celsius)
}
private def convertTemperature(celsius) {
// log.debug "convertTemperature()"
if(getTemperatureScale() == "C"){
return celsius
} else {
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
// log.debug "converted to F: ${fahrenheit}"
return fahrenheit
}
}
private def makeSerialResult(serial) {
log.debug "makeSerialResult: " + serial
def linkText = getLinkText(device)
sendEvent([
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ])
return [
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ]
}
/**** COMMAND METHODS ****/
// def mfgCode() {
// ["zcl mfg-code 0x115B", "delay 200"]
// }
def on() {
log.debug "on()"
sendEvent(makeOnOffResult(1))
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
}
def off() {
log.debug "off()"
sendEvent(makeOnOffResult(0))
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
}
// does this work?
def toggle() {
log.debug "toggle()"
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
}
def setLevel(value) {
log.debug "setting level: ${value}"
def linkText = getLinkText(device)
sendEvent(name: "level", value: value)
if (value > 0) {
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
}
else {
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
}
def rangeMax = 254
def computedLevel = Math.round(value * rangeMax / 100)
log.debug "computedLevel: ${computedLevel}"
def level = new BigInteger(computedLevel.toString()).toString(16)
log.debug "level: ${level}"
if (level.size() < 2){
level = '0' + level
}
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
}
def getOnOff() {
log.debug "getOnOff()"
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
}
def getPressure() {
log.debug "getPressure()"
[
"zcl mfg-code 0x115B", "delay 200",
"zcl global read 0x0403 0x20", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
}
def getLevel() {
log.debug "getLevel()"
// rattr = read attribute
// 0x${} = device net id
// 1 = endpoint
// 8 = cluster id (level control, in this case)
// 0 = attribute within cluster
// sendEvent(name: "level", value: value)
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
}
def getTemperature() {
log.debug "getTemperature()"
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
}
def getBattery() {
log.debug "getBattery()"
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
}
def setZigBeeIdTile() {
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
def linkText = getLinkText(device)
sendEvent([
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
return [
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
}
def refresh() {
getOnOff() +
getLevel() +
getTemperature() +
getPressure() +
getBattery()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
def configure() {
log.debug "CONFIGURE"
log.debug "zigbeeId: ${device.hub.zigbeeId}"
setZigBeeIdTile()
def configCmds = [
// binding commands
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
// configure report commands
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
// mike 2015/06/22: preconfigured; see tech spec
// vent on/off state - type: boolean, change: 1
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: preconfigured; see tech spec
// vent level - type: int8u, change: 1
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: temp and pressure reports are preconfigured, but
// we'd like to override their settings for our own purposes
// temperature - type: int16s, change: 0xA = 10 = 0.1C
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: use new custom pressure attribute
// pressure - type: int32u, change: 1 = 0.1Pa
"zcl mfg-code 0x115B", "delay 200",
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
// mike 2015/06/22: preconfigured; see tech spec
// battery - type: int8u, change: 1
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
]
return configCmds + refresh()
}

View 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
}

View 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
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View File

@@ -56,7 +56,7 @@ metadata {
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
}
graphTile(name: "powerGraph", attribute: "device.power")
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
main(["power", "energy"])
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
@@ -68,8 +68,16 @@ metadata {
// ========================================================
preferences {
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions()
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
}
def selectedGraphPrecision() {
graphPrecision ?: "Daily"
}
def selectedGraphType() {
graphType ?: "line"
}
// ========================================================
@@ -83,6 +91,22 @@ mappings {
GET: "renderGraph"
]
}
path("/graphDataSizes") { // for testing. remove before publishing
action:
[
GET: "graphDataSizes"
]
}
}
def graphDataSizes() { // for testing. remove before publishing
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
dateTypes[dateTypeData.key] = dateTypeData.value.size()
dateTypes
}
attributes
}
}
// ========================================================
@@ -97,7 +121,8 @@ def parse(String description) {
}
log.debug "Parse returned ${result?.descriptionText}"
storeGraphData(result.name, result.value)
PLATFORM_migrateGraphDataIfNeeded()
PLATFORM_storeData(result.name, result.value)
return result
}
@@ -151,15 +176,535 @@ def configure() {
def renderGraph() {
def data = fetchGraphData(params.attribute)
def data = PLATFORM_fetchGraphData(params.attribute)
def totalData = data*.runningSum
def xValues = data*.unixTime
def yValues = [
Total: [color: "#49a201", data: totalData]
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
]
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
}
// TODO: // ========================================================
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
// TODO: // ========================================================
// ========================================================
// PLATFORM TILES
// ========================================================
def PLATFORM_graphTile(Map tileParams) {
def cleanAttribute = tileParams.attribute - "device." - "capability."
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
}
// ========================================================
// PLATFORM GRAPH RENDERING
// ========================================================
private PLATFORM_graphTypeOptions() {
[
"line", // DEFAULT
"spline",
"step",
"area",
"area-spline",
"area-step",
"bar",
"scatter",
"pie",
"donut",
"gauge",
]
}
private PLATFORM_renderGraph(graphParams) {
String attribute = graphParams.attribute
List xValues = graphParams.xValues
Map yValues = graphParams.yValues
String focus = graphParams.focus ?: ""
String label = graphParams.label ?: ""
/*
def xValues = [1, 2]
def yValues = [
High: [type: "spline", data: [5, 6], color: "#bc2323"],
Low: [type: "spline", data: [0, 1], color: "#153591"]
]
Available type values:
line // DEFAULT
spline
step
area
area-spline
area-step
bar
scatter
pie
donut
gauge
*/
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
def legendData = yValues*.key
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
renderHTML(htmlTitle) { html ->
html.head {
"""
<!-- Load c3.css -->
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
<!-- Load d3.js and c3.js -->
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
<script>
function getDocumentHeight() {
var body = document.body;
var html = document.documentElement;
return html.clientHeight;
}
function getDocumentWidth() {
var body = document.body;
var html = document.documentElement;
return html.clientWidth;
}
</script>
<style>
.legend {
position: absolute;
width: 80%;
padding-left: 15%;
z-index: 999;
padding-top: 5px;
}
.legend span {
width: ${100 / yValues.size()}%;
display: inline-block;
text-align: center;
cursor: pointer;
color: white;
}
</style>
"""
}
html.body {
"""
<div class="legend"></div>
<div id="chart" style="max-height: 120px; position: relative;"></div>
<script>
// Generate the chart
var chart = c3.generate(${graphData as grails.converters.JSON});
// Resize the chart to the size of the device tile
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
// Focus data if specified
${focusJS}
// Update the chart when ${attribute} events are received
function ${attribute}(evt) {
var newValue = ['${flowColumn}'];
newValue.push(evt.value);
var newX = ['x'];
newX.push(evt.unixTime);
chart.flow({
columns: [
newX,
newValue
]
});
}
// Build the custom legend
d3.select('.legend').selectAll('span')
.data(${legendData as grails.converters.JSON})
.enter().append('span')
.attr('data-id', function (id) { return id; })
.html(function (id) { return id; })
.each(function (id) {
d3.select(this).style('background-color', chart.color(id));
})
.on('mouseover', function (id) {
chart.focus(id);
})
.on('mouseout', function (id) {
chart.revert();
})
.on('click', function (id) {
chart.toggle(id);
});
</script>
"""
}
}
}
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
/*
def xValues = [1, 2]
def yValues = [
High: [type: "spline", data: [5, 6], color: "#bc2323"],
Low: [type: "spline", data: [0, 1], color: "#153591"]
]
*/
[
interaction: [
enabled: false
],
bindto : '#chart',
padding : [
left : 30,
right : 30,
bottom: 0,
top : 0
],
legend : [
show: false,
// hide : false,//(yValues.keySet().size() < 2),
// position: 'inset',
// inset: [
// anchor: "top-right"
// ],
// item: [
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
// ]
],
data : [
x : "x",
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
],
axis : [
x: [
type: 'timeseries',
tick: [
centered: true,
culling : [max: 7],
fit : true,
format : PLATFORM_getGraphDateFormat()
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
]
],
y: [
label : label,
padding: [
top: 50
]
]
]
]
}
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
// https://github.com/mbostock/d3/wiki/Time-Formatting
def graphDateFormat
switch (dateType) {
case "Live":
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
break
case "Hourly":
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
break
case "Daily":
graphDateFormat = "%a" // abbreviated weekday name
break
case "Monthly":
graphDateFormat = "%b" // abbreviated month name
break
case "Annually":
graphDateFormat = "%y" // year without century as a decimal number [00,99]
break
}
graphDateFormat
}
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
def graphDateFunction = "function(date) { return date; }"
switch (dateType) {
case "Live":
graphDateFunction = """
function(date) {
return.getMinutes();
}
"""
break;
case "Hourly":
graphDateFunction = """ function(date) {
var hour = date.getHours();
if (hour == 0) {
return String(/12 am/).substring(1).slice(0,-1);
} else if (hour > 12) {
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
} else {
return hour + String(/ am/).substring(1).slice(0,-1);
}
}"""
break
case "Daily":
graphDateFunction = """ function(date) {
var day = date.getDay();
switch(day) {
case 0: return String(/Sun/).substring(1).slice(0,-1);
case 1: return String(/Mon/).substring(1).slice(0,-1);
case 2: return String(/Tue/).substring(1).slice(0,-1);
case 3: return String(/Wed/).substring(1).slice(0,-1);
case 4: return String(/Thu/).substring(1).slice(0,-1);
case 5: return String(/Fri/).substring(1).slice(0,-1);
case 6: return String(/Sat/).substring(1).slice(0,-1);
}
}"""
break
case "Monthly":
graphDateFunction = """ function(date) {
var month = date.getMonth();
switch(month) {
case 0: return String(/Jan/).substring(1).slice(0,-1);
case 1: return String(/Feb/).substring(1).slice(0,-1);
case 2: return String(/Mar/).substring(1).slice(0,-1);
case 3: return String(/Apr/).substring(1).slice(0,-1);
case 4: return String(/May/).substring(1).slice(0,-1);
case 5: return String(/Jun/).substring(1).slice(0,-1);
case 6: return String(/Jul/).substring(1).slice(0,-1);
case 7: return String(/Aug/).substring(1).slice(0,-1);
case 8: return String(/Sep/).substring(1).slice(0,-1);
case 9: return String(/Oct/).substring(1).slice(0,-1);
case 10: return String(/Nov/).substring(1).slice(0,-1);
case 11: return String(/Dec/).substring(1).slice(0,-1);
}
}"""
break
case "Annually":
graphDateFunction = """
function(date) {
return.getFullYear();
}
"""
break
}
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
}
private jsEscapeString(str = "") {
"String(/${str}/).substring(1).slice(0,-1);"
}
private PLATFORM_fetchGraphData(attribute) {
log.debug "PLATFORM_fetchGraphData(${attribute})"
/*
[
[
dateString: "2014-12-1",
unixTime: 1421931600000,
min: 0,
max: 10,
average: 5
],
...
]
*/
def attributeBucket = state["measure.${attribute}"] ?: [:]
def dateType = selectedGraphPrecision()
attributeBucket[dateType]
}
// ========================================================
// PLATFORM DATA STORAGE
// ========================================================
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
private PLATFORM_storeData(attribute, value) {
PLATFORM_graphPrecisionOptions().each { dateType ->
PLATFORM_addDataToBucket(attribute, value, dateType)
}
}
/*
[
Hourly: [
[
dateString: "2014-12-1",
unixTime: 1421931600000,
min: 0,
max: 10,
average: 5
],
...
],
...
]
*/
private PLATFORM_addDataToBucket(attribute, value, dateType) {
def numberValue = value.toBigDecimal()
def attributeKey = "measure.${attribute}"
def attributeBucket = state[attributeKey] ?: [:]
def dateTypeBucket = attributeBucket[dateType] ?: []
def now = new Date()
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
def item = dateTypeBucket.find { it.dateString == itemDateString }
if (!item) {
// no entry for this data point yet, fill with initial values
item = [:]
item.average = numberValue
item.runningSum = numberValue
item.runningCount = 1
item.min = numberValue
item.max = numberValue
item.unixTime = now.getTime()
item.dateString = itemDateString
// add the new data point
dateTypeBucket << item
// clear out old data points
def old = PLATFORM_getOldDateString(dateType)
if (old) { // annual data never gets cleared
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
}
// limit the size of the bucket. Live data can stack up fast
def sizeLimit = 25
if (dateTypeBucket.size() > sizeLimit) {
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
}
} else {
//re-calculate average/min/max for this bucket
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
item.runningCount = item.runningCount.toInteger() + 1
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
if (item.min == null) {
item.min = numberValue
} else if (numberValue < item.min.toBigDecimal()) {
item.min = numberValue
}
if (item.max == null) {
item.max = numberValue
} else if (numberValue > item.max.toBigDecimal()) {
item.max = numberValue
}
}
attributeBucket[dateType] = dateTypeBucket
state[attributeKey] = attributeBucket
}
private PLATFORM_getOldDateString(dateType) {
def now = new Date()
def date
switch (dateType) {
case "Live":
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
break
case "Hourly":
date = (now - 1).getTime()
break
case "Daily":
date = (now - 10).getTime()
break
case "Monthly":
date = (now - 30).getTime()
break
case "Annually":
break
}
date
}
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
private PLATFORM_getAnnuallyFormat() { "yyyy" }
// ========================================================
// PLATFORM GRAPH DATA MIGRATION
// ========================================================
private PLATFORM_migrateGraphDataIfNeeded() {
if (!state.hasMigratedOldGraphData) {
def acceptableKeys = PLATFORM_graphPrecisionOptions()
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
needsMigration.each { PLATFORM_migrateGraphData(it) }
state.hasMigratedOldGraphData = true
}
}
private PLATFORM_migrateGraphData(attribute) {
log.trace "about to migrate ${attribute}"
def attributeBucket = state[attribute] ?: [:]
def migratedAttributeBucket = [:]
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
"PLATFORM_get${it}Format"().size() == oldDateString.size()
}
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
def newBucket = attributeBucket[dateType] ?: []
/*
def existingNewItem = newBucket.find { it.dateString == oldDateString }
if (existingNewItem) {
newBucket.remove(existingNewItem)
}
*/
def newItem = [
min : oldItem.min,
max : oldItem.max,
average : oldItem.average,
runningSum : oldItem.runningSum,
runningCount: oldItem.runningCount,
dateString : oldDateString,
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
]
newBucket << newItem
migratedAttributeBucket[dateType] = newBucket
}
state[attribute] = migratedAttributeBucket
}

View File

@@ -34,8 +34,8 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
])
}
}
@@ -43,7 +43,7 @@ metadata {
tiles {
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ebeef2"
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
}
standardTile("beep", "device.beep", decoration: "flat") {
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"

View File

@@ -55,136 +55,141 @@ metadata {
}
}
standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
}
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["switch", "level", "indicator", "refresh"])
details(["switch", "refresh", "indicator"])
}
}
def parse(String description) {
def result = null
if (description != "updated") {
log.debug "parse() >> zwave.parse($description)"
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
if (cmd) {
result = zwaveEvent(cmd)
}
def item1 = [
canBeCurrentState: false,
linkText: getLinkText(device),
isStateChange: false,
displayed: false,
descriptionText: description,
value: description
]
def result
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
if (cmd) {
result = createEvent(cmd, item1)
}
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
result = [result, response(zwave.basicV1.basicGet())]
log.debug "Was hailed: requesting state update"
} else {
log.debug "Parse returned ${result?.descriptionText}"
else {
item1.displayed = displayed(description, item1.isStateChange)
result = [item1]
}
return result
log.debug "Parse returned ${result?.descriptionText}"
result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
dimmerEvents(cmd)
}
private dimmerEvents(physicalgraph.zwave.Command cmd) {
def value = (cmd.value ? "on" : "off")
def result = [createEvent(name: "switch", value: value)]
if (cmd.value && cmd.value <= 100) {
result << createEvent(name: "level", value: cmd.value, unit: "%")
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
return result
result
}
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
[]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
[response(zwave.basicV1.basicGet())]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
for (int i = 0; i < result.size(); i++) {
result[i].type = "digital"
}
result
}
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
def result = [item1]
item1.name = "switch"
item1.value = cmd.value ? "on" : "off"
item1.handlerName = item1.value
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
item1.canBeCurrentState = true
item1.isStateChange = isStateChange(device, item1.name, item1.value)
item1.displayed = item1.isStateChange
if (cmd.value >= 5) {
def item2 = new LinkedHashMap(item1)
item2.name = "level"
item2.value = cmd.value as String
item2.unit = "%"
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
item2.canBeCurrentState = true
item2.isStateChange = isStateChange(device, item2.name, item2.value)
item2.displayed = false
result << item2
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
log.debug "ConfigurationReport $cmd"
def value = "when off"
if (cmd.configurationValue[0] == 1) {value = "when on"}
if (cmd.configurationValue[0] == 2) {value = "never"}
createEvent([name: "indicatorStatus", value: value])
[name: "indicatorStatus", value: value, display: false]
}
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
[:]
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
// Handles any Z-Wave commands we aren't interested in
log.debug "UNHANDLED COMMAND $cmd"
}
def on() {
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
log.info "on"
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
}
def off() {
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
}
def setLevel(value) {
log.debug "setLevel >> value: $value"
def valueaux = value as Integer
def level = Math.max(Math.min(valueaux, 99), 0)
if (level > 0) {
sendEvent(name: "switch", value: "on")
} else {
sendEvent(name: "switch", value: "off")
}
sendEvent(name: "level", value: level, unit: "%")
def level = Math.min(valueaux, 99)
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
}
def setLevel(value, duration) {
log.debug "setLevel >> value: $value, duration: $duration"
def valueaux = value as Integer
def level = Math.max(Math.min(valueaux, 99), 0)
def level = Math.min(valueaux, 99)
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000
delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay)
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
}
def poll() {
@@ -192,27 +197,21 @@ def poll() {
}
def refresh() {
log.debug "refresh() is called"
def commands = []
commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
if (getDataValue("MSR") == null) {
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
delayBetween(commands,100)
zwave.switchMultilevelV1.switchMultilevelGet().format()
}
def indicatorWhenOn() {
sendEvent(name: "indicatorStatus", value: "when on")
sendEvent(name: "indicatorStatus", value: "when on", display: false)
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
}
def indicatorWhenOff() {
sendEvent(name: "indicatorStatus", value: "when off")
sendEvent(name: "indicatorStatus", value: "when off", display: false)
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
}
def indicatorNever() {
sendEvent(name: "indicatorStatus", value: "never")
sendEvent(name: "indicatorStatus", value: "never", display: false)
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
}
@@ -223,4 +222,4 @@ def invertSwitch(invert=true) {
else {
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
}
}
}

View File

@@ -1,455 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Battery" //attributes: battery
capability "Configuration" //commands: configure()
capability "Sensor"
capability "Smoke Detector" //attributes: smoke ("detected","clear","tested")
capability "Temperature Measurement" //attributes: temperature
attribute "tamper", "enum", ["detected", "clear"]
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
}
simulator {
//battery
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
).incomingMessage()
}
status "battery 100%": "command: 8003, payload: 64"
status "battery 5%": "command: 8003, payload: 05"
//smoke
status "smoke detected": "command: 7105, payload: 01 01"
status "smoke clear": "command: 7105, payload: 01 00"
status "smoke tested": "command: 7105, payload: 01 03"
//temperature
for (int i = 0; i <= 100; i += 20) {
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
).incomingMessage()
}
}
preferences {
input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration",
title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN",
title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input description: "Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings",
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true
input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true
input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status",
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true
input "soundNotificationStatus", "enum", title: "Sound Notifications Status",
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${soundNotificationStatus}", displayDuringSetup: true
input "temperatureReportInterval", "enum", title: "Temperature Report Interval",
options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true
input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true
input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true
input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval",
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true
input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval",
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true
}
tiles (scale: 2){
multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){
tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") {
attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff")
attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13")
attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13")
attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66")
attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff")
}
tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
attributeState "battery", label:'Battery: ${currentValue}%', unit:"%"
}
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:"%"
}
valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "temperature", label:'${currentValue}°', unit:"C"
}
valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff"
state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff"
state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff"
state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff"
}
valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "clear", label:'NO TAMPER', backgroundColor:"#ffffff"
state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff"
}
main "smoke"
details(["smoke","temperature"])
}
}
def updated() {
log.debug "Updated with settings: ${settings}"
setConfigured("false") //wait until the next time device wakeup to send configure command
}
def parse(String description) {
log.debug "parse() >> description: $description"
def result = null
if (description.startsWith("Err 106")) {
log.debug "parse() >> Err 106"
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
descriptionText: "This sensor failed to complete the network security key exchange. " +
"If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
} else if (description != "updated") {
log.debug "parse() >> zwave.parse(description)"
def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd"
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
updateDataValue("fw", fw)
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
createEvent(descriptionText: text, isStateChange: false)
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "${device.displayName} battery is low"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
setConfigured("true") //when battery is reported back meaning configuration is done
//Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
state.lastbatt = now()
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
def msg = cmd.status == 0 ? "try again later" :
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
cmd.status == 2 ? "request queued" : "sorry"
createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg")
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request")
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
setSecured()
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1])
if (encapsulatedCommand) {
log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand"
zwaveEvent(encapsulatedCommand)
} else {
log.warn "Unable to extract encapsulated cmd from $cmd"
createEvent(descriptionText: cmd.toString())
}
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
setSecured()
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
if (getDataValue("MSR")?.startsWith("010F-0C02")){
response(configure()) //configure device using SmartThings default settings
}
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true)
//after device securely joined the network, call configure() to config device
setSecured()
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
if (getDataValue("MSR")?.startsWith("010F-0C02")){
response(configure()) //configure device using SmartThings default settings
}
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd"
def result = []
if (cmd.notificationType == 7) {
switch (cmd.event) {
case 0:
result << createEvent(name: "tamper", value: "clear", displayed: false)
break
case 3:
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened")
break
}
} else if (cmd.notificationType == 1) { //Smoke Alarm (V2)
log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)"
result << smokeAlarmEvent(cmd.event)
} else if (cmd.notificationType == 4) { // Heat Alarm (V2)
log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)"
result << heatAlarmEvent(cmd.event)
} else {
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
result
}
def smokeAlarmEvent(value) {
log.debug "smokeAlarmEvent(value): $value"
def map = [name: "smoke"]
if (value == 1 || value == 2) {
map.value = "detected"
map.descriptionText = "$device.displayName detected smoke"
} else if (value == 0) {
map.value = "clear"
map.descriptionText = "$device.displayName is clear (no smoke)"
} else if (value == 3) {
map.value = "tested"
map.descriptionText = "$device.displayName smoke alarm test"
} else if (value == 4) {
map.value = "replacement required"
map.descriptionText = "$device.displayName replacement required"
} else {
map.value = "unknown"
map.descriptionText = "$device.displayName unknown event"
}
createEvent(map)
}
def heatAlarmEvent(value) {
log.debug "heatAlarmEvent(value): $value"
def map = [name: "heatAlarm"]
if (value == 1 || value == 2) {
map.value = "overheat detected"
map.descriptionText = "$device.displayName overheat detected"
} else if (value == 0) {
map.value = "clear"
map.descriptionText = "$device.displayName heat alarm cleared (no overheat)"
} else if (value == 3 || value == 4) {
map.value = "rapid temperature rise"
map.descriptionText = "$device.displayName rapid temperature rise"
} else if (value == 5 || value == 6) {
map.value = "underheat detected"
map.descriptionText = "$device.displayName underheat detected"
} else {
map.value = "unknown"
map.descriptionText = "$device.displayName unknown event"
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd"
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
def cmds = []
/* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */
if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) {
result << response(configure()) // configure a newly joined device or joined device with preference update
} else {
//Only ask for battery if we haven't had a BatteryReport in a while
if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
log.debug("Device has been configured sending >> batteryGet()")
cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()
cmds << "delay 1200"
}
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
result << response(cmds) //tell device back to sleep
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd"
def map = [:]
switch (cmd.sensorType) {
case 1:
map.name = "temperature"
def cmdScale = cmd.scale == 1 ? "F" : "C"
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
map.unit = getTemperatureScale()
break
default:
map.descriptionText = cmd.toString()
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd"
createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true)
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def result = []
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
log.debug "After device is securely joined, send commands to update tiles"
result << zwave.batteryV1.batteryGet()
result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
result << zwave.wakeUpV1.wakeUpNoMoreInformation()
[[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))]
}
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
def result = []
if (cmd.nodeId.any { it == zwaveHubNodeId }) {
result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}")
} else if (cmd.groupingIdentifier == 1) {
result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}")
result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId))
}
result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "General zwaveEvent cmd: ${cmd}"
createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
def configure() {
// This sensor joins as a secure device if you tripple-click the button to include it
log.debug "configure() >> isSecured() : ${isSecured()}"
if (!isSecured()) {
log.debug "Fibaro smoke sensor not sending configure until secure"
return []
} else {
log.info "${device.displayName} is configuring its settings"
def request = []
//1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr)
request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId)
//2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW
if (smokeSensorSensitivity && smokeSensorSensitivity != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1,
scaledConfigurationValue:
smokeSensorSensitivity == "High" ? 1 :
smokeSensorSensitivity == "Medium" ? 2 :
smokeSensorSensitivity == "Low" ? 3 : 2)
}
//3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable
if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){
request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0)
}
//4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0)
}
//5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
if (soundNotificationStatus && soundNotificationStatus != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0)
}
//6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
if (temperatureReportInterval && temperatureReportInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180)
} else { //send SmartThings default configuration
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180)
}
//7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C)
if (temperatureReportHysteresis && temperatureReportHysteresis != null) {
request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis)
}
//8. Temperature threshold: 1-100 (C), default 55 (C)
if (temperatureThreshold && temperatureThreshold != null) {
request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold)
}
//9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180)
} else { //send SmartThings default configuration
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180)
}
//10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours)
if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160)
} else {
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160)
}
//11. get battery level when device is paired
request += zwave.batteryV1.batteryGet()
//12. get temperature reading from device
request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
}
}
private def getTimeOptionValueMap() { [
"5 minutes" : 30,
"15 minutes" : 90,
"30 minutes" : 180,
"1 hour" : 360,
"6 hours" : 2160,
"12 hours" : 4320,
"18 hours" : 6480,
"24 hours" : 8640,
"reports inactive" : 0,
]}
private def getNotificationOptionValueMap() { [
"disabled" : 0,
"casing opened" : 1,
"exceeding temperature threshold" : 2,
"lack of Z-Wave range" : 4,
"all notifications" : 7,
]}
private command(physicalgraph.zwave.Command cmd) {
if (isSecured()) {
log.info "Sending secured command: ${cmd}"
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
} else {
log.info "Sending unsecured command: ${cmd}"
cmd.format()
}
}
private commands(commands, delay=200) {
log.info "inside commands: ${commands}"
delayBetween(commands.collect{ command(it) }, delay)
}
private setConfigured(configure) {
updateDataValue("configured", configure)
}
private isConfigured() {
getDataValue("configured") == "true"
}
private setSecured() {
updateDataValue("secured", "true")
}
private isSecured() {
getDataValue("secured") == "true"
}

View File

@@ -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()
}

View File

@@ -19,6 +19,7 @@ metadata {
capability "Sensor"
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
fingerprint deviceId: "0x1006", inClusters: "0x25"
}
// simulator metadata

View File

@@ -317,7 +317,7 @@ def setLevel(value) {
state.trigger = "setLevel"
state.lvl = "${level}"
if (dimRate && (state?.rate != null)) {
if (dimRate) {
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
}
else {

View File

@@ -15,27 +15,19 @@ metadata {
// TODO: define status and reply messages here
}
tiles(scale: 2) {
multiAttributeTile(name:"rich-control"){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
}
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
attributeState "default", label:'SN: ${currentValue}'
}
}
tiles {
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
}
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
state "default", label:'SN: ${currentValue}'
}
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
}
main (["icon"])
details(["rich-control", "networkAddress"])
details(["networkAddress","serialNumber"])
}
}
@@ -44,6 +36,7 @@ def parse(description) {
log.debug "Parsing '${description}'"
def results = []
def result = parent.parse(this, description)
if (result instanceof physicalgraph.device.HubAction){
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
results << result
@@ -51,30 +44,32 @@ def parse(description) {
//do nothing
log.trace "HUE BRIDGE was updated"
} else {
log.trace "HUE BRIDGE, OTHER"
def map = description
if (description instanceof String) {
map = stringToMap(description)
}
if (map?.name && map?.value) {
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
results << createEvent(name: "${map.name}", value: "${map.value}")
} else {
log.trace "Parsing description"
results << createEvent(name: "${map?.name}", value: "${map?.value}")
}
else {
log.trace "HUE BRIDGE, OTHER"
def msg = parseLanMessage(description)
if (msg.body) {
def contentType = msg.headers["Content-Type"]
if (contentType?.contains("json")) {
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
if (bulbs.state) {
log.info "Bridge response: $msg.body"
} else {
// Sending Bulbs List to parent"
if (parent.state.inBulbDiscovery)
log.info parent.bulbListHandler(device.hub.id, msg.body)
log.warn "NOT PROCESSED: $msg.body"
}
else {
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
}
}
else if (contentType?.contains("xml")) {
log.debug "HUE BRIDGE ALREADY PRESENT"
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
}
}
}

View File

@@ -1,4 +1,3 @@
/**
* Hue Bulb
*
@@ -16,8 +15,8 @@ metadata {
capability "Sensor"
command "setAdjustedColor"
command "reset"
command "refresh"
command "reset"
command "refresh"
}
simulator {
@@ -50,6 +49,7 @@ metadata {
main(["switch"])
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
}
// parse events into attributes
@@ -68,13 +68,13 @@ def parse(description) {
}
// handle commands
def on() {
log.trace parent.on(this)
def on(transition = "4") {
log.trace parent.on(this,transition)
sendEvent(name: "switch", value: "on")
}
def off() {
log.trace parent.off(this)
def off(transition = "4") {
log.trace parent.off(this,transition)
sendEvent(name: "switch", value: "off")
}
@@ -107,9 +107,9 @@ def setHue(percent) {
sendEvent(name: "hue", value: percent)
}
def setColor(value) {
def setColor(value,alert = "none",transition = 4) {
log.debug "setColor: ${value}, $this"
parent.setColor(this, value)
parent.setColor(this, value, alert, transition)
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
if (value.hex) { sendEvent(name: "color", value: value.hex)}

View File

@@ -19,41 +19,24 @@ metadata {
simulator {
// TODO: define status and reply messages here
}
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
}
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
attributeState "level", label: 'Level ${currentValue}%'
}
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["rich-control", "refresh"])
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
main(["switch"])
details(["switch", "levelSliderControl", "refresh"])
}
// parse events into attributes

View File

@@ -25,7 +25,7 @@ metadata {
tiles {
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
}
main "presence"
details "presence"

View File

@@ -24,8 +24,8 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
}
tiles {

View File

@@ -24,10 +24,10 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
}
simulator {

View File

@@ -22,8 +22,10 @@ metadata {
command "setAdjustedColor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
}
// simulator metadata

View File

@@ -48,8 +48,8 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
])
}
}

View File

@@ -280,14 +280,16 @@ def configure() {
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -297,7 +299,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -39,9 +39,9 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
])
}
section {
@@ -301,18 +301,20 @@ def configure() {
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
]
return configCmds + refresh() // send refresh cmds as part of config
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
@@ -320,12 +322,12 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
]
}
private getEndpointId() {

View File

@@ -292,16 +292,18 @@ def configure() {
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -311,7 +313,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -54,31 +54,33 @@
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
])
}
section {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
/*
section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
}
*/
}
tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
}
}
standardTile("contact", "device.contact", width: 2, height: 2) {
standardTile("status", "device.contact", width: 2, height: 2) {
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
}
@@ -110,8 +112,8 @@
}
main(["status", "acceleration", "temperature"])
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
main(["contact", "acceleration", "temperature"])
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
}
}
@@ -346,8 +348,8 @@ def getTemperature(value) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
//def linkText = getLinkText(device)
def descriptionText = "was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
@@ -395,34 +397,35 @@ def getTemperature(value) {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
@@ -439,7 +442,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -297,27 +297,29 @@ def getTemperature(value) {
return refreshCmds + enrollResponse()
}
def configure() {
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
@@ -325,7 +327,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -275,16 +275,22 @@ def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
//"raw 0x500 {01 23 00 00 00}", "delay 200",
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -294,7 +300,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -253,19 +253,22 @@ def configure() {
log.debug "Configuring Reporting and Bindings."
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
]
return configCmds + refresh() // send refresh cmds as part of config
return configCmds + refresh() // send refresh cmds as part of config
}
private hex(value) {

View File

@@ -1,98 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Power Meter"
capability "Sensor"
capability "Switch"
capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("power", key: "SECONDARY_CONTROL") {
attributeState "power", label:'${currentValue} W'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
}

View File

@@ -11,77 +11,133 @@
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Switch Level"
capability "Actuator"
capability "Switch"
capability "Configuration"
capability "Sensor"
capability "Refresh"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019"
}
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch", "refresh"])
}
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
}
main "switch"
details(["switch", "refresh", "level", "levelSliderControl"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
log.info description
if (description?.startsWith("catchall:")) {
def msg = zigbee.parse(description)
log.trace msg
log.trace "data: $msg.data"
}
else {
def name = description?.startsWith("on/off: ") ? "switch" : null
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
// Commands to device
def on() {
log.debug "on()"
sendEvent(name: "switch", value: "on")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
}
def off() {
zigbee.off()
log.debug "off()"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
log.trace "setLevel($value)"
def cmds = []
if (value == 0) {
sendEvent(name: "switch", value: "off")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
else if (device.latestValue("switch") == "off") {
sendEvent(name: "switch", value: "on")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
}
sendEvent(name: "level", value: value)
def level = hexString(Math.round(value * 255/100))
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
//log.debug cmds
cmds
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
[
"st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200",
"st wattr 0x${device.deviceNetworkId} 1 8 0"
]
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
/*log.debug "binding to switch and level control cluster"
[
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}"
]
*/
//set transition time to 2 seconds. Not currently working.
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}"
}
private hex(value, width=2) {
def s = new BigInteger(Math.round(value).toString()).toString(16)
while (s.size() < width) {
s = "0" + s
}
s
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}

View File

@@ -1,161 +0,0 @@
/**
* ZigBee Lock
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings")
{
capability "Actuator"
capability "Lock"
capability "Refresh"
capability "Sensor"
capability "Battery"
capability "Configuration"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale YRL220 Lock"
}
tiles(scale: 2) {
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
tileAttribute ("device.lock", key:"PRIMARY_CONTROL") {
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
}
}
standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
}
standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
}
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "battery", label:'${currentValue}% battery', unit:""
}
standardTile("refresh", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "toggle"
details(["toggle", "lock", "unlock", "battery", "refresh"])
}
}
// Globals
private getCLUSTER_POWER() { 0x0001 }
private getCLUSTER_DOORLOCK() { 0x0101 }
private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 }
private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 }
private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 }
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
private getTYPE_U8() { 0x20 }
private getTYPE_ENUM8() { 0x30 }
// Public methods
def installed() {
log.trace "installed()"
}
def uninstalled() {
log.trace "uninstalled()"
}
def configure() {
def cmds =
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
"${TYPE_ENUM8}", 0, 3600, "{01}") +
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
"${TYPE_U8}", 3600, 3600, "{01}")
log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config
}
def refresh() {
def cmds =
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
log.info "refresh() --- cmds: $cmds"
return cmds
}
def parse(String description) {
log.trace "parse() --- description: $description"
Map map = [:]
if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
log.debug "parse() --- Parse returned $map"
def result = map ? createEvent(map) : null
return result
}
// Lock capability commands
def lock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
log.info "lock() -- cmds: $cmds"
return cmds
}
def unlock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
log.info "unlock() -- cmds: $cmds"
return cmds
}
// Private methods
private Map parseReportAttributeMessage(String description) {
log.trace "parseReportAttributeMessage() --- description: $description"
Map descMap = zigbee.parseDescriptionAsMap(description)
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery"
// BatteryPercentageRemaining is specified in .5% increments
resultMap.value = Integer.parseInt(descMap.value, 16) / 2
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
}
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
def value = Integer.parseInt(descMap.value, 16)
resultMap.name = "lock"
resultMap.putAll([0:["value":"unknown",
"descriptionText":"Not fully locked"],
1:["value":"locked"],
2:["value":"unlocked"]].get(value,
["value":"unknown",
"descriptionText":"Unknown lock state"]))
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
}
else {
log.debug "parseReportAttributeMessage() --- ignoring attribute"
}
return resultMap
}

View File

@@ -1,90 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Power Meter"
capability "Sensor"
capability "Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("power", key: "SECONDARY_CONTROL") {
attributeState "power", label:'${currentValue} W'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def refresh() {
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
}

View File

@@ -1,89 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
}
// 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(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def refresh() {
zigbee.onOffRefresh() + zigbee.onOffConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.onOffRefresh()
}

View File

@@ -1,130 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* ZigBee White Color Temperature Bulb
*
* Author: SmartThings
* Date: 2015-09-22
*/
metadata {
definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Color Temperature"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
attribute "colorName", "string"
command "setGenericName"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
}
// UI tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
attributeState "colorName", label:'${currentValue}'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getKnownDescription(description)
if (finalResult) {
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
}
def setColorTemperature(value) {
setGenericName(value)
zigbee.setColorTemperature(value)
}
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
def setGenericName(value){
if (value != null) {
def genericName = "White"
if (value < 3300) {
genericName = "Soft White"
} else if (value < 4150) {
genericName = "Moonlight"
} else if (value <= 5000) {
genericName = "Cool White"
} else if (value >= 5000) {
genericName = "Daylight"
}
sendEvent(name: "colorName", value: genericName)
}
}

View File

@@ -1,124 +0,0 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Valve"
capability "Polling"
capability "Refresh"
capability "Sensor"
fingerprint deviceId: "0x1006", inClusters: "0x25"
}
// simulator metadata
simulator {
status "open": "command: 2503, payload: FF"
status "close": "command: 2503, payload: 00"
// reply messages
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
reply "200100,delay 100,2502": "command: 2503, payload: 00"
}
// tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
}
}
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "valve"
details(["valve","refresh"])
}
}
def updated() {
response(refresh())
}
def parse(String description) {
log.trace "parse description : $description"
def result = null
def cmd = zwave.parse(description, [0x20: 1])
if (cmd) {
result = createEvent(zwaveEvent(cmd))
}
log.debug "Parse returned ${result?.descriptionText}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
}
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
[:] // Handles all Z-Wave commands we aren't interested in
}
def open() {
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
],10000) //wait for a water valve to be completely opened
}
def close() {
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
],10000) //wait for a water valve to be completely closed
}
def poll() {
zwave.switchBinaryV1.switchBinaryGet().format()
}
def refresh() {
log.debug "refresh() is called"
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
if (getDataValue("MSR") == null) {
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
delayBetween(commands,100)
}

View File

@@ -0,0 +1,48 @@
/**
* Switch Too
*
* Copyright 2015 Bob Florian
*
* 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: "Switch Too", author: "Bob Florian") {
capability "Switch"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
// TODO: define your main and details tiles here
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle 'switch' attribute
}
// handle commands
def on() {
log.debug "Executing 'on'"
// TODO: handle 'on' command
}
def off() {
log.debug "Executing 'off'"
// TODO: handle 'off' command
}

View File

@@ -8,7 +8,6 @@ metadata {
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
capability "Actuator"
capability "Thermostat"
capability "Temperature Measurement"
capability "Configuration"
capability "Refresh"
capability "Sensor"

View File

@@ -11,13 +11,8 @@
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
* devices for which the user has authorized. Likewise, Initial State's services call this
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
*
*/
definition(
name: "Initial State Event Streamer",
namespace: "initialstate.events",
@@ -33,31 +28,32 @@ import groovy.json.JsonSlurper
preferences {
section("Choose which devices to monitor...") {
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
}
}
@@ -78,71 +74,77 @@ mappings {
}
def subscribeToEvents() {
if (accelerometers != null) {
/*if (accelerometers != null) {
subscribe(accelerometers, "acceleration", genericHandler)
}
}*/
if (alarms != null) {
subscribe(alarms, "alarm", genericHandler)
}
if (batteries != null) {
/*if (batteries != null) {
subscribe(batteries, "battery", genericHandler)
}
if (beacons != null) {
}*/
/*if (beacons != null) {
subscribe(beacons, "presence", genericHandler)
}
if (cos != null) {
}*/
/*
if (buttons != null) {
subscribe(buttons, "button", genericHandler)
}*/
/*if (cos != null) {
subscribe(cos, "carbonMonoxide", genericHandler)
}
if (colors != null) {
}*/
/*if (colors != null) {
subscribe(colors, "hue", genericHandler)
subscribe(colors, "saturation", genericHandler)
subscribe(colors, "color", genericHandler)
}
}*/
if (contacts != null) {
subscribe(contacts, "contact", genericHandler)
}
if (energyMeters != null) {
/*if (doorsControllers != null) {
subscribe(doorsControllers, "door", genericHandler)
}*/
/*if (energyMeters != null) {
subscribe(energyMeters, "energy", genericHandler)
}
if (illuminances != null) {
}*/
/*if (illuminances != null) {
subscribe(illuminances, "illuminance", genericHandler)
}
}*/
if (locks != null) {
subscribe(locks, "lock", genericHandler)
}
if (motions != null) {
subscribe(motions, "motion", genericHandler)
}
if (musicPlayers != null) {
/*if (musicPlayers != null) {
subscribe(musicPlayers, "status", genericHandler)
subscribe(musicPlayers, "level", genericHandler)
subscribe(musicPlayers, "trackDescription", genericHandler)
subscribe(musicPlayers, "trackData", genericHandler)
subscribe(musicPlayers, "mute", genericHandler)
}
if (powerMeters != null) {
}*/
/*if (powerMeters != null) {
subscribe(powerMeters, "power", genericHandler)
}
}*/
if (presences != null) {
subscribe(presences, "presence", genericHandler)
}
if (humidities != null) {
subscribe(humidities, "humidity", genericHandler)
}
if (relaySwitches != null) {
/*if (relaySwitches != null) {
subscribe(relaySwitches, "switch", genericHandler)
}
if (sleepSensors != null) {
}*/
/*if (sleepSensors != null) {
subscribe(sleepSensors, "sleeping", genericHandler)
}
if (smokeDetectors != null) {
}*/
/*if (smokeDetectors != null) {
subscribe(smokeDetectors, "smoke", genericHandler)
}
if (peds != null) {
}*/
/*if (peds != null) {
subscribe(peds, "steps", genericHandler)
subscribe(peds, "goal", genericHandler)
}
}*/
if (switches != null) {
subscribe(switches, "switch", genericHandler)
}
@@ -161,9 +163,9 @@ def subscribeToEvents() {
subscribe(thermostats, "thermostatFanMode", genericHandler)
subscribe(thermostats, "thermostatOperatingState", genericHandler)
}
if (valves != null) {
/*if (valves != null) {
subscribe(valves, "contact", genericHandler)
}
}*/
if (waterSensors != null) {
subscribe(waterSensors, "water", genericHandler)
}
@@ -171,23 +173,23 @@ def subscribeToEvents() {
def getAccessKey() {
log.trace "get access key"
if (atomicState.accessKey == null) {
if (state.accessKey == null) {
httpError(404, "Access Key Not Found")
} else {
[
accessKey: atomicState.accessKey
accessKey: state.accessKey
]
}
}
def getBucketKey() {
log.trace "get bucket key"
if (atomicState.bucketKey == null) {
if (state.bucketKey == null) {
httpError(404, "Bucket key Not Found")
} else {
[
bucketKey: atomicState.bucketKey,
bucketName: atomicState.bucketName
bucketKey: state.bucketKey,
bucketName: state.bucketName
]
}
}
@@ -200,94 +202,53 @@ def setBucketKey() {
log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
atomicState.bucketKey = "$newBucketKey"
atomicState.bucketName = "$newBucketName"
atomicState.isBucketCreated = false
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
state.bucketKey = "$newBucketKey"
state.bucketName = "$newBucketName"
state.isBucketCreated = false
}
tryCreateBucket()
}
def setAccessKey() {
log.trace "set access key"
def newAccessKey = request.JSON?.accessKey
def newGrokerSubdomain = request.JSON?.grokerSubdomain
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
atomicState.grokerSubdomain = "$newGrokerSubdomain"
atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
if (newAccessKey && newAccessKey != state.accessKey) {
state.accessKey = "$newAccessKey"
state.isBucketCreated = false
}
}
def installed() {
atomicState.version = "1.0.18"
subscribeToEvents()
atomicState.isBucketCreated = false
atomicState.grokerSubdomain = "groker"
atomicState.eventBuffer = []
runEvery15Minutes(flushBuffer)
log.debug "installed (version $atomicState.version)"
state.isBucketCreated = false
}
def updated() {
atomicState.version = "1.0.18"
unsubscribe()
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
atomicState.isBucketCreated = false
if (state.bucketKey != null && state.accessKey != null) {
state.isBucketCreated = false
}
if (atomicState.eventBuffer == null) {
atomicState.eventBuffer = []
}
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
atomicState.grokerSubdomain = "groker"
}
subscribeToEvents()
log.debug "updated (version $atomicState.version)"
}
def uninstalled() {
log.debug "uninstalled (version $atomicState.version)"
}
def tryCreateBucket() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
subscribeToEvents()
}
// if the bucket has already been created, no need to continue
if (atomicState.isBucketCreated) {
return
}
def createBucket() {
if (!atomicState.bucketName) {
atomicState.bucketName = atomicState.bucketKey
if (!state.bucketName) {
state.bucketName = state.bucketKey
}
if (!atomicState.accessKey) {
return
}
def bucketName = "${atomicState.bucketName}"
def bucketKey = "${atomicState.bucketKey}"
def accessKey = "${atomicState.accessKey}"
def bucketName = "${state.bucketName}"
def bucketKey = "${state.bucketKey}"
def accessKey = "${state.accessKey}"
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
def bucketCreatePost = [
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
uri: 'https://groker.initialstate.com/api/buckets',
headers: [
"Content-Type": "application/json",
"X-IS-AccessKey": accessKey
@@ -297,20 +258,10 @@ def tryCreateBucket() {
log.debug bucketCreatePost
try {
// Create a bucket on Initial State so the data has a logical grouping
httpPostJson(bucketCreatePost) { resp ->
log.debug "bucket posted"
if (resp.status >= 400) {
log.error "bucket not created successfully"
} else {
atomicState.isBucketCreated = true
}
}
} catch (e) {
log.error "bucket creation error: $e"
httpPostJson(bucketCreatePost) {
log.debug "bucket posted"
state.isBucketCreated = true
}
}
def genericHandler(evt) {
@@ -322,80 +273,33 @@ def genericHandler(evt) {
}
def value = "$evt.value"
tryCreateBucket()
eventHandler(key, value)
}
// This is a handler function for flushing the event buffer
// after a specified amount of time to reduce the load on ST servers
def flushBuffer() {
log.trace "About to flush the buffer on schedule"
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
tryShipEvents()
}
}
def eventHandler(name, value) {
log.debug atomicState.eventBuffer
def eventBuffer = atomicState.eventBuffer
def epoch = now() / 1000
// if for some reason this code block is being run
// but the SmartApp wasn't propery setup during install
// we need to set initialize the eventBuffer.
if (!atomicState.eventBuffer) {
atomicState.eventBuffer = []
}
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
log.debug eventBuffer
atomicState.eventBuffer = eventBuffer
if (eventBuffer.size() >= 10) {
tryShipEvents()
}
}
// a helper function for shipping the atomicState.eventBuffer to Initial State
def tryShipEvents() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
// can't ship if access key and bucket key are null, so finish trying
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
if (state.accessKey == null || state.bucketKey == null) {
return
}
if (!state.isBucketCreated) {
createBucket()
}
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
def eventPost = [
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
uri: 'https://groker.initialstate.com/api/events',
headers: [
"Content-Type": "application/json",
"X-IS-BucketKey": "${atomicState.bucketKey}",
"X-IS-AccessKey": "${atomicState.accessKey}",
"Accept-Version": "0.0.2"
"X-IS-BucketKey": "${state.bucketKey}",
"X-IS-AccessKey": "${state.accessKey}"
],
body: atomicState.eventBuffer
body: eventBody
]
try {
// post the events to initial state
httpPostJson(eventPost) { resp ->
log.debug "shipped events and got ${resp.status}"
if (resp.status >= 400) {
log.error "shipping failed... ${resp.data}"
} else {
// clear the buffer
atomicState.eventBuffer = []
}
}
} catch (e) {
log.error "shipping events failed: $e"
log.debug eventPost
httpPostJson(eventPost) {
log.debug "event data posted"
}
}

View File

@@ -15,7 +15,7 @@
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "Hue (Connect)",
namespace: "smartthings",
@@ -64,14 +64,10 @@ def bridgeDiscovery(params=[:])
def options = bridges ?: []
def numFound = options.size() ?: 0
if (numFound == 0 && state.bridgeRefreshCount > 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
state.bridgeRefreshCount = 0
app.updateSetting("selectedHue", "")
}
subscribe(location, null, locationHandler, [filterEvents:false])
if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
//bridge discovery request every 15 //25 seconds
if((bridgeRefreshCount % 5) == 0) {
@@ -98,20 +94,11 @@ def bridgeLinking()
def nextPage = ""
def title = "Linking with your Hue"
def paragraphText
def hueimage = null
if (selectedHue) {
paragraphText = "Press the button on your Hue Bridge to setup a link. "
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
} else {
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
hueimage = null
}
def paragraphText = "Press the button on your Hue Bridge to setup a link."
if (state.username) { //if discovery worked
nextPage = "bulbDiscovery"
title = "Success!"
title = "Success! - click 'Next'"
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
hueimage = null
}
if((linkRefreshcount % 2) == 0 && !state.username) {
@@ -119,39 +106,30 @@ def bridgeLinking()
}
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
section("") {
section("Button Press") {
paragraph """${paragraphText}"""
if (hueimage != null)
image "${hueimage}"
}
}
}
def bulbDiscovery() {
def bulbDiscovery()
{
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
state.bulbRefreshCount = bulbRefreshCount + 1
def refreshInterval = 3
state.inBulbDiscovery = true
def bridge = null
if (selectedHue) {
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListData)
}
state.bridgeRefreshCount = 0
def bulboptions = bulbsDiscovered() ?: [:]
def numFound = bulboptions.size() ?: 0
if (numFound == 0)
app.updateSetting("selectedBulbs", "")
def options = bulbsDiscovered() ?: []
def numFound = options.size() ?: 0
if((bulbRefreshCount % 3) == 0) {
discoverHueBulbs()
}
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
}
section {
section {
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
@@ -216,29 +194,24 @@ Map bridgesDiscovered() {
Map bulbsDiscovered() {
def bulbs = getHueBulbs()
def bulbmap = [:]
def map = [:]
if (bulbs instanceof java.util.Map) {
bulbs.each {
def value = "${it.value.name}"
def key = app.id +"/"+ it.value.id
bulbmap["${key}"] = value
def value = "${it?.value?.name}"
def key = app.id +"/"+ it?.value?.id
map["${key}"] = value
}
} else { //backwards compatable
bulbs.each {
def value = "${it.name}"
def key = app.id +"/"+ it.id
logg += "$value - $key, "
bulbmap["${key}"] = value
def value = "${it?.name}"
def key = app.id +"/"+ it?.id
map["${key}"] = value
}
}
return bulbmap
map
}
def bulbListData(evt) {
state.bulbs = evt.jsonData
}
Map getHueBulbs() {
def getHueBulbs() {
state.bulbs = state.bulbs ?: [:]
}
@@ -258,19 +231,24 @@ def installed() {
def updated() {
log.trace "Updated with settings: ${settings}"
unschedule()
unsubscribe()
unsubscribe()
initialize()
}
def initialize() {
log.debug "Initializing"
unsubscribe(bridge)
state.inBulbDiscovery = false
state.bridgeRefreshCount = 0
state.bulbRefreshCount = 0
log.debug "Initializing"
state.subscribe = false
state.bridgeSelectedOverride = false
def bridge = null
if (selectedHue) {
addBridge()
addBulbs()
addBridge()
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListHandler)
}
if (selectedBulbs) {
addBulbs()
doDeviceSync()
runEvery5Minutes("doDeviceSync")
}
@@ -285,34 +263,27 @@ def manualRefresh() {
def uninstalled(){
state.bridges = [:]
state.username = null
state.subscribe = false
}
// Handles events to add new bulbs
def bulbListHandler(hub, data = "") {
def msg = "Bulbs list not processed. Only while in settings menu."
def bulbs = [:]
if (state.inBulbDiscovery) {
def logg = ""
log.trace "Adding bulbs to state..."
state.bridgeProcessedLightList = true
def object = new groovy.json.JsonSlurper().parseText(data)
object.each { k,v ->
if (v instanceof Map)
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
}
}
def bridge = null
if (selectedHue)
bridge = getChildDevice(selectedHue)
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
msg = "${bulbs.size()} bulbs found. ${bulbs}"
return msg
def bulbListHandler(evt) {
def bulbs = [:]
log.trace "Adding bulbs to state..."
state.bridgeProcessedLightList = true
evt.jsonData.each { k,v ->
log.trace "$k: $v"
if (v instanceof Map) {
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
}
}
state.bulbs = bulbs
log.info "${bulbs.size()} bulbs found"
}
def addBulbs() {
def bulbs = getHueBulbs()
selectedBulbs?.each { dni ->
selectedBulbs.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newHueBulb
@@ -323,7 +294,7 @@ def addBulbs() {
} else {
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
}
} else {
} else {
//backwards compatable
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
@@ -351,7 +322,7 @@ def addBridge() {
def d = getChildDevice(selectedHue)
if(!d) {
// compatibility with old devices
def newbridge = true
def newbridge = true
childDevices.each {
if (it.getDeviceDataByName("mac")) {
def newDNI = "${it.getDeviceDataByName("mac")}"
@@ -361,27 +332,22 @@ def addBridge() {
it.setDeviceNetworkId("${newDNI}")
if (oldDNI == selectedHue)
app.updateSetting("selectedHue", newDNI)
newbridge = false
newbridge = false
}
}
}
}
}
if (newbridge) {
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
def childDevice = getChildDevice(d.deviceNetworkId)
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
if (vbridge.value.ip && vbridge.value.port) {
if (vbridge.value.ip.contains(".")) {
if (vbridge.value.ip.contains("."))
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
}
} else {
else
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
} else
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
}
}
} else {
log.debug "found ${d.displayName} with id $selectedHue already exists"
@@ -389,6 +355,7 @@ def addBridge() {
}
}
def locationHandler(evt) {
def description = evt.description
log.trace "Location: $description"
@@ -430,11 +397,8 @@ def locationHandler(evt) {
}
}
}
} else {
if (d.getDeviceDataByName("networkAddress"))
networkAddress = d.getDeviceDataByName("networkAddress")
else
networkAddress = d.latestState('networkAddress').stringValue
} else {
networkAddress = d.latestState('networkAddress').stringValue
log.trace "Host: $host - $networkAddress"
if(host != networkAddress) {
log.debug "Device's port or ip changed for device $d..."
@@ -442,8 +406,7 @@ def locationHandler(evt) {
dstate.port = port
dstate.name = "Philips hue ($ip)"
d.sendEvent(name:"networkAddress", value: host)
d.updateDataValue("networkAddress", host)
}
}
}
}
}
@@ -491,13 +454,17 @@ def locationHandler(evt) {
def doDeviceSync(){
log.trace "Doing Hue Device Sync!"
//shrink the large bulb lists
convertBulbListToMap()
poll()
try {
if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
} catch (all) {
log.trace "Subscription already exist"
}
state.subscribe = true
}
discoverBridges()
}
@@ -506,49 +473,44 @@ def doDeviceSync(){
/////////////////////////////////////
def parse(childDevice, description) {
def parsedEvent = parseLanMessage(description)
def parsedEvent = parseLanMessage(description)
if (parsedEvent.headers && parsedEvent.body) {
def headerString = parsedEvent.headers.toString()
def bodyString = parsedEvent.body.toString()
if (headerString?.contains("json")) {
def body
try {
body = new groovy.json.JsonSlurper().parseText(bodyString)
} catch (all) {
log.warn "Parsing Body failed - trying again..."
poll()
}
if (body instanceof java.util.HashMap) {
//poll response
if (headerString?.contains("json")) {
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
if (body instanceof java.util.HashMap)
{ //poll response
def bulbs = getChildDevices()
//for each bulb
for (bulb in body) {
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
if (d) {
if (bulb.value.state?.reachable) {
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
if (bulb.value.state.sat) {
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
def hex = colorUtil.hslToHex(hue, sat)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
} else {
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
if (bulb.value.state.sat) {
def hue = 23
def sat = 56
def hex = colorUtil.hslToHex(23, 56)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
}
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
if (bulb.value.state.sat) {
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
def hex = colorUtil.hslToHex(hue, sat)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
} else {
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
if (bulb.value.state.sat) {
def hue = 23
def sat = 56
def hex = colorUtil.hslToHex(23, 56)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
}
}
}
}
}
else
{ //put response
@@ -597,25 +559,25 @@ def parse(childDevice, description) {
}
}
}
}
} else {
log.debug "parse - got something other than headers,body..."
return []
}
}
def on(childDevice, transition_deprecated = 0) {
def on(childDevice, transition = 4) {
log.debug "Executing 'on'"
def percent = childDevice.device?.currentValue("level") as Integer
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
def level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
return "level: $percent"
}
def off(childDevice, transition_deprecated = 0) {
def off(childDevice, transition = 4) {
log.debug "Executing 'off'"
put("lights/${getId(childDevice)}/state", [on: false])
return "level: 0"
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
}
def setLevel(childDevice, percent) {
@@ -636,21 +598,19 @@ def setHue(childDevice, percent) {
put("lights/${getId(childDevice)}/state", [hue: level])
}
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
log.debug "Executing 'setColor($huesettings)'"
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
def alert = huesettings.alert ? huesettings.alert : "none"
def transition = huesettings.transition ? huesettings.transition : 4
def setColor(childDevice, color, alert = "none", transition = 4) {
log.debug "Executing 'setColor($color)'"
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
if (huesettings.level != null) {
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
if (color.level != null) {
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
value.on = value.bri > 0
}
if (huesettings.switch) {
value.on = huesettings.switch == "on"
if (color.switch) {
value.on = color.switch == "on"
}
log.debug "sending command $value"
@@ -680,19 +640,15 @@ private getId(childDevice) {
private poll() {
def host = getBridgeIP()
def uri = "/api/${state.username}/lights/"
try {
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
log.debug "GET: $host$uri"
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
HOST: ${host}
""", physicalgraph.device.Protocol.LAN, selectedHue))
} catch (all) {
log.warn "Parsing Body failed - trying again..."
doDeviceSync()
}
}
private put(path, body) {
def host = getBridgeIP()
def host = getBridgeIP()
def uri = "/api/${state.username}/$path"
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
def length = bodyJSON.getBytes().size().toString()
@@ -712,21 +668,12 @@ ${bodyJSON}
private getBridgeIP() {
def host = null
if (selectedHue) {
def d = getChildDevice(selectedHue)
if (d) {
if (d.getDeviceDataByName("networkAddress"))
host = d.getDeviceDataByName("networkAddress")
else
host = d.latestState('networkAddress').stringValue
}
def d = getChildDevice(dni)
if (d)
host = d.latestState('networkAddress').stringValue
if (host == null || host == "") {
def serialNumber = selectedHue
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
if (!bridge) {
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
//in this case we will look up the bridge by comparing the incorrect mac addresses
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
}
if (bridge?.ip && bridge?.port) {
if (bridge?.ip.contains("."))
host = "${bridge?.ip}:${bridge?.port}"
@@ -734,9 +681,9 @@ private getBridgeIP() {
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
} else if (bridge?.networkAddress && bridge?.deviceAddress)
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
}
}
log.trace "Bridge: $selectedHue - Host: $host"
}
}
return host
}

View File

@@ -62,7 +62,6 @@ def authPage() {
def options = locationOptions() ?: []
def count = options.size()
def refreshInterval = 3
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Select your location") {
@@ -373,9 +372,9 @@ def updateDevices() {
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
data["hue"] = device.color.hue / 3.6
data["saturation"] = device.color.saturation * 100
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
childDevice = addChildDevice("lifx", "LIFX Color Bulb", device.id, null, data)
} else {
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
childDevice = addChildDevice("lifx", "LIFX White Bulb", device.id, null, data)
}
}
}
@@ -391,4 +390,4 @@ def refreshDevices() {
getChildDevices().each { device ->
device.refresh()
}
}
}

View File

@@ -38,7 +38,7 @@
definition(
name: "Logitech Harmony (Connect)",
namespace: "smartthings",
author: "SmartThings",
author: "Juan Pablo Risso",
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
@@ -394,9 +394,7 @@ def discovery() {
} catch (java.net.SocketTimeoutException e) {
log.warn "Connection to the hub timed out. Please restart the hub and try again."
state.resethub = true
} catch (e) {
log.warn "Hostname in certificate didn't match. Please try again later."
}
}
return null
}
@@ -461,9 +459,7 @@ def activity(dni,mode) {
msg = ex
state.aux = 0
}
} catch(Exception ex) {
msg = ex
}
}
runIn(10, "poll", [overwrite: true])
return msg
}
@@ -521,9 +517,7 @@ def poll() {
state.remove("HarmonyAccessToken")
return "Harmony Access token has expired"
}
} catch(Exception e) {
log.trace e
}
}
}
}
@@ -556,9 +550,7 @@ def getActivityList() {
log.trace e
} catch (java.net.SocketTimeoutException e) {
log.trace e
} catch(Exception e) {
log.trace e
}
}
}
return activity
}
@@ -573,9 +565,9 @@ def getActivityName(activity,hubId) {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
actname = response.data.data.activities[activity].name
}
} catch(Exception e) {
} catch (groovyx.net.http.HttpResponseException e) {
log.trace e
}
}
}
return actname
}
@@ -593,9 +585,9 @@ def getActivityId(activity,hubId) {
actid = it.key
}
}
} catch(Exception e) {
} catch (groovyx.net.http.HttpResponseException e) {
log.trace e
}
}
}
return actid
}
@@ -610,9 +602,9 @@ def getHubName(hubId) {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
hubname = response.data.data.name
}
} catch(Exception e) {
} catch (groovyx.net.http.HttpResponseException e) {
log.trace e
}
}
}
return hubname
}

View File

@@ -79,9 +79,12 @@ def scenePage(params=[:]) {
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
}
section {
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
}
if (sceneId == currentSceneId) {
section {
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
}
}
}
}
@@ -222,7 +225,7 @@ private restoreStates(sceneId) {
if (type == "level") {
log.debug "${light.displayName} level is '$level'"
if (level != null) {
light.setLevel(level)
light.setLevel(value)
}
}
else if (type == "color") {

View File

@@ -346,20 +346,18 @@ private getSensorJSON(id, key) {
def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
httpGet(uri: sensorUrl) { response ->
httpGet(uri: sensorUrl) { response ->
def sensors = [:]
def json = new org.json.JSONObject(response.data)
response.data.each { sensorId, sensorName ->
sensors[sensorId] = sensorName
state.sensors = json
json.each { sensorId, sensorName ->
createChild(sensorId, sensorName)
}
state.sensors = sensors
}
return "success"
}
}
def createChild(sensorId, sensorName) {

View File

@@ -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>
"""
}

View File

@@ -1,4 +1,4 @@
/**
/**
* Magic Home
*
* Copyright 2014 Tim Slagle
@@ -198,7 +198,7 @@
//set home mode when house is occupied
def setHome() {
sendOutOfDateNotification()
log.info("Setting Home Mode!!")
if(anyoneIsHome()) {
if(state.sunMode == "sunset"){
@@ -319,14 +319,3 @@
private hideOptionsSection() {
(starting || ending || days || modes) ? false : true
}
def sendOutOfDateNotification(){
if(!state.lastTime){
state.lastTime = (new Date() + 31).getTime()
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
}
else if (((new Date()).getTime()) >= state.lastTime){
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
state.lastTime = (new Date() + 31).getTime()
}
}

View File

@@ -1,342 +0,0 @@
/**
* Rotuine Director
*
*
* Changelog
*
* 2015-09-01
* --Added Contact Book
* --Removed references to phrases and replaced with routines
* --Added bool logic to inputs instead of enum for "yes" "no" options
* --Fixed halting error with code installation
*
* Copyright 2015 Tim Slagle
*
* 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: "Routine Director",
namespace: "tslagle13",
author: "Tim Slagle",
description: "Monitor a set of presence sensors and activate routines based on whether your home is empty or occupied. Each presence status change will check against the current 'sun state' to run routines based on occupancy and whether the sun is up or down.",
category: "Convenience",
iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png",
iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png"
)
preferences {
page(name: "selectRoutines")
page(name: "Settings", title: "Settings", uninstall: true, install: true) {
section("False alarm threshold (defaults to 10 min)") {
input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
}
section("Zip code (for sunrise/sunset)") {
input "zip", "text", required: true
}
section("Notifications") {
input "sendPushMessage", "bool", title: "Send notifications when house is empty?"
input "sendPushMessageHome", "bool", title: "Send notifications when home is occupied?"
}
section("Send Notifications?") {
input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Send an SMS to this number?"
}
}
section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
label title: "Assign a name", required: false
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
}
}
def selectRoutines() {
def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight)
dynamicPage(name: "selectRoutines", title: "Configure", nextPage: "Settings", uninstall: true) {
section("Who?") {
input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange: true
}
def routines = location.helloHome?.getPhrases()*.label
if (routines) {
routines.sort()
section("Run This Routine When...") {
log.trace routines
input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: routines, submitOnChange: true
input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: routines, submitOnChange: true
input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: routines, submitOnChange: true
input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: routines, submitOnChange: true
}
/* section("Select modes used for each condition.") { This allows the director to know which rotuine has already been ran so it does not run again if someone else comes home.
input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true
input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true
}*/
}
}
}
def installed() {
log.debug "Updated with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(people, "presence", presence)
checkSun()
subscribe(location, "sunrise", setSunrise)
subscribe(location, "sunset", setSunset)
state.homestate = null
}
//check current sun state when installed.
def checkSun() {
def zip = settings.zip as String
def sunInfo = getSunriseAndSunset(zipCode: zip)
def current = now()
if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) {
state.sunMode = "sunrise"
runIn(60,"setSunrise")
}
else {
state.sunMode = "sunset"
runIn(60,"setSunset")
}
}
//change to sunrise mode on sunrise event
def setSunrise(evt) {
state.sunMode = "sunrise";
changeSunMode(newMode);
log.debug "Current sun mode is ${state.sunMode}"
}
//change to sunset mode on sunset event
def setSunset(evt) {
state.sunMode = "sunset";
changeSunMode(newMode)
log.debug "Current sun mode is ${state.sunMode}"
}
//change mode on sun event
def changeSunMode(newMode) {
if (allOk) {
if (everyoneIsAway()) /*&& (state.sunMode == "sunrise")*/ {
log.info("Home is Empty Setting New Away Mode")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
setAway()
}
/*
else if (everyoneIsAway() && (state.sunMode == "sunset")) {
log.info("Home is Empty Setting New Away Mode")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
setAway()
}*/
else if (anyoneIsHome()) {
log.info("Home is Occupied Setting New Home Mode")
setHome()
}
}
}
//presence change run logic based on presence state of home
def presence(evt) {
if (allOk) {
if (evt.value == "not present") {
log.debug("Checking if everyone is away")
if (everyoneIsAway()) {
log.info("Nobody is home, running away sequence")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
runIn(delay, "setAway")
}
}
else {
def lastTime = state[evt.deviceId]
if (lastTime == null || now() - lastTime >= 1 * 60000) {
log.info("Someone is home, running home sequence")
setHome()
}
state[evt.deviceId] = now()
}
}
}
//if empty set home to one of the away modes
def setAway() {
if (everyoneIsAway()) {
if (state.sunMode == "sunset") {
def message = "Performing \"${awayNight}\" for you as requested."
log.info(message)
sendAway(message)
location.helloHome.execute(settings.awayNight)
state.homestate = "away"
}
else if (state.sunMode == "sunrise") {
def message = "Performing \"${awayDay}\" for you as requested."
log.info(message)
sendAway(message)
location.helloHome.execute(settings.awayDay)
state.homestate = "away"
}
else {
log.debug("Mode is the same, not evaluating")
}
}
}
//set home mode when house is occupied
def setHome() {
log.info("Setting Home Mode!!")
if (anyoneIsHome()) {
if (state.sunMode == "sunset") {
if (state.homestate != "homeNight") {
def message = "Performing \"${homeNight}\" for you as requested."
log.info(message)
sendHome(message)
location.helloHome.execute(settings.homeNight)
state.homestate = "homeNight"
}
}
if (state.sunMode == "sunrise") {
if (state.homestate != "homeDay") {
def message = "Performing \"${homeDay}\" for you as requested."
log.info(message)
sendHome(message)
location.helloHome.execute(settings.homeDay)
state.homestate = "homeDay"
}
}
}
}
private everyoneIsAway() {
def result = true
if(people.findAll { it?.currentPresence == "present" }) {
result = false
}
log.debug("everyoneIsAway: ${result}")
return result
}
private anyoneIsHome() {
def result = false
if(people.findAll { it?.currentPresence == "present" }) {
result = true
}
log.debug("anyoneIsHome: ${result}")
return result
}
def sendAway(msg) {
if (sendPushMessage) {
if (recipients) {
sendNotificationToContacts(msg, recipients)
}
else {
sendPush(msg)
sendSms(phone, msg)
}
}
log.debug(msg)
}
def sendHome(msg) {
if (sendPushMessageHome) {
if (recipients) {
sendNotificationToContacts(msg, recipients)
}
else {
sendPush(msg)
sendSms(phone, msg)
}
}
log.debug(msg)
}
private getAllOk() {
modeOk && daysOk && timeOk
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
log.trace "modeOk = $result"
result
}
private getDaysOk() {
def result = true
if (days) {
def df = new java.text.SimpleDateFormat("EEEE")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
def day = df.format(new Date())
result = days.contains(day)
}
log.trace "daysOk = $result"
result
}
private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting, location?.timeZone).time
def stop = timeToday(ending, location?.timeZone).time
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
}
log.trace "timeOk = $result"
result
}
private hhmm(time, fmt = "h:mm a") {
def t = timeToday(time, location.timeZone)
def f = new java.text.SimpleDateFormat(fmt)
f.setTimeZone(location.timeZone?:timeZone(time))
f.format(t)
}
private getTimeIntervalLabel() {
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z"): ""
}
private hideOptionsSection() {
(starting || ending || days || modes) ? false: true
}

View File

@@ -1,307 +0,0 @@
/**
* WeatherBug Home
*
* Copyright 2015 WeatherBug
*
*/
definition(
name: "WeatherBug Home",
namespace: "WeatherBug",
author: "WeatherBug Home",
description: "WeatherBug Home",
category: "My Apps",
iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png",
oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"])
preferences {
section("Select thermostats") {
input "thermostatDevice", "capability.thermostat", multiple: true
}
}
mappings {
path("/appInfo") { action: [ GET: "getAppInfo" ] }
path("/getLocation") { action: [ GET: "getLoc" ] }
path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] }
path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] }
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* data from the installed SmartApp, including the location data, and
* a list of the devices that were authorized to be accessed. The WeatherBug
* Home Service will leverage this data to represent the connected devices as well as their
* location and associated the data with a WeatherBug user account.
* Privacy Policy: http://weatherbughome.com/privacy/
* @return Location, including id, latitude, longitude, zip code, and name, and the list of devices
*/
def getAppInfo() {
def devices = thermostatDevice
def lat = location.latitude
def lon = location.longitude
if(!(devices instanceof Collection))
{
devices = [devices]
}
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [ "Devices": devices,
"Location":[
"Id": location.id,
"Latitude":lat,
"Longitude":lon,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* location data from the installed SmartApp. The WeatherBug
* Home Service will leverage this data to associate the location to a WeatherBug Home account
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @return Location, including id, latitude, longitude, zip code, and name
*/
def getLoc() {
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* thermostat data and store it for display to a WeatherBug user.
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @param id The id of the device to get data for
* @return Thermostat data including temperature, set points, running modes, and operating states
*/
def getCurrentReport() {
log.debug "device id parameter=" + params.id
def unixTime = (int)((new Date().getTime() / 1000))
def device = thermostatDevice.find{ it.id == params.id}
if(device == null)
{
return [
Id: UUID.randomUUID().toString(),
Code: 404,
ErrorMessage: "Device not found. id=" + params.id,
Result: null
]
}
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [
DeviceId: device.id,
LocationId: location.id,
ReportType: 2,
ReportList: [
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
],
UnixTime: unixTime
]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to set
* the thermostat setpoint.
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @param id The id of the device to set
* @return Indication of whether the operation succeeded or failed
def setTemperature() {
log.debug "device id parameter=" + params.id
def device = thermostatDevice.find{ it.id == params.id}
if(device != null)
{
def mode = device.latestState('thermostatMode').stringValue
def value = params.temp as Integer
log.trace "Suggested temperature: $value, $mode"
if ( mode == "cool")
device.setCoolingSetpoint(value)
else if ( mode == "heat")
device.setHeatingSetpoint(value)
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: null
]
}
return [
Id: UUID.randomUUID().toString(),
Code : 404,
ErrorMessage: "Device not found. id=" + params.id,
Result: null
]
}
*/
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
/**
* The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
* Data that will be sent includes the list of devices, and location data
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def updated() {
log.debug "Updated with settings: ${settings}"
log.debug "Updated with state: ${state}"
log.debug "Updated with location ${location} ${location.id} ${location.name}"
unsubscribe()
initialize()
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update',
body: [
"Devices": devices,
"Location":[
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
sendToWeatherBug(postParams)
}
/*
* Subscribe to changes on the thermostat attributes
*/
def initialize() {
log.trace "initialize enter"
subscribe(thermostatDevice, "heatingSetpoint", pushLatest)
subscribe(thermostatDevice, "coolingSetpoint", pushLatest)
subscribe(thermostatDevice, "thermostatSetpoint", pushLatest)
subscribe(thermostatDevice, "thermostatMode", pushLatest)
subscribe(thermostatDevice, "thermostatFanMode", pushLatest)
subscribe(thermostatDevice, "thermostatOperatingState", pushLatest)
subscribe(thermostatDevice, "temperature", pushLatest)
}
/**
* The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
* Data that will be sent includes the list of devices, and location data
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def uninstalled() {
log.trace "uninstall entered"
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete',
body: [
"Devices": devices,
"Location":[
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
sendToWeatherBug(postParams)
}
/**
* This method will push the latest thermostat data to the WeatherBug Home Service so it can store
* and display the data to the WeatherBug user. Data pushed includes the thermostat data as well
* as location id.
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def pushLatest(evt) {
def unixTime = (int)((new Date().getTime() / 1000))
def device = thermostatDevice.find{ it.id == evt.deviceId}
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive',
body: [
DeviceId: evt.deviceId,
LocationId: location.id,
ReportType: 2,
ReportList: [
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
],
UnixTime: unixTime
]
]
log.debug postParams
sendToWeatherBug(postParams)
}
/*
* This method attempts to get the value of a device attribute, but if an error occurs null is returned
* @return The device attribute value, or null
*/
def GetOrDefault(device, attrib)
{
def val
try{
val = device.latestValue(attrib)
}catch(ex)
{
log.debug "Failed to get attribute " + attrib + " from device " + device
val = null
}
return val
}
/*
* Convenience method that sends data to WeatherBug, logging any exceptions that may occur
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def sendToWeatherBug(postParams)
{
try{
log.debug postParams
httpPostJson(postParams) { resp ->
resp.headers.each {
log.debug "${it.name} : ${it.value}"
}
log.debug "response contentType: ${resp.contentType}"
log.debug "response data: ${resp.data}"
}
log.debug "Communication with WeatherBug succeeded";
}catch(ex)
{
log.debug "Communication with WeatherBug failed.\n${ex}";
}
}