Xiaomi smart light sensor

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...)

Thanks @markus and @roberto for your work.
I've just purchased the sensor and it will be very useful to me.

1 Like

I will see why mine doesn't seem to follow this setting. Will have to look at the actual traffic sent in Wireshark to be sure all is done right when I send the configuration. I would really prefer to have this working, but I will still leave my additional filter in-place to prevent crazy amounts of events in case of failure.

They're what the zigbee spec says they should be, so the specs tell you how to convert them. My code has the conversion calculation in Groovy.

Can you please share the code?

I'm trying with Markus' driver but no illuminance value is being displayed with that.

I have two devices, would like to compare them side by side.

Do these devices still only report lux on motion, or also between times or at a setable interval?

Thanks.

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.

3 Likes

Did you follow the post-pairing instructions? Until the device receives and accepts the configuration with the bind and report config it will not send any data to HE. Since it is battery powered it only receives commands right after pressing the button on the device. If there are connectivity issues on your mesh these commands may not arrive.

Maybe the driver can be made to do this with better feedback and in an easier way, but as is mentioned, it is an early release so that the community get to have something that works. @roberto is now doing the same thing in his driver as mine for the binding and configuration and the differences are in other areas.

Ciao Roberto
I have some problem with the Xiaomi GZCGQ01LM
once paired it loose the connection after 30s/1 minute.
Do you have suggestion?
i don't know how to solve it
Thanks

Sorry for the delay. I'm not a compulsive frequent visitor of the community.
You could try the markus's drivers at : Hubitat/drivers/expanded at development ยท markus-li/Hubitat ยท GitHub
Hope it can help you

1 Like

Already done does not work