Skip to content

Commit c131b98

Browse files
authored
[UPDATE] v1.3 - for firmware 25.20 and newer ONLY
changelist * added support for multi-click actions (double, triple, quadruple, and > 5-click) * removed preference setting to send `button 2 pushed` event on hold (button hold now always sends `button 1 held` event) * added method imports to match device handlers on GitHub/SmartThingsPublic * removed unneeded `endpointId` and `profileId` from device fingerprint * added `ocfDeviceType` to definition in attempt to get device handler working with new SmartThings Samsung Connect mobile app * added preliminary support for Device Watch * added initial button event for proper configuration during device pairing * improved followup in `updated()` after user saves preference settings * added more helpful inline comments to code * minor code cleanup and reorganization
1 parent ec95aec commit c131b98

1 file changed

Lines changed: 85 additions & 52 deletions

File tree

devicetypes/bspranger/xiaomi-button.src/xiaomi-button.groovy

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
/**
2-
* Xiaomi Zigbee Button
3-
* Version 1.2
2+
* Xiaomi Zigbee Button - model WXKG01LM
3+
* Device Handler for SmartThings - Firmware version 25.20 and newer ONLY
4+
* Version 1.3
45
*
6+
* NOTE: Do NOT use this device handler on any SmartThings hub running Firmware 24.x and older
7+
* Instead use the xiaomi-button-old-firmware device handler
58
*
69
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
710
* in compliance with the License. You may obtain a copy of the License at:
@@ -12,27 +15,33 @@
1215
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
1316
* for the specific language governing permissions and limitations under the License.
1417
*
15-
* Original device handler code by a4refillpad, adapted for use with Aqara model by bspranger
18+
* Original device handler code by a4refillpad, adapted for use with Aqara model by bspranger, updated for changes in firmware 25.20 by veeceeoh
1619
* Additional contributions to code by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh
1720
*
1821
* Known issues:
19-
* Xiaomi sensors do not seem to respond to refresh requests
20-
* Inconsistent rendering of user interface text/graphics between iOS and Android devices - This is due to SmartThings, not this device handler
21-
* Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub.
22-
*
22+
* + As of March 2019, the SmartThings Samsung Connect mobile app does NOT support custom device handlers such as this one
23+
* + The SmartThings Classic mobile app UI text/graphics is rendered differently on iOS vs Android devices - This is due to SmartThings, not this device handler
24+
* + Pairing Xiaomi/Aqara devices can be difficult as they were not designed to use with a SmartThings hub.
25+
* + The battery level is not reported at pairing. Wait for the first status report, 50-60 minutes after pairing.
26+
* + Xiaomi devices do not respond to refresh requests
27+
* + Most ZigBee repeater devices (generally mains-powered ZigBee devices) are NOT compatible with Xiaomi/Aqara devices, causing them to drop off the network.
28+
* + Only XBee ZigBee modules, the IKEA Tradfri Outlet / Tradfri Bulb, and ST user @iharyadi's custom multi-sensor ZigBee repeater device are confirmed to be compatible.
2329
*
2430
*/
2531

32+
import groovy.json.JsonOutput
33+
import physicalgraph.zigbee.zcl.DataType
34+
2635
metadata {
27-
definition (name: "Xiaomi Button", namespace: "bspranger", author: "bspranger") {
36+
definition (name: "Xiaomi Button", namespace: "bspranger", author: "bspranger", minHubCoreVersion: "000.022.0002", ocfDeviceType: "x.com.st.d.remotecontroller") {
37+
capability "Actuator"
2838
capability "Battery"
29-
capability "Sensor"
3039
capability "Button"
31-
capability "Holdable Button"
32-
capability "Actuator"
33-
capability "Momentary"
3440
capability "Configuration"
3541
capability "Health Check"
42+
capability "Holdable Button"
43+
capability "Momentary"
44+
capability "Sensor"
3645

3746
attribute "lastCheckin", "string"
3847
attribute "lastCheckinCoRE", "string"
@@ -41,9 +50,9 @@ metadata {
4150
attribute "lastReleasedCoRE", "string"
4251
attribute "lastButtonMssg", "string"
4352
attribute "batteryRuntime", "string"
44-
attribute "buttonStatus", "enum", ["pushed", "held", "released"]
53+
attribute "buttonStatus", "enum", ["pushed", "held", "released", "double", "triple", "quadruple", "shizzle"]
4554

46-
fingerprint endpointId: "01", profileId: "0104", deviceId: "0104", inClusters: "0000,0003,FFFF,0019", outClusters: "0000,0004,0003,0006,0008,0005,0019", manufacturer: "LUMI", model: "lumi.sensor_switch", deviceJoinName: "Original Xiaomi Button"
55+
fingerprint deviceId: "0104", inClusters: "0000,0003,FFFF,0019", outClusters: "0000,0004,0003,0006,0008,0005,0019", manufacturer: "LUMI", model: "lumi.sensor_switch", deviceJoinName: "Original Xiaomi Button"
4756

4857
command "resetBatteryRuntime"
4958
}
@@ -60,6 +69,10 @@ metadata {
6069
attributeState("pushed", label:'Pushed', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
6170
attributeState("held", label:'Held', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
6271
attributeState("released", label:'Released', action: "momentary.push", backgroundColor:"#ffffff", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonReleased.png")
72+
attributeState("double", label:'Double-Clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
73+
attributeState("triple", label:'Triple-Clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
74+
attributeState("quadruple", label:'Quadruple-Clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
75+
attributeState("shizzle", label:'Shizzle-Clicked', backgroundColor:"#00a0dc", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/ButtonPushed.png")
6376
}
6477
tileAttribute("device.lastPressed", key: "SECONDARY_CONTROL") {
6578
attributeState "lastPressed", label:'Last Pressed: ${currentValue}'
@@ -85,9 +98,8 @@ metadata {
8598

8699
preferences {
87100
//Button Config
88-
input description: "A button click always sends a 'button 1 pushed' event, but as a default if it is held for at least 2 seconds, a 'button 1 held' event is sent when released. The settings below allow changes to the minimum time needed for held and what type of event should be sent.", type: "paragraph", element: "paragraph", title: "BUTTON CONFIGURATION"
101+
input description: "As a default if the button is held for at least 2 seconds, a 'button 1 held' event is sent when released. The settings below allow changes to the minimum time needed for held.", type: "paragraph", element: "paragraph", title: "BUTTON CONFIGURATION"
89102
input name: "waittoHeld", type: "decimal", title: "Minimum hold time required for button held:", description: "Number of seconds (default = 2.0)", range: "0.1..120"
90-
input name: "holdIsButton2", type: "bool", title: "On button hold, send 'button 2 pushed' event instead of 'button 1 held'?"
91103

92104
//Date & Time Config
93105
input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK"
@@ -134,10 +146,10 @@ def parse(String description) {
134146
} else if (description == 'on/off: 1') {
135147
result = createButtonEvent()
136148
updateLastPressed("Released")
137-
} else if (description?.startsWith('catchall:')) {
138-
result = parseCatchAllMessage(description)
139149
} else if (description?.startsWith('read attr - raw: ')) {
140150
result = parseReadAttrMessage(description)
151+
} else if (description?.startsWith('catchall:')) {
152+
result = parseCatchAllMessage(description)
141153
}
142154
if (result != [:]) {
143155
displayDebugLog(": Creating event $result")
@@ -149,7 +161,7 @@ def parse(String description) {
149161
// on any type of button pressed update lastPressed and lastPressedCoRE or lastReleasedCoRE to current date/time
150162
def updateLastPressed(pressType) {
151163
if (pressType == "Pressed")
152-
displayInfoLog(": Button press detected")
164+
displayInfoLog(": Single button press detected")
153165
sendEvent(name: "lastPressed", value: formatDate(), displayed: false)
154166
displayDebugLog(": Setting Last $pressType to current date/time")
155167
sendEvent(name: "last${pressType}CoRE", value: now(), displayed: false)
@@ -164,47 +176,60 @@ private createButtonEvent() {
164176
displayDebugLog(": Time difference = $timeDif ms, Hold time setting = $holdTimeMillisec ms")
165177
// compare waittoHeld setting with difference between current time and lastButtonMssg
166178
def buttonHeld = (timeDif >= holdTimeMillisec & timeDif < holdTimeMillisec + 10000) ? true : false
167-
def eventValue = (buttonHeld & !(holdIsButton2 == true)) ? "held" : "pushed"
168-
def buttonNum = (buttonHeld & holdIsButton2 == true) ? 2 : 1
169179
def pressType = buttonHeld ? "held" : "pushed"
170-
def descText = " was $pressType (button $buttonNum $eventValue)"
180+
def descText = " was $pressType (button 1 $pressType)"
171181
sendEvent(name: "buttonStatus", value: pressType, isStateChange: true, displayed: false)
172182
runIn(1, clearButtonStatus)
173183
displayInfoLog(descText)
174184
return [
175185
name: 'button',
176-
value: eventValue,
177-
data: [buttonNumber: buttonNum],
186+
value: pressType,
187+
data: [buttonNumber: 1],
178188
descriptionText: "$device.displayName$descText",
179189
isStateChange: true
180190
]
181191
}
182192

183193
def clearButtonStatus() {
184-
sendEvent(name: "buttonStatus", value: "released", isStateChange: true, displayed: false)
194+
sendEvent(name: "buttonStatus", value: "released", isStateChange: true, displayed: false)
185195
}
186196

187197
private Map parseReadAttrMessage(String description) {
188198
def cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
189199
def attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
190-
def value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
191-
def data = ""
192-
def modelName = ""
193-
def model = value
200+
def valueHex = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
194201
Map resultMap = [:]
195202

203+
// Process message for double-click, triple-click, quadruple-click, and 5 or more-click
204+
if (cluster == "0006" && attrId == "8000") {
205+
def buttonNum = (valueHex == "80") ? 5 : Integer.parseInt(valueHex, 16)
206+
def clickType = [2: "double", 3: "triple", 4: "quadruple", 5: "shizzle"]
207+
def descText = " was ${clickType[buttonNum]}-clicked (Button $buttonNum pushed)"
208+
sendEvent(name: "buttonStatus", value: clickType[buttonNum], isStateChange: true, displayed: false)
209+
runIn(1, clearButtonStatus)
210+
displayInfoLog(descText)
211+
resultMap = [
212+
name: 'button',
213+
value: 'pushed',
214+
data: [buttonNumber: buttonNum],
215+
descriptionText: "$device.displayName$descText",
216+
isStateChange: true
217+
]
218+
}
196219
// Process message on short-button press containing model name and battery voltage report
197-
if (cluster == "0000" && attrId == "0005") {
198-
if (value.length() > 45) {
199-
model = value.split("02FF")[0]
200-
data = value.split("02FF")[1]
220+
else if (cluster == "0000" && attrId == "0005") {
221+
def data = ""
222+
def modelName = ""
223+
def model = valueHex
224+
if (valueHex.length() > 45) {
225+
model = valueHex.split("02FF")[0]
226+
data = valueHex.split("02FF")[1]
201227
if (data[4..7] == "0121") {
202-
def BatteryVoltage = (Integer.parseInt((data[10..11] + data[8..9]),16))
228+
def BatteryVoltage = (Integer.parseInt((data[10..11] + data[8..9]), 16))
203229
resultMap = getBatteryResult(BatteryVoltage)
204230
}
205-
data = ", data: ${value.split("02FF")[1]}"
231+
data = ", data: ${valueHex.split("02FF")[1]}"
206232
}
207-
208233
// Parsing the model name
209234
for (int i = 0; i < model.length(); i+=2) {
210235
def str = model.substring(i, i+2);
@@ -253,7 +278,7 @@ private Map getBatteryResult(rawValue) {
253278
value: roundedPct,
254279
unit: "%",
255280
isStateChange:true,
256-
descriptionText : "$device.displayName $descText"
281+
descriptionText : "$descText"
257282
]
258283
}
259284

@@ -278,47 +303,55 @@ def resetBatteryRuntime(paired) {
278303
def installed() {
279304
state.prefsSetCount = 0
280305
displayInfoLog(": Installing")
281-
if (!device.currentState('batteryRuntime')?.value)
282-
resetBatteryRuntime(true)
306+
// initialize battery replaced date
307+
initialize(true)
308+
// initialize default button states
309+
sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false)
283310
checkIntervalEvent("")
284-
sendEvent(name: "numberOfButtons", value: 1)
285311
}
286312

287313
// configure() runs after installed() when a sensor is paired
288314
def configure() {
289-
def numButtons = (holdIsButton2 == true) ? 2 : 1
290315
displayInfoLog(": Configuring")
291-
if (!device.currentState('batteryRuntime')?.value)
292-
resetBatteryRuntime(true)
293-
clearButtonStatus()
294-
checkIntervalEvent("configured")
295-
sendEvent(name: "numberOfButtons", value: numButtons)
316+
initialize(false)
317+
device.currentValue("numberOfButtons")?.times {
318+
sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false)
319+
}
296320
sendEvent(name: "lastButtonMssg", value: now(), displayed: false)
321+
checkIntervalEvent("configured")
297322
return
298323
}
299324

300325
// updated() will run twice every time user presses save in preference settings page
301326
def updated() {
302-
def numButtons = (holdIsButton2 == true) ? 2 : 1
303327
displayInfoLog(": Updating preference settings")
304328
if (!state.prefsSetCount)
305329
state.prefsSetCount = 1
306330
else if (state.prefsSetCount < 3)
307331
state.prefsSetCount = state.prefsSetCount + 1
308-
if (!device.currentState('batteryRuntime')?.value)
309-
resetBatteryRuntime()
332+
initialize(false)
333+
// set battery replaced date if user toggled preference setting
310334
if (battReset){
311335
resetBatteryRuntime()
312336
device.updateSetting("battReset", false)
313337
}
314-
clearButtonStatus()
315-
sendEvent(name: "numberOfButtons", value: numButtons)
316-
displayInfoLog(": Number of buttons = $numButtons")
317338
displayInfoLog(": Info message logging enabled")
318339
displayDebugLog(": Debug message logging enabled")
319340
checkIntervalEvent("preferences updated")
320341
}
321342

343+
def initialize (paired) {
344+
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
345+
// initialize battery replaced date if not yet set
346+
if (!device.currentState('batteryRuntime')?.value)
347+
resetBatteryRuntime(paired)
348+
clearButtonStatus()
349+
if (device.currentValue("numberOfButtons") != 5) {
350+
sendEvent(name: "numberOfButtons", value: 5, displayed: false)
351+
displayInfoLog(": Number of buttons set to 5")
352+
}
353+
}
354+
322355
private checkIntervalEvent(text) {
323356
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
324357
if (text)

0 commit comments

Comments
 (0)