Xiaomi smart light sensor

Great !!! Looking forward.

Hi, I've got one of the xiaomi illuminance sensor. Any advice about existing drivers (if any) to let it work?

Hi @karl2, I wouldn't bother you, but have you any good news for us :wink:?

1 Like

Hi mate, did you get this driver working, just received my sensor today and really desperate to get it working. Happy to test for you if you want a guinea pig :slight_smile:

This is my first device driver.
I cannot get the illuminance value. The driver detects the button pushes, it receives and parses correctly the messages but never intercept the illuminance value.
Some more experienced DD/Groovy guy can take a look at the code?
Please do not expect much from it...

/**
 *  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
import hubitat.zigbee.zcl.DataType
import groovy.json.JsonSlurper

def getVersionNum() { return "0.0.0" }
private def getVersionLabel() { return "My Xiaomi Mijia Smart Light Sensor ${getVersionNum()}" }

metadata {
    definition (name: "My Xiaomi Mijia Smart Light Sensor", namespace: "r.brunialti", author: "r.brunialti") {
        capability "Illuminance Measurement"
        capability "Sensor"
        capability "Battery"
        
        attribute "batteryLastReplaced", "String"
		attribute "lastCheckin", "String"
		attribute "version", "String"
        
        //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", ocfDeviceType: "oic.r.illuminance"      

        command "resetBatteryReplacedDate"
        command "configure"
    }
    preferences {
		input "luxOffset", "decimal", title: "Lux Value Offsetv", description: "", 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("A - Received message: ${description}")  
    displayDebugLog("B - Parsed message: ${descMap}")  

    // lastCheckin can be used with webCoRE
    sendEvent(name: "lastCheckin", value: now())    
    
    if (description?.startsWith('read attr - ')) {
		description = description - "read attr - "
        //def encoding = Integer.parseInt(description.split(",").find {it.split(":")[0].trim() == "encoding"}?.split(":")[1].trim(), 16)
    	def encoding = Integer.parseInt(descMap.encoding, 16)
        if (descMap.value != null & encoding > 0x18 & encoding < 0x3e) {
    		displayDebugLog("C1 - 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
    		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 == "0400" & descMap.attrInt == 0)
    		// Parse illuminance value report
    		map = parseIlluminance(descMap.value)
    	else if (descMap.cluster == "0000" & descMap.attrInt == 5)
    		// Button short-pressed
            displayDebugLog("C3 - Reset button of [${descMap.value}] was short-pressed")
            // Button management: insert code here following
    	else if (descMap.cluster == "0001" & descMap.attrInt == 32)
    		// Parse battery level from hourly announcement message
	    	map = parseBattery(descMap.value)
	    else
            displayDebugLogelvalueHex.("C4 - Unknown cluster/attribute: cluster=${cluster}, attribute=${descMap.attrId}")
    } else if (description?.startsWith('cat')){ 
		displayDebugLog("C5 - Catchall message ignored")
    } else
		displayDebugLog("C6 - Unknown message type")
        
	if (map != [:]) {
		displayDebugLog("D - Creating event $map")
		return createEvent(map)   
    }
}

private parseIlluminance(description) {
	def offset = luxOffset ? luxOffset : 0
	def lux = Integer.parseInt(description,16) + offset
	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)"
	// 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())
	}
	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() {
    if (!device.currentValue('batteryLastReplaced'))
		resetBatteryReplacedDate(true)
}
    
// configure() runs after installed() when a sensor is paired
    def configure() {
	displayInfoLog("Configuring")
    //Configure: empty stublatest/ref-doc/zigbee-ref.html 
    //zigbee.configureReporting(Integer Cluster, Integer attributeId, Integer dataType, Integer minReportTime, Integer MaxReportTime, [Integer reportableChange],    Map additionalParams=[:])
    //zigbee.readAttribute(Integer Cluster, Integer attributeId, Map additionalParams=[])
	init()
	sendEvent(name: "version", value:getVersionNum())
	state.prefsSetCount = 1
}

//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 Laast Replace date")
    }
    
// updated() will run every time user saves preferences
def updated() {
	displayInfoLog("Updating preference settings")
	init()
	displayInfoLog("Info message logging enabled")
	displayInfoLog("Debug message logging enabled")
}
    
//
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}"
}
2 Likes

Thank you for sharing!

I did write a complete driver with full functionality I had planned to test for a bit more before release, but since you shared this and I saw it is not complete I release mine now, hope you don't mind:

5 Likes

Of course I do'nt!
Thank you for the driver.
I would use this device to learn how to write a driver (yes I know it is Xiaomi same as I know they are unsupported devices in HE....).
I've got inspiration by other xiaomi drivers. It seems my driver lacks the attribute methods. Do you have any reference/example poiter to how to code them?

2 Likes

Just that you released a driver and a few hours later I do. Some people might not like that. :stuck_out_tongue:

You chose a rather hard device to learn with. To get full functionality you even need to write raw zigbee commands... And parse hex-based datastructures.

Attribute methods? You mean readAttribute and writeAttribute? I use them a lot in my Aqara Curtain drivers.

I do'nt really released it, as it is just a POC for me :slight_smile: I never mind about such things. I'm a not competitive man in the open source/community coding area. (Any code I released in the past to communities (e.g. Arduino and others) is free for anyone, in the broadest sense).

Yes. I got inspiration by Xiaomi motion sensor that does'nt use that functions for Illuminance. Evidently the base protocol is different in this sensor.

EDIT: any reading suggestion ? :slight_smile:

It is, you also have to convert the number. This sensor is actually doing things according to standard.

In general, the Zigbee Protocol definition, which I should read more myself.

As for documentation of those methods, it is sparse, some can be found in the forum, but not much. You can see some examples in my curtain driver.
Check line 476 for writeAttribute and around 500 for readAttribute. The "defines" are actually get methods on line 779 and down. I also have a "zigbeeWriteLongAttribute" method on line 798.

Feel free to open a new thread and ask more about these things :slight_smile: Just @-me and I will notice it.

1 Like

Ok my proof-of-concept (1st) driver is working.

It is for sure less complete than @markus one but also simplier for me.
Neverthless I'm studying the markus's driver structure/template.

I have just a question. When I receive a "read attr - raw:" message I use the zigbee.parseDescriptionAsMap function. In my code I noticed that the returned map reversed (correctly) the illuminance attribute value in description argument.
Does it imply the functions parseDescriptionAsMap check allways for encoding and reverse the values only for the appropriate encoding values ?
That would allow me to eliminate the code to check encoding type and reverse function.

Congratulations! :slight_smile: Not a simple device to get right.

It does, except for special types where it may fail to parse completely (STRUCT is one). Also, there's the special Xiaomi/Aqara format, when parsing that one values need to be reversed.

It is not really checking for encoding, it is just reversing since it is received the opposite way from how you would need them anyway.

If you look at my code you see that I only reverse the order when I parse more complex datatypes.

mmhh... I do not reverse the value in my driver, I use directly the values provided by the parseDescriptionAsMap function and the illuminance values seems consistent .

.. the ones i can intercept with code like the following ?

if (descMap.value != null & encoding > 0x18 & encoding < 0x3e)

It is correct once converted to Lux in accordance with the standard

Encoding 0x42 and 0x4C, 0x42 parses into string, but it shouldn't, 0x4C breaks. These are in many other Xiaomi/Aqara devices on cluster FF01 and FF02.

In your code I noticed you are using a variance value in order to avoid refresh illuminance display for small change values. Why do not use reportableChange parameter of the zigbee.configureReporting ? It should trigger the report only for changes bigger than reportableChange values (if I understood correctly...)

I've tried, it doesn't seem to follow that setting, I really wish it did. I do set it to 200 when I set the min and max intervals. The min and max intervals work as expected, the reportablechange does not. Maybe it does but interprets it differently than the standard? If you come up with a solution, please share :slight_smile:

That is true. I tried now min and max are ok. reportableChanges do'nt !

EDIT it is reported to use that optional parameter only for analog devices. Is this one?

I believe so, but it is a Xiaomi device, not following standard is what they do.

Thank you Markus !!!

1 Like

@markus May be I'm wrong but it seems to work (it reports only chages equal o higher than a trigger level).
My report is as following:
zigbee.configureReporting(0x0400, 0x0000, 0x21, (secondsMinLux == null ? 3000 : MinLux).intValue(), (MaxLux == null ? 30000 : secondsMaxLux).intValue(), 200, [:], msDelay)

and the logs are:

the lux are not real lux but raw illuminance values(I have some problems using maths in groovy...)