Compare commits

...

1 Commits

Author SHA1 Message Date
Valentino Baccolini
97bdd767eb MSA-2980: backup 2018-09-07 13:56:53 -07:00
11 changed files with 4385 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff