Compare commits
1 Commits
user866372
...
MSA-2980-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97bdd767eb |
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Xiaomi Aqara Door/Window Sensor
|
||||
*Funziona zippy83
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* added DH Colours
|
||||
* added 100% battery max
|
||||
* fixed battery parsing problem
|
||||
* added lastcheckin attribute and tile
|
||||
* added extra tile to show when last opened
|
||||
* colours to confirm to new smartthings standards
|
||||
* added ability to force override current state to Open or Closed.
|
||||
* added experimental health check as worked out by rolled54.Why
|
||||
* Bspranger - Adding Aqara Support
|
||||
* Rinkelk - added date-attribute support for Webcore
|
||||
* Rinkelk - Changed battery percentage with code from cancrusher
|
||||
* Rinkelk - Changed battery icon according to Mobile785
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Vale Xiaomi Aqara Door/Window Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastOpened", "String"
|
||||
attribute "lastOpenedDate", "Date"
|
||||
attribute "lastCheckinDate", "Date"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "5F01", inClusters: "0000, 0003, FFFF, 0006", outClusters: "0000, 0004, FFFF", manufacturer: "LUMI", model: "lumi.sensor_magnet.aq2", deviceJoinName: "Xiaomi Aqara Door Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
command "resetClosed"
|
||||
command "resetOpen"
|
||||
command "Refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "closed": "on/off: 0"
|
||||
status "open": "on/off: 1"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
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:"#e86d13"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc"
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
standardTile("icon", "device.refresh", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "default", label:'Last Opened:', icon:"st.Entertainment.entertainment15"
|
||||
}
|
||||
valueTile("lastopened", "device.lastOpened", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}%', unit:"",
|
||||
backgroundColors: [
|
||||
[value: 10, color: "#bc2323"],
|
||||
[value: 26, color: "#f1d801"],
|
||||
[value: 51, color: "#44b621"] ]
|
||||
}
|
||||
|
||||
standardTile("resetClosed", "device.resetClosed", inactiveLabel: false, decoration: "flat", width: 3, height: 1) {
|
||||
state "default", action:"resetClosed", label: "Override Close", icon:"st.contact.contact.closed"
|
||||
}
|
||||
standardTile("resetOpen", "device.resetOpen", inactiveLabel: false, decoration: "flat", width: 3, height: 1) {
|
||||
state "default", action:"resetOpen", label: "Override Open", icon:"st.contact.contact.open"
|
||||
}
|
||||
standardTile("refresh", "command.refresh", inactiveLabel: false) {
|
||||
state "default", label:'refresh', action:"refresh.refresh", icon:"st.secondary.refresh-icon"
|
||||
}
|
||||
|
||||
main (["contact"])
|
||||
details(["contact","battery","icon","lastopened","resetClosed","resetOpen","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Parsing '${description}'"
|
||||
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
def nowDate = new Date(now).getTime()
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
sendEvent(name: "lastCheckinDate", value: nowDate)
|
||||
|
||||
Map map = [:]
|
||||
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
sendEvent(name: "lastOpened", value: now)
|
||||
sendEvent(name: "lastOpenedDate", value: nowDate)
|
||||
}
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
log.debug "${linkText}: Parse returned $map"
|
||||
def results = map ? createEvent(map) : null
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
def linkText = getLinkText(device)
|
||||
//log.debug '${linkText} Battery'
|
||||
|
||||
//log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 1000
|
||||
def minVolts = 2.0
|
||||
def maxVolts = 3.04
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.translatable = true
|
||||
result.descriptionText = "${device.displayName} battery was ${result.value}%, ${volts} volts"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug "${linkText}: Parsing CatchAll: '${cluster}'"
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
if ((cluster.data.get(4) == 1) && (cluster.data.get(5) == 0x21)) // Check CMD and Data Type
|
||||
{
|
||||
resultMap = getBatteryResult((cluster.data.get(7)<<8) + cluster.data.get(6))
|
||||
}
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug '${linkText}: ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug '${linkText}: TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
|
||||
def configure() {
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "${linkText}: ${device.deviceNetworkId}"
|
||||
def endpointId = 1
|
||||
log.debug "${linkText}: ${device.zigbeeId}"
|
||||
log.debug "${linkText}: ${zigbeeEui}"
|
||||
def configCmds = [
|
||||
//battery reporting and heartbeat
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||
|
||||
|
||||
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
]
|
||||
|
||||
log.debug "${linkText}: configure: Write IAS CIE"
|
||||
return configCmds
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Enrolling device into the IAS Zone"
|
||||
[
|
||||
// Enrolling device into the IAS Zone
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
]
|
||||
}
|
||||
|
||||
/*
|
||||
def refresh() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Refreshing Battery"
|
||||
def endpointId = 0x01
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000 0x0000", "delay 200"
|
||||
] //+ enrollResponse()
|
||||
}
|
||||
*/
|
||||
|
||||
def refresh() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: refreshing"
|
||||
// [
|
||||
// "st rattr 0x${device.deviceNetworkId} 1 0 0", "delay 500",
|
||||
// "st rattr 0x${device.deviceNetworkId} 1 0", "delay 250",
|
||||
// ]
|
||||
zigbee.configureReporting(0x0001, 0x0021, 0x20, 300, 600, 0x01)
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
def result
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
if (description == 'on/off: 0') //contact closed
|
||||
result = getContactResult("closed")
|
||||
else if (description == 'on/off: 1') //contact opened
|
||||
result = getContactResult("open")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
return [
|
||||
name: 'contact',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
def resetClosed() {
|
||||
sendEvent(name:"contact", value:"closed")
|
||||
}
|
||||
|
||||
def resetOpen() {
|
||||
sendEvent(name:"contact", value:"open")
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Device wakes up every 1 hour, this interval allows us to miss one wakeup notification before marking offline
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Configured health checkInterval when installed()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Configured health checkInterval when updated()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Xiaomi Aqara Motion Sensor
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015
|
||||
* modified 29/12/2016 a4refillpad
|
||||
* Added fingerprinting
|
||||
* Added heartbeat/lastcheckin for monitoring
|
||||
* Added battery and refresh
|
||||
* Motion background colours consistent with latest DH
|
||||
* Fixed max battery percentage to be 100%
|
||||
* Added Last update to main tile
|
||||
* Added last motion tile
|
||||
* Heartdeat icon plus improved localisation of date
|
||||
* removed non working tiles and changed layout and incorporated latest colours
|
||||
* added experimental health check as worked out by rolled54.Why
|
||||
*Funziona Zipppy83 https://github.com/bspranger/Xiaomi/blob/master/devicetypes/a4refillpad/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Vale Xiaomi Aqara Motion Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Motion Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastMotion", "String"
|
||||
attribute "Light", "number"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0003, FFFF, 0019", outClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0019", manufacturer: "LUMI", model: "lumi.sensor_motion", deviceJoinName: "Xiaomi Motion"
|
||||
|
||||
command "reset"
|
||||
command "Refresh"
|
||||
|
||||
}
|
||||
|
||||
simulator {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input "motionReset", "number", title: "Number of seconds after the last reported activity to report that motion is inactive (in seconds). \n\n(The device will always remain blind to motion for 60seconds following first detected motion. This value just clears the 'active' status after the number of seconds you set here but the device will still remain blind for 60seconds in normal operation.)", description: "", value:120, displayDuringSetup: false
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||
attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00a0dc"
|
||||
attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
valueTile("Light", "device.Light", decoration: "flat", inactiveLabel: false, width: 2, height: 2){
|
||||
state "Light", label:'${currentValue}% \nLight', unit: ""
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
|
||||
state "default", action:"reset", label: "Reset Motion"
|
||||
}
|
||||
standardTile("icon", "device.refresh", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "default", label:'Last Motion:', icon:"st.Entertainment.entertainment15"
|
||||
}
|
||||
valueTile("lastmotion", "device.lastMotion", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
standardTile("refresh", "command.refresh", inactiveLabel: false) {
|
||||
state "default", label:'refresh', action:"refresh.refresh", icon:"st.secondary.refresh-icon"
|
||||
}
|
||||
|
||||
main(["motion"])
|
||||
details(["motion", "Light", "battery", "icon", "lastmotion", "reset", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText} Parsing: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('illuminance:')) {
|
||||
map = parseIlluminanceMessage(description)
|
||||
}
|
||||
|
||||
log.debug "${linkText} Parse returned: $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "${linkText} enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseIlluminanceMessage(String description)
|
||||
{
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'Light',
|
||||
value: '--'
|
||||
]
|
||||
def value = ((description - "illuminance: ").trim()) as Float
|
||||
|
||||
result.value = value
|
||||
result.descriptionText = "${linkText} Light was ${result.value}"
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
def linkText = getLinkText(device)
|
||||
//log.debug '${linkText} Battery'
|
||||
|
||||
//log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 1000
|
||||
def minVolts = 2.0
|
||||
def maxVolts = 3.04
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.translatable = true
|
||||
result.descriptionText = "${device.displayName} battery was ${result.value}%, ${volts} volts"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
if ((cluster.data.get(4) == 1) && (cluster.data.get(5) == 0x21)) // Check CMD and Data Type
|
||||
{
|
||||
resultMap = getBatteryResult((cluster.data.get(7)<<8) + cluster.data.get(6))
|
||||
}
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug '${linkText}: ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug '${linkText}: TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
|
||||
def configure() {
|
||||
def linkText = getLinkText(device)
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "${linkText}: ${device.deviceNetworkId}"
|
||||
def endpointId = 1
|
||||
log.debug "${linkText}: ${device.zigbeeId}"
|
||||
log.debug "${linkText}: ${zigbeeEui}"
|
||||
def configCmds = [
|
||||
//battery reporting and heartbeat
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||
|
||||
|
||||
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
]
|
||||
|
||||
log.debug "${linkText} configure: Write IAS CIE"
|
||||
return configCmds
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Enrolling device into the IAS Zone"
|
||||
[
|
||||
// Enrolling device into the IAS Zone
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Refreshing Battery"
|
||||
// def endpointId = 0x01
|
||||
// [
|
||||
// "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000 0x0000", "delay 200"
|
||||
// "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000", "delay 200"
|
||||
// ] //+ enrollResponse()
|
||||
|
||||
zigbee.configureReporting(0x0001, 0x0021, 0x20, 300, 600, 0x01)
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
//log.debug "Desc Map: $descMap"
|
||||
|
||||
Map resultMap = [:]
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
|
||||
if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
sendEvent(name: "lastMotion", value: now)
|
||||
if (settings.motionReset == null || settings.motionReset == "" ) settings.motionReset = 120
|
||||
if (value == "active") runIn(settings.motionReset, stopMotion)
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
log.debug '${linkText}: motion with tamper alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug '${linkText}: no motion with tamper alarm'
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
log.debug '${linkText}: motion with failure alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map getMotionResult(value) {
|
||||
def linkText = getLinkText(device)
|
||||
//log.debug "${linkText}: motion"
|
||||
String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped"
|
||||
def commands = [
|
||||
name: 'motion',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
return commands
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def stopMotion() {
|
||||
sendEvent(name:"motion", value:"inactive")
|
||||
}
|
||||
|
||||
def reset() {
|
||||
sendEvent(name:"motion", value:"inactive")
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Device wakes up every 1 hour, this interval allows us to miss one wakeup notification before marking offline
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Configured health checkInterval when installed()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Configured health checkInterval when updated()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Copyright 2017 A4refillpad
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 2017-03 First release of the Xiaomi Temp/Humidity Device Handler
|
||||
* 2017-03 Includes battery level (hope it works, I've only had access to a device for a limited period, time will tell!)
|
||||
* 2017-03 Last checkin activity to help monitor health of device and multiattribute tile
|
||||
* 2017-03 Changed temperature to update on .1° changes - much more useful
|
||||
* 2017-03-08 Changed the way the battery level is being measured. Very different to other Xiaomi sensors.
|
||||
* 2017-03-23 Added Fahrenheit support
|
||||
* 2017-03-25 Minor update to display unknown battery as "--", added fahrenheit colours to main and device tiles
|
||||
* 2017-03-29 Temperature offset preference added to handler
|
||||
*
|
||||
* known issue: these devices do not seem to respond to refresh requests left in place in case things change
|
||||
* known issue: tile formatting on ios and android devices vary a little due to smartthings app - again, nothing I can do about this
|
||||
* known issue: there's nothing I can do about the pairing process with smartthings. it is indeed non standard, please refer to community forum for details
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Vale Xiaomi Aqara Temperature Humidity Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000, 0003, FFFF, 0402, 0403, 0405", outClusters: "0000, 0004, FFFF", manufacturer: "LUMI", model: "lumi.weather", deviceJoinName: "Xiaomi Aqara Temp Sensor"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
for (int i = 0; i <= 100; i += 10) {
|
||||
status "${i}F": "temperature: $i F"
|
||||
}
|
||||
|
||||
for (int i = 0; i <= 100; i += 10) {
|
||||
status "${i}%": "humidity: ${i}%"
|
||||
}
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
input title: "Temperature Offset", 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'. Please note, any changes will take effect only on the NEXT temperature change.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
section {
|
||||
input title: "Pressure Offset", description: "This feature allows you to correct any pressure variations by selecting an offset. Ex: If your sensor consistently reports a pressure that's 5 kPa too high, you'd enter '-5'. If 3 kPa too low, enter '+3'. Please note, any changes will take effect only on the NEXT pressure change.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "pressOffset", "number", title: "kPa", description: "Adjust prssure by this many kPa", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"temperature", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key:"PRIMARY_CONTROL"){
|
||||
attributeState("default", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 5, color: "#1e9cbb"],
|
||||
[value: 10, color: "#90d2a7"],
|
||||
[value: 15, color: "#44b621"],
|
||||
[value: 20, color: "#f1d801"],
|
||||
[value: 25, color: "#d04e00"],
|
||||
[value: 30, color: "#bc2323"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}', icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
standardTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}%', icon:"st.Weather.weather12"
|
||||
}
|
||||
|
||||
standardTile("pressure", "device.pressure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'${currentValue} kPa', icon:"st.Weather.weather1"
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
valueTile("temperature2", "device.temperature", decoration: "flat", inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}°', icon: "st.Weather.weather2",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 5, color: "#1e9cbb"],
|
||||
[value: 10, color: "#90d2a7"],
|
||||
[value: 15, color: "#44b621"],
|
||||
[value: 20, color: "#f1d801"],
|
||||
[value: 25, color: "#d04e00"],
|
||||
[value: 30, color: "#bc2323"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["temperature2"])
|
||||
details(["temperature", "battery", "humidity","pressure","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Device wakes up every 1 hour, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when installed()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when updated()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText} Parsing: $description"
|
||||
def name = parseName(description)
|
||||
log.debug "${linkText} Parsename: $name"
|
||||
def value = parseValue(description)
|
||||
log.debug "${linkText} Parsevalue: $value"
|
||||
def unit = (name == "temperature") ? getTemperatureScale() : ((name == "humidity") ? "%" : ((name == "pressure")? "kpa": null))
|
||||
def result = createEvent(name: name, value: value, unit: unit)
|
||||
log.debug "${linkText} Evencreated: $name, $value, $unit"
|
||||
log.debug "${linkText} Parse returned: ${result?.descriptionText}"
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
return result
|
||||
}
|
||||
|
||||
private String parseName(String description) {
|
||||
|
||||
|
||||
if (description?.startsWith("temperature: ")) {
|
||||
return "temperature"
|
||||
|
||||
} else if (description?.startsWith("humidity: ")) {
|
||||
return "humidity"
|
||||
|
||||
} else if (description?.startsWith("catchall: ")) {
|
||||
return "battery"
|
||||
|
||||
} else if (description?.startsWith("read attr - raw: "))
|
||||
{
|
||||
def attrId
|
||||
attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
|
||||
if(attrId == "0000")
|
||||
{
|
||||
return "pressure"
|
||||
|
||||
} else if (attrId == "0005")
|
||||
{
|
||||
return "model"
|
||||
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private String parseValue(String description) {
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
if (description?.startsWith("temperature: ")) {
|
||||
def value = ((description - "temperature: ").trim()) as Float
|
||||
|
||||
if (value > 100)
|
||||
{
|
||||
value = 100.0 - value
|
||||
}
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
if (tempOffset) {
|
||||
return (Math.round(value * 10))/ 10 + tempOffset as Float
|
||||
} else {
|
||||
return (Math.round(value * 10))/ 10 as Float
|
||||
}
|
||||
} else {
|
||||
if (tempOffset) {
|
||||
return (Math.round(value * 90/5))/10 + 32 + offset as Float
|
||||
} else {
|
||||
return (Math.round(value * 90/5))/10 + 32 as Float
|
||||
}
|
||||
}
|
||||
|
||||
} else if (description?.startsWith("humidity: ")) {
|
||||
def pct = (description - "humidity: " - "%").trim()
|
||||
|
||||
if (pct.isNumber()) {
|
||||
return Math.round(new BigDecimal(pct)).toString()
|
||||
}
|
||||
} else if (description?.startsWith("catchall: ")) {
|
||||
return parseCatchAllMessage(description)
|
||||
|
||||
} else if (description?.startsWith("read attr - raw: ")){
|
||||
return parseReadAttrMessage(description)
|
||||
|
||||
}else {
|
||||
log.debug "${linkText} unknown: $description"
|
||||
sendEvent(name: "unknown", value: description)
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
private String parseReadAttrMessage(String description) {
|
||||
def result = '--'
|
||||
def cluster
|
||||
def attrId
|
||||
def value
|
||||
|
||||
cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
|
||||
attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
//log.debug "cluster: ${cluster}, attrId: ${attrId}, value: ${value}"
|
||||
|
||||
if (cluster == "0403" && attrId == "0000") {
|
||||
result = value[0..3]
|
||||
int pressureval = Integer.parseInt(result, 16)
|
||||
if (pressOffset)
|
||||
{
|
||||
result = ((pressureval/100) as Float) + pressOffset
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (pressureval/100 as Float)
|
||||
}
|
||||
}
|
||||
else if (cluster == "0000" && attrId == "0005")
|
||||
{
|
||||
for (int i = 0; i < value.length(); i+=2)
|
||||
{
|
||||
def str = value.substring(i, i+2);
|
||||
def NextChar = (char)Integer.parseInt(str, 16);
|
||||
result = result + NextChar
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private String parseCatchAllMessage(String description) {
|
||||
def result = '--'
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
result = getBatteryResult(cluster.data.get(6))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private String getBatteryResult(rawValue) {
|
||||
//log.debug 'Battery'
|
||||
//def linkText = getLinkText(device)
|
||||
//log.debug rawValue
|
||||
|
||||
def result = '--'
|
||||
def maxBatt = 100
|
||||
def battLevel = Math.round(rawValue * 100 / 255)
|
||||
|
||||
if (battLevel > maxBatt) {
|
||||
battLevel = maxBatt
|
||||
}
|
||||
|
||||
return battLevel
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: refresh called"
|
||||
def refreshCmds = [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x00", "delay 2000",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 2000"
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 900) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "${linkText}: Sending enroll response"
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 2000"
|
||||
]
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Xiaomi Zigbee Buttonx
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* added 100% battery max
|
||||
* fixed battery parsing problem
|
||||
* added lastcheckin attribute and tile
|
||||
* added a means to also push button in as tile on smartthings app
|
||||
* fixed ios tile label problem and battery bug
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Zigbee Buttonx", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Battery"
|
||||
capability "Button"
|
||||
capability "Holdable Button"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Momentary"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "lastPress", "string"
|
||||
attribute "batterylevel", "string"
|
||||
attribute "lastCheckin", "string"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0003", outClusters: "0000, 0004, 0003, 0006, 0008, 0005", manufacturer: "LUMI", model: "lumi.sensor_switch", deviceJoinName: "Xiaomi Button"
|
||||
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "button 1 pressed": "on/off: 0"
|
||||
status "button 1 released": "on/off: 1"
|
||||
}
|
||||
|
||||
preferences{
|
||||
input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
|
||||
defaultValue: 4, displayDuringSetup: false)
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState("on", label:' push', action: "momentary.push", backgroundColor:"#53a7c0")
|
||||
attributeState("off", label:' push', action: "momentary.push", backgroundColor:"#ffffff", nextState: "on")
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
main (["switch"])
|
||||
details(["switch", "battery", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
def results = []
|
||||
if (description?.startsWith('on/off: '))
|
||||
results = parseCustomMessage(description)
|
||||
if (description?.startsWith('catchall:'))
|
||||
results = parseCatchAllMessage(description)
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
def configure(){
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 2 0 {${device.zigbeeId}} {}", "delay 5000",
|
||||
"zcl global send-me-a-report 2 0 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 2"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh(){
|
||||
"st rattr 0x${device.deviceNetworkId} 1 2 0"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0 0"
|
||||
log.debug "refreshing"
|
||||
|
||||
createEvent([name: 'batterylevel', value: '100', data:[buttonNumber: 1], displayed: false])
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
int battValue = rawValue
|
||||
|
||||
def maxbatt = 100
|
||||
|
||||
if (battValue > maxbatt) {
|
||||
battValue = maxbatt
|
||||
}
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: battValue,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${linkText} battery was ${battValue}%"
|
||||
]
|
||||
|
||||
log.debug result.descriptionText
|
||||
state.lastbatt = new Date().time
|
||||
return createEvent(result)
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
if (description == 'on/off: 0') //button pressed
|
||||
return createPressEvent(1)
|
||||
else if (description == 'on/off: 1') //button released
|
||||
return createButtonEvent(1)
|
||||
}
|
||||
}
|
||||
|
||||
//this method determines if a press should count as a push or a hold and returns the relevant event type
|
||||
private createButtonEvent(button) {
|
||||
def currentTime = now()
|
||||
def startOfPress = device.latestState('lastPress').date.getTime()
|
||||
def timeDif = currentTime - startOfPress
|
||||
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000
|
||||
|
||||
if (timeDif < 0)
|
||||
return [] //likely a message sequence issue. Drop this press and wait for another. Probably won't happen...
|
||||
else if (timeDif < holdTimeMillisec)
|
||||
return createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true)
|
||||
else
|
||||
return createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true)
|
||||
}
|
||||
|
||||
private createPressEvent(button) {
|
||||
return createEvent([name: 'lastPress', value: now(), data:[buttonNumber: button], displayed: false])
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def push() {
|
||||
sendEvent(name: "switch", value: "on", isStateChange: true, displayed: false)
|
||||
sendEvent(name: "switch", value: "off", isStateChange: true, displayed: false)
|
||||
sendEvent(name: "momentary", value: "pushed", isStateChange: true)
|
||||
sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$device.displayName button 1 was pushed", isStateChange: true)
|
||||
}
|
||||
|
||||
def on() {
|
||||
push()
|
||||
}
|
||||
|
||||
def off() {
|
||||
push()
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Xiaomi Aqara Door/Window Sensor
|
||||
* Version 1.1
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original device handler code by a4refillpad, adapted for use with Aqara model by bspranger
|
||||
* Additional contributions to code by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh
|
||||
*
|
||||
* Known issues:
|
||||
* Xiaomi sensors do not seem to respond to refresh requests
|
||||
* Inconsistent rendering of user interface text/graphics between iOS and Android devices - This is due to SmartThings, not this device handler
|
||||
* Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. See
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Vale test 2 Xiaomi Aqara Door/Window Sensor", namespace: "bspranger", author: "bspranger") {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastCheckinDate", "Date"
|
||||
attribute "lastOpened", "String"
|
||||
attribute "lastOpenedDate", "Date"
|
||||
attribute "batteryRuntime", "String"
|
||||
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0003,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_magnet.aq2", deviceJoinName: "Xiaomi Aqara Door Sensor"
|
||||
|
||||
command "resetBatteryRuntime"
|
||||
command "resetClosed"
|
||||
command "resetOpen"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "closed": "on/off: 0"
|
||||
status "open": "on/off: 1"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
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:"#e86d13"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc"
|
||||
}
|
||||
tileAttribute("device.lastOpened", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Opened: ${currentValue}')
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}%', unit:"%", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/XiaomiBattery.png",
|
||||
backgroundColors:[
|
||||
[value: 10, color: "#bc2323"],
|
||||
[value: 26, color: "#f1d801"],
|
||||
[value: 51, color: "#44b621"]
|
||||
]
|
||||
}
|
||||
valueTile("spacer", "spacer", decoration: "flat", inactiveLabel: false, width: 1, height: 1) {
|
||||
state "default", label:''
|
||||
}
|
||||
valueTile("lastcheckin", "device.lastCheckin", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'Last Event:\n${currentValue}'
|
||||
}
|
||||
standardTile("resetClosed", "device.resetClosed", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"resetClosed", label:'Override Close', icon:"st.contact.contact.closed"
|
||||
}
|
||||
standardTile("resetOpen", "device.resetOpen", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"resetOpen", label:'Override Open', icon:"st.contact.contact.open"
|
||||
}
|
||||
valueTile("batteryRuntime", "device.batteryRuntime", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "batteryRuntime", label:'Battery Changed:\n ${currentValue}'
|
||||
}
|
||||
|
||||
main (["contact"])
|
||||
details(["contact","battery","resetClosed","resetOpen","spacer","lastcheckin", "spacer", "spacer", "batteryRuntime", "spacer"])
|
||||
}
|
||||
preferences {
|
||||
//Date & Time Config
|
||||
input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK"
|
||||
input name: "dateformat", type: "enum", title: "Set Date Format\n US (MDY) - UK (DMY) - Other (YMD)", description: "Date Format", options:["US","UK","Other"]
|
||||
input name: "clockformat", type: "bool", title: "Use 24 hour clock?"
|
||||
//Battery Reset Config
|
||||
input description: "If you have installed a new battery, the toggle below will reset the Changed Battery date to help remember when it was changed.", type: "paragraph", element: "paragraph", title: "CHANGED BATTERY DATE RESET"
|
||||
input name: "battReset", type: "bool", title: "Battery Changed?"
|
||||
//Battery Voltage Offset
|
||||
input description: "Only change the settings below if you know what you're doing.", type: "paragraph", element: "paragraph", title: "ADVANCED SETTINGS"
|
||||
input name: "voltsmax", title: "Max Volts\nA battery is at 100% at __ volts\nRange 2.8 to 3.4", type: "decimal", range: "2.8..3.4", defaultValue: 3, required: false
|
||||
input name: "voltsmin", title: "Min Volts\nA battery is at 0% (needs replacing) at __ volts\nRange 2.0 to 2.7", type: "decimal", range: "2..2.7", defaultValue: 2.5, required: false
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
def result = zigbee.getEvent(description)
|
||||
|
||||
// Determine current time and date in the user-selected date format and clock style
|
||||
def now = formatDate()
|
||||
def nowDate = new Date(now).getTime()
|
||||
// Any report - contact sensor & Battery - results in a lastCheckin event and update to Last Checkin tile
|
||||
// However, only a non-parseable report results in lastCheckin being displayed in events log
|
||||
sendEvent(name: "lastCheckin", value: now, displayed: false)
|
||||
sendEvent(name: "lastCheckinDate", value: nowDate, displayed: false)
|
||||
|
||||
Map map = [:]
|
||||
|
||||
// Send message data to appropriate parsing function based on the type of report
|
||||
if (result) {
|
||||
log.debug "${device.displayName} Event: ${result}"
|
||||
map = getContactResult(result)
|
||||
if (map.value == "open") {
|
||||
sendEvent(name: "lastOpened", value: now, displayed: false)
|
||||
sendEvent(name: "lastOpenedDate", value: nowDate, displayed: false)
|
||||
}
|
||||
} else if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
} else if (description?.startsWith('read attr - raw:')) {
|
||||
map = parseReadAttr(description)
|
||||
}
|
||||
|
||||
log.debug "${device.displayName}: Parse returned ${map}"
|
||||
def results = map ? createEvent(map) : null
|
||||
return results
|
||||
}
|
||||
|
||||
// Convert raw 4 digit integer voltage value into percentage based on minVolts/maxVolts range
|
||||
private Map getBatteryResult(rawValue) {
|
||||
// raw voltage is normally supplied as a 4 digit integer that needs to be divided by 1000
|
||||
// but in the case the final zero is dropped then divide by 100 to get actual voltage value
|
||||
def rawVolts = rawValue / 1000
|
||||
|
||||
def minVolts
|
||||
def maxVolts
|
||||
|
||||
if(voltsmin == null || voltsmin == "")
|
||||
minVolts = 2.5
|
||||
else
|
||||
minVolts = voltsmin
|
||||
|
||||
if(voltsmax == null || voltsmax == "")
|
||||
maxVolts = 3.0
|
||||
else
|
||||
maxVolts = voltsmax
|
||||
|
||||
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.min(100, Math.round(pct * 100))
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: roundedPct,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${device.displayName} Battery at ${roundedPct}% (${rawVolts} Volts)"
|
||||
]
|
||||
|
||||
log.debug "${device.displayName}: ${result}"
|
||||
return result
|
||||
}
|
||||
|
||||
// Check catchall for battery voltage data to pass to getBatteryResult for conversion to percentage report
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def catchall = zigbee.parse(description)
|
||||
log.debug catchall
|
||||
|
||||
if (catchall.clusterId == 0x0000) {
|
||||
def MsgLength = catchall.data.size()
|
||||
// Xiaomi CatchAll does not have identifiers, first UINT16 is Battery
|
||||
if ((catchall.data.get(0) == 0x01 || catchall.data.get(0) == 0x02) && (catchall.data.get(1) == 0xFF)) {
|
||||
for (int i = 4; i < (MsgLength-3); i++) {
|
||||
if (catchall.data.get(i) == 0x21) { // check the data ID and data type
|
||||
// next two bytes are the battery voltage
|
||||
resultMap = getBatteryResult((catchall.data.get(i+2)<<8) + catchall.data.get(i+1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Parse raw data on reset button press to retrieve reported battery voltage
|
||||
private Map parseReadAttr(String description) {
|
||||
log.debug "${device.displayName}: reset button press detected"
|
||||
def buttonRaw = (description - "read attr - raw:")
|
||||
Map resultMap = [:]
|
||||
|
||||
def cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
|
||||
def attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
def value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
def model = value.split("01FF")[0]
|
||||
def data = value.split("01FF")[1]
|
||||
|
||||
if (cluster == "0000" && attrId == "0005") {
|
||||
def modelName = ""
|
||||
// Parsing the model
|
||||
for (int i = 0; i < model.length(); i+=2)
|
||||
{
|
||||
def str = model.substring(i, i+2);
|
||||
def NextChar = (char)Integer.parseInt(str, 16);
|
||||
modelName = modelName + NextChar
|
||||
}
|
||||
log.debug "${device.displayName} reported: cluster: ${cluster}, attrId: ${attrId}, value: ${value}, model:${modelName}, data:${data}"
|
||||
}
|
||||
if (data[4..7] == "0121") {
|
||||
resultMap = getBatteryResult(Integer.parseInt((data[10..11] + data[8..9]),16))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getContactResult(result) {
|
||||
def value = result.value == "on" ? "open" : "closed"
|
||||
def descriptionText = "${device.displayName} was ${value == "open" ? value + "ed" : value}"
|
||||
return [
|
||||
name: 'contact',
|
||||
value: value,
|
||||
isStateChange: true,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
def resetClosed() {
|
||||
sendEvent(name: "contact", value: "closed", descriptionText: "${device.displayName} was manually reset to closed")
|
||||
}
|
||||
|
||||
def resetOpen() {
|
||||
def now = formatDate()
|
||||
def nowDate = new Date(now).getTime()
|
||||
sendEvent(name: "lastOpened", value: now, displayed: false)
|
||||
sendEvent(name: "lastOpenedDate", value: nowDate, displayed: false)
|
||||
sendEvent(name: "contact", value: "open", descriptionText: "${device.displayName} was manually reset to open")
|
||||
}
|
||||
|
||||
//Reset the date displayed in Battery Changed tile to current date
|
||||
def resetBatteryRuntime(paired) {
|
||||
def now = formatDate(true)
|
||||
def newlyPaired = paired ? " for newly paired sensor" : ""
|
||||
sendEvent(name: "batteryRuntime", value: now)
|
||||
log.debug "${device.displayName}: Setting Battery Changed to current date${newlyPaired}"
|
||||
}
|
||||
|
||||
// installed() runs just after a sensor is paired using the "Add a Thing" method in the SmartThings mobile app
|
||||
def installed() {
|
||||
state.battery = 0
|
||||
if (!batteryRuntime) resetBatteryRuntime(true)
|
||||
checkIntervalEvent("installed")
|
||||
}
|
||||
|
||||
// configure() runs after installed() when a sensor is paired
|
||||
def configure() {
|
||||
log.debug "${device.displayName}: configuring"
|
||||
state.battery = 0
|
||||
if (!batteryRuntime) resetBatteryRuntime(true)
|
||||
checkIntervalEvent("configured")
|
||||
return
|
||||
}
|
||||
|
||||
// updated() will run twice every time user presses save in preference settings page
|
||||
def updated() {
|
||||
checkIntervalEvent("updated")
|
||||
if(battReset){
|
||||
resetBatteryRuntime()
|
||||
device.updateSetting("battReset", false)
|
||||
}
|
||||
}
|
||||
|
||||
private checkIntervalEvent(text) {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "${device.displayName}: Configured health checkInterval when ${text}()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def formatDate(batteryReset) {
|
||||
def correctedTimezone = ""
|
||||
def timeString = clockformat ? "HH:mm:ss" : "h:mm:ss aa"
|
||||
|
||||
// If user's hub timezone is not set, display error messages in log and events log, and set timezone to GMT to avoid errors
|
||||
if (!(location.timeZone)) {
|
||||
correctedTimezone = TimeZone.getTimeZone("GMT")
|
||||
log.error "${device.displayName}: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app."
|
||||
sendEvent(name: "error", value: "", descriptionText: "ERROR: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app.")
|
||||
}
|
||||
else {
|
||||
correctedTimezone = location.timeZone
|
||||
}
|
||||
if (dateformat == "US" || dateformat == "" || dateformat == null) {
|
||||
if (batteryReset)
|
||||
return new Date().format("MMM dd yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE MMM dd yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else if (dateformat == "UK") {
|
||||
if (batteryReset)
|
||||
return new Date().format("dd MMM yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE dd MMM yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else {
|
||||
if (batteryReset)
|
||||
return new Date().format("yyyy MMM dd", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE yyyy MMM dd ${timeString}", correctedTimezone)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Xiaomi Aqara Zigbee Button
|
||||
* Works with Aqara Button models WXKG11LM / WXKG12LM
|
||||
* and Aqara Smart Light Switch models WXKG02LM / WXKG03LM
|
||||
* Version 1.2.1
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original device handler code by a4refillpad, adapted for use with Aqara model by bspranger
|
||||
* Additional contributions to code by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, veeceeoh, & xtianpaiva
|
||||
*
|
||||
* Notes on capabilities of the different models:
|
||||
* Models WXKG11LM, WXKG02LM, WXKG03LM
|
||||
* - Only single press is supported, sent as button 1 "pushed" event
|
||||
* - The 2-button Aqara Smart Light Switch model WXKG02LM is only recognized as ONE button.
|
||||
* This is because the SmartThings API ignores the data that distinguishes between left, right, or both-button presses.
|
||||
* Model WXKG12LM:
|
||||
* - Single click results in button 1 "pushed" event
|
||||
* - Hold for longer than 400ms results in button 1 "held" event
|
||||
* - Double click results in button 2 "pushed" event
|
||||
* - Shaking the button results in button 3 "pushed" event
|
||||
* - Single or double click results in custom "lastPressedCoRE" event for webCoRE use
|
||||
* - Release of button results in "lastReleasedCoRE" event for webCoRE use
|
||||
*
|
||||
* Known issues:
|
||||
* Xiaomi sensors do not seem to respond to refresh requests
|
||||
* Inconsistent rendering of user interface text/graphics between iOS and Android devices - This is due to SmartThings, not this device handler
|
||||
* Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Vale Xiaomi Aqara Button", namespace: "bspranger", author: "bspranger") {
|
||||
capability "Battery"
|
||||
capability "Sensor"
|
||||
capability "Button"
|
||||
capability "Holdable Button"
|
||||
capability "Actuator"
|
||||
capability "Momentary"
|
||||
capability "Configuration"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "string"
|
||||
attribute "lastCheckinCoRE", "string"
|
||||
attribute "lastPressed", "string"
|
||||
attribute "lastPressedCoRE", "string"
|
||||
attribute "lastReleased", "string"
|
||||
attribute "lastReleasedCoRE", "string"
|
||||
attribute "batteryRuntime", "string"
|
||||
attribute "buttonStatus", "enum", ["pushed", "held", "single-clicked", "double-clicked", "shaken", "released"]
|
||||
|
||||
// Aqara Button - original revision - model WXKG11LM
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_switch.aq2", deviceJoinName: "Aqara Button WXKG11LM"
|
||||
// Aqara Button - new revision - model WXKG12LM
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0001,0006,0012", outClusters: "0000", manufacturer: "LUMI", model: "lumi.sensor_switch.aq3", deviceJoinName: "Aqara Button WXKG12LM"
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0001,0006,0012", outClusters: "0000", manufacturer: "LUMI", model: "lumi.sensor_swit", deviceJoinName: "Aqara Button WXKG12LM"
|
||||
// Aqara Smart Light Switch - single button - model WXKG03LM
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.sensor_86sw1lu", deviceJoinName: "Aqara Switch WXKG03LM"
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.sensor_86sw1", deviceJoinName: "Aqara Switch WXKG03LM"
|
||||
// Aqara Smart Light Switch - dual button - model WXKG02LM
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.sensor_86sw2Un", deviceJoinName: "Aqara Switch WXKG02LM"
|
||||
|
||||
command "resetBatteryRuntime"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "Press button": "on/off: 0"
|
||||
status "Release button": "on/off: 1"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"buttonStatus", type: "lighting", width: 6, height: 4, canChangeIcon: false) {
|
||||
tileAttribute ("device.buttonStatus", key: "PRIMARY_CONTROL") {
|
||||
attributeState("default", label:'Pushed', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("pushed", label:'Pushed', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("held", label:'Held', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("single-clicked", label:'Single-clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("double-clicked", label:'Double-clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("shaken", label:'Shaken', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
|
||||
attributeState("released", label:'Released', action: "momentary.push", backgroundColor:"#ffffff", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonReleased.png")
|
||||
}
|
||||
tileAttribute("device.lastPressed", key: "SECONDARY_CONTROL") {
|
||||
attributeState "lastPressed", label:'Last Pressed: ${currentValue}'
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}%', unit:"%", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/XiaomiBattery.png",
|
||||
backgroundColors:[
|
||||
[value: 10, color: "#bc2323"],
|
||||
[value: 26, color: "#f1d801"],
|
||||
[value: 51, color: "#44b621"]
|
||||
]
|
||||
}
|
||||
valueTile("lastCheckin", "device.lastCheckin", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "lastCheckin", label:'Last Event:\n${currentValue}'
|
||||
}
|
||||
valueTile("batteryRuntime", "device.batteryRuntime", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "batteryRuntime", label:'Battery Changed: ${currentValue}'
|
||||
}
|
||||
main (["buttonStatus"])
|
||||
details(["buttonStatus","battery","lastCheckin","batteryRuntime"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
//Date & Time Config
|
||||
input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK"
|
||||
input name: "dateformat", type: "enum", title: "Set Date Format\nUS (MDY) - UK (DMY) - Other (YMD)", description: "Date Format", options:["US","UK","Other"]
|
||||
input name: "clockformat", type: "bool", title: "Use 24 hour clock?"
|
||||
//Battery Reset Config
|
||||
input description: "If you have installed a new battery, the toggle below will reset the Changed Battery date to help remember when it was changed.", type: "paragraph", element: "paragraph", title: "CHANGED BATTERY DATE RESET"
|
||||
input name: "battReset", type: "bool", title: "Battery Changed?"
|
||||
//Advanced Settings
|
||||
input description: "Only change the settings below if you know what you're doing.", type: "paragraph", element: "paragraph", title: "ADVANCED SETTINGS"
|
||||
//Battery Voltage Range
|
||||
input description: "", type: "paragraph", element: "paragraph", title: "BATTERY VOLTAGE RANGE"
|
||||
input name: "voltsmax", type: "decimal", title: "Max Volts\nA battery is at 100% at __ volts\nRange 2.8 to 3.4", range: "2.8..3.4", defaultValue: 3
|
||||
input name: "voltsmin", type: "decimal", title: "Min Volts\nA battery is at 0% (needs replacing) at __ volts\nRange 2.0 to 2.7", range: "2..2.7", defaultValue: 2.5
|
||||
//Live Logging Message Display Config
|
||||
input description: "These settings affect the display of messages in the Live Logging tab of the SmartThings IDE.", type: "paragraph", element: "paragraph", title: "LIVE LOGGING"
|
||||
input name: "infoLogging", type: "bool", title: "Display info log messages?", defaultValue: true
|
||||
input name: "debugLogging", type: "bool", title: "Display debug log messages?"
|
||||
}
|
||||
}
|
||||
|
||||
//adds functionality to press the centre tile as a virtualApp Button
|
||||
def push() {
|
||||
displayInfoLog(": Virtual App Button Pressed")
|
||||
sendEvent(mapButtonEvent(0))
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(description) {
|
||||
displayDebugLog(": Parsing '${description}'")
|
||||
def result = [:]
|
||||
|
||||
// Any report - button press & Battery - results in a lastCheckin event and update to Last Checkin tile
|
||||
sendEvent(name: "lastCheckin", value: formatDate(), displayed: false)
|
||||
sendEvent(name: "lastCheckinCoRE", value: now(), displayed: false)
|
||||
|
||||
// Send message data to appropriate parsing function based on the type of report
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
// Model WXKG11LM only - button press generates pushed event
|
||||
updateLastPressed("pressed")
|
||||
result = mapButtonEvent(0)
|
||||
} else if (description?.startsWith("read attr - raw: ")) {
|
||||
// Parse any model WXKG12LM button messages, or messages on short-press of reset button
|
||||
result = parseReadAttrMessage(description)
|
||||
} else if (description?.startsWith('catchall:')) {
|
||||
// Parse battery level from regular hourly announcement messages
|
||||
result = parseCatchAllMessage(description)
|
||||
}
|
||||
if (result != [:]) {
|
||||
displayDebugLog(": Creating event $result")
|
||||
return createEvent(result)
|
||||
} else
|
||||
return [:]
|
||||
}
|
||||
|
||||
private Map parseReadAttrMessage(String description) {
|
||||
def cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
|
||||
def attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
def value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
def data = ""
|
||||
def modelName = ""
|
||||
def model = value
|
||||
Map resultMap = [:]
|
||||
|
||||
// Process model WXKG12LM button message
|
||||
if (cluster == "0012") {
|
||||
// Button message values (as integer): 1 = push, 2 = double-click, 16 = hold, 17 = release, 18 = shake
|
||||
value = Integer.parseInt(value[2..3],16)
|
||||
// Convert values 16-18 to 3-5 to use as list for mapButtonEvent function
|
||||
data = (value < 3) ? value : (value - 13)
|
||||
resultMap = mapButtonEvent(data)
|
||||
}
|
||||
|
||||
// Process message on short-button press containing model name and battery voltage report
|
||||
if (cluster == "0000" && attrId == "0005") {
|
||||
if (value.length() > 45) {
|
||||
model = value.split("01FF")[0]
|
||||
data = value.split("01FF")[1]
|
||||
if (data[4..7] == "0121") {
|
||||
def BatteryVoltage = (Integer.parseInt((data[10..11] + data[8..9]),16))
|
||||
resultMap = getBatteryResult(BatteryVoltage)
|
||||
}
|
||||
data = ", data: ${value.split("01FF")[1]}"
|
||||
}
|
||||
|
||||
// Parsing the model name
|
||||
for (int i = 0; i < model.length(); i+=2) {
|
||||
def str = model.substring(i, i+2);
|
||||
def NextChar = (char)Integer.parseInt(str, 16);
|
||||
modelName = modelName + NextChar
|
||||
}
|
||||
displayDebugLog(" reported model: $modelName$data")
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Create map of values to be used for button event
|
||||
private mapButtonEvent(value) {
|
||||
def messageType = ["pushed", "single-clicked", "double-clicked", "held", "", "shaken"]
|
||||
def eventType = ["pushed", "pushed", "pushed", "held", "", "pushed"]
|
||||
def buttonNum = [1, 1, 2, 1, 0, 3]
|
||||
if (value == 4) {
|
||||
displayInfoLog(" was released")
|
||||
updateLastPressed("Released")
|
||||
sendEvent(name: "buttonStatus", value: "released", isStateChange: true, displayed: false)
|
||||
return [:]
|
||||
} else {
|
||||
displayInfoLog(" was ${messageType[value]} (Button ${buttonNum[value]} ${eventType[value]})")
|
||||
updateLastPressed("Pressed")
|
||||
sendEvent(name: "buttonStatus", value: messageType[value], isStateChange: true, displayed: false)
|
||||
if (value != 3)
|
||||
runIn(1, clearButtonStatus)
|
||||
return [
|
||||
name: 'button',
|
||||
value: eventType[value],
|
||||
data: [buttonNumber: buttonNum[value]],
|
||||
descriptionText: "$device.displayName was ${messageType[value]}",
|
||||
isStateChange: true
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// on any type of button pressed update lastPressed or lastReleased to current date/time
|
||||
def updateLastPressed(pressType) {
|
||||
displayDebugLog(": Setting Last $pressType to current date/time")
|
||||
sendEvent(name: "last${pressType}", value: formatDate(), displayed: false)
|
||||
sendEvent(name: "last${pressType}CoRE", value: now(), displayed: false)
|
||||
}
|
||||
|
||||
def clearButtonStatus() {
|
||||
sendEvent(name: "buttonStatus", value: "released", isStateChange: true, displayed: false)
|
||||
}
|
||||
|
||||
// Check catchall for battery voltage data to pass to getBatteryResult for conversion to percentage report
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def catchall = zigbee.parse(description)
|
||||
|
||||
if (catchall.clusterId == 0x0000) {
|
||||
def MsgLength = catchall.data.size()
|
||||
// Xiaomi CatchAll does not have identifiers, first UINT16 is Battery
|
||||
if ((catchall.data.get(0) == 0x01 || catchall.data.get(0) == 0x02) && (catchall.data.get(1) == 0xFF)) {
|
||||
for (int i = 4; i < (MsgLength-3); i++) {
|
||||
if (catchall.data.get(i) == 0x21) { // check the data ID and data type
|
||||
// next two bytes are the battery voltage
|
||||
resultMap = getBatteryResult((catchall.data.get(i+2)<<8) + catchall.data.get(i+1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Convert raw 4 digit integer voltage value into percentage based on minVolts/maxVolts range
|
||||
private Map getBatteryResult(rawValue) {
|
||||
// raw voltage is normally supplied as a 4 digit integer that needs to be divided by 1000
|
||||
// but in the case the final zero is dropped then divide by 100 to get actual voltage value
|
||||
def rawVolts = rawValue / 1000
|
||||
def minVolts = voltsmin ? voltsmin : 2.5
|
||||
def maxVolts = voltsmax ? voltsmax : 3.0
|
||||
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.min(100, Math.round(pct * 100))
|
||||
def descText = "Battery at ${roundedPct}% (${rawVolts} Volts)"
|
||||
displayInfoLog(": $descText")
|
||||
return [
|
||||
name: 'battery',
|
||||
value: roundedPct,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "$device.displayName $descText"
|
||||
]
|
||||
}
|
||||
|
||||
private def displayDebugLog(message) {
|
||||
if (debugLogging)
|
||||
log.debug "${device.displayName}${message}"
|
||||
}
|
||||
|
||||
private def displayInfoLog(message) {
|
||||
if (infoLogging || state.prefsSetCount < 3)
|
||||
log.info "${device.displayName}${message}"
|
||||
}
|
||||
|
||||
//Reset the date displayed in Battery Changed tile to current date
|
||||
def resetBatteryRuntime(paired) {
|
||||
def newlyPaired = paired ? " for newly paired sensor" : ""
|
||||
sendEvent(name: "batteryRuntime", value: formatDate(true))
|
||||
displayInfoLog(": Setting Battery Changed to current date${newlyPaired}")
|
||||
}
|
||||
|
||||
// installed() runs just after a sensor is paired using the "Add a Thing" method in the SmartThings mobile app
|
||||
def installed() {
|
||||
state.prefsSetCount = 0
|
||||
displayInfoLog(": Installing")
|
||||
init(0)
|
||||
checkIntervalEvent("")
|
||||
}
|
||||
|
||||
// configure() runs after installed() when a sensor is paired
|
||||
def configure() {
|
||||
displayInfoLog(": Configuring")
|
||||
init(1)
|
||||
checkIntervalEvent("configured")
|
||||
return
|
||||
}
|
||||
|
||||
// updated() will run twice every time user presses save in preference settings page
|
||||
def updated() {
|
||||
displayInfoLog(": Updating preference settings")
|
||||
if (!state.prefsSetCount)
|
||||
state.prefsSetCount = 1
|
||||
else if (state.prefsSetCount < 3)
|
||||
state.prefsSetCount = state.prefsSetCount + 1
|
||||
init(0)
|
||||
if (battReset){
|
||||
resetBatteryRuntime()
|
||||
device.updateSetting("battReset", false)
|
||||
}
|
||||
checkIntervalEvent("preferences updated")
|
||||
displayInfoLog(": Info message logging enabled")
|
||||
displayDebugLog(": Debug message logging enabled")
|
||||
}
|
||||
|
||||
def init(displayLog) {
|
||||
def modelName = device.getDataValue("model")
|
||||
def numButtons = (modelName == "lumi.sensor_switch.aq3" || modelName == "lumi.sensor_swit") ? 3 : 1
|
||||
clearButtonStatus()
|
||||
if (!device.currentState('batteryRuntime')?.value)
|
||||
resetBatteryRuntime(true)
|
||||
sendEvent(name: "numberOfButtons", value: numButtons)
|
||||
if (displayLog)
|
||||
displayInfoLog(": Number of buttons = $numButtons")
|
||||
}
|
||||
|
||||
private checkIntervalEvent(text) {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
if (text)
|
||||
displayInfoLog(": Set health checkInterval when ${text}")
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def formatDate(batteryReset) {
|
||||
def correctedTimezone = ""
|
||||
def timeString = clockformat ? "HH:mm:ss" : "h:mm:ss aa"
|
||||
|
||||
// If user's hub timezone is not set, display error messages in log and events log, and set timezone to GMT to avoid errors
|
||||
if (!(location.timeZone)) {
|
||||
correctedTimezone = TimeZone.getTimeZone("GMT")
|
||||
log.error "${device.displayName}: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app."
|
||||
sendEvent(name: "error", value: "", descriptionText: "ERROR: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app.")
|
||||
}
|
||||
else {
|
||||
correctedTimezone = location.timeZone
|
||||
}
|
||||
if (dateformat == "US" || dateformat == "" || dateformat == null) {
|
||||
if (batteryReset)
|
||||
return new Date().format("MMM dd yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE MMM dd yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else if (dateformat == "UK") {
|
||||
if (batteryReset)
|
||||
return new Date().format("dd MMM yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE dd MMM yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else {
|
||||
if (batteryReset)
|
||||
return new Date().format("yyyy MMM dd", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE yyyy MMM dd ${timeString}", correctedTimezone)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Xiaomi Aqara Zigbee Button
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* added 100% battery max
|
||||
* fixed battery parsing problem
|
||||
* added lastcheckin attribute and tile
|
||||
* added a means to also push button in as tile on smartthings app
|
||||
* fixed ios tile label problem and battery bug
|
||||
* sulee: change battery calculation
|
||||
* sulee: changed to work as a push button
|
||||
* sulee: added endpoint for Smartthings to detect properly
|
||||
* sulee: cleaned everything up
|
||||
* bspranger: renamed to bspranger to remove confusion of a4refillpad
|
||||
*
|
||||
* Fingerprint Endpoint data:
|
||||
* zbjoin: {"dni":"xxxx","d":"xxxxxxxxxxx","capabilities":"80","endpoints":[{"simple":"01 0104 5F01 01 03 0000 FFFF 0006 03 0000 0004 FFFF","application":"03","manufacturer":"LUMI","model":"lumi.sensor_switch.aq2"}],"parent":"0000","joinType":1}
|
||||
* endpoints data
|
||||
* 01 - endpoint id
|
||||
* 0104 - profile id
|
||||
* 5F01 - device id
|
||||
* 01 - ignored
|
||||
* 03 - number of in clusters
|
||||
* 0000 ffff 0006 - inClusters
|
||||
* 03 - number of out clusters
|
||||
* 0000 0004 ffff - outClusters
|
||||
* manufacturer "LUMI" - must match manufacturer field in fingerprint
|
||||
* model "lumi.sensor_switch.aq2" - must match model in fingerprint
|
||||
* deviceJoinName: whatever you want it to show in the app as a Thing
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Aqara Button", namespace: "bspranger", author: "bspranger") {
|
||||
capability "Battery"
|
||||
capability "Button"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "lastPress", "string"
|
||||
attribute "batterylevel", "string"
|
||||
attribute "lastCheckin", "string"
|
||||
attribute "lastCheckinDate", "Date"
|
||||
attribute "batteryRuntime", "String"
|
||||
|
||||
command "resetBatteryRuntime"
|
||||
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_switch.aq2", deviceJoinName: "Xiaomi Aqara Button"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "button 1 pressed": "on/off: 0"
|
||||
status "button 1 released": "on/off: 1"
|
||||
}
|
||||
|
||||
preferences{
|
||||
input ("ReleaseTime", "number", title: "Minimum time in seconds for a press to clear", defaultValue: 2, displayDuringSetup: false)
|
||||
input name: "PressType", type: "enum", options: ["Toggle", "Momentary"], description: "Effects how the button toggles", defaultValue: "Toggle", displayDuringSetup: true
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
multiAttributeTile(name:"button", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
|
||||
tileAttribute ("device.button", key: "PRIMARY_CONTROL") {
|
||||
attributeState("pushed", label:'${name}', backgroundColor:"#53a7c0")
|
||||
attributeState("released", label:'${name}', backgroundColor:"#ffffff")
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}%', unit:"",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#c0392b"],
|
||||
[value: 25, color: "#f1c40f"],
|
||||
[value: 50, color: "#e67e22"],
|
||||
[value: 75, color: "#27ae60"]
|
||||
]
|
||||
}
|
||||
valueTile("lastcheckin", "device.lastCheckin", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'Last Checkin:\n${currentValue}'
|
||||
}
|
||||
valueTile("lastpressed", "device.lastpressed", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'Last Pressed:\n${currentValue}'
|
||||
}
|
||||
standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
|
||||
state "default", label:'refresh', action:"refresh.refresh", icon:"st.secondary.refresh-icon"
|
||||
}
|
||||
standardTile("batteryRuntime", "device.batteryRuntime", inactiveLabel: false, decoration: "flat", width: 6, height: 2) {
|
||||
state "batteryRuntime", label:'Battery Changed: ${currentValue} - Tap To Reset Date', unit:"", action:"resetBatteryRuntime"
|
||||
}
|
||||
|
||||
main (["button"])
|
||||
details(["button","battery","lastcheckin","lastpressed","refresh","batteryRuntime"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def result = zigbee.getEvent(description)
|
||||
|
||||
if(result) {
|
||||
log.debug "${device.displayName}: Parsing '${description}' Event Result: ${result}"
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "${device.displayName}: Parsing '${description}'"
|
||||
}
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
def nowDate = new Date(now).getTime()
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
sendEvent(name: "lastCheckinDate", value: nowDate)
|
||||
|
||||
Map map = [:]
|
||||
|
||||
if (description?.startsWith('on/off: '))
|
||||
{
|
||||
map = parseCustomMessage(description)
|
||||
sendEvent(name: "lastpressed", value: now)
|
||||
sendEvent(name: "lastpressedDate", value: nowDate)
|
||||
}
|
||||
else if (description?.startsWith('catchall:'))
|
||||
{
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith("read attr - raw: "))
|
||||
{
|
||||
map = parseReadAttrMessage(description)
|
||||
}
|
||||
log.debug "${device.displayName}: Parse returned $map"
|
||||
def results = map ? createEvent(map) : null
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
def configure(){
|
||||
state.battery = 0
|
||||
state.button = "released"
|
||||
log.debug "${device.displayName}: configuring"
|
||||
return zigbee.readAttribute(0x0001, 0x0020) + zigbee.configureReporting(0x0001, 0x0020, 0x21, 600, 21600, 0x01)
|
||||
}
|
||||
|
||||
def refresh(){
|
||||
log.debug "${device.displayName}: refreshing"
|
||||
return zigbee.readAttribute(0x0001, 0x0020) + zigbee.configureReporting(0x0001, 0x0020, 0x21, 600, 21600, 0x01)
|
||||
}
|
||||
|
||||
private Map parseReadAttrMessage(String description) {
|
||||
def buttonRaw = (description - "read attr - raw:")
|
||||
Map resultMap = [:]
|
||||
|
||||
def cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
|
||||
def attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
def value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
def model = value.split("01FF")[0]
|
||||
def data = value.split("01FF")[1]
|
||||
//log.debug "cluster: ${cluster}, attrId: ${attrId}, value: ${value}, model:${model}, data:${data}"
|
||||
|
||||
if (data[4..7] == "0121") {
|
||||
def BatteryVoltage = (Integer.parseInt((data[10..11] + data[8..9]),16))
|
||||
resultMap = getBatteryResult(BatteryVoltage)
|
||||
log.debug "${device.displayName}: Parse returned $resultMap"
|
||||
createEvent(resultMap)
|
||||
}
|
||||
|
||||
if (cluster == "0000" && attrId == "0005") {
|
||||
resultMap.name = 'Model'
|
||||
resultMap.value = ""
|
||||
resultMap.descriptionText = "device model"
|
||||
// Parsing the model
|
||||
for (int i = 0; i < model.length(); i+=2)
|
||||
{
|
||||
def str = model.substring(i, i+2);
|
||||
def NextChar = (char)Integer.parseInt(str, 16);
|
||||
resultMap.value = resultMap.value + NextChar
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
return [:]
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
def MsgLength
|
||||
def i
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
MsgLength = cluster.data.size();
|
||||
for (i = 0; i < (MsgLength-3); i++)
|
||||
{
|
||||
if ((cluster.data.get(i) == 0x01) && (cluster.data.get(i+1) == 0x21)) // check the data ID and data type
|
||||
{
|
||||
// next two bytes are the battery voltage.
|
||||
resultMap = getBatteryResult((cluster.data.get(i+3)<<8) + cluster.data.get(i+2))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
def rawVolts = rawValue / 1000
|
||||
|
||||
def minVolts = 2.7
|
||||
def maxVolts = 3.3
|
||||
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.min(100, Math.round(pct * 100))
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: roundedPct,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${device.displayName} raw battery is ${rawVolts}v"
|
||||
]
|
||||
|
||||
log.debug "${device.displayName}: ${result}"
|
||||
if (state.battery != result.value)
|
||||
{
|
||||
state.battery = result.value
|
||||
resetBatteryRuntime()
|
||||
}
|
||||
return createEvent(result)
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
def result = [:]
|
||||
if (description?.startsWith('on/off: '))
|
||||
{
|
||||
if (PressType == "Toggle")
|
||||
{
|
||||
if ((state.button != "pushed") && (state.button != "released"))
|
||||
{
|
||||
state.button = "released"
|
||||
}
|
||||
if (state.button == "released")
|
||||
{
|
||||
result = getContactResult("pushed")
|
||||
state.button = "pushed"
|
||||
}
|
||||
else
|
||||
{
|
||||
result = getContactResult("released")
|
||||
state.button = "released"
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = getContactResult("pushed")
|
||||
state.button = "pushed"
|
||||
runIn(ReleaseTime, ReleaseButton)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def ReleaseButton()
|
||||
{
|
||||
def result = [:]
|
||||
|
||||
log.debug "${device.displayName}: Calling Release Button"
|
||||
|
||||
result = getContactResult("released")
|
||||
state.button = "released"
|
||||
log.debug "${device.displayName}: ${result}"
|
||||
sendEvent(result)
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
def descriptionText = "${device.displayName} was ${value == 'pushed' ? 'pushed' : 'released'}"
|
||||
return [
|
||||
name: 'button',
|
||||
value: value,
|
||||
isStateChange: true,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
def resetBatteryRuntime() {
|
||||
def now = new Date().format("EEE dd MMM yyyy h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "batteryRuntime", value: now)
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Fibaro Wall Plug ZW5
|
||||
* Requires: Fibaro Double Switch 2 Child Device
|
||||
*
|
||||
* Copyright 2017 Artur Draga
|
||||
*
|
||||
* 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: "Vale Fibaro Wall Plug ZW5", namespace: "ClassicGOD", author: "Artur Draga") {
|
||||
capability "Switch"
|
||||
capability "Energy Meter"
|
||||
capability "Power Meter"
|
||||
capability "Configuration"
|
||||
capability "Health Check"
|
||||
|
||||
command "reset"
|
||||
command "refresh"
|
||||
|
||||
fingerprint mfr: "010F", prod: "0602"
|
||||
fingerprint deviceId: "0x1001", inClusters:"0x5E,0x22,0x59,0x56,0x7A,0x32,0x71,0x73,0x98,0x31,0x85,0x70,0x72,0x5A,0x8E,0x25,0x86"
|
||||
fingerprint deviceId: "0x1001", inClusters:"0x5E,0x22,0x59,0x56,0x7A,0x32,0x71,0x73,0x31,0x85,0x70,0x72,0x5A,0x8E,0x25,0x86"
|
||||
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "off", label: 'Off', action: "switch.on", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/fibaro-wall-plug-zw5-v2.src/images/plug0.png", backgroundColor: "#ffffff"
|
||||
attributeState "on", label: 'On', action: "switch.off", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/fibaro-wall-plug-zw5-v2.src/images/plug2.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") {
|
||||
attributeState("multiStatus", label:'${currentValue}')
|
||||
}
|
||||
}
|
||||
valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) {
|
||||
state "power", label:'${currentValue}\nW', action:"refresh"
|
||||
}
|
||||
valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) {
|
||||
state "energy", label:'${currentValue}\nkWh', action:"refresh"
|
||||
}
|
||||
valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) {
|
||||
state "reset", label:'reset\nkWh', action:"reset"
|
||||
}
|
||||
}
|
||||
|
||||
preferences {
|
||||
|
||||
input (
|
||||
title: "Fibaro Wall Plug manual",
|
||||
description: "Tap to view the manual.",
|
||||
image: "http://manuals.fibaro.com/wp-content/uploads/2017/02/wp_icon.png",
|
||||
url: "http://manuals.fibaro.com/content/manuals/en/FGWPEF-102/FGWPEF-102-EN-A-v2.0.pdf",
|
||||
type: "href",
|
||||
element: "href"
|
||||
)
|
||||
|
||||
parameterMap().each {
|
||||
input (
|
||||
title: "${it.num}. ${it.title}",
|
||||
description: it.descr,
|
||||
type: "paragraph",
|
||||
element: "paragraph"
|
||||
)
|
||||
|
||||
input (
|
||||
name: it.key,
|
||||
title: null,
|
||||
description: "Default: $it.def" ,
|
||||
type: it.type,
|
||||
options: it.options,
|
||||
range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null,
|
||||
defaultValue: it.def,
|
||||
required: false
|
||||
)
|
||||
}
|
||||
|
||||
input ( name: "logging", title: "Logging", type: "boolean", required: false )
|
||||
}
|
||||
}
|
||||
|
||||
//UI and tile functions
|
||||
def on() {
|
||||
encap(zwave.basicV1.basicSet(value: 255))
|
||||
}
|
||||
|
||||
def off() {
|
||||
encap(zwave.basicV1.basicSet(value: 0))
|
||||
}
|
||||
|
||||
def reset() {
|
||||
def cmds = []
|
||||
cmds << zwave.meterV3.meterReset()
|
||||
cmds << zwave.meterV3.meterGet(scale: 0)
|
||||
cmds << zwave.meterV3.meterGet(scale: 2)
|
||||
encapSequence(cmds,1000)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def cmds = []
|
||||
cmds << zwave.meterV3.meterGet(scale: 0)
|
||||
cmds << zwave.meterV3.meterGet(scale: 2)
|
||||
cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 4)
|
||||
encapSequence(cmds,1000)
|
||||
}
|
||||
|
||||
//Configuration and synchronization
|
||||
def updated() {
|
||||
if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return
|
||||
def cmds = []
|
||||
logging("Executing updated()","info")
|
||||
|
||||
if (device.currentValue("numberOfButtons") != 6) { sendEvent(name: "numberOfButtons", value: 6) }
|
||||
|
||||
state.lastUpdated = now()
|
||||
syncStart()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
encap(zwave.basicV1.basicSet(value: 0))
|
||||
}
|
||||
|
||||
private syncStart() {
|
||||
boolean syncNeeded = false
|
||||
Integer settingValue = null
|
||||
parameterMap().each {
|
||||
if(settings."$it.key" != null) {
|
||||
settingValue = settings."$it.key" as Integer
|
||||
if (state."$it.key" == null) { state."$it.key" = [value: null, state: "synced"] }
|
||||
if (state."$it.key".value != settingValue || state."$it.key".state != "synced" ) {
|
||||
state."$it.key".value = settingValue
|
||||
state."$it.key".state = "notSynced"
|
||||
syncNeeded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( syncNeeded ) {
|
||||
logging("sync needed.", "info")
|
||||
syncNext()
|
||||
}
|
||||
}
|
||||
|
||||
private syncNext() {
|
||||
logging("Executing syncNext()","info")
|
||||
def cmds = []
|
||||
for ( param in parameterMap() ) {
|
||||
if ( state."$param.key"?.value != null && state."$param.key"?.state in ["notSynced","inProgress"] ) {
|
||||
multiStatusEvent("Sync in progress. (param: ${param.num})", true)
|
||||
state."$param.key"?.state = "inProgress"
|
||||
cmds << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size)))
|
||||
cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num)))
|
||||
break
|
||||
}
|
||||
}
|
||||
if (cmds) {
|
||||
runIn(10, "syncCheck")
|
||||
sendHubCommand(cmds,1000)
|
||||
} else {
|
||||
runIn(1, "syncCheck")
|
||||
}
|
||||
}
|
||||
|
||||
private syncCheck() {
|
||||
logging("Executing syncCheck()","info")
|
||||
def failed = []
|
||||
def incorrect = []
|
||||
def notSynced = []
|
||||
parameterMap().each {
|
||||
if (state."$it.key"?.state == "incorrect" ) {
|
||||
incorrect << it
|
||||
} else if ( state."$it.key"?.state == "failed" ) {
|
||||
failed << it
|
||||
} else if ( state."$it.key"?.state in ["inProgress","notSynced"] ) {
|
||||
notSynced << it
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
multiStatusEvent("Sync failed! Verify parameter: ${failed[0].num}", true, true)
|
||||
} else if (incorrect) {
|
||||
multiStatusEvent("Sync mismatch! Verify parameter: ${incorrect[0].num}", true, true)
|
||||
} else if (notSynced) {
|
||||
multiStatusEvent("Sync incomplete! Open settings and tap Done to try again.", true, true)
|
||||
} else {
|
||||
if (device.currentValue("multiStatus")?.contains("Sync")) { multiStatusEvent("Sync OK.", true, true) }
|
||||
}
|
||||
}
|
||||
|
||||
private multiStatusEvent(String statusValue, boolean force = false, boolean display = false) {
|
||||
if (!device.currentValue("multiStatus")?.contains("Sync") || device.currentValue("multiStatus") == "Sync OK." || force) {
|
||||
sendEvent(name: "multiStatus", value: statusValue, descriptionText: statusValue, displayed: display)
|
||||
}
|
||||
}
|
||||
|
||||
//event handlers related to configuration and sync
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||
def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } ).key
|
||||
logging("Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "info")
|
||||
state."$paramKey".state = (state."$paramKey".value == cmd.scaledConfigurationValue) ? "synced" : "incorrect"
|
||||
syncNext()
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
|
||||
logging("rejected request!","warn")
|
||||
for ( param in parameterMap() ) {
|
||||
if ( state."$param.key"?.state == "inProgress" ) {
|
||||
state."$param.key"?.state = "failed"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//event handlers
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
logging("${device.displayName} - SwitchBinaryReport received, value: ${cmd.value}","info")
|
||||
sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"])
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
logging("${device.displayName} - SensorMultilevelReport received, value: ${cmd.scaledSensorValue} scale: ${cmd.scale}","info")
|
||||
if (cmd.sensorType == 4) {
|
||||
sendEvent([name: "power", value: cmd.scaledSensorValue, unit: "W"])
|
||||
multiStatusEvent("${device.currentValue("power")} W / ${device.currentValue("energy")} kWh")
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
|
||||
logging("${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale}","info")
|
||||
switch (cmd.scale) {
|
||||
case 0: sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]); break;
|
||||
case 2: sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]); break;
|
||||
}
|
||||
multiStatusEvent("${device.currentValue("power")} W / ${device.currentValue("energy")} kWh")
|
||||
}
|
||||
|
||||
/*
|
||||
####################
|
||||
## Z-Wave Toolkit ##
|
||||
####################
|
||||
*/
|
||||
def parse(String description) {
|
||||
def result = []
|
||||
logging("${device.displayName} - Parsing: ${description}")
|
||||
if (description.startsWith("Err 106")) {
|
||||
result = createEvent(
|
||||
descriptionText: "Failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
|
||||
eventType: "ALERT",
|
||||
name: "secureInclusion",
|
||||
value: "failed",
|
||||
displayed: true,
|
||||
)
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, cmdVersions())
|
||||
if (cmd) {
|
||||
logging("${device.displayName} - Parsed: ${cmd}")
|
||||
zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions())
|
||||
if (encapsulatedCommand) {
|
||||
logging("${device.displayName} - Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}")
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract secure cmd from $cmd"
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
|
||||
def version = cmdVersions()[cmd.commandClass as Integer]
|
||||
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||
if (encapsulatedCommand) {
|
||||
logging("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}")
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Could not extract crc16 command from $cmd"
|
||||
}
|
||||
}
|
||||
|
||||
private logging(text, type = "debug") {
|
||||
if (settings.logging == "true") {
|
||||
log."$type" text
|
||||
}
|
||||
}
|
||||
|
||||
private secEncap(physicalgraph.zwave.Command cmd) {
|
||||
logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info")
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private crcEncap(physicalgraph.zwave.Command cmd) {
|
||||
logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info")
|
||||
zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private encap(physicalgraph.zwave.Command cmd) {
|
||||
if (zwaveInfo.zw.contains("s")) {
|
||||
secEncap(cmd)
|
||||
} else if (zwaveInfo.cc.contains("56")){
|
||||
crcEncap(cmd)
|
||||
} else {
|
||||
logging("${device.displayName} - no encapsulation supported for command: $cmd","info")
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private encapSequence(cmds, Integer delay=250) {
|
||||
delayBetween(cmds.collect{ encap(it) }, delay)
|
||||
}
|
||||
|
||||
private List intToParam(Long value, Integer size = 1) {
|
||||
def result = []
|
||||
size.times {
|
||||
result = result.plus(0, (value & 0xFF) as Short)
|
||||
value = (value >> 8)
|
||||
}
|
||||
return result
|
||||
}
|
||||
/*
|
||||
##########################
|
||||
## Device Configuration ##
|
||||
##########################
|
||||
*/
|
||||
private Map cmdVersions() {
|
||||
[0x5E: 2, 0x22: 1, 0x59: 1, 0x56: 1, 0x7A: 1, 0x32: 3, 0x71: 1, 0x73: 1, 0x98: 1, 0x31: 5, 0x85: 2, 0x70: 2, 0x72: 2, 0x5A: 1, 0x8E: 2, 0x25: 1, 0x86: 2] //Fibaro Wall Plug ZW5
|
||||
}
|
||||
|
||||
private parameterMap() {[
|
||||
[key: "alwaysActive", num: 1, size: 1, type: "enum", options: [0: "0 - function inactive", 1: "1 - function activated"], def: "0", title: "Always On function",
|
||||
descr: "Turns the Wall Plug into a power and energy meter. Wall Plug will turn on connected device permanently and will stop reacting to attempts of turning it off."],
|
||||
[key: "restoreState", num: 2, size: 1, type: "enum", options: [0: "0 - device remains switched off", 1: "1 - device restores the state"], def: "1", title: "Restore state after power failure",
|
||||
descr: "After the power supply is back on, the Wall Plug can be restored to previous state or remain switched off."],
|
||||
[key: "overloadSafety", num: 3, size: 2, type: "number", def: 0, min: 0, max: 30000 , title: "Overload safety switch",
|
||||
descr: "Allows to turn off the controlled device in case of exceeding the defined power; 1-3000 W.\n0 - function inactive\n10-30000 (1.0-3000.0W, step 0.1W)"],
|
||||
[key: "standardPowerReports", num: 11, size: 1, type: "number", def: 15, min: 1, max: 100, title: "Standard power reports",
|
||||
descr: "This parameter determines the minimum percentage change in active power that will result in sending a power report.\n1-99 - power change in percent\n100 - reports are disabled"],
|
||||
[key: "powerReportFrequency", num: 12, size: 2, type: "number", def: 30, min: 5, max: 600, title: "Power reporting interval",
|
||||
descr: "Time interval of sending at most 5 standard power reports.\n5-600 (in seconds)"],
|
||||
[key: "periodicReports", num: 14, size: 2, type: "number", def: 3600, min: 0, max: 32400, title: "Periodic power and energy reports",
|
||||
descr: "Time period between independent reports.\n0 - periodic reports inactive\n5-32400 (in seconds)"],
|
||||
[key: "ringColorOn", num: 41, size: 1, type: "enum", options: [
|
||||
0: "0 - Off",
|
||||
1: "1 - Load based - continuous",
|
||||
2: "2 - Load based - steps",
|
||||
3: "3 - White",
|
||||
4: "4 - Red",
|
||||
5: "5 - Green",
|
||||
6: "6 - Blue",
|
||||
7: "7 - Yellow",
|
||||
8: "8 - Cyan",
|
||||
9: "9 - Magenta"
|
||||
], def: "1", title: "Ring LED color when on", descr: "Ring LED colour when the device is ON."],
|
||||
[key: "ringColorOff", num: 42, size: 1, type: "enum", options: [
|
||||
0: "0 - Off",
|
||||
1: "1 - Last measured power",
|
||||
3: "3 - White",
|
||||
4: "4 - Red",
|
||||
5: "5 - Green",
|
||||
6: "6 - Blue",
|
||||
7: "7 - Yellow",
|
||||
8: "8 - Cyan",
|
||||
9: "9 - Magenta"
|
||||
], def: "0", title: "Ring LED color when off", descr: "Ring LED colour when the device is OFF."]
|
||||
]}
|
||||
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* "Xiaomi Magic Cube Controller"
|
||||
*
|
||||
* Author: Artur Draga
|
||||
*
|
||||
* Based on code by: Oleg "DroidSector" Smirnov
|
||||
*
|
||||
* 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: "Vale Xiaomi Magic Cube Controller", namespace: "ClassicGOD", author: "Artur Draga") {
|
||||
capability "Actuator"
|
||||
capability "Button"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Three Axis" //Simulated!
|
||||
capability "Sensor"
|
||||
|
||||
attribute "face", "number"
|
||||
attribute "angle", "number"
|
||||
|
||||
command "setFace0"
|
||||
command "setFace1"
|
||||
command "setFace2"
|
||||
command "setFace3"
|
||||
command "setFace4"
|
||||
command "setFace5"
|
||||
|
||||
command "flip90"
|
||||
command "flip180"
|
||||
command "slide"
|
||||
command "knock"
|
||||
command "rotateR"
|
||||
command "rotateL"
|
||||
command "shake"
|
||||
|
||||
}
|
||||
|
||||
simulator {
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
//button tiles!
|
||||
standardTile("face0", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 0', action:"setFace0", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#ffffff"
|
||||
state "0", label:'Face 0', action:"setFace0", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
standardTile("face1", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 1', action:"setFace1", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#ffffff"
|
||||
state "1", label:'Face 1', action:"setFace1", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
standardTile("face2", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 2', action:"setFace2", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#ffffff"
|
||||
state "2", label:'Face 2', action:"setFace2", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
standardTile("face3", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 3', action:"setFace3", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/bat_face.png", backgroundColor: "#ffffff"
|
||||
state "3", label:'Face 3', action:"setFace3", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/bat_face.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
standardTile("face4", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 4', action:"setFace4", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#ffffff"
|
||||
state "4", label:'Face 4', action:"setFace4", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/def_face.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
standardTile("face5", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face 5', action:"setFace5", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/mi_face.png", backgroundColor: "#ffffff"
|
||||
state "5", label:'Face 5', action:"setFace5", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/mi_face_s.png", backgroundColor: "#00a0dc"
|
||||
}
|
||||
//function tiles!
|
||||
standardTile("flip90", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "90°", action: "flip90", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/90.png", backgroundColor: "#ffffff" }
|
||||
standardTile("flip180", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "180°", action: "flip180", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/180.png", backgroundColor: "#ffffff" }
|
||||
standardTile("rotateL", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "rotate left", action: "rotateL", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/rotate_left.png", backgroundColor: "#ffffff" }
|
||||
standardTile("rotateR", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "rotate right", action: "rotateR", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/rotate_right.png", backgroundColor: "#ffffff" }
|
||||
standardTile("slide", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "slide", action: "slide", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/slide.png", backgroundColor: "#ffffff" }
|
||||
standardTile("knock", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "knock", action: "knock", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/knock.png", backgroundColor: "#ffffff" }
|
||||
standardTile("shake", "device.button", decoration: "flat", width: 2, height: 2) { state "default", label: "shake" , action: "shake", icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/shake.png", backgroundColor: "#ffffff" }
|
||||
valueTile("battery", "device.battery", decoration: "flat", width: 4, height: 2) { state "val", label: '${currentValue}% battery', backgroundColor: "#ffffff" }
|
||||
standardTile("faceMain", "device.face", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'Face: ${currentValue} ', icon: "https://raw.githubusercontent.com/ClassicGOD/SmartThingsPublic/master/devicetypes/classicgod/xiaomi-magic-cube-controller.src/images/cube_icon.png", backgroundColor: "#ffffff"
|
||||
}
|
||||
|
||||
main(["faceMain"])
|
||||
details(["flip90","face0","flip180","face4","face5","face1","rotateL","face3","rotateR","slide","face2","knock","battery","shake"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
input (
|
||||
name: "cubeMode",
|
||||
title: "Cube Mode",
|
||||
description: "Select how many button events should be sent by the device handler.\n\nSimple presents just 7 buttons on basic gestures (shake,flip 90, flip 180, slide, knock, rotate R, rotate L).\n\nAdvanced presents 36 buttons for separate actions on every face (activate, slide, knock, rotate R, rotate L, shake).\n\nCombined ofers both for total of 43 buttons. ",
|
||||
type: "enum",
|
||||
options: [
|
||||
0: "Simple - 7 buttons",
|
||||
1: "Advanced - 36 buttons",
|
||||
2: "Combined - 43 buttons"
|
||||
],
|
||||
required: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def setFace0() { setFace(0) }
|
||||
def setFace1() { setFace(1) }
|
||||
def setFace2() { setFace(2) }
|
||||
def setFace3() { setFace(3) }
|
||||
def setFace4() { setFace(4) }
|
||||
def setFace5() { setFace(5) }
|
||||
def flip90() {
|
||||
def flipMap = [0:5, 1:2, 2:0, 3:2, 4:5, 5:3]
|
||||
flipEvents(flipMap[device.currentValue("face") as Integer], "90")
|
||||
}
|
||||
def flip180() {
|
||||
def flipMap = [0:3, 1:4, 2:5, 3:0, 4:1, 5:2]
|
||||
flipEvents(flipMap[device.currentValue("face") as Integer], "180")
|
||||
}
|
||||
def rotateL() { rotateEvents(-90) }
|
||||
def rotateR() { rotateEvents(90) }
|
||||
def slide() { slideEvents(device.currentValue("face") as Integer) }
|
||||
def knock() { knockEvents(device.currentValue("face") as Integer) }
|
||||
def shake() { shakeEvents() }
|
||||
|
||||
def setFace(Integer faceId) {
|
||||
def Integer prevFaceId = device.currentValue("face")
|
||||
if (prevFaceId == faceId) {
|
||||
flipEvents(faceId, "0")
|
||||
} else if ((prevFaceId == 0 && faceId == 3)||(prevFaceId == 1 && faceId == 4)||(prevFaceId == 2 && faceId == 5)||(prevFaceId == 3 && faceId == 0)||(prevFaceId == 4 && faceId == 1)||(prevFaceId == 5 && faceId == 2)){
|
||||
flipEvents(faceId, "180")
|
||||
} else {
|
||||
flipEvents(faceId, "90")
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def value = zigbee.parse(description)?.text
|
||||
|
||||
|
||||
if (description?.startsWith('catchall:')) {
|
||||
parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
parseReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// not tested
|
||||
private parseCatchAllMessage(String description) {
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
log.debug "battery! " + cluster.data + " : " + cluster.data.get(6) + " : " + cluster
|
||||
getBatteryResult(cluster.data.get(6))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not tested
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0012" && descMap.attrId == "0055") { // Shake, flip, knock, slide
|
||||
getMotionResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "000C" && descMap.attrId == "ff05") { // Rotation (90 and 180 degrees)
|
||||
getRotationResult(descMap.value)
|
||||
} else { log.debug descMap }
|
||||
|
||||
}
|
||||
|
||||
def String hexToBinOld(String thisByte) {
|
||||
return String.format("%8s", Integer.toBinaryString(Integer.parseInt(thisByte,16))).replace(' ', '0')
|
||||
}
|
||||
|
||||
def String hexToBin(String thisByte, Integer size = 8) {
|
||||
String binaryValue = new BigInteger(thisByte, 16).toString(2);
|
||||
return String.format("%${size}s", binaryValue).replace(' ', '0')
|
||||
}
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private getBatteryResult(rawValue) {
|
||||
def battLevel = Math.round(rawValue * 100 / 255)
|
||||
|
||||
if (battLevel > 100) {
|
||||
battLevel = 100
|
||||
}
|
||||
|
||||
if (battLevel) { sendEvent( [ name: "battery", value: battLevel, descriptionText: "$device.displayName battery is ${battLevel}%", isStateChange: true] ) }
|
||||
}
|
||||
|
||||
private Map getMotionResult(String value) {
|
||||
String motionType = value[0..1]
|
||||
String binaryValue = hexToBin(value[2..3])
|
||||
Integer sourceFace = Integer.parseInt(binaryValue[2..4],2)
|
||||
Integer targetFace = Integer.parseInt(binaryValue[5..7],2)
|
||||
|
||||
if (motionType == "00") {
|
||||
switch(binaryValue[0..1]) {
|
||||
case "00":
|
||||
if (targetFace==0) { shakeEvents() }
|
||||
break
|
||||
case "01":
|
||||
flipEvents(targetFace, "90")
|
||||
break
|
||||
case "10":
|
||||
flipEvents(targetFace, "180")
|
||||
break
|
||||
}
|
||||
} else if (motionType == "01") {
|
||||
slideEvents(targetFace)
|
||||
} else if (motionType == "02") {
|
||||
knockEvents(targetFace)
|
||||
}
|
||||
}
|
||||
|
||||
private Map getRotationResult(value) {
|
||||
Integer angle = Math.round(Float.intBitsToFloat(Long.parseLong(value[0..7],16).intValue()));
|
||||
rotateEvents(angle)
|
||||
}
|
||||
|
||||
def Map shakeEvents() {
|
||||
if (!settings.cubeMode || settings.cubeMode in ['0','2'] ) {
|
||||
sendEvent([
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: 1, face: device.currentValue("face")],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName was shaken" : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
])
|
||||
}
|
||||
|
||||
if (settings.cubeMode in ['1','2'] ){
|
||||
sendEvent([
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: (device.currentValue("face") as Integer) + ((settings.cubeMode == '1') ? 31 : 38),
|
||||
face: device.currentValue("face")],
|
||||
descriptionText: "$device.displayName was shaken (Face # ${device.currentValue("face")}).",
|
||||
isStateChange: true])
|
||||
}
|
||||
}
|
||||
|
||||
def flipEvents(Integer faceId, String flipType) {
|
||||
if (flipType == "0") {
|
||||
sendEvent( [name: 'face', value: -1, isStateChange: false] )
|
||||
sendEvent( [name: 'face', value: faceId, isStateChange: false] )
|
||||
} else if (flipType == "90") {
|
||||
if (settings.cubeMode in ['0','2']) {
|
||||
sendEvent( [
|
||||
name: 'button',
|
||||
value: "pushed" ,
|
||||
data: [buttonNumber: 2, face: faceId],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName detected $flipType degree flip" : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
} else if (flipType == "180") {
|
||||
if (settings.cubeMode in ['0','2']) {
|
||||
sendEvent( [
|
||||
name: 'button',
|
||||
value: "pushed" ,
|
||||
data: [buttonNumber: 3, face: faceId],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName detected $flipType degree flip" : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
}
|
||||
sendEvent( [name: 'face', value: faceId, isStateChange: true, displayed: false ] )
|
||||
if (settings.cubeMode in ['1','2']) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: faceId+((settings.cubeMode == '1') ? 1 : 8), face: faceId],
|
||||
descriptionText: "$device.displayName was fliped to face # $faceId",
|
||||
isStateChange: true
|
||||
] )
|
||||
}
|
||||
switch (faceId) {
|
||||
case 0: sendEvent( [ name: "threeAxis", value: "0,-1000,0", isStateChange: true, displayed: false] ); break
|
||||
case 1: sendEvent( [ name: "threeAxis", value: "-1000,0,0", isStateChange: true, displayed: false] ); break
|
||||
case 2: sendEvent( [ name: "threeAxis", value: "0,0,1000", isStateChange: true, displayed: false] ); break
|
||||
case 3: sendEvent( [ name: "threeAxis", value: "1000,0,0", isStateChange: true, displayed: false] ); break
|
||||
case 4: sendEvent( [ name: "threeAxis", value: "0,1000,0", isStateChange: true, displayed: false] ); break
|
||||
case 5: sendEvent( [ name: "threeAxis", value: "0,0,-1000", isStateChange: true, displayed: false] ); break
|
||||
}
|
||||
}
|
||||
|
||||
def Map slideEvents(Integer targetFace) {
|
||||
if ( targetFace != device.currentValue("face") as Integer ) { log.info "Stale face data, updating."; setFace(targetFace) }
|
||||
if (!settings.cubeMode || settings.cubeMode in ['0','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: 4, face: targetFace],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName detected slide motion." : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
|
||||
if ( settings.cubeMode in ['1','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: targetFace+((settings.cubeMode == '1') ? 7 : 14), face: targetFace],
|
||||
descriptionText: "$device.displayName was slid with face # $targetFace up.",
|
||||
isStateChange: true
|
||||
] ) }
|
||||
}
|
||||
|
||||
def knockEvents(Integer targetFace) {
|
||||
if ( targetFace != device.currentValue("face") as Integer ) { log.info "Stale face data, updating."; setFace(targetFace) }
|
||||
if (!settings.cubeMode || settings.cubeMode in ['0','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: 5, face: targetFace],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName detected knock motion." : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
if ( settings.cubeMode in ['1','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: targetFace+((settings.cubeMode == '1') ? 13 : 20), face: targetFace],
|
||||
descriptionText: "$device.displayName was knocked with face # $targetFace up",
|
||||
isStateChange: true
|
||||
] )
|
||||
}
|
||||
}
|
||||
|
||||
def rotateEvents(Integer angle) {
|
||||
sendEvent( [ name: "angle", value: angle, isStateChange: true, displayed: false] )
|
||||
if ( angle > 0 ) {
|
||||
if (!settings.cubeMode || settings.cubeMode in ['0','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: 6, face: device.currentValue("face"), angle: angle],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName was rotated right." : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
if ( settings.cubeMode in ['1','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: (device.currentValue("face") as Integer) + ((settings.cubeMode == '1') ? 19 : 26), face: device.currentValue("face")],
|
||||
descriptionText: "$device.displayName was rotated right (Face # ${device.currentValue("face")}).",
|
||||
isStateChange: true
|
||||
] )
|
||||
}
|
||||
} else {
|
||||
if (!settings.cubeMode || settings.cubeMode in ['0','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: 7, face: device.currentValue("face"), angle: angle],
|
||||
descriptionText: (settings.cubeMode == '0') ? "$device.displayName was rotated left." : null,
|
||||
isStateChange: true,
|
||||
displayed: (settings.cubeMode == '0') ? true : false
|
||||
] )
|
||||
}
|
||||
if ( settings.cubeMode in ['1','2'] ) {
|
||||
sendEvent( [
|
||||
name: "button",
|
||||
value: "pushed",
|
||||
data: [buttonNumber: (device.currentValue("face") as Integer) + ((settings.cubeMode == '1') ? 25 : 32), face: device.currentValue("face")],
|
||||
descriptionText: "$device.displayName was rotated left (Face # ${device.currentValue("face")}).",
|
||||
isStateChange: true
|
||||
] )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = []
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
}
|
||||
|
||||
def reset() {
|
||||
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
sendState()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
//sendState()
|
||||
}
|
||||
|
||||
def sendState() {
|
||||
sendEvent(name: "numberOfButtons", value: 7)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return
|
||||
switch(settings.cubeMode) {
|
||||
case "1": sendEvent(name: "numberOfButtons", value: 36); break
|
||||
case "2": sendEvent(name: "numberOfButtons", value: 43); break
|
||||
default: sendEvent(name: "numberOfButtons", value: 7); break
|
||||
}
|
||||
state.lastUpdated = now()
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Xiaomi Aqara Light Switch x (Zigbee)
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by a4refillpad 2017
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* modified to allow button capability
|
||||
*
|
||||
* Ver 1.0 - 9-12-2017
|
||||
* Converted to support button presses with Xiaomi Zigbee Aqara Light Switch (Single Switch)
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Aqara Light Switch", namespace: "ericyew", author: "Eric Yew") {
|
||||
// capability "Battery"
|
||||
capability "Button"
|
||||
// capability "Holdable Button"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Momentary"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "lastPress", "string"
|
||||
attribute "batterylevel", "string"
|
||||
attribute "lastCheckin", "string"
|
||||
|
||||
fingerprint endpointId: "01", inClusters: "0000,FFFF,0006", outClusters: "0000,0004,FFFF"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "button 1 pressed": "on/off: 1"
|
||||
}
|
||||
|
||||
preferences{
|
||||
// input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
|
||||
// defaultValue: 4, displayDuringSetup: false)
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState("on", label:' push', action: "momentary.push", backgroundColor:"#53a7c0")
|
||||
attributeState("off", label:' push', action: "momentary.push", backgroundColor:"#ffffff", nextState: "on")
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
// standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
// state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
// }
|
||||
|
||||
main (["switch"])
|
||||
details(["switch", "refresh", "battery"]) //, "configure"
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
|
||||
// Send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
def results = []
|
||||
if (description?.startsWith('on/off: '))
|
||||
results = parseCustomMessage(description)
|
||||
if (description?.startsWith('catchall:'))
|
||||
results = parseCatchAllMessage(description)
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
def configure(){
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 2 0 {${device.zigbeeId}} {}", "delay 5000",
|
||||
"zcl global send-me-a-report 2 0 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 2"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh(){
|
||||
"st rattr 0x${device.deviceNetworkId} 1 2 0"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0 0"
|
||||
log.debug "refreshing"
|
||||
sendEvent(name: 'numberOfButtons', value: 1)
|
||||
//createEvent([name: 'batterylevel', value: '100', data:[buttonNumber: 1], displayed: false])
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
log.debug "${cluster.clusterId}"
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
int battValue = 100
|
||||
|
||||
def maxbatt = 100
|
||||
|
||||
if (battValue > maxbatt) {
|
||||
battValue = maxbatt
|
||||
}
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: battValue,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${linkText} battery was ${battValue}%"
|
||||
]
|
||||
|
||||
log.debug result.descriptionText
|
||||
state.lastbatt = new Date().time
|
||||
return createEvent(result)
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
if (description == 'on/off: 1') //button pushed
|
||||
return push()
|
||||
}
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def push() {
|
||||
// log.debug "App Button Pressed"
|
||||
// sendEvent(name: "switch", value: "on", isStateChange: true, displayed: false)
|
||||
// sendEvent(name: "switch", value: "off", isStateChange: true, displayed: false)
|
||||
// sendEvent(name: "momentary", value: "pushed", isStateChange: true)
|
||||
sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$device.displayName button 1 was pushed", isStateChange: true)
|
||||
}
|
||||
|
||||
def on() {
|
||||
push()
|
||||
}
|
||||
|
||||
def off() {
|
||||
push()
|
||||
}
|
||||
1106
devicetypes/krlaframboise/vale-dome-siren.src/vale-dome-siren.groovy
Normal file
1106
devicetypes/krlaframboise/vale-dome-siren.src/vale-dome-siren.groovy
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user