mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 21:03:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0850eae33c | ||
|
|
7980793f81 |
@@ -1,313 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
@@ -1,989 +0,0 @@
|
||||
/**
|
||||
* Bose SoundTouch
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Needed to be able to serialize the XmlSlurper data back to XML
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
|
||||
/**
|
||||
* List our capabilties. Doing so adds predefined command(s) which
|
||||
* belong to the capability.
|
||||
*/
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Music Player"
|
||||
capability "Polling"
|
||||
|
||||
/**
|
||||
* Define all commands, ie, if you have a custom action not
|
||||
* covered by a capability, you NEED to define it here or
|
||||
* the call will not be made.
|
||||
*
|
||||
* To call a capability function, just prefix it with the name
|
||||
* of the capability, for example, refresh would be "refresh.refresh"
|
||||
*/
|
||||
command "preset1"
|
||||
command "preset2"
|
||||
command "preset3"
|
||||
command "preset4"
|
||||
command "preset5"
|
||||
command "preset6"
|
||||
command "aux"
|
||||
|
||||
command "everywhereJoin"
|
||||
command "everywhereLeave"
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the various tiles and the states that they can be in.
|
||||
* The 2nd parameter defines an event which the tile listens to,
|
||||
* if received, it tries to map it to a state.
|
||||
*
|
||||
* You can also use ${currentValue} for the value of the event
|
||||
* or ${name} for the name of the event. Just make SURE to use
|
||||
* single quotes, otherwise it will only be interpreted at time of
|
||||
* launch, instead of every time the event triggers.
|
||||
*/
|
||||
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
|
||||
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff"
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821"
|
||||
}
|
||||
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
|
||||
state "station1", label:'${currentValue}', action:"preset1"
|
||||
}
|
||||
valueTile("2", "device.station2", decoration: "flat", canChangeIcon: false) {
|
||||
state "station2", label:'${currentValue}', action:"preset2"
|
||||
}
|
||||
valueTile("3", "device.station3", decoration: "flat", canChangeIcon: false) {
|
||||
state "station3", label:'${currentValue}', action:"preset3"
|
||||
}
|
||||
valueTile("4", "device.station4", decoration: "flat", canChangeIcon: false) {
|
||||
state "station4", label:'${currentValue}', action:"preset4"
|
||||
}
|
||||
valueTile("5", "device.station5", decoration: "flat", canChangeIcon: false) {
|
||||
state "station5", label:'${currentValue}', action:"preset5"
|
||||
}
|
||||
valueTile("6", "device.station6", decoration: "flat", canChangeIcon: false) {
|
||||
state "station6", label:'${currentValue}', action:"preset6"
|
||||
}
|
||||
valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'Auxillary\nInput', action:"aux"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") {
|
||||
state "volume", action:"music Player.setLevel"
|
||||
}
|
||||
|
||||
standardTile("playpause", "device.playpause", decoration: "flat") {
|
||||
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
|
||||
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
|
||||
}
|
||||
|
||||
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn"
|
||||
}
|
||||
standardTile("next", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn"
|
||||
}
|
||||
|
||||
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
|
||||
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
|
||||
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
|
||||
// Final state is used if the device is in a state where joining is not possible
|
||||
state "unavailable", label:"Not Available"
|
||||
}
|
||||
|
||||
// Defines which tile to show in the overview
|
||||
main "switch"
|
||||
|
||||
// Defines which tile(s) to show when user opens the detailed view
|
||||
details ([
|
||||
"nowplaying", "refresh", // Row 1 (112)
|
||||
"prev", "playpause", "next", // Row 2 (123)
|
||||
"volume", // Row 3 (111)
|
||||
"1", "2", "3", // Row 4 (123)
|
||||
"4", "5", "6", // Row 5 (123)
|
||||
"aux", "everywhere"]) // Row 6 (122)
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* The following section simply maps the actions as defined in
|
||||
* the metadata into onAction() calls.
|
||||
*
|
||||
* This is preferred since some actions can be dealt with more
|
||||
* efficiently this way. Also keeps all user interaction code in
|
||||
* one place.
|
||||
*
|
||||
*/
|
||||
def off() { onAction("off") }
|
||||
def on() { onAction("on") }
|
||||
def volup() { onAction("volup") }
|
||||
def voldown() { onAction("voldown") }
|
||||
def preset1() { onAction("1") }
|
||||
def preset2() { onAction("2") }
|
||||
def preset3() { onAction("3") }
|
||||
def preset4() { onAction("4") }
|
||||
def preset5() { onAction("5") }
|
||||
def preset6() { onAction("6") }
|
||||
def aux() { onAction("aux") }
|
||||
def refresh() { onAction("refresh") }
|
||||
def setLevel(level) { onAction("volume", level) }
|
||||
def play() { onAction("play") }
|
||||
def pause() { onAction("pause") }
|
||||
def mute() { onAction("mute") }
|
||||
def unmute() { onAction("unmute") }
|
||||
def previousTrack() { onAction("previous") }
|
||||
def nextTrack() { onAction("next") }
|
||||
def everywhereJoin() { onAction("ejoin") }
|
||||
def everywhereLeave() { onAction("eleave") }
|
||||
/**************************************************************************/
|
||||
|
||||
/**
|
||||
* Main point of interaction with things.
|
||||
* This function is called by SmartThings Cloud with the resulting data from
|
||||
* any action (see HubAction()).
|
||||
*
|
||||
* Conversely, to execute any actions, you need to return them as a single
|
||||
* item or a list (flattened).
|
||||
*
|
||||
* @param data Data provided by the cloud
|
||||
* @return an action or a list() of actions. Can also return null if no further
|
||||
* action is desired at this point.
|
||||
*/
|
||||
def parse(String event) {
|
||||
def data = parseLanMessage(event)
|
||||
def actions = []
|
||||
|
||||
// List of permanent root node handlers
|
||||
def handlers = [
|
||||
"nowPlaying" : "boseParseNowPlaying",
|
||||
"volume" : "boseParseVolume",
|
||||
"presets" : "boseParsePresets",
|
||||
"zone" : "boseParseEverywhere",
|
||||
]
|
||||
|
||||
// No need to deal with non-XML data
|
||||
if (!data.headers || !data.headers?."content-type".contains("xml"))
|
||||
return null
|
||||
|
||||
// Move any pending callbacks into ready state
|
||||
prepareCallbacks()
|
||||
|
||||
def xml = new XmlSlurper().parseText(data.body)
|
||||
// Let each parser take a stab at it
|
||||
handlers.each { node,func ->
|
||||
if (xml.name() == node)
|
||||
actions << "$func"(xml)
|
||||
}
|
||||
// If we have callbacks waiting for this...
|
||||
actions << processCallbacks(xml)
|
||||
|
||||
// Be nice and helpful
|
||||
if (actions.size() == 0) {
|
||||
log.warn "parse(): Unhandled data = " + lan
|
||||
return null
|
||||
}
|
||||
|
||||
// Issue new actions
|
||||
return actions.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the devicetype is first installed.
|
||||
*
|
||||
* @return action(s) to take or null
|
||||
*/
|
||||
def installed() {
|
||||
onAction("refresh")
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for dealing with user input and taking the
|
||||
* appropiate action.
|
||||
*
|
||||
* @param user The user interaction
|
||||
* @param data Additional data (optional)
|
||||
* @return action(s) to take (or null if none)
|
||||
*/
|
||||
def onAction(String user, data=null) {
|
||||
log.info "onAction(${user})"
|
||||
|
||||
// Keep IP address current (since device may have changed)
|
||||
state.address = parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
|
||||
// Process action
|
||||
def actions = null
|
||||
switch (user) {
|
||||
case "on":
|
||||
actions = boseSetPowerState(true)
|
||||
break
|
||||
case "off":
|
||||
boseSetNowPlaying(null, "STANDBY")
|
||||
actions = boseSetPowerState(false)
|
||||
break
|
||||
case "volume":
|
||||
actions = boseSetVolume(data)
|
||||
break
|
||||
case "aux":
|
||||
boseSetNowPlaying(null, "AUX")
|
||||
boseZoneReset()
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
actions = boseSetInput(user)
|
||||
break
|
||||
case "refresh":
|
||||
boseSetNowPlaying(null, "REFRESH")
|
||||
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
|
||||
break
|
||||
case "play":
|
||||
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "pause":
|
||||
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "previous":
|
||||
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "next":
|
||||
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "mute":
|
||||
actions = boseSetMute(true)
|
||||
break
|
||||
case "unmute":
|
||||
actions = boseSetMute(false)
|
||||
break
|
||||
case "ejoin":
|
||||
actions = boseZoneJoin()
|
||||
break
|
||||
case "eleave":
|
||||
actions = boseZoneLeave()
|
||||
break
|
||||
default:
|
||||
log.error "Unhandled action: " + user
|
||||
}
|
||||
|
||||
// Make sure we don't have nested lists
|
||||
if (actions instanceof List)
|
||||
return actions.flatten()
|
||||
return actions
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every so often (every 5 minutes actually) to refresh the
|
||||
* tiles so the user gets the correct information.
|
||||
*/
|
||||
def poll() {
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins this speaker into the everywhere zone
|
||||
*/
|
||||
def boseZoneJoin() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneJoin(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker from the everywhere zone
|
||||
*/
|
||||
def boseZoneLeave() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneLeave(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker and any children WITHOUT
|
||||
* signaling the speakers themselves. This is needed
|
||||
* in certain cases where we know the user action will
|
||||
* cause the zone to collapse (for example, AUX)
|
||||
*/
|
||||
def boseZoneReset() {
|
||||
parent.boseZoneReset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles <nowPlaying></nowPlaying> information and can also
|
||||
* perform addtional actions if there is a pending command
|
||||
* stored in the state variable. For example, the power is
|
||||
* handled this way.
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseNowPlaying(xmlData) {
|
||||
def result = []
|
||||
|
||||
// Perform display update, allow it to add additional commands
|
||||
if (boseSetNowPlaying(xmlData)) {
|
||||
result << boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses volume data
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseVolume(xmlData) {
|
||||
def result = []
|
||||
|
||||
sendEvent(name:"volume", value:xmlData.actualvolume.text())
|
||||
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the result of the boseGetEverywhereState() call
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
def boseParseEverywhere(xmlData) {
|
||||
// No good way of detecting the correct state right now
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses presets and updates the buttons
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParsePresets(xmlData) {
|
||||
def result = []
|
||||
|
||||
state.preset = [:]
|
||||
|
||||
def missing = ["1", "2", "3", "4", "5", "6"]
|
||||
for (preset in xmlData.preset) {
|
||||
def id = preset.attributes()['id']
|
||||
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
|
||||
if (name == "##TRANS_SONGS##")
|
||||
name = "Local\nPlaylist"
|
||||
sendEvent(name:"station${id}", value:name)
|
||||
missing = missing.findAll { it -> it != id }
|
||||
|
||||
// Store the presets into the state for recall later
|
||||
state.preset["$id"] = XmlUtil.serialize(preset.ContentItem)
|
||||
}
|
||||
|
||||
for (id in missing) {
|
||||
state.preset["$id"] = null
|
||||
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on <nowPlaying></nowPlaying>, updates the visual
|
||||
* representation of the speaker
|
||||
*
|
||||
* @param xmlData The nowPlaying info
|
||||
* @param override Provide the source type manually (optional)
|
||||
*
|
||||
* @return true if it would prefer a refresh soon
|
||||
*/
|
||||
def boseSetNowPlaying(xmlData, override=null) {
|
||||
def needrefresh = false
|
||||
def nowplaying = null
|
||||
|
||||
if (xmlData && xmlData.playStatus) {
|
||||
switch(xmlData.playStatus) {
|
||||
case "BUFFERING_STATE":
|
||||
nowplaying = "Please wait\nBuffering..."
|
||||
needrefresh = true
|
||||
break
|
||||
case "PLAY_STATE":
|
||||
sendEvent(name:"playpause", value:"play")
|
||||
break
|
||||
case "PAUSE_STATE":
|
||||
case "STOP_STATE":
|
||||
sendEvent(name:"playpause", value:"pause")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the previous section didn't handle this, take another stab at it
|
||||
if (!nowplaying) {
|
||||
nowplaying = ""
|
||||
switch (override ? override : xmlData.attributes()['source']) {
|
||||
case "AUX":
|
||||
nowplaying = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
nowplaying = "Air Play"
|
||||
break
|
||||
case "STANDBY":
|
||||
nowplaying = "Standby"
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
nowplaying = "${xmlData.stationName.text()}\n\n${xmlData.description.text()}"
|
||||
break
|
||||
case "REFRESH":
|
||||
nowplaying = "Please wait"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
if (xmlData.ContentItem.itemName[0])
|
||||
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
|
||||
case "STORED_MUSIC":
|
||||
nowplaying += "${xmlData.track.text()}"
|
||||
if (xmlData.artist)
|
||||
nowplaying += "\nby\n${xmlData.artist.text()}"
|
||||
if (xmlData.album)
|
||||
nowplaying += "\n\n(${xmlData.album.text()})"
|
||||
break
|
||||
default:
|
||||
if (xmlData != null)
|
||||
nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
|
||||
else
|
||||
nowplaying = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Some last parsing which only deals with actual data from device
|
||||
if (xmlData) {
|
||||
if (xmlData.attributes()['source'] == "STANDBY") {
|
||||
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
|
||||
sendEvent(name:"switch", value:"off")
|
||||
} else {
|
||||
sendEvent(name:"switch", value:"on")
|
||||
}
|
||||
boseSetPlayerAttributes(xmlData)
|
||||
}
|
||||
|
||||
// Do not allow a standby device or AUX to be master
|
||||
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if ((override ? override : xmlData.attributes()['source']) == "AUX")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if (boseGetZone()) {
|
||||
log.info "We're in the zone: " + boseGetZone()
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
|
||||
sendEvent(name:"nowplaying", value:nowplaying)
|
||||
|
||||
return needrefresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the attributes exposed by the music Player capability
|
||||
*
|
||||
* @param xmlData The NowPlaying XML data
|
||||
*/
|
||||
def boseSetPlayerAttributes(xmlData) {
|
||||
// Refresh attributes
|
||||
def trackText = ""
|
||||
def trackDesc = ""
|
||||
def trackData = [:]
|
||||
|
||||
switch (xmlData.attributes()['source']) {
|
||||
case "STANDBY":
|
||||
trackData["station"] = trackText = trackDesc = "Standby"
|
||||
break
|
||||
case "AUX":
|
||||
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
trackData["station"] = trackText = trackDesc = "Air Play"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
case "STORED_MUSIC":
|
||||
trackText = trackDesc = "${xmlData.track.text()}"
|
||||
trackData["name"] = xmlData.track.text()
|
||||
if (xmlData.artist) {
|
||||
trackText += " by ${xmlData.artist.text()}"
|
||||
trackDesc += " - ${xmlData.artist.text()}"
|
||||
trackData["artist"] = xmlData.artist.text()
|
||||
}
|
||||
if (xmlData.album) {
|
||||
trackText += " (${xmlData.album.text()})"
|
||||
trackData["album"] = xmlData.album.text()
|
||||
}
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
trackDesc = xmlData.stationName.text()
|
||||
trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
|
||||
trackData["station"] = xmlData.stationName.text()
|
||||
break
|
||||
default:
|
||||
trackText = trackDesc = xmlData.ContentItem.itemName[0].text()
|
||||
}
|
||||
|
||||
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the state of the "play everywhere" mode
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetEverywhereState() {
|
||||
return boseGET("/getZone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a remote key event
|
||||
*
|
||||
* @param key The name of the key
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note It's VITAL that it's done as two requests, or it will ignore the
|
||||
* the second key info.
|
||||
*/
|
||||
def boseKeypress(key) {
|
||||
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
|
||||
def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
|
||||
|
||||
return [bosePOST("/key", press), bosePOST("/key", release)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses or plays current preset
|
||||
*
|
||||
* @param play If true, plays, else it pauses (depending on preset, may stop)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetPlayMode(boolean play) {
|
||||
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
|
||||
return boseKeypress(play ? "PLAY" : "PAUSE")
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume in a deterministic way.
|
||||
*
|
||||
* @param New volume level, ranging from 0 to 100
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetVolume(int level) {
|
||||
def result = []
|
||||
int vol = Math.min(100, Math.max(level, 0))
|
||||
|
||||
sendEvent(name:"volume", value:"${vol}")
|
||||
|
||||
return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mute state, unfortunately, for now, we need to query current
|
||||
* state before taking action (no discrete mute/unmute)
|
||||
*
|
||||
* @param mute If true, mutes the system
|
||||
* @return command
|
||||
*/
|
||||
def boseSetMute(boolean mute) {
|
||||
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
|
||||
return boseGetVolume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for boseSetMute(), checks current state and changes it
|
||||
* if it doesn't match the requested state.
|
||||
*
|
||||
* @param xml The volume XML data
|
||||
* @param mute The new state of mute
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetMute(xml, mute) {
|
||||
def result = []
|
||||
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
|
||||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
|
||||
{
|
||||
result << boseKeypress("MUTE")
|
||||
}
|
||||
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "mute"))
|
||||
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of the volume
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetVolume() {
|
||||
return boseGET("/volume")
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the track to either the previous or next
|
||||
*
|
||||
* @param direction > 0 = next track, < 0 = previous track, 0 = no action
|
||||
* @return command
|
||||
*/
|
||||
def boseChangeTrack(int direction) {
|
||||
if (direction < 0) {
|
||||
return boseKeypress("PREV_TRACK")
|
||||
} else if (direction > 0) {
|
||||
return boseKeypress("NEXT_TRACK")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to preset 1-6 or AUX
|
||||
*
|
||||
* @param input The input (one of 1,2,3,4,5,6,aux)
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note If no presets have been loaded, it will first refresh the presets.
|
||||
*/
|
||||
def boseSetInput(input) {
|
||||
log.info "boseSetInput(${input})"
|
||||
def result = []
|
||||
|
||||
if (!state.preset) {
|
||||
result << boseGetPresets()
|
||||
queueCallback('presets', 'cb_boseSetInput', input)
|
||||
} else {
|
||||
result << cb_boseSetInput(null, input)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by boseSetInput(), either called directly by
|
||||
* boseSetInput() if we already have presets, or called after
|
||||
* retreiving the presets for the first time.
|
||||
*
|
||||
* @param xml The presets XML data
|
||||
* @param input Desired input
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Uses KEY commands for AUX, otherwise /select endpoint.
|
||||
* Reason for this is latency. Since keypresses are done
|
||||
* in pairs (press + release), you could accidentally change
|
||||
* the preset if there is a long delay between the two.
|
||||
*/
|
||||
def cb_boseSetInput(xml, input) {
|
||||
def result = []
|
||||
|
||||
if (input >= "1" && input <= "6" && state.preset["$input"])
|
||||
result << bosePOST("/select", state.preset["$input"])
|
||||
else if (input.toLowerCase() == "aux") {
|
||||
result << boseKeypress("AUX_INPUT")
|
||||
}
|
||||
|
||||
// Horrible workaround... but we need to delay
|
||||
// the update by at least a few seconds...
|
||||
result << boseRefreshNowPlaying(3000)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the power state of the bose unit
|
||||
*
|
||||
* @param device The device in-question
|
||||
* @param enable True to power on, false to power off
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Will first query state before acting since there
|
||||
* is no discreete call.
|
||||
*/
|
||||
def boseSetPowerState(boolean enable) {
|
||||
log.info "boseSetPowerState(${enable})"
|
||||
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used by boseSetPowerState(), is used
|
||||
* to handle the fact that we only have a toggle for power.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param state The requested state
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetPowerState(xml, state) {
|
||||
def result = []
|
||||
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
|
||||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
|
||||
{
|
||||
result << boseKeypress("POWER")
|
||||
if (state == "POWERON") {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
|
||||
}
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* We're sometimes too quick on the draw and get a refreshed nowPlaying
|
||||
* which shows standby (essentially, the device has yet to completely
|
||||
* transition to awake state), so we need to poll a few times extra
|
||||
* to make sure we get it right.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param tries A counter which will decrease, once it reaches zero,
|
||||
* we give up and assume that whatever we got was correct.
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseConfirmPowerOn(xml, tries) {
|
||||
def result = []
|
||||
log.warn "boseConfirmPowerOn() attempt #" + tries
|
||||
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an update on currently playing item(s)
|
||||
*
|
||||
* @param delay If set to non-zero, delays x ms before issuing
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseRefreshNowPlaying(delay=0) {
|
||||
if (delay > 0) {
|
||||
return ["delay ${delay}", boseGET("/now_playing")]
|
||||
}
|
||||
return boseGET("/now_playing")
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the list of presets
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetPresets() {
|
||||
return boseGET("/presets")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes GET requests to BOSE device
|
||||
*
|
||||
* @param path What endpoint
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGET(String path) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: path,
|
||||
headers: [
|
||||
HOST: state.address + ":8090",
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes a POST request to the BOSE device with
|
||||
* the provided data.
|
||||
*
|
||||
* @param path What endpoint
|
||||
* @param data What data
|
||||
* @param address Specific ip and port (optional)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def bosePOST(String path, String data, String address=null) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "POST",
|
||||
path: path,
|
||||
body: data,
|
||||
headers: [
|
||||
HOST: address ?: (state.address + ":8090"),
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a callback function for when a specific XML root is received
|
||||
* Will execute on subsequent parse() call(s), never on the current
|
||||
* parse() call.
|
||||
*
|
||||
* @param root The root node that this callback should react to
|
||||
* @param func Name of the function
|
||||
* @param param Parameters for function (optional)
|
||||
*/
|
||||
def queueCallback(String root, String func, param=null) {
|
||||
if (!state.pending)
|
||||
state.pending = [:]
|
||||
if (!state.pending[root])
|
||||
state.pending[root] = []
|
||||
state.pending[root] << ["$func":"$param"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers the pending callbacks into readiness state
|
||||
* so they can be executed by processCallbacks()
|
||||
*
|
||||
* This is needed to avoid reacting to queueCallbacks() within
|
||||
* the same loop.
|
||||
*/
|
||||
def prepareCallbacks() {
|
||||
if (!state.pending)
|
||||
return
|
||||
if (!state.ready)
|
||||
state.ready = [:]
|
||||
state.ready << state.pending
|
||||
state.pending = [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes any ready callback for a specific root node
|
||||
* with associated parameter and then clears that entry.
|
||||
*
|
||||
* If a callback returns data, it's added to a list of
|
||||
* commands which is returned to the caller of this function
|
||||
*
|
||||
* Once a callback has been used, it's removed from the list
|
||||
* of queued callbacks (ie, it executes only once!)
|
||||
*
|
||||
* @param xml The XML data to be examined and delegated
|
||||
* @return list of commands
|
||||
*/
|
||||
def processCallbacks(xml) {
|
||||
def result = []
|
||||
|
||||
if (!state.ready)
|
||||
return result
|
||||
|
||||
if (state.ready[xml.name()]) {
|
||||
state.ready[xml.name()].each { callback ->
|
||||
callback.each { func, param ->
|
||||
if (func != "func") {
|
||||
if (param)
|
||||
result << "$func"(xml, param)
|
||||
else
|
||||
result << "$func"(xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ready.remove(xml.name())
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* State managament for the Play Everywhere zone.
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* A device is either:
|
||||
*
|
||||
* null = Not participating
|
||||
* server = running the show
|
||||
* client = under the control of the server
|
||||
*
|
||||
* @param newstate (see above for types)
|
||||
*/
|
||||
def boseSetZone(String newstate) {
|
||||
log.debug "boseSetZone($newstate)"
|
||||
state.zone = newstate
|
||||
|
||||
// Refresh our state
|
||||
if (newstate) {
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else {
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the Everywhere zone, returns the current state
|
||||
* of zone membership (null, server, client)
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* @return state
|
||||
*/
|
||||
def boseGetZone() {
|
||||
return state.zone
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DeviceID of this particular device.
|
||||
*
|
||||
* Needs to be done this way since DNI is not always
|
||||
* the same as DeviceID which is used internally by
|
||||
* BOSE.
|
||||
*
|
||||
* @param devID The DeviceID
|
||||
*/
|
||||
def boseSetDeviceID(String devID) {
|
||||
state.deviceID = devID
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the DeviceID for this device
|
||||
*
|
||||
* @return deviceID
|
||||
*/
|
||||
def boseGetDeviceID() {
|
||||
return state.deviceID
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IP of this device
|
||||
*
|
||||
* @return IP address
|
||||
*/
|
||||
def getDeviceIP() {
|
||||
return parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ metadata {
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4){
|
||||
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"
|
||||
|
||||
@@ -153,37 +153,31 @@ def refresh()
|
||||
//}
|
||||
|
||||
def getTemperature(value) {
|
||||
if (value != null) {
|
||||
def celsius = Integer.parseInt(value, 16) / 100
|
||||
if (getTemperatureScale() == "C") {
|
||||
return celsius
|
||||
} else {
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
def celsius = Integer.parseInt(value, 16) / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(degrees) {
|
||||
if (degrees != null) {
|
||||
def temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = Math.round(degrees)
|
||||
log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||
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 temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = degrees as Integer
|
||||
log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||
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) {
|
||||
if (degrees != null) {
|
||||
def degreesInteger = Math.round(degrees)
|
||||
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||
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 degreesInteger = degrees as Integer
|
||||
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||
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() {
|
||||
|
||||
@@ -143,7 +143,8 @@ def setLevel(value) {
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
|
||||
@@ -330,7 +330,8 @@ def setLevel(value) {
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
|
||||
@@ -23,28 +23,36 @@ metadata {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
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.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"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setAdjustedColor"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"Color Reset", action:"reset", icon:"st.lights.philips.hue-single"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setAdjustedColor"
|
||||
}
|
||||
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}%'
|
||||
}
|
||||
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "saturation", action:"color control.setSaturation"
|
||||
}
|
||||
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
||||
state "saturation", label: 'Sat ${currentValue} '
|
||||
}
|
||||
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "hue", action:"color control.setHue"
|
||||
}
|
||||
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||
state "hue", label: 'Hue ${currentValue} '
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* LIFX Color Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setColor"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
||||
state "level", label: '${currentValue}%'
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setHue(percentage) {
|
||||
log.debug "setHue ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "hue", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setHue result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setSaturation(percentage) {
|
||||
log.debug "setSaturation ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "saturation", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setSaturation result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColor(Map color) {
|
||||
log.debug "setColor ${color}"
|
||||
def attrs = []
|
||||
def events = []
|
||||
color.each { key, value ->
|
||||
switch (key) {
|
||||
case "hue":
|
||||
attrs << "hue:${value * 3.6}"
|
||||
events << createEvent(name: "hue", value: value)
|
||||
break
|
||||
case "saturation":
|
||||
attrs << "saturation:${value / 100}"
|
||||
events << createEvent(name: "saturation", value: value)
|
||||
break
|
||||
case "colorTemperature":
|
||||
attrs << "kelvin:${value}"
|
||||
events << createEvent(name: "colorTemperature", value: value)
|
||||
break
|
||||
}
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "color", value: color.hex)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
events.each { sendEvent(it) }
|
||||
} else {
|
||||
log.error("Bad setColor result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
||||
if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data
|
||||
|
||||
sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
|
||||
sendEvent(name: "hue", value: data.color.hue / 3.6)
|
||||
sendEvent(name: "saturation", value: data.color.saturation * 100)
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* LIFX White Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
||||
state "level", label: '${currentValue}%'
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
||||
if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data
|
||||
|
||||
sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
@@ -201,10 +201,10 @@ def refresh()
|
||||
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 0x3600 0x3600 {01}", "delay 200",
|
||||
|
||||
@@ -280,7 +280,7 @@ private List parseIasMessage(String description) {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
|
||||
def configCmds = [
|
||||
//battery reporting and heartbeat
|
||||
@@ -290,7 +290,7 @@ def configure() {
|
||||
|
||||
|
||||
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
]
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ metadata {
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4){
|
||||
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"
|
||||
@@ -287,8 +287,7 @@ def isDescriptionPower(descMap) {
|
||||
def powerValue = "undefined"
|
||||
if (descMap.cluster == "0B04") {
|
||||
if (descMap.attrId == "050b") {
|
||||
if(descMap.value!="ffff")
|
||||
powerValue = convertHexToInt(descMap.value)
|
||||
powerValue = convertHexToInt(descMap.value)
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0B04") {
|
||||
@@ -328,9 +327,10 @@ def levelConfig() {
|
||||
//min change in value is 05
|
||||
def powerConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
//Meter (Power) Reporting
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0B04 0x050B 0x2A 1 600 {05}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* SmartPower Outlet (CentraLite)
|
||||
* CentraLite Switch
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2015-08-23
|
||||
* Date: 2013-12-02
|
||||
*/
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
@@ -28,9 +17,6 @@ metadata {
|
||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||
attribute "heartbeat", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019"
|
||||
}
|
||||
|
||||
@@ -45,33 +31,22 @@ metadata {
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4){
|
||||
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"
|
||||
attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||
attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
main "switch"
|
||||
details(["switch","refresh"])
|
||||
}
|
||||
@@ -79,92 +54,67 @@ metadata {
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
log.debug "Parse description $description"
|
||||
def name = null
|
||||
def value = null
|
||||
|
||||
// save heartbeat (i.e. last time we got a message from device)
|
||||
state.heartbeat = Calendar.getInstance().getTimeInMillis()
|
||||
|
||||
def finalResult = zigbee.getKnownDescription(description)
|
||||
|
||||
//TODO: Remove this after getKnownDescription can parse it automatically
|
||||
if (!finalResult && description!="updated")
|
||||
finalResult = getPowerDescription(zigbee.parseDescriptionAsMap(description))
|
||||
|
||||
if (finalResult) {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "power") {
|
||||
def powerValue = (finalResult.value as Integer)/10
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
/*
|
||||
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||
|
||||
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||
*/
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Read attr: $description"
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
name = "switch"
|
||||
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||
} else if (descMap.cluster.equalsIgnoreCase("0B04") && descMap.attrId.equalsIgnoreCase("050b")) {
|
||||
def reportValue = descMap.value
|
||||
name = "power"
|
||||
//power divisor is 10
|
||||
value = Integer.parseInt(reportValue, 16) / 10
|
||||
}
|
||||
} else if (description?.startsWith("on/off:")) {
|
||||
log.debug "Switch command"
|
||||
name = "switch"
|
||||
value = description?.endsWith(" 1") ? "on" : "off"
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
'zcl on-off on'
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
'zcl on-off off'
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
def meter() {
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
sendEvent(name: "heartbeat", value: "alive", displayed:false)
|
||||
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
|
||||
}
|
||||
|
||||
def configure() {
|
||||
zigbee.onOffConfig() + powerConfig() + refresh()
|
||||
}
|
||||
|
||||
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||
//min change in value is 01
|
||||
def powerConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
//TODO: Remove this after getKnownDescription can parse it automatically
|
||||
def getPowerDescription(descMap) {
|
||||
def powerValue = "undefined"
|
||||
if (descMap.cluster == "0B04") {
|
||||
if (descMap.attrId == "050b") {
|
||||
if(descMap.value!="ffff")
|
||||
powerValue = zigbee.convertHexToInt(descMap.value)
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0B04") {
|
||||
if(descMap.command=="07"){
|
||||
return [type: "update", value : "power (0B04) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (powerValue != "undefined"){
|
||||
return [type: "power", value : powerValue]
|
||||
}
|
||||
else {
|
||||
return [:]
|
||||
}
|
||||
def configure() {
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}", "delay 200"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ metadata {
|
||||
capability "Water Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu"
|
||||
|
||||
}
|
||||
|
||||
@@ -35,27 +35,18 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Moisture/Moisture3.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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
||||
attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||
attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||
attributeState "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||
attributeState "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||
}
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
@@ -73,7 +64,7 @@ metadata {
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
main (["water", "temperature"])
|
||||
details(["water", "temperature", "battery", "refresh"])
|
||||
}
|
||||
@@ -264,53 +255,52 @@ private Map getMoistureResult(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
def refreshCmds = [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
]
|
||||
[
|
||||
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"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 0x001 {${device.zigbeeId}} {}", "delay 500"
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1000",
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
[
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ metadata {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
||||
attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||
attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||
attributeState "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||
attributeState "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||
}
|
||||
}
|
||||
standardTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
@@ -47,7 +47,6 @@ metadata {
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["water", "temperature"])
|
||||
details(["water", "temperature", "battery"])
|
||||
}
|
||||
@@ -129,15 +128,6 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
|
||||
map
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd)
|
||||
{
|
||||
def map = [:]
|
||||
map.name = "water"
|
||||
map.value = cmd.value ? "wet" : "dry"
|
||||
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||
map
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd)
|
||||
{
|
||||
log.debug "COMMAND CLASS: $cmd"
|
||||
|
||||
@@ -25,7 +25,7 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
@@ -37,17 +37,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
|
||||
])
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
@@ -146,6 +137,10 @@ private boolean shouldProcessMessage(cluster) {
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private int getHumidity(value) {
|
||||
return Math.round(Double.parseDouble(value))
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
@@ -231,30 +226,29 @@ def getTemperature(value) {
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 0) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else if (volts > 0){
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
if (rawValue == 0) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else if (volts > 0){
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -286,54 +280,53 @@ private Map getMotionResult(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
def refreshCmds = [
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"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 1500",
|
||||
|
||||
"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 1500",
|
||||
|
||||
"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} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x001 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
|
||||
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
]
|
||||
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
[
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ metadata {
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -137,6 +136,10 @@ private boolean shouldProcessMessage(cluster) {
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private int getHumidity(value) {
|
||||
return Math.round(Double.parseDouble(value))
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
@@ -222,29 +225,22 @@ def getTemperature(value) {
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 0) {}
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else if (volts > 0){
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -277,54 +273,50 @@ private Map getMotionResult(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
def refreshCmds = [
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"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"
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500",
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
[
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,8 @@
|
||||
command "enrollResponse"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S"
|
||||
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -52,23 +51,8 @@
|
||||
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
||||
}
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
|
||||
])
|
||||
}
|
||||
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)
|
||||
}
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
@@ -76,14 +60,9 @@
|
||||
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("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")
|
||||
}
|
||||
|
||||
standardTile("acceleration", "device.acceleration", width: 2, height: 2) {
|
||||
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||
@@ -111,7 +90,6 @@
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
}
|
||||
@@ -180,6 +158,10 @@ private boolean shouldProcessMessage(cluster) {
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private int getHumidity(value) {
|
||||
return Math.round(Double.parseDouble(value))
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
@@ -220,15 +202,11 @@ private Map parseIasMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
resultMap = getContactResult('closed')
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
resultMap = getContactResult('open')
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
@@ -238,15 +216,11 @@ private Map parseIasMessage(String description) {
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
resultMap = getContactResult('closed')
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
resultMap = getContactResult('open')
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
@@ -258,29 +232,6 @@ private Map parseIasMessage(String description) {
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "updated called"
|
||||
log.info "garage value : $garageSensor"
|
||||
if (garageSensor == "Yes") {
|
||||
def descriptionText = "Updating device to garage sensor"
|
||||
if (device.latestValue("status") == "open") {
|
||||
sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText)
|
||||
}
|
||||
else if (device.latestValue("status") == "closed") {
|
||||
sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def descriptionText = "Updating device to open/close sensor"
|
||||
if (device.latestValue("status") == "garage-open") {
|
||||
sendEvent(name: 'status', value: 'open', descriptionText: descriptionText)
|
||||
}
|
||||
else if (device.latestValue("status") == "garage-closed") {
|
||||
sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
@@ -340,8 +291,11 @@ def getTemperature(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
return [
|
||||
name: 'contact',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
@@ -359,48 +313,52 @@ def getTemperature(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "Refreshing Values "
|
||||
def refreshCmds = [
|
||||
[
|
||||
|
||||
/* sensitivity - default value (8) */
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0000", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"zcl global read 0xFC02 0x0010", "delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0012",
|
||||
"zcl global read 0xFC02 0x0012", "delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0013",
|
||||
"zcl global read 0xFC02 0x0013", "delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0014",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
|
||||
]
|
||||
"zcl global read 0xFC02 0x0014", "delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
@@ -439,14 +397,11 @@ private getEndpointId() {
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
@@ -475,33 +430,13 @@ private Map parseAxis(String description) {
|
||||
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||
xyzResults.z = signedZ
|
||||
log.debug "Z Part: ${signedZ}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(signedZ)
|
||||
}
|
||||
}
|
||||
|
||||
getXyzResult(xyzResults, description)
|
||||
}
|
||||
|
||||
def garageEvent(zValue) {
|
||||
def absValue = zValue.abs()
|
||||
def contactValue = null
|
||||
def garageValue = null
|
||||
if (absValue>900) {
|
||||
contactValue = 'closed'
|
||||
garageValue = 'garage-closed'
|
||||
}
|
||||
else if (absValue < 100) {
|
||||
contactValue = 'open'
|
||||
garageValue = 'garage-open'
|
||||
}
|
||||
if (contactValue != null){
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${contactValue == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Map getXyzResult(results, description) {
|
||||
def name = "threeAxis"
|
||||
|
||||
@@ -284,61 +284,52 @@ def getTemperature(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "Refreshing Temperature and Battery "
|
||||
def refreshCmds = [
|
||||
[
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
//"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"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",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"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",
|
||||
"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 0xFC02 2 0x18 300 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"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 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
@@ -260,29 +260,30 @@ private Map getContactResult(value) {
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def refresh()
|
||||
{
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
def refreshCmds = [
|
||||
[
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
]
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"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 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 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 1500",
|
||||
|
||||
|
||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
@@ -290,28 +291,20 @@ def configure() {
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
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}}",
|
||||
"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"
|
||||
]
|
||||
[
|
||||
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Signal Strength"
|
||||
@@ -31,15 +31,6 @@ metadata {
|
||||
status "battery": "battery: 27, batteryDivisor: 0A, rssi: 100, lqi: 64"
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
@@ -251,7 +251,8 @@ def refresh()
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ metadata {
|
||||
capability "Illuminance Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "localSunrise", "string"
|
||||
attribute "localSunset", "string"
|
||||
|
||||
@@ -270,12 +270,12 @@ def refresh()
|
||||
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"delay 1000",
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
|
||||
@@ -134,7 +134,8 @@ def setLevel(value) {
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
/**
|
||||
* Bose SoundTouch (Connect)
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Bose SoundTouch (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control your Bose SoundTouch speakers",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the urn that we're looking for
|
||||
*
|
||||
* @return URN which we are looking for
|
||||
*
|
||||
* @todo This + getUSNQualifier should be one and should use regular expressions
|
||||
*/
|
||||
def getDeviceType() {
|
||||
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
|
||||
}
|
||||
|
||||
/**
|
||||
* If not null, returns an additional qualifier for ssdUSN
|
||||
* to avoid spamming the network
|
||||
*
|
||||
* @return Additional qualifier OR null if not needed
|
||||
*/
|
||||
def getUSNQualifier() {
|
||||
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the new device to instantiate in the user's smartapps
|
||||
* This must be an app owned by the namespace (see #getNameSpace).
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
def getDeviceName() {
|
||||
return "Bose SoundTouch"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace this app and siblings use
|
||||
*
|
||||
* @return namespace
|
||||
*/
|
||||
def getNameSpace() {
|
||||
return "smartthings"
|
||||
}
|
||||
|
||||
/**
|
||||
* The deviceDiscovery page used by preferences. Will automatically
|
||||
* make calls to the underlying discovery mechanisms as well as update
|
||||
* whenever new devices are discovered AND verified.
|
||||
*
|
||||
* @return a dynamicPage() object
|
||||
*/
|
||||
def deviceDiscovery()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
def refreshInterval = 3 // Number of seconds between refresh
|
||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
|
||||
|
||||
def devices = getSelectableDevice()
|
||||
def numFound = devices.size() ?: 0
|
||||
|
||||
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
|
||||
subscribeNetworkEvents()
|
||||
|
||||
//device discovery request every 15s
|
||||
if((deviceRefreshCount % 15) == 0) {
|
||||
discoverDevices()
|
||||
}
|
||||
|
||||
// Verify request every 3 seconds except on discoveries
|
||||
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
|
||||
verifyDevices()
|
||||
}
|
||||
|
||||
log.trace "Discovered devices: ${devices}"
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user has selected device(s) and
|
||||
* pressed "Install".
|
||||
*/
|
||||
def installed() {
|
||||
log.trace "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when app has been updated
|
||||
*/
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user uninstalls the app
|
||||
*
|
||||
* We don't need to manually do anything here because any children
|
||||
* are automatically removed upon the removal of the parent.
|
||||
*
|
||||
* Only time to do anything here is when you need to notify
|
||||
* the remote end. And even then you're discouraged from removing
|
||||
* the children manually.
|
||||
*/
|
||||
def uninstalled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* If user has selected devices, will start monitoring devices
|
||||
* for changes (new address, port, etc...)
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize()"
|
||||
state.subscribe = false
|
||||
if (selecteddevice) {
|
||||
addDevice()
|
||||
refreshDevices()
|
||||
subscribeNetworkEvents(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the child devices based on the user's selection
|
||||
*
|
||||
* Uses selecteddevice defined in the deviceDiscovery() page
|
||||
*/
|
||||
def addDevice(){
|
||||
def devices = getVerifiedDevices()
|
||||
def devlist
|
||||
log.trace "Adding childs"
|
||||
|
||||
// If only one device is selected, we don't get a list (when using simulator)
|
||||
if (!(selecteddevice instanceof List)) {
|
||||
devlist = [selecteddevice]
|
||||
} else {
|
||||
devlist = selecteddevice
|
||||
}
|
||||
|
||||
log.trace "These are being installed: ${devlist}"
|
||||
|
||||
devlist.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newDevice = devices.find { (it.value.mac) == dni }
|
||||
def deviceName = newDevice?.value.name
|
||||
if (!deviceName)
|
||||
deviceName = getDeviceName() + "[${newDevice?.value.name}]"
|
||||
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
|
||||
d.boseSetDeviceID(newDevice.value.deviceID)
|
||||
log.trace "Created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.trace "${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a DeviceNetworkId to an address. Primarily used by children
|
||||
*
|
||||
* @param dni Device Network id
|
||||
* @return address or null
|
||||
*/
|
||||
def resolveDNI2Address(dni) {
|
||||
def device = getVerifiedDevices().find { (it.value.mac) == dni }
|
||||
if (device) {
|
||||
return convertHexToIP(device.value.networkAddress)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a child to the "Play Everywhere" zone
|
||||
*
|
||||
* @param child The speaker joining the zone
|
||||
* @return A list of maps with POST data
|
||||
*/
|
||||
def boseZoneJoin(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server) {
|
||||
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
|
||||
child.boseSetZone("client")
|
||||
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
getChildDevices().each{ it ->
|
||||
log.trace "child: " + child
|
||||
log.trace "zone : " + it.boseGetZone()
|
||||
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
} else {
|
||||
log.debug "boseJoinZone() No server, add it!"
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
child.boseSetZone("server")
|
||||
}
|
||||
results << result
|
||||
return results
|
||||
}
|
||||
|
||||
def boseZoneReset() {
|
||||
getChildDevices().each{ it.boseSetZone(null) }
|
||||
}
|
||||
|
||||
def boseZoneHasMaster() {
|
||||
return getChildDevices().find{ it.boseGetZone() == "server" } != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a speaker from the play everywhere zone.
|
||||
*
|
||||
* @param child Which speaker is leaving
|
||||
* @return a list of maps with POST data
|
||||
*/
|
||||
def boseZoneLeave(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// First, tag us as a non-member
|
||||
child.boseSetZone(null)
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
} else {
|
||||
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
|
||||
// Dismantle the entire thing, first send this to master
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
|
||||
// Also issue this to each individual client
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.trace "Additional device: " + dev
|
||||
result['host'] = dev.getDeviceIP() + ":8090"
|
||||
results << result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Define our XML parsers
|
||||
*
|
||||
* @return mapping of root-node <-> parser function
|
||||
*/
|
||||
def getParsers() {
|
||||
[
|
||||
"root" : "parseDESC",
|
||||
"info" : "parseINFO"
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when location has changed, contains information from
|
||||
* network transactions. See deviceDiscovery() for where it is
|
||||
* registered.
|
||||
*
|
||||
* @param evt Holds event information
|
||||
*/
|
||||
def onLocation(evt) {
|
||||
// Convert the event into something we can use
|
||||
def lanEvent = parseLanMessage(evt.description, true)
|
||||
lanEvent << ["hub":evt?.hubId]
|
||||
|
||||
// Determine what we need to do...
|
||||
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
|
||||
(getUSNQualifier() == null ||
|
||||
lanEvent?.ssdpUSN?.contains(getUSNQualifier())
|
||||
)
|
||||
)
|
||||
{
|
||||
parseSSDP(lanEvent)
|
||||
}
|
||||
else if (
|
||||
lanEvent.headers && lanEvent.body &&
|
||||
lanEvent.headers."content-type".contains("xml")
|
||||
)
|
||||
{
|
||||
def parsers = getParsers()
|
||||
def xmlData = new XmlSlurper().parseText(lanEvent.body)
|
||||
|
||||
// Let each parser take a stab at it
|
||||
parsers.each { node,func ->
|
||||
if (xmlData.name() == node)
|
||||
"$func"(xmlData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP description file.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseDESC(xmlData) {
|
||||
log.info "parseDESC()"
|
||||
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
def devicetxml = body.device.deviceType.text().toLowerCase()
|
||||
|
||||
// Make sure it's the type we want
|
||||
if (devicetxml == devicetype) {
|
||||
def devices = getDevices()
|
||||
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
|
||||
if (device && !device.value?.verified) {
|
||||
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
|
||||
device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
|
||||
} else {
|
||||
log.error "parseDESC(): The xml file returned a device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle BOSE <info></info> result. This is an alternative to
|
||||
* using the SSDP description standard. Some of the speakers do
|
||||
* not support SSDP description, so we need this as well.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseINFO(xmlData) {
|
||||
log.info "parseINFO()"
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
|
||||
def deviceID = xmlData.attributes()['deviceID']
|
||||
def device = getDevices().find {it?.key?.contains(deviceID)}
|
||||
if (device && !device.value?.verified) {
|
||||
device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP discovery messages and adds them to the list
|
||||
* of discovered devices. If it already exists, it will update
|
||||
* the port and location (in case it was moved).
|
||||
*
|
||||
* @param lanEvent
|
||||
*/
|
||||
def parseSSDP(lanEvent) {
|
||||
//SSDP DISCOVERY EVENTS
|
||||
def USN = lanEvent.ssdpUSN.toString()
|
||||
def devices = getDevices()
|
||||
|
||||
if (!(devices."${USN}")) {
|
||||
//device does not exist
|
||||
log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
|
||||
devices << ["${USN}":lanEvent]
|
||||
} else {
|
||||
// update the values
|
||||
def d = devices."${USN}"
|
||||
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
|
||||
log.trace "parseSSDP() Updating device location (ip & port)"
|
||||
d.networkAddress = lanEvent.networkAddress
|
||||
d.deviceAddress = lanEvent.deviceAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Map object which can be used with a preference page
|
||||
* to represent a list of devices detected and verified.
|
||||
*
|
||||
* @return Map with zero or more devices
|
||||
*/
|
||||
Map getSelectableDevice() {
|
||||
def devices = getVerifiedDevices()
|
||||
def map = [:]
|
||||
devices.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = it.value.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the refresh loop, making sure to keep us up-to-date with changes
|
||||
*
|
||||
*/
|
||||
private refreshDevices() {
|
||||
discoverDevices()
|
||||
verifyDevices()
|
||||
runIn(300, "refreshDevices")
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a subscription for network events
|
||||
*
|
||||
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
|
||||
*/
|
||||
private subscribeNetworkEvents(force=false) {
|
||||
if (force) {
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, onLocation, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType())
|
||||
*/
|
||||
private discoverDevices() {
|
||||
log.trace "discoverDevice() Issuing SSDP request"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through the list of unverified devices and issues a verification
|
||||
* request for each of them (basically calling verifyDevice() per unverified)
|
||||
*/
|
||||
private verifyDevices() {
|
||||
def devices = getDevices().findAll { it?.value?.verified != true }
|
||||
|
||||
devices.each {
|
||||
verifyDevice(
|
||||
it?.value?.mac,
|
||||
convertHexToIP(it?.value?.networkAddress),
|
||||
convertHexToInt(it?.value?.deviceAddress),
|
||||
it?.value?.ssdpPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the device, in this case, we need to obtain the info block which
|
||||
* holds information such as the actual mac to use in certain scenarios.
|
||||
*
|
||||
* Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
|
||||
* functions.
|
||||
*
|
||||
* @param deviceNetworkId The DNI of the device
|
||||
* @param ip The address of the device on the network (not the same as DNI)
|
||||
* @param port The port to use (0 will be treated as invalid and will use 80)
|
||||
* @param devicessdpPath The URL path (for example, /desc)
|
||||
*
|
||||
* @note Result is captured in locationHandler()
|
||||
*/
|
||||
private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) {
|
||||
if(ip) {
|
||||
def address = ip + ":8090"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/info",
|
||||
headers: [
|
||||
HOST: address,
|
||||
]]))
|
||||
} else {
|
||||
log.warn("verifyDevice() IP address was empty")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of devices which have been verified
|
||||
*
|
||||
* @return array of verified devices
|
||||
*/
|
||||
def getVerifiedDevices() {
|
||||
getDevices().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all discovered devices or an empty array if none
|
||||
*
|
||||
* @return array of devices
|
||||
*/
|
||||
def getDevices() {
|
||||
state.devices = state.devices ?: [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal string to an integer
|
||||
*
|
||||
* @param hex The string with a hexadecimal value
|
||||
* @return An integer
|
||||
*/
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD
|
||||
*
|
||||
* @param hex Address represented in hex
|
||||
* @return String containing normal IPv4 dot notation
|
||||
*/
|
||||
private String convertHexToIP(hex) {
|
||||
if (hex)
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
else
|
||||
hex
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this setup can support SmarthThing Labs items
|
||||
*
|
||||
* @return true if it supports it.
|
||||
*/
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the firmwares on all hubs owned by user match or exceed the
|
||||
* provided version number.
|
||||
*
|
||||
* @param desiredFirmware The version that must match or exceed
|
||||
* @return true if hub has same or newer
|
||||
*/
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of firmware version for every hub the user has
|
||||
*
|
||||
* @return List of firmwares
|
||||
*/
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -30,7 +30,7 @@ definition(
|
||||
name: "Carpool Notifier",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send notifications to your carpooling buddies when you arrive to pick them up. If the person you are picking up is home, and has been for 5 minutes or more, they will get a notification when you arrive.",
|
||||
description: "This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you. If the person you are picking up is present, and has been for 5 minutes or more, they will get a notification when you become present.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png"
|
||||
|
||||
@@ -27,7 +27,7 @@ definition(
|
||||
name: "Gentle Wake Up",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Dim your lights up slowly, allowing you to wake up more naturally.",
|
||||
description: "Gentle Wake Up dims your lights slowly, allowing you to wake up more naturally. Once your lights have finished dimming, optionally turn on more things or send yourself a text for a more gentle nudge into the waking world (you may want to set your normal alarm as a backup plan).",
|
||||
category: "Health & Wellness",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Laundry Monitor Using Power Meter
|
||||
*
|
||||
* Author: Daniel De Leo
|
||||
*
|
||||
* Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.
|
||||
* This is a modification of the SmartThings Laundry Monitor Template App. This app uses a power meter
|
||||
* to determine your laundry status instead of an accelerometer.
|
||||
*
|
||||
* Date: 2013-02-21
|
||||
*/
|
||||
|
||||
definition (
|
||||
name: "Laundry Monitor Using Power Meter",
|
||||
namespace: "danieldeleo",
|
||||
author: "Daniel De Leo",
|
||||
description: "Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner%402x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Tell me when this washer/dryer has stopped..."){
|
||||
input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false)
|
||||
}
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"], defaultValue: "No"
|
||||
}
|
||||
section("And by turning on these lights (optional)") {
|
||||
input "switches", "capability.switch", required: false, multiple: true, title: "Which lights?"
|
||||
input "lightMode", "enum", options: ["Flash Lights", "Turn On Lights"], required: false, defaultValue: "Flash Lights", title: "Action?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(meter, "power", meterHandler)
|
||||
}
|
||||
|
||||
def meterHandler(evt) {
|
||||
def meterValue = evt.value as double
|
||||
if(meterValue == 0 && state.running) {
|
||||
// if meterValue is at 0 Watts and the state is running,
|
||||
// this means washer/dryer is either done or in a soak cycle
|
||||
state.running = false
|
||||
def msg = "${meter.displayName} is finished"
|
||||
|
||||
if(phone) {// Send SMS message if phone number provided
|
||||
sendSms phone, msg
|
||||
}
|
||||
else {// Send Push Notification if no phone number provided
|
||||
sendPush msg
|
||||
}
|
||||
if(pushAndPhone == "Yes" && phone) {//Send both Push and SMS
|
||||
sendPush msg
|
||||
}
|
||||
|
||||
if (switches) {
|
||||
if (lightMode?.equals("Turn On Lights")) {
|
||||
switches.on()
|
||||
}
|
||||
else {
|
||||
flashLights()
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(meterValue > 10 && !state.running) {
|
||||
// if meterValue is greater than 10Watts and the
|
||||
// previous read was 0, this means the machine is
|
||||
// now running
|
||||
state.running = true
|
||||
}
|
||||
}
|
||||
|
||||
private flashLights() {
|
||||
def doFlash = true
|
||||
def onFor = onFor ?: 2000
|
||||
def offFor = offFor ?: 2000
|
||||
def numFlashes = numFlashes ?: 3
|
||||
|
||||
log.debug "LAST ACTIVATED IS: ${state.lastActivated}"
|
||||
if (state.lastActivated) {
|
||||
def elapsed = now() - state.lastActivated
|
||||
def sequenceTime = (numFlashes + 1) * (onFor + offFor)
|
||||
doFlash = elapsed > sequenceTime
|
||||
log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}"
|
||||
}
|
||||
|
||||
if (doFlash) {
|
||||
log.debug "FLASHING $numFlashes times"
|
||||
state.lastActivated = now()
|
||||
log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}"
|
||||
def initialActionOn = switches.collect{it.currentSwitch != "on"}
|
||||
def delay = 1L
|
||||
numFlashes.times {
|
||||
log.trace "Switch on after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.on(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.off(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += onFor
|
||||
log.trace "Switch off after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.off(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.on(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += offFor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
/**
|
||||
* LIFX
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "LIFX (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "LIFX",
|
||||
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
oauth: true) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
}
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "Credentials", title: "LIFX", content: "authPage", install: false)
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/callback") { action: [ GET: "oauthCallback" ] }
|
||||
path("/oauth/initialize") { action: [ GET: "oauthInit"] }
|
||||
path("/test") { action: [ GET: "oauthSuccess" ] }
|
||||
}
|
||||
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" }
|
||||
def buildRedirectUrl(page) {
|
||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage"
|
||||
if (!state.lifxAccessToken) {
|
||||
log.debug "no LIFX access token"
|
||||
// This is the SmartThings access token
|
||||
if (!state.accessToken) {
|
||||
log.debug "no access token, create access token"
|
||||
createAccessToken() // predefined method
|
||||
}
|
||||
def description = "Tap to enter LIFX credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below
|
||||
log.debug "app id: ${app.id}"
|
||||
log.debug "redirect url: ${redirectUrl}"
|
||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) {
|
||||
section {
|
||||
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
||||
// href(url:buildRedirectUrl("test"), title: "Message test")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "have LIFX access token"
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size()
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// OAuth
|
||||
|
||||
def oauthInit() {
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ]
|
||||
log.info("Redirecting user to OAuth setup")
|
||||
redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def oauthCallback() {
|
||||
def redirectUrl = null
|
||||
if (params.authQueryString) {
|
||||
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
success()
|
||||
} else {
|
||||
def code = params.code
|
||||
if (code) {
|
||||
if (code.size() > 6) {
|
||||
// LIFX code
|
||||
log.debug "Exchanging code for access token"
|
||||
oauthReceiveToken(redirectUrl)
|
||||
} else {
|
||||
// Initiate the LIFX OAuth flow.
|
||||
oauthInit()
|
||||
}
|
||||
} else {
|
||||
log.debug "This code should be unreachable"
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def oauthReceiveToken(redirectUrl = null) {
|
||||
|
||||
log.debug "receiveToken - params: ${params}"
|
||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
|
||||
def params = [
|
||||
uri: "https://cloud.lifx.com/oauth/token",
|
||||
body: oauthParams,
|
||||
headers: [
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
]
|
||||
httpPost(params) { response ->
|
||||
state.lifxAccessToken = response.data.access_token
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
oauthSuccess()
|
||||
} else {
|
||||
oauthFailure()
|
||||
}
|
||||
}
|
||||
|
||||
def oauthSuccess() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthFailure() {
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthReceivedToken() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is already connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthConnectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>SmartThings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 280;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 15px;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
${redirectHtml}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
|
||||
<p>
|
||||
${message}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
// App lifecycle hooks
|
||||
|
||||
def installed() {
|
||||
enableCallback() // wtf does this do?
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
// Check for new devices and remove old ones every 3 hours
|
||||
runEvery3Hours('updateDevices')
|
||||
}
|
||||
|
||||
// called after settings are changed
|
||||
def updated() {
|
||||
enableCallback() // not sure what this does
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.info("Uninstalling, removing child devices...")
|
||||
unschedule('updateDevices')
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
private removeChildDevices(devices) {
|
||||
devices.each {
|
||||
deleteChildDevice(it.deviceNetworkId) // 'it' is default
|
||||
}
|
||||
}
|
||||
|
||||
// called after Done is hit after selecting a Location
|
||||
def initialize() {
|
||||
log.debug "initialize"
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
// Misc
|
||||
|
||||
Map apiRequestHeaders() {
|
||||
return ["Authorization": "Bearer ${state.lifxAccessToken}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
}
|
||||
|
||||
// Requests
|
||||
|
||||
def logResponse(response) {
|
||||
log.info("Status: ${response.status}")
|
||||
log.info("Body: ${response.data}")
|
||||
}
|
||||
|
||||
// API Requests
|
||||
// logObject is because log doesn't work if this method is being called from a Device
|
||||
def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
||||
try {
|
||||
return c()
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}")
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("lifxAccessToken")
|
||||
options.logObject.warn "Access token is not valid"
|
||||
}
|
||||
return options.errerReturn
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
options.logObject.warn "Connection timed out, not much we can do here"
|
||||
return options.errerReturn
|
||||
}
|
||||
}
|
||||
|
||||
def apiGET(path) {
|
||||
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
def apiPUT(path, body = [:]) {
|
||||
log.debug("Beginning API PUT: ${path}, ${body}")
|
||||
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
def devicesList(selector = '') {
|
||||
logErrors([]) {
|
||||
def resp = apiGET("/lights/${selector}")
|
||||
if (resp.status == 200) {
|
||||
return resp.data
|
||||
} else {
|
||||
log.error("Non-200 from device list call. ${resp.status} ${resp.data}")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map locationOptions() {
|
||||
|
||||
def options = [:]
|
||||
def devices = devicesList()
|
||||
devices.each { device ->
|
||||
options[device.location.id] = device.location.name
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
def devicesInLocation() {
|
||||
return devicesList("location_id:${settings.selectedLocationId}")
|
||||
}
|
||||
|
||||
// ensures the devices list is up to date
|
||||
def updateDevices() {
|
||||
if (!state.devices) {
|
||||
state.devices = [:]
|
||||
}
|
||||
def devices = devicesInLocation()
|
||||
def deviceIds = devices*.id
|
||||
devices.each { device ->
|
||||
def childDevice = getChildDevice(device.id)
|
||||
if (!childDevice) {
|
||||
log.info("Adding device ${device.id}: ${device.capabilities}")
|
||||
def data = [
|
||||
label: device.label,
|
||||
level: sprintf("%f", (device.brightness ?: 1) * 100),
|
||||
switch: device.connected ? device.power : "unreachable",
|
||||
colorTemperature: device.color.kelvin
|
||||
]
|
||||
if (device.capabilities.has_color) {
|
||||
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
||||
data["hue"] = device.color.hue / 3.6
|
||||
data["saturation"] = device.color.saturation * 100
|
||||
childDevice = addChildDevice("lifx", "LIFX Color Bulb", device.id, null, data)
|
||||
} else {
|
||||
childDevice = addChildDevice("lifx", "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each {
|
||||
log.info("Deleting ${it.deviceNetworkId}")
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block
|
||||
}
|
||||
|
||||
def refreshDevices() {
|
||||
log.info("Refreshing all devices...")
|
||||
getChildDevices().each { device ->
|
||||
device.refresh()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ definition(
|
||||
name: "Notify Me When",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Receive notifications when anything happens in your home.",
|
||||
description: "Get a push notification or text message when any of a variety of SmartThings is activated. Supports button push, motion, contact, acceleration, moisture and presence sensors as well as switches.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
|
||||
@@ -622,7 +622,7 @@ def greyedOutTime(starting, ending){
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
private anyoneIsHome() {
|
||||
def result = false
|
||||
|
||||
@@ -634,10 +634,10 @@ private anyoneIsHome() {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
|
||||
section {
|
||||
input "starting", "time", title: "Starting (both are required)", required: false
|
||||
input "ending", "time", title: "Ending (both are required)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user