Please find here following. Please consider also it is still to clean up and to refine.
Even more the value is illuminance not lux. I'll post again here asap it is finished.
But .. it works.
pure illuminance device. fast and precise. it is possible to configure min and max time for reporting (illuminance reading) that is the minimum and maximum interval between value readings. In my driver it is configurable (but it is only for debug)
/**
* My Xiaomi Mijia Smart Light 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.
**/
// see:https://github.com/dresden-elektronik/deconz-rest-plugin/issues/2300
// see:https://community.smartthings.com/t/release-xiaomi-mijia-smart-light-sensor-march-2020/189266
// see:https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/zigbee-example.html
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.math.BigDecimal
def getVersionNum() { return "0.0.2" }
private def getVersionLabel() { return "Xiaomi Mijia Smart Light Sensor" }
metadata {
definition (name: "My Xiaomi Mijia Smart Light Sensor", namespace: "r.brunialti", author: "r.brunialti") {
capability "Illuminance Measurement"
capability "Sensor"
capability "Battery"
capability "Refresh"
attribute "batteryLastReplaced", "String"
attribute "lastCheckin", "String"
attribute "device", "String"
attribute "illuminance", "number"
//fingerprint profileId: "0104", inClusters: "0000,0400,0003,0001", outClusters: "0003", manufacturer: "LUMI", model: "lumi.sen_ill.mgl01", deviceJoinName: "Xiaomi Mijia Smart Home Light Sensor", ocfDeviceType: "oic.r.sensor.illuminance"
fingerprint profileId: "0104", inClusters: "0000,0400,0003,0001", outClusters: "0003", manufacturer: "LUMI", model: "lumi.sen_ill.mgl01", deviceJoinName: "Xiaomi Mijia Smart Home Light Sensor"
command "resetBatteryReplacedDate"
command "configure"
command "refresh"
}
preferences {
input name: "luxOffset", title: "Lux Value Offset", description: "", range: "*..*"
input name: "secondsMinLux", title: "Refresh minimum time", description: "Default = 3000 ms", type: "decimal", range: "*..*"
input name: "secondsMaxLux", title: "Refresh maximum time", description: "Default = 30000 ms", type: "decimal", range: "*..*"
input name: "minTriggerLux", title: "Minimum value change to report", description: "Default = 1", type: "decimal", range: "*..*"
//Battery Voltage Range
input name: "voltsmin", title: "Min Volts (0% battery = ___ volts, range 2.0 to 2.9). Default = 2.5 Volts", description: "", type: "decimal", range: "2..2.7"
input name: "voltsmax", title: "Max Volts (100% battery = ___ volts, range 2.8 to 3.5). Default = 3.0 Volts", description: "Default = 3.0 Volts", type: "decimal", range: "2.8..3.4"
//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: ""
}
}
def parse(String description) {
Map map = [:]
def descMap = zigbee.parseDescriptionAsMap(description)
displayDebugLog("Received message: ${description}")
displayDebugLog("Parsed message: ${descMap}")
//sendEvent(name: "lastCheckin", value: new Date().format("dd/mm/yy hh:mm:ss a", location.timeZone))
sendEvent(name: "lastCheckin", value: new Date())
if (description?.startsWith('read attr - raw:')) {
description = description - "read attr - raw:"
def encoding = Integer.parseInt(descMap.encoding, 16)
if (descMap.value != null & encoding > 0x18 & encoding < 0x3e)
displayDebugLog("Reversed payload value: ${descMap.value}")
/*
if (descMap.value != null & encoding > 0x18 & encoding < 0x3e) {
displayDebugLog("C1 - Data type of payload is little-endian; parseDescriptionAsMap reversed order")
// Reverse order of bytes in description's payload for LE data types - required for Hubitat firmware 2.0.5 or newer
// descMap.value = reverseHexString(descMap.value)
displayDebugLog("C2 - Reversed payload value: ${descMap.value}")
}
*/
// Send message data to appropriate parsing function based on the type of report
if (descMap.cluster == "0000" & descMap.attrInt == 5)
// Button short-pressed
displayDebugLog("Reset button of [${descMap.value}] was short-pressed")
else if (descMap.cluster == "0400" & descMap.attrInt == 0)
// Parse illuminance value report
map = parseIlluminance(descMap.value)
else if (descMap.cluster == "0001" & descMap.attrInt == 32)
// Parse battery level from hourly announcement message
map getBatteryResult(descMap.value)
else
displayDebugLog("Unknown cluster/attribute: cluster=${descMap.cluster}, attribute=${descMap.attrId}")
} else if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
displayDebugLog("Trapped catchall: cluster=${descMap.cluster}")
// cluster=="0600" -> OTA update request
// cluster=="0300" & command==1 -> IDENTIFY request response
} else
displayDebugLog("Unknown message type")
if (map != [:]) {
displayInfoLog("$map.descriptionText")
return createEvent(map)
}
}
//============================================
// Check catchall for battery voltage data to pass to getBatteryResult for conversion to percentage report
private Map parseCatchAllMessage(description) {
Map resultMap = [:]
def catchall = zigbee.parse(description)
def MsgLength = catchall.data.size()
if (catchall.clusterId == 0x0000) {
// 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))
displayInfoLog(": $descText")
return [
name: 'battery',
value: roundedPct,
unit: "%",
isStateChange:true,
descriptionText : "Battery at ${roundedPct}% (${rawVolts} Volts)"
]
}
//
private parseIlluminance(rawValue) {
def offset = luxOffset ? luxOffset : 0
Integer value = Integer.parseInt(rawValue, 16)
//illuminance -> lux = 10,000 x log10(Illuminance) + 1
BigDecimal lux = value > 0 ? Math.pow(10, value / 10000.0) - 1.0 : 0
lux = lux.setScale(2, BigDecimal.ROUND_HALF_UP)
return [
name: 'illuminance',
value: lux,
unit: 'lux',
isStateChange: true,
descriptionText: "Illuminance is ${lux} lux"
]
}
// Convert raw 4 digit integer voltage value -> 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)"
if (lastCheckinEnable) {
sendEvent(name: "lastCheckinEpoch", value: now())
}
displayInfoLog(descText)
return [
name: 'battery',
value: roundedPct,
unit: "%",
descriptionText: descText
]
}
// installed() runs just after a sensor is paired
def installed() {
displayDebugLog("Installing")
state.prefsSetCount = 0
init()
}
//
def init() {
configure()
}
// updated() will run every time user saves preferences
def updated() {
displayInfoLog("Updating preference settings")
init()
}
//Reset the batteryLastReplaced date to current date
def resetBatteryReplacedDate(paired) {
displayInfoLog("Setting Battery Last Replace date")
sendEvent(name: "batteryLastReplaced", value: new Date())
}
//
private def displayDebugLog(message) {
if (debugLogging)
log.debug "${device.displayName}: ${message}"
}
// 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
}
//
private def displayInfoLog(message) {
if (infoLogging || state.prefsSetCount != 1)
log.info "${device.displayName}: ${message}"
}
//
def configure() {
displayDebugLog("Configuration starting")
displayDebugLog("...bindings and reporting")
List<String> cmds = []
sendEvent(name: "device", value: getVersionLabel())
Integer minReportTime = secondsMinLux == null ? 30 : secondsMinLux.toInteger()
Integer maxReportTime = secondsMaxLux == null ? 120 : secondsMaxLux.toInteger()
Integer minLuminance = minTriggerLux == null ? 1 : minTriggerLux.toInteger()
cmds.addAll(
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x001 {${device.zigbeeId}} {}", // identify cluster
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x000 {${device.zigbeeId}} {}", // identify cluster
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x003 {${device.zigbeeId}} {}", // identify cluster
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x400 {${device.zigbeeId}} {}", // illuminance cluster
"send 0x${device.deviceNetworkId} 1 1",
"he cr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0001 0x0020 0x20 60 3600 {01}", // identify cluster
"he cr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0000 0x0005 0xff 30 3600 {}", // identify cluster
"he cr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0003 0x0000 0xff 1 36000 {}", // identify cluster
"he cr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0400 0x0000 0x21 ${minReportTime} ${maxReportTime} {01}" // illuminance cluster
)
//sendHubCommand(new HubMultiAction(delayBetween(cmds,200), Protocol.ZIGBEE))
displayDebugLog("...returning commands: ${cmds+refresh()}")
return cmds
}
// configure() runs after installed() when a sensor is paired
// refresh() runned from init() and command refresh
def refresh() {
displayDebugLog("...refreshing values")
List<String> cmds = []
Integer msDelay = 200
cmds.addAll(
"he rattr 0x${device.deviceNetworkId} 1 0x000 0", "delay $msDelay",
"he rattr 0x${device.deviceNetworkId} 1 0x001 0", "delay $msDelay",
"he rattr 0x${device.deviceNetworkId} 1 0x003 0", "delay $msDelay",
"he rattr 0x${device.deviceNetworkId} 1 0x400 0", "delay $msDelay")
return cmds
}
void sendZigbeeCommands(ArrayList<String> cmd) {
displayDebugLog("sendZigbeeCommands($cmd)")
hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction()
cmd.each {
//displayDebugLog(">>>>>$it")
if(it.startsWith('delay')) {
allActions.add(new hubitat.device.HubAction(it))
} else {
allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE))
}
}
sendHubCommand(allActions)
}
EDIT: now it should report the correct lux value.