-
Notifications
You must be signed in to change notification settings - Fork 0
/
Xiamomi-Temp-Humidity-Sensor.txt
372 lines (351 loc) · 16 KB
/
Xiamomi-Temp-Humidity-Sensor.txt
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/**
* Xiaomi "Original" Temperature Humidity Sensor - model RTCGQ01LM
* & Aqara Temperature Humidity Sensor - model WSDCGQ11LM
* Device Driver for Hubitat Elevation hub
* Version 1.0.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.
*
* Based on SmartThings device handler code by a4refillpad
* With contributions by alecm, alixjg, bspranger, gn0st1c, foz333, guyeeba, jmagnuson, mike.maxwell, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh
* Code reworked for use with Hubitat Elevation hub by veeceeoh
*
* Known issues:
* + Xiaomi devices send reports based on changes, and a status report every 50-60 minutes. These settings cannot be adjusted.
* + The battery level / voltage is not reported at pairing. Wait for the first status report, 50-60 minutes after pairing.
* + Pairing Xiaomi devices can be difficult as they were not designed to use with a Hubitat hub.
* Holding the sensor's reset button until the LED blinks will start pairing mode.
* 3 quick flashes indicates success, while one long flash means pairing has not started yet.
* In either case, keep the sensor "awake" by short-pressing the reset button repeatedly, until recognized by Hubitat.
* + The connection can be dropped without warning. To reconnect, put Hubitat in "Discover Devices" mode, then short-press
* the sensor's reset button, and wait for the LED - 3 quick flashes indicates reconnection. Otherwise, short-press again.
*
*/
metadata {
definition (name: "Xiaomi Temperature Humidity Sensor", namespace: "veeceeoh", author: "veeceeoh", importUrl: "https://raw.githubusercontent.com/veeceeoh/xiaomi-hubitat/master/devicedrivers/xiaomi-temperature-humidity-sensor-hubitat.src/xiaomi-temperature-humidity-sensor-hubitat.groovy") {
capability "Battery"
capability "PressureMeasurement"
capability "RelativeHumidityMeasurement"
capability "Sensor"
capability "TemperatureMeasurement"
command "resetBatteryReplacedDate"
attribute "lastCheckinEpoch", "String"
attribute "lastCheckinTime", "Date"
attribute "batteryLastReplaced", "String"
//fingerprint for Xioami "original" Temperature Humidity Sensor - model RTCGQ01LM
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", model: "lumi.sens"
fingerprint profileId: "0104", inClusters: "0000,0003,0019,FFFF,0012", outClusters: "0000,0004,0003,0005,0019,FFFF,0012", model: "lumi.sensor_ht"
//fingerprint for Xioami Aqara Temperature Humidity Sensor - model WSDCGQ11LM
fingerprint profileId: "0104", inClusters: "0000,0003,FFFF,0402,0403,0405", outClusters: "0000,0004,FFFF", model: "lumi.weather"
}
preferences {
//Temp and Humidity Offsets
input "tempOffset", "decimal", title:"Temperature Offset", description:"", range:"*..*"
input "humidityOffset", "decimal", title:"Humidity Offset", description:"", range: "*..*"
if (getDataValue("modelType") == "Aqara WSDCGQ11LM" || getDataValue("modelType") == "unknown") {
input name: "pressOffset", type: "decimal", title: "Pressure Offset", description: "", range: "*..*"
input name: "pressureUnits", type: "enum", title: "Pressure Units (default: mbar)", description: "", options: ["mbar", "kPa", "inHg", "mmHg"], default: "mbar"
}
//Battery Voltage Range
input name: "voltsmin", type: "decimal", title: "Min Volts (0% battery = ___ volts). Default = 2.8 Volts", description: ""
input name: "voltsmax", type: "decimal", title: "Max Volts (100% battery = ___ volts). Default = 3.05 Volts", description: ""
//Date/Time Stamp Events Config
input name: "lastCheckinEnable", type: "bool", title: "Enable custom date/time stamp events for lastCheckin", description: ""
//Event Generation Behavior Config
input name: "recordAllReadings", type: "bool", title: "Enable events for unchanged sensor readings between hourly check-in", description: ""
input name: "recordAllBattery", type: "bool", title: "Enable events for unchanged battery level", 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: ""
}
}
// Parse incoming device messages to generate events
def parse(String description) {
displayDebugLog("Parsing message: $description")
if (description?.startsWith('cat')) {
Map descMap = zigbee.parseDescriptionAsMap(description)
displayDebugLog("Zigbee parse map of catchall = $descMap")
displayDebugLog("No action taken on catchall message")
} else if (description?.startsWith('re')) {
description = description - "read attr - "
Map descMap = (description).split(",").inject([:]) {
map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
// Reverse payload byte order for little-endian data types - required for Hubitat firmware 2.0.5 or newer
def intEncoding = Integer.parseInt(descMap.encoding, 16)
if (descMap.value != null && intEncoding > 0x18 && intEncoding < 0x3e) {
descMap.value = reverseHexString(descMap.value)
displayDebugLog("Little-endian payload data type; Hex value reversed to: ${descMap.value}")
}
// Send message data to appropriate parsing function based on the type of report
switch (descMap.cluster) {
case "0000": // Announcement or Check-in report
if (descMap.attrId == "0005")
displayDebugLog("Reset button was short-pressed")
else if (descMap.attrId == "FF01" || descMap.attrId == "FF02") // Hourly check-in report
parseCheckinMessage(descMap.value)
break
case "0402": // Temperature report
parseTemperature(descMap.value, false)
break
case "0403": // Pressure report (Aqara model only)
parsePressure(Integer.parseInt(descMap.value[0..3], 16)*10, false)
break
case "0405": // Humidity report
parseHumidity(descMap.value, false)
break
default:
displayDebugLog("Unknown read attribute message")
}
}
return [:]
}
// 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
}
// Calculate temperature with 0.01 precision in C or F unit as set by hub location settings
private parseTemperature(hexString, checkin) {
float temp = hexStrToSignedInt(hexString)/100
def tempScale = location.temperatureScale
def debugText = "Reported temperature: raw = $temp°C"
if (temp < -50) {
log.warn "${device.displayName}: Out-of-bounds temperature value received. Battery voltage may be too low."
} else {
if (tempScale == "F") {
temp = ((temp * 1.8) + 32)
debugText += ", converted = $temp°F"
}
if (tempOffset) {
temp = (temp + tempOffset)
debugText += ", offset = $tempOffset"
}
displayDebugLog(debugText)
temp = temp.round(2)
generateEvent([
name: 'temperature',
value: temp,
unit: "°$tempScale",
descriptionText: "Temperature is $temp°$tempScale",
translatable:true
], checkin)
}
}
// Calculate humidity with 0.1 precision
private parseHumidity(hexString, checkin) {
float humidity = Integer.parseInt(hexString,16)/100
def debugText = "Reported humidity: raw = $humidity"
if (humidity > 100) {
log.warn "${device.displayName}: Out-of-bounds humidity value received. Battery voltage may be too low."
return ""
} else {
if (humidityOffset) {
debugText += ", offset = $humidityOffset"
humidity = (humidity + humidityOffset)
}
displayDebugLog(debugText)
humidity = humidity.round(1)
generateEvent([
name: 'humidity',
value: humidity,
unit: "%",
descriptionText: "Humidity is $humidity%"
], checkin)
}
}
// Parse pressure report
private parsePressure(float pressureval, checkin) {
def debugText = "Reported pressure: raw = $pressureval Pa"
if (!(pressureUnits)) {
pressureUnits = "mbar"
}
switch (pressureUnits) {
case "mbar":
pressureval = (pressureval/100) as Float
pressureval = pressureval.round(1);
break;
case "kPa":
pressureval = (pressureval/1000) as Float
pressureval = pressureval.round(2);
break;
case "inHg":
pressureval = (((pressureval/100) as Float) * 0.0295300)
pressureval = pressureval.round(2);
break;
case "mmHg":
pressureval = (((pressureval/100) as Float) * 0.750062)
pressureval = pressureval.round(2);
break;
}
debugText += ", converted = $pressureval $pressureUnits"
if (pressOffset) {
debugText += ", offset = $pressOffset"
pressureval = (pressureval + pressOffset)
}
displayDebugLog(debugText)
pressureval = pressureval.round(2)
generateEvent([
name: 'pressure',
value: pressureval,
unit: pressureUnits,
descriptionText: "Pressure is $pressureval $pressureUnits"
], checkin)
}
// Convert raw 4 digit integer voltage value into percentage based on minVolts/maxVolts range
private parseBattery(hexString) {
def rawVolts = Integer.parseInt(hexString,16) / 1000
def minVolts = voltsmin ? voltsmin : 2.8
def maxVolts = voltsmax ? voltsmax : 3.05
displayDebugLog("Battery report: $rawVolts Volts, calculating level based on min/max range of $minVolts to $maxVolts")
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.min(100, Math.round(pct * 100))
generateEvent([
name: 'battery',
value: roundedPct,
unit: "%",
descriptionText: "Battery level is $roundedPct% ($rawVolts Volts)"
], false)
}
def generateEvent(eventResult, checkin) {
eventResult.descriptionText += checkin ? " (check-in report)" : ""
eventResult = (recordAllReadings || checkin || ((eventResult.name == battery) && recordAllBattery)) ? (eventResult + [isStateChange: true]) : eventResult
displayDebugLog("Creating event $eventResult")
sendEvent(eventResult)
displayInfoLog(eventResult.descriptionText)
}
def parseCheckinMessage(hexString) {
if (lastCheckinEnable) {
// lastCheckinEpoch is for apps that can use Epoch time/date and lastCheckinTime can be used with Hubitat Dashboard
sendEvent(name: "lastCheckinEpoch", value: now())
sendEvent(name: "lastCheckinTime", value: new Date().toLocaleString())
}
displayDebugLog("Received check-in message")
def result
// First byte of hexString is UINT8 of payload length in bytes, so it is skipped
def strPosition = 2
def strLength = hexString.size() - 2
while (strPosition < strLength) {
def dataTag = Integer.parseInt(hexString[strPosition++..strPosition++], 16) // Each attribute of the check-in message payload is preceded by a unique 1-byte tag value
def dataType = Integer.parseInt(hexString[strPosition++..strPosition++], 16) // After each attribute tag, the following byte gives the data type of the attribute data
def dataLength = DataType.getLength(dataType) // This looks up the length of data for the determined data type
def dataPayload // This is used to collect the payload data of each check-in message attribute
if (dataLength == null || dataLength == -1 || dataLength == 0) { // A length of null or -1 means the data type is probably variable-length, and 0 length is invalid
displayDebugLog("Check-in message contains unsupported dataType 0x${Integer.toHexString(dataType)} for dataTag 0x${Integer.toHexString(dataTag)} with dataLength $dataLength")
return
} else {
if (strPosition > (strLength - dataLength)) {
displayDebugLog("Ran out of data before finishing parse of check-in message")
return
}
dataPayload = hexString[strPosition++..(strPosition+=(dataLength * 2) - 1)-1] // Collect attribute tag payload according to data length of its data type
dataPayload = reverseHexString(dataPayload) // Reverse order of bytes for big endian payload
def dataDebug1 = "Check-in message: Found dataTag 0x${Integer.toHexString(dataTag)}"
def dataDebug2 = "dataType 0x${Integer.toHexString(dataType)}, dataLength $dataLength, dataPayload $dataPayload"
switch (dataTag) {
case 0x01: // Battery voltage
displayDebugLog("$dataDebug1 (battery), $dataDebug2")
parseBattery(dataPayload)
break
case 0x05: // RSSI dB
def convertedPayload = Integer.parseInt(dataPayload,16)
displayDebugLog("$dataDebug1 (RSSI dB), $dataDebug2 ($convertedPayload)")
state.RSSI = convertedPayload
break
case 0x06: // LQI
def convertedPayload = Integer.parseInt(dataPayload,16)
displayDebugLog("$dataDebug1 (LQI), $dataDebug2 ($convertedPayload)")
state.LQI = convertedPayload
break
case 0x64: // Temperature in Celcius
displayDebugLog("$dataDebug1 (temperature), $dataDebug2")
parseTemperature(dataPayload, true)
break
case 0x65: // Relative humidity
displayDebugLog("$dataDebug1 (humidity), $dataDebug2")
parseHumidity(dataPayload, true)
break
case 0x66: // Atmospheric pressure
displayDebugLog("$dataDebug1 (pressure), $dataDebug2")
parsePressure(Integer.parseInt(dataPayload,16), true)
break
case 0x0A: // ZigBee parent DNI (device network identifier)
displayDebugLog("$dataDebug1 (ZigBee parent DNI), $dataDebug2")
state.zigbeeParentDNI = dataPayload
break
default:
displayDebugLog("$dataDebug1 (unknown), $dataDebug2")
}
}
}
}
// installed() runs just after a sensor is paired
def installed() {
displayDebugLog("Installing")
init()
}
// configure() runs after installed() when a sensor is paired or re-joined
def configure() {
displayInfoLog("Configuring")
init()
device.updateSetting("infoLogging",[value:"true",type:"bool"])
device.updateSetting("debugLogging",[value:"true",type:"bool"])
displayInfoLog("Info and debug message logging automatically enabled for 2 hours. This can be overidden by saving preferences for this device.")
runIn(7200, stopAutoLogging, [data: [stopLogging: true]])
}
// updated() will run every time user saves preferences
def updated() {
displayInfoLog("Updating preference settings")
init()
displayInfoLog("Info message logging enabled")
displayDebugLog("Debug message logging enabled")
runIn(2, stopAutoLogging, [data: [stopLogging: false]])
}
def init() {
if (!(getDataValue("modelType")) || getDataValue("modelType") == "unknown") {
def sensorModel = "unknown"
if (device.data.model != "") {
if (device.data.model[5] == "s") // for model: "lumi.sensor_ht" or "lumi.sens"
sensorModel = "Xiaomi RTCGQ01LM"
else if (device.data.model[5] == "w") // for model: "lumi.weather"
sensorModel = "Aqara WSDCGQ11LM"
}
updateDataValue("modelType", sensorModel)
displayInfoLog("Detected sensor model is ${sensorModel}")
}
if (!device.currentValue('batteryLastReplaced'))
resetBatteryReplacedDate(true)
}
def stopAutoLogging(data) {
if (data.stopLogging) {
displayInfoLog("Automatic info and debug message logging now finished")
device.updateSetting("infoLogging",[value:"false",type:"bool"])
device.updateSetting("debugLogging",[value:"false",type:"bool"])
}
}
//Reset the batteryLastReplaced date to current date
def resetBatteryReplacedDate(paired) {
def newlyPaired = paired ? " for newly paired sensor" : ""
sendEvent(name: "batteryLastReplaced", value: new Date().format("MMM dd yyyy", location.timeZone))
displayInfoLog("Setting Battery Last Replaced to current date${newlyPaired}")
}
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}"
}