mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 21:03:00 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e0995e43 | ||
|
|
bf7a7830cc | ||
|
|
7d90555006 | ||
|
|
04c19990cf | ||
|
|
8d4a73cd30 | ||
|
|
4bf2b55c26 | ||
|
|
13fcbfcdc7 | ||
|
|
288a4e99fa | ||
|
|
55a3842b00 | ||
|
|
a25fb6d84d | ||
|
|
d9a2d8109e | ||
|
|
0376a5823f | ||
|
|
78e25c6958 | ||
|
|
7e5d6e99d1 | ||
|
|
f5e3a16ad1 | ||
|
|
c361387bf8 | ||
|
|
f0c6e085eb | ||
|
|
67a1bd1256 | ||
|
|
32182ae13b |
@@ -1,246 +0,0 @@
|
||||
/**
|
||||
* Copyright 2015 Intraix
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Billion Smart Meter
|
||||
*
|
||||
* Author: Intraix
|
||||
* Date: 2015-09-03
|
||||
*/
|
||||
metadata {
|
||||
definition(name: "Billion Smart Meter", namespace: "intraix", author: "Intraix") {
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Energy Meter"
|
||||
capability "Power Meter"
|
||||
|
||||
// Custom attiributes for capabilities not supported by ST
|
||||
attribute "voltage", "number"
|
||||
attribute "current", "number"
|
||||
attribute "frequency", "number"
|
||||
attribute "powerFactor", "number"
|
||||
attribute "apparentPower", "number"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0006,0702", outClusters: ""
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
state "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
state "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh"
|
||||
}
|
||||
valueTile("energy", "device.energy", decoration: "flat", width: 1, height: 1) {
|
||||
state "energy", label: '${currentValue} kWh'
|
||||
}
|
||||
valueTile("power", "device.power", decoration: "flat", width: 1, height: 1) {
|
||||
state "power", label: '${currentValue} W'
|
||||
}
|
||||
valueTile("voltage", "device.voltage", decoration: "flat", width: 1, height: 1) {
|
||||
state "voltage", label: '${currentValue} V'
|
||||
}
|
||||
valueTile("current", "device.current", decoration: "flat", width: 1, height: 1) {
|
||||
state "current", label: '${currentValue} A'
|
||||
}
|
||||
valueTile("frequency", "device.frequency", decoration: "flat", width: 1, height: 1) {
|
||||
state "frequency", label: '${currentValue} Hz'
|
||||
}
|
||||
valueTile("powerFactor", "device.powerFactor", decoration: "flat", width: 1, height: 1) {
|
||||
state "powerFactor", label: 'Power Factor ${currentValue}'
|
||||
}
|
||||
valueTile("apparentPower", "device.apparentPower", decoration: "flat", width: 1, height: 1) {
|
||||
state "apparentPower", label: '${currentValue} VA'
|
||||
}
|
||||
main(["switch", "energy", "power", "voltage", "current", "frequency", "powerFactor", "apparentPower"])
|
||||
details(["switch", "energy", "power", "voltage", "current", "frequency", "powerFactor", "apparentPower", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def events = []
|
||||
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def descMap = parseCatchAllAsMap(description)
|
||||
log.debug "Catch all parsing: $description"
|
||||
// Command 01 is Read Attributes response and Command 0A is Report Attributes response.
|
||||
if (descMap.command == "01" || descMap.command == "0A") {
|
||||
if (descMap.clusterId == "0006") {
|
||||
// The last byte is the on/off value = 01/00.
|
||||
def value = descMap.raw.endsWith("01") ? "on" : "off"
|
||||
def event = createEvent(name: "switch", value: value)
|
||||
events.add(event)
|
||||
} else if (descMap.clusterId == "0702") {
|
||||
// The last 21 bytes are the energy data payload, which is 42 characters.
|
||||
def payload = descMap.raw.substring(descMap.raw.length() - 42, descMap.raw.length())
|
||||
log.debug "payload is $payload"
|
||||
events.addAll(parseEnergyPayload(payload))
|
||||
}
|
||||
}
|
||||
} else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Read attr: $description"
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "on" : "off"
|
||||
def event = createEvent(name: "switch", value: value)
|
||||
events.add(event)
|
||||
} else if (descMap.cluster == "0702" && descMap.attrId == "8000") {
|
||||
// Payload length is 21 bytes, which is 42 characters
|
||||
def payload = descMap.raw.substring(descMap.raw.length() - 42, descMap.raw.length())
|
||||
log.trace "payload is $payload"
|
||||
events.addAll(parseEnergyPayload(payload))
|
||||
}
|
||||
} else if (description?.startsWith("on/off:")) {
|
||||
log.debug "Switch command"
|
||||
def value = description?.endsWith(" 1") ? "on" : "off"
|
||||
def event = createEvent(name: "switch", value: value)
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
log.debug "Parse returned ${events}"
|
||||
return events
|
||||
}
|
||||
|
||||
|
||||
def parseEnergyPayload(payload) {
|
||||
def events = []
|
||||
|
||||
// Decode the various parameters from the payload
|
||||
def voltage = Integer.parseInt(payload.substring(0, 4), 16) / 100
|
||||
log.trace "voltage is $voltage"
|
||||
def voltageEvent = createEvent(name: "voltage", value: voltage)
|
||||
events.add(voltageEvent)
|
||||
|
||||
def current = Integer.parseInt(payload.substring(4, 8), 16) / 100
|
||||
log.trace "current is $current"
|
||||
def currentEvent = createEvent(name: "current", value: current)
|
||||
events.add(currentEvent)
|
||||
|
||||
def frequency = Integer.parseInt(payload.substring(8, 12), 16) / 100
|
||||
log.trace "frequency is $frequency"
|
||||
def frequencyEvent = createEvent(name: "frequency", value: frequency)
|
||||
events.add(frequencyEvent)
|
||||
|
||||
def powerFactor = Integer.parseInt(payload.substring(12, 14), 16) / 100
|
||||
log.trace "powerFactor is $powerFactor"
|
||||
def powerFactorEvent = createEvent(name: "powerFactor", value: powerFactor)
|
||||
events.add(powerFactorEvent)
|
||||
|
||||
def activePower = Integer.parseInt(payload.substring(14, 22), 16) / 100
|
||||
log.trace "activePower is $activePower"
|
||||
def powerEvent = createEvent(name: "power", value: activePower)
|
||||
events.add(powerEvent)
|
||||
|
||||
def apparentPower = Integer.parseInt(payload.substring(22, 30), 16) / 100
|
||||
log.trace "apparentPower is $apparentPower"
|
||||
def apparentPowerEvent = createEvent(name: "apparentPower", value: apparentPower)
|
||||
events.add(apparentPowerEvent)
|
||||
|
||||
def mainEnergy = Integer.parseInt(payload.substring(30, 42), 16) / 1000
|
||||
log.trace "mainEnergy is $mainEnergy"
|
||||
def energyEvent = createEvent(name: "energy", value: mainEnergy)
|
||||
events.add(energyEvent)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
def parseCatchAllAsMap(description) {
|
||||
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
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
// Fire event for on since meter doesn't suppport zigbee bind
|
||||
sendEvent(name: "switch", value: "on")
|
||||
'zcl on-off on'
|
||||
}
|
||||
|
||||
def off() {
|
||||
// Fire event for off since meter doesn't suppport zigbee bind
|
||||
sendEvent(name: "switch", value: "off")
|
||||
'zcl on-off off'
|
||||
}
|
||||
|
||||
def configure() {
|
||||
meterConfig() + onOffConfig() + refresh()
|
||||
}
|
||||
|
||||
// Meter reporting, min inteval 3 min and reporting interval if no activity as 4 min
|
||||
// min change in value is 01
|
||||
def meterConfig() {
|
||||
[
|
||||
"zcl global send-me-a-report 0x0702 0x8000 0x41 180 240 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
}
|
||||
|
||||
// Switch reporting, min interval 5 min and reporting interval if no activity as 10 min
|
||||
// min change in value is 01
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zcl global send-me-a-report 6 0 0x10 300 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
}
|
||||
|
||||
// Read the meter and on/off cluster attributes
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0702 0x8000"
|
||||
]
|
||||
}
|
||||
313
devicetypes/lumivietnam/lumi-dimmer.src/lumi-dimmer.groovy
Normal file
313
devicetypes/lumivietnam/lumi-dimmer.src/lumi-dimmer.groovy
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Lumi Dimmer
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Dimmer", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", endpointId: "9", deviceId: "0101", inClusters: "0000, 0003, 0006, 0008", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-DZ1"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: "Off", action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on"
|
||||
state "on", label: "On", action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off"
|
||||
}
|
||||
standardTile("on", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On', action: "on", icon: "st.switches.light.on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("off", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off', action: "off", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
}
|
||||
controlTile("levelControl", "device.levelControl", "slider", width: 2, height: 1) {
|
||||
state "default", action:"switch level.setLevel", backgroundColor:"#79b821"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "on", "off", "levelControl", "level", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
if (finalResult.type == "switch") {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
else if (finalResult.type == "level") {
|
||||
sendEvent(name: "level", value: finalResult.value)
|
||||
sendEvent(name: "levelControl", value: finalResult.value)
|
||||
if (finalResult.value == 0)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
else
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "0")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "50")
|
||||
sendEvent(name: "levelControl", value: "50")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
value = value as Integer
|
||||
sendEvent(name: "level", value: value)
|
||||
sendEvent(name: "levelControl", value: value)
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevelWithRate(value, "0000")// + on()
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||
isDescriptionLevel(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//@return - false or "success" or level [0-100]
|
||||
def isDescriptionLevel(descMap) {
|
||||
def dimmerValue = -1
|
||||
if (descMap.cluster == "0008"){
|
||||
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||
def value = convertHexToInt(descMap.value)
|
||||
dimmerValue = Math.round(value * 100 / 255)
|
||||
if(dimmerValue==0 && value > 0) {
|
||||
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||
}
|
||||
}
|
||||
else if(descMap.clusterId == "0008") {
|
||||
if(descMap.command=="0B"){
|
||||
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmerValue != -1){
|
||||
return [type: "level", value : dimmerValue]
|
||||
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||
//min level change is 01
|
||||
def levelConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 8 0 0x20 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setLevelWithRate(level, rate) {
|
||||
if(rate == null){
|
||||
rate = "0000"
|
||||
}
|
||||
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
317
devicetypes/lumivietnam/lumi-shade.src/lumi-shade.groovy
Normal file
317
devicetypes/lumivietnam/lumi-shade.src/lumi-shade.groovy
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Lumi Shade
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Shade", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", endpointId: "9", deviceId: "0200", inClusters: "0000, 0003, 0006, 0008", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-BZ1"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: "Closed", action: "switch.on", icon: "st.doors.garage.garage-closed", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
state "turningOn", label: "Opening", action:" switch.on", icon: "st.doors.garage.garage-opening", backgroundColor: "#79b821"
|
||||
state "on", label: "Opened", action: "switch.off", icon: "st.doors.garage.garage-open", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
state "turningOff", label: "Closing", action:" switch.off", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("on", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'Open', action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("off", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Close', action: "off", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffffff"
|
||||
}
|
||||
controlTile("levelControl", "device.levelControl", "slider", width: 2, height: 1) {
|
||||
state "default", action:"switch level.setLevel", backgroundColor:"#79b821"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Shade ${currentValue}%'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "on", "off", "levelControl", "level", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
if (finalResult.type == "switch") {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
else if (finalResult.type == "level") {
|
||||
sendEvent(name: "level", value: finalResult.value)
|
||||
sendEvent(name: "levelControl", value: finalResult.value)
|
||||
if (finalResult.value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "100")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "0x${device.deviceNetworkId} Endpoint ${endpointId}"
|
||||
sendEvent(name: "level", value: "100")
|
||||
sendEvent(name: "levelControl", value: "0")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
value = value as Integer
|
||||
sendEvent(name: "level", value: value)
|
||||
sendEvent(name: "levelControl", value: value)
|
||||
if (value == 100) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevelWithRate(value, "0000")// + on()
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||
isDescriptionLevel(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//@return - false or "success" or level [0-100]
|
||||
def isDescriptionLevel(descMap) {
|
||||
def dimmerValue = -1
|
||||
if (descMap.cluster == "0008"){
|
||||
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||
def value = convertHexToInt(descMap.value)
|
||||
dimmerValue = Math.round(value * 100 / 255)
|
||||
if(dimmerValue==0 && value > 0) {
|
||||
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||
}
|
||||
}
|
||||
else if(descMap.clusterId == "0008") {
|
||||
if(descMap.command=="0B"){
|
||||
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmerValue != -1){
|
||||
return [type: "level", value : dimmerValue]
|
||||
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||
//min level change is 01
|
||||
def levelConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 8 0 0x20 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setLevelWithRate(level, rate) {
|
||||
if(rate == null){
|
||||
rate = "0000"
|
||||
}
|
||||
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
195
devicetypes/lumivietnam/lumi-switch-1.src/lumi-switch-1.groovy
Normal file
195
devicetypes/lumivietnam/lumi-switch-1.src/lumi-switch-1.groovy
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Lumi Switch 4
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 1", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ1"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On', action: "on1", icon: "st.switches.light.on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off', action: "off1", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "switch1"
|
||||
details(["switch1", "onAll", "offAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
253
devicetypes/lumivietnam/lumi-switch-2.src/lumi-switch-2.groovy
Normal file
253
devicetypes/lumivietnam/lumi-switch-2.src/lumi-switch-2.groovy
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Lumi Switch 2
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 2", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ2"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2"])
|
||||
details(["switch1", "switch2", "switchAll", "onAll", "offAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
281
devicetypes/lumivietnam/lumi-switch-3.src/lumi-switch-3.groovy
Normal file
281
devicetypes/lumivietnam/lumi-switch-3.src/lumi-switch-3.groovy
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Lumi Switch 3
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 3", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switch3", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "on3"
|
||||
command "off3"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ3"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switch3", "device.switch3", canChangeIcon: true) {
|
||||
state "on3", label: "SW3", action: "off3", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off3"
|
||||
state "off3", label: "SW3", action:"on3", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on3"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2", "switch3"])
|
||||
details(["switch1", "switch2", "switch3", "onAll", "offAll", "switchAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
else if (finalResult.srcEP == "05") {
|
||||
state.sw3 = finalResult.value;
|
||||
sendEvent(name: "switch3", value: finalResult.value=="on"?"on3":"off3")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off" && state.sw3 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on" && state.sw3 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 5 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 5"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 5 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on3() {
|
||||
log.debug "Executing 'on3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off3() {
|
||||
log.debug "Executing 'off3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3 5"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3 5"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
309
devicetypes/lumivietnam/lumi-switch-4.src/lumi-switch-4.groovy
Normal file
309
devicetypes/lumivietnam/lumi-switch-4.src/lumi-switch-4.groovy
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Lumi Switch 4
|
||||
*
|
||||
* Copyright 2015 Lumi Vietnam
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Lumi Switch 4", namespace: "lumivietnam", author: "Lumi Vietnam") {
|
||||
capability "Actuator"
|
||||
//capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switch3", "string"
|
||||
attribute "switch4", "string"
|
||||
attribute "switchAll", "string"
|
||||
|
||||
command "on1"
|
||||
command "off1"
|
||||
command "on2"
|
||||
command "off2"
|
||||
command "on3"
|
||||
command "off3"
|
||||
command "on4"
|
||||
command "off4"
|
||||
command "onAll"
|
||||
command "offAll"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0100", inClusters: "0000, 0003, 0006", outClusters: "0000", manufacturer: "Lumi Vietnam", model: "LM-SZ4"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off 1": "on/off: 1"
|
||||
reply "zcl on-off 0": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch1", "device.switch1", canChangeIcon: true) {
|
||||
state "on1", label: "SW1", action: "off1", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off1"
|
||||
state "off1", label: "SW1", action: "on1", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on1"
|
||||
}
|
||||
standardTile("switch2", "device.switch2", canChangeIcon: true) {
|
||||
state "on2", label: "SW2", action: "off2", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off2"
|
||||
state "off2", label: "SW2", action: "on2", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on2"
|
||||
}
|
||||
standardTile("switch3", "device.switch3", canChangeIcon: true) {
|
||||
state "on3", label: "SW3", action: "off3", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off3"
|
||||
state "off3", label: "SW3", action:"on3", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on3"
|
||||
}
|
||||
standardTile("switch4", "device.switch4", canChangeIcon: true) {
|
||||
state "on4", label: "SW4", action: "off4", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "off4"
|
||||
state "off4", label: "SW4", action:"on4", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "on4"
|
||||
}
|
||||
standardTile("switchAll", "device.switchAll", canChangeIcon: false) {
|
||||
state "onAll", label: "All", action: "offAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#79b821", nextState: "offAll"
|
||||
state "offAll", label: "All", action: "onAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff", nextState: "onAll"
|
||||
}
|
||||
standardTile("onAll", "device.onAll", decoration: "flat") {
|
||||
state "default", label: 'On All', action: "onAll", icon: "st.lights.multi-light-bulb-on", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("offAll", "device.offAll", decoration: "flat") {
|
||||
state "default", label: 'Off All', action: "offAll", icon: "st.lights.multi-light-bulb-off", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["switchAll", "switch1", "switch2", "switch3", "switch4"])
|
||||
details(["switch1", "switch2", "onAll", "switch3", "switch4", "offAll", "switchAll", "refresh" ])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "switch") {
|
||||
if (finalResult.srcEP == "01") {
|
||||
state.sw1 = finalResult.value;
|
||||
sendEvent(name: "switch1", value: finalResult.value=="on"?"on1":"off1")
|
||||
}
|
||||
else if (finalResult.srcEP == "03") {
|
||||
state.sw2 = finalResult.value;
|
||||
sendEvent(name: "switch2", value: finalResult.value=="on"?"on2":"off2")
|
||||
}
|
||||
else if (finalResult.srcEP == "05") {
|
||||
state.sw3 = finalResult.value;
|
||||
sendEvent(name: "switch3", value: finalResult.value=="on"?"on3":"off3")
|
||||
}
|
||||
else if (finalResult.srcEP == "07") {
|
||||
state.sw4 = finalResult.value;
|
||||
sendEvent(name: "switch4", value: finalResult.value=="on"?"on4":"off4")
|
||||
}
|
||||
//update state for switchAll Tile
|
||||
if (state.sw1 == "off" && state.sw2 == "off" && state.sw3 == "off" && state.sw4 == "off") {
|
||||
//log.debug "offalll"
|
||||
sendEvent(name: "switchAll", value: "offAll")
|
||||
}
|
||||
else if (state.sw1 == "on" && state.sw2 == "on" && state.sw3 == "on" && state.sw4 == "on") {
|
||||
//log.debug "onall"
|
||||
sendEvent(name: "switchAll", value: "onAll")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
//Config binding and report for each endpoint
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 3", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 5 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 5", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 7 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0006 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 7"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
//Read Attribute On Off Value of each endpoint
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 5 0x0006 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 7 0x0006 0"
|
||||
]
|
||||
}
|
||||
|
||||
def on1() {
|
||||
log.debug "Executing 'on1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off1() {
|
||||
log.debug "Executing 'off1' 0x${device.deviceNetworkId} endpoint 1"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on2() {
|
||||
log.debug "Executing 'on2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off2() {
|
||||
log.debug "Executing 'off2' 0x${device.deviceNetworkId} endpoint 3"
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on3() {
|
||||
log.debug "Executing 'on3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off3() {
|
||||
log.debug "Executing 'off3' 0x${device.deviceNetworkId} endpoint 5"
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def on4() {
|
||||
log.debug "Executing 'on4' 0x${device.deviceNetworkId} endpoint 7"
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 1 {}"
|
||||
}
|
||||
|
||||
def off4() {
|
||||
log.debug "Executing 'off4' 0x${device.deviceNetworkId} endpoint 7"
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 0 {}"
|
||||
}
|
||||
|
||||
def onAll() {
|
||||
log.debug "Executing 'onAll' 0x${device.deviceNetworkId} endpoint 1 3 5 7"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 1 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 1 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def offAll() {
|
||||
log.debug "Executing 'offAll' 0x${device.deviceNetworkId} endpoint 1 3 5 7"
|
||||
[
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 3 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 5 0x0006 0 {}", "delay 200",
|
||||
"st cmd 0x${device.deviceNetworkId} 7 0x0006 0 {}"
|
||||
]
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue, srcEP : descMap.sourceEndpoint]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Signal Strength"
|
||||
@@ -0,0 +1,989 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
@@ -28,6 +28,9 @@ 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"
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
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-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"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
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: "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"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
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"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
@@ -64,21 +64,23 @@
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
/*
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
|
||||
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||
standardTile("status", "device.contact", width: 2, height: 2) {
|
||||
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
@@ -110,8 +112,8 @@
|
||||
}
|
||||
|
||||
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
393
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
393
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user