/
xiaomi-aqara-wireless-switch.groovy
283 lines (262 loc) · 13.1 KB
/
xiaomi-aqara-wireless-switch.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
* Xiaomi Aqara Wireless Smart Light Switch
* 2016 & 2018 revisions of models WXKG03LM (1 button) and WXKG02LM (2 buttons)
* Device Driver for Hubitat Elevation hub
* Version 0.8
*
*
* 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 SmartThings device handler code by a4refillpad
* Reworked for use with Hubitat Elevation hub by gn0st1c with additional code by veeceeoh
* With contributions by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, veeceeoh, & xtianpaiva
*
* Notes on capabilities of the different models:
* Model WXKG03LM (1 button) - 2016 Revision (lumi.sensor_86sw1lu):
* - Single press results in button 1 "pushed" event
* Model WXKG03LM (1 button) - 2018 Revision (lumi.remote.b186acn01):
* - Single press results in button 1 "pushed" event
* - Double click results in button 1 "doubleTapped" event
* - Hold for longer than 400ms results in button 1 "held" event
* Model WXKG02LM (2 button) - 2016 Revision (lumi.sensor_86sw2Un):
* - Single press of left button results in button 1 "pushed" event
* - Single press of right button results in button 2 "pushed" event
* - Single press of both buttons results in button 3 "pushed" event
* Model WXKG02LM (2 button) - 2018 Revision (lumi.remote.b286acn01):
* - Single press of left/right/both button(s) results in button 1/2/3 "pushed" event
* - Double click of left/right/both button(s) results in button 1/2/3 "doubleTapped" event
* - Hold of left/right/both button(s) for longer than 400ms results in button 1/2/3 "held" event
*
* With contributions by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, mike.maxwell, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh
*
*/
metadata {
definition (name: "Aqara Wireless Smart Light Switch", namespace: "veeceeoh", author: "veeceeoh") {
capability "Battery"
capability "DoubleTapableButton"
capability "HoldableButton"
capability "PushableButton"
capability "Sensor"
attribute "lastCheckinEpoch", "String"
attribute "lastCheckinTime", "String"
attribute "batteryLastReplaced", "String"
attribute "buttonPressedEpoch", "String"
attribute "buttonPressedTime", "String"
attribute "buttonDoubleTappedEpoch", "String"
attribute "buttonDoubleTappedTime", "String"
attribute "buttonHeldEpoch", "String"
attribute "buttonHeldTime", "String"
// Aqara Wireless Smart Light Switch - one button - model WXKG03LM - 2016 Revision
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.sensor_86sw1lu", deviceJoinName: "Aqara 1-button Wireless Light Switch (2016)"
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.sensor_86sw1", deviceJoinName: "Aqara 1-button Wireless Light Switch (2016)"
// Aqara Wireless Smart Light Switch - two button - model WXKG02LM - 2016 Revision
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.sensor_86sw2Un", deviceJoinName: "Aqara 2-button Wireless Light Switch (2016)"
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.sensor_86sw2", deviceJoinName: "Aqara 2-button Wireless Light Switch (2016)"
// Aqara Wireless Smart Light Switch - one button - model WXKG03LM - 2018 Revision
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.remote.b186acn01", deviceJoinName: "Aqara 1-button Wireless Light Switch (2018)"
// Aqara Wireless Smart Light Switch - two button - model WXKG02LM - 2018 Revision
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", manufacturer: "LUMI", model: "lumi.remote.b286acn01", deviceJoinName: "Aqara 2-button Wireless Light Switch (2018)"
command "resetBatteryReplacedDate"
}
preferences {
//Battery Voltage Range
input name: "voltsmin", title: "Min Volts (0% battery = ___ volts, range 2.0 to 2.9). Default = 2.9 Volts", description: "", type: "decimal", range: "2..2.9"
input name: "voltsmax", title: "Max Volts (100% battery = ___ volts, range 2.95 to 3.4). Default = 3.05 Volts", description: "", type: "decimal", range: "2.95..3.4"
//Date/Time Stamp Events Config
input name: "lastCheckinEnable", type: "bool", title: "Enable custom date/time stamp events for lastCheckin", description: ""
input name: "otherDateTimeEnable", type: "bool", title: "Enable custom date/time stamp events for buttonPressed, buttonDoubleTapped, and buttonHeld", description: ""
//Logging Message Config
input name: "infoLogging", type: "bool", title: "Enable info message logging", description: ""
input name: "debugLogging", type: "bool", title: "Enable debug message logging", description: ""
//Firmware 2.0.5 Compatibility Fix Config
input name: "oldFirmware", type: "bool", title: "DISABLE 2.0.5 firmware compatibility fix (for users of 2.0.4 or earlier)", description: ""
}
}
// Parse incoming device messages to generate events
def parse(String description) {
def endpoint = description.split(",").find {it.split(":")[0].trim() == "endpoint"}?.split(":")[1].trim()
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 encoding = Integer.parseInt(description.split(",").find {it.split(":")[0].trim() == "encoding"}?.split(":")[1].trim(), 16)
def valueHex = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
Map map = [:]
if (!oldFirmware & valueHex != null & encoding > 0x18 & encoding < 0x3e) {
displayDebugLog("Data type of payload is little-endian; reversing byte order")
// Reverse order of bytes in description's payload for LE data types - required for Hubitat firmware 2.0.5 or newer
valueHex = reverseHexString(valueHex)
}
displayDebugLog("Parsing message: ${description}")
displayDebugLog("Message payload: ${valueHex}")
// Send message data to appropriate parsing function based on the type of report
if (cluster == "0006") {
// Parse revision 2016 button messages
// Model WXKG03LM: endpoint 1 = button pushed
// Model WXKG02LM: endpoint 1 = left button pushed, 2 = right button pushed, 3 = both pushed
map = parseButtonMessage(Integer.parseInt(endpoint), 1)
} else if (cluster == "0012") {
// Parse revision 2018 button messages
// Model WXKG03LM: endpoint 1 = button pushed
// Model WXKG02LM: endpoint 1 = left button pushed, 2 = right button pushed, 3 = both pushed
// Both models: valueHex 0 = held, 1 = pushed, 2 = double-tapped
map = parseButtonMessage(Integer.parseInt(endpoint), Integer.parseInt(valueHex[2..3],16))
} else if (cluster == "0000" & attrId == "0005") {
displayDebugLog "Button was long-pressed"
// Parse battery level from longer type of announcement message
map = (valueHex.size() > 60) ? parseBattery(valueHex.split('FF42')[1]) : [:]
} else if (cluster == "0000" & (attrId == "FF01" || attrId == "FF02")) {
// Parse battery level from hourly announcement message
map = (valueHex.size() > 30) ? parseBattery(valueHex) : [:]
} else {
displayDebugLog "Unable to parse message"
}
if (map != [:]) {
displayDebugLog("Creating event $map")
return createEvent(map)
} else
return map
}
// Reverses order of bytes in hex string
def reverseHexString(hexString) {
def reversed = ""
for (int i = hexString.length(); i > 0; i -= 2) {
reversed += hexString.substring(i - 2, i )
}
return reversed
}
// Build event map based on button press
private parseButtonMessage(buttonNum, pressType) {
def whichButton = [1: (state.numOfButtons == 1) ? "Button" : "Left button", 2: "Right button", 3: "Both buttons"]
def messageType = ["held", "pressed", "double-tapped"]
def eventType = ["held", "pushed", "doubleTapped"]
def timeStampType = ["Held", "Pressed", "DoubleTapped"]
def descText = "${whichButton[buttonNum]} was ${messageType[pressType]} (Button $buttonNum ${eventType[pressType]})"
displayInfoLog(descText)
updateDateTimeStamp(timeStampType[pressType])
return [
name: eventType[pressType],
value: buttonNum,
isStateChange: true,
descriptionText: descText
]
}
// Generate buttonPressedEpoch/Time, buttonHeldEpoch/Time, or buttonReleasedEpoch/Time event for Epoch time/date app or Hubitat dashboard use
def updateDateTimeStamp(timeStampType) {
if (otherDateTimeEnable) {
displayDebugLog("Setting button${timeStampType}Epoch and button${timeStampType}Time to current date/time")
sendEvent(name: "button${timeStampType}Epoch", value: now(), descriptionText: "Updated button${timeStampType}Epoch")
sendEvent(name: "button${timeStampType}Time", value: new Date().toLocaleString(), descriptionText: "Updated button${timeStampType}Time")
}
}
// Convert raw 4 digit integer voltage value into percentage based on minVolts/maxVolts range
private parseBattery(description) {
displayDebugLog("Battery parse string = ${description}")
def MsgLength = description.size()
def rawValue
for (int i = 4; i < (MsgLength-3); i+=2) {
if (description[i..(i+1)] == "21") { // Search for byte preceeding battery voltage bytes
rawValue = Integer.parseInt((description[(i+4)..(i+5)] + description[(i+2)..(i+3)]),16)
break
}
}
def rawVolts = rawValue / 1000
def minVolts = voltsmin ? voltsmin : 2.9
def maxVolts = voltsmax ? voltsmax : 3.05
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.min(100, Math.round(pct * 100))
def descText = "Battery level is ${roundedPct}% (${rawVolts} Volts)"
displayInfoLog(descText)
// lastCheckinEpoch is for apps that can use Epoch time/date and lastCheckinTime can be used with Hubitat Dashboard
if (lastCheckinEnable) {
sendEvent(name: "lastCheckinEpoch", value: now())
sendEvent(name: "lastCheckinTime", value: new Date().toLocaleString())
}
def result = [
name: 'battery',
value: roundedPct,
unit: "%",
descriptionText: descText
]
return result
}
private def displayDebugLog(message) {
if (debugLogging)
log.debug "${device.displayName}: ${message}"
}
private def displayInfoLog(message) {
if (infoLogging || state.prefsSetCount != 1)
log.info "${device.displayName}: ${message}"
}
//Reset the batteryLastReplaced date to current date
def resetBatteryReplacedDate(paired) {
def newlyPaired = paired ? " for newly paired device" : ""
sendEvent(name: "batteryLastReplaced", value: new Date().format("MMM dd yyyy", location.timeZone))
displayInfoLog("Setting Battery Last Replaced to current date${newlyPaired}")
}
// installed() runs just after a sensor is paired
def installed() {
state.prefsSetCount = 0
displayInfoLog("Installing")
}
// configure() runs after installed() when a sensor is paired or reconnected
def configure() {
displayInfoLog("Configuring")
init()
state.prefsSetCount = 1
return
}
// updated() runs every time user saves preferences
def updated() {
displayInfoLog("Updating preference settings")
init()
displayInfoLog("Info message logging enabled")
displayDebugLog("Debug message logging enabled")
}
def init() {
def nButtons = 0
def revYear = "16"
def zigbeeModel = device.data.model ? device.data.model : "unknown"
displayInfoLog("Reported ZigBee model ID is $zigbeeModel")
switch (zigbeeModel.length() > 16 ? zigbeeModel[0..16] : zigbeeModel) {
case "lumi.sensor_86sw1":
nButtons = 1
break;
case "lumi.remote.b186a":
nButtons = 1
revYear = "18"
break;
case "lumi.sensor_86sw2":
nButtons = 3
break;
case "lumi.remote.b286a":
nButtons = 3
revYear = "18"
break;
case "lumi.sensor_ht":
log.warn "Model RTCGQ01LM Xiaomi Temperature Humidity Sensor Detected. Please manually assign Xiaomi Temperature Humidity Sensor device driver"
break;
case "unknown":
log.warn "Reported device model is unknown"
nButtons = 3
break;
}
if (nButtons != 0 & zigbeeModel != "unknown") {
def modelText = (nButtons == 1) ? "3" : "2"
def numPanels = (nButtons == 1) ? "Single" : "Dual"
displayInfoLog("Reported model is WXKG0${modelText}LM - 20$revYear revision ($numPanels panel Aqara Wireless Smart Light Switch)")
}
displayInfoLog("Number of buttons set to $nButtons")
if (!state.numOfButtons) {
sendEvent(name: "numberOfButtons", value: nButtons)
displayInfoLog("Number of buttons set to $nButtons")
state.numOfButtons = nButtons
}
if (!device.currentState('batteryLastReplaced')?.value)
resetBatteryReplacedDate(true)
}