Double switch without neutral aqara

Maybe try this one. I use this in conjunction with the Generic Child driver.

// 12.18.2018: Added double click capability 
//             Added temperature measurement capability
//             Buttons can be separated from switch (buttons and switches work just fine, but they work independently)

metadata {
    definition (name: "Aqara 1/2 Wall Sw, no neutral)", namespace: "guyee", author: "P�ter Guly�s") {
        capability "Configuration"
        capability "Refresh"
		capability "PushableButton"
		capability "HoldableButton"
		capability "ReleasableButton"
		capability "DoubleTapableButton"
		capability "Temperature Measurement"

        command "childOn"
        command "childOff"
        command "childRefresh"
        command "recreateChildDevices"
        command "deleteChildren"

		// QBKG04LM: one button, no neutral required
		// Disconnect button from relay: write uint8 (0x20) value (connected: 0x12, disconnected: 0xFE) to attribute 0xFF22 of endpoint 0x01, cluster 0x0000
        fingerprint profileId: "0104", inClusters: "0000,0003,0001,0002,0019,000A", outClusters: "0000,000A,0019", manufacturer: "LUMI", model: "lumi.ctrl_neutral1", deviceJoinName: "Aqara Wall switch"
        // QBKG03LM: two buttons, no neutral required
		// Disconnect left button from relay: write uint8 (0x20) value (connected: 0x12, disconnected: 0xFE) to attribute 0xFF22 of endpoint 0x01, cluster 0x0000
		// Disconnect right button from relay: write uint8 (0x20) value (connected: 0x22, disconnected: 0xFE) to attribute 0xFF22 of endpoint 0x01, cluster 0x0000
        fingerprint profileId: "0104", inClusters: "0000,0003,0001,0002,0019,000A", outClusters: "0000,000A,0019", manufacturer: "LUMI", model: "lumi.ctrl_neutral2", deviceJoinName: "Aqara Wall switch"
    }

    preferences {
        input name: "numButtons", type: "enum", description: "", title: "Number of buttons", options: [[1:"1"],[2:"2"]], defaultValue: 1
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
        input name: "leftButtonDisconnect", type: "bool", title: "Disconnect left button from switch", defaultValue: false
        input name: "rightButtonDisconnect", type: "bool", title: "Disconnect right button from switch (double button devices)", defaultValue: false
    }
}

def logsOff(){
    log.warn "debug logging disabled..."
//    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def childRefresh(String deviceId)
{
    def endpointId = endpointNumber(deviceId);
    return  [
            "he rattr 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}","delay 200",  //light state
    ]
}

def refresh() {
    def cmds = []
    def children = getChildDevices()
    children?.each{
        cmds += childRefresh(it.deviceNetworkId)
    }

	return cmds
}

def parse(String description) {
	if (txtEnable) {
		log.debug description
	}
    
    if (description.startsWith("catchall"))
    	return
    
    def events = []

    def cluster = Integer.parseInt(description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim(), 16)
    def endpoint = Integer.parseInt(description.split(",").find {it.split(":")[0].trim() == "endpoint"}?.split(":")[1].trim(), 16)
	def attrId = Integer.parseInt(description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim(), 16)
	def valueHex = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
    
    if (cluster == 0x0006) { // on/off
        if (isSwitchEP(endpoint)) {
            if (attrId == 0x0000) {
			    def childDevice = getChildDevice("${device.zigbeeId}-${endpoint}")

                def switchState
                if (Integer.parseInt(valueHex[0..1], 16))
                	switchState = "on"
                else
                    switchState = "off"
                
                childDevice.sendEvent(name:"switch", value:switchState, descriptionText:"Switch #${EPtoIndex(endpoint)} has been turned $switchState")
	        } else {
	            log.warn("Unknown attribute $attrId for endpoint $endpoint")
            }
        } else if (isButtonEP(endpoint)) {
            if (attrId == 0x0000) {
                def messageMap = [
                    "00": "held",
                    "0000001001": "single-clicked",
                    "01": "released",
					"02": "double-clicked"
                    ]
                def eventTypeMap = [
                    "00": "held",
                    "0000001001": "pushed",
                    "01": "released",
					"02": "doubleTapped"
                    ]
                
                events += [
                    name: eventTypeMap[valueHex],
                    value: EPtoIndex(endpoint),
                    isStateChange: true,
                    descriptionText: "Button was ${messageMap[valueHex]}"
                ]
	        } else {
	            log.warn("Unknown attribute $attrId for endpoint $endpoint")
            }
        } else {
            log.warn("Unknown endpoint $endpoint")
        }
    } else if (cluster == 0x0000) {
	    def descMap = zigbee.parseDescriptionAsMap(description)
        def prefixLen = 4 + 2 + 4  // dni + endpoint + cluster
        def msgLen = Integer.parseInt(descMap.raw[prefixLen..(prefixLen + 1)], 16)
        events += parseXiaomiReport(descMap.raw[(prefixLen + 2)..(descMap.raw.length() - 1)])
    } else {
        log.info("Unknown message: $description")
    }
	
    return events
}

private int toBigEndian(String hex) {
    int ret = 0;
    String hexBigEndian = "";
    if (hex.length() % 2 != 0) return ret;
    for (int i = hex.length() - 2; i >= 0; i -= 2) {
        hexBigEndian += hex.substring(i, i + 2);
    }
    ret = Integer.parseInt(hexBigEndian, 16);
    return ret;
}

private parseXiaomiReport(description) {
	displayDebugLog("Xiaomi parse string = ${description}")
	def msgPos = 0
	def msgLength = description.size()

    def modelId = null;
    def manufacturerSpecificValues = null;
    
    while (msgPos < msgLength) {
    	def attrId = toBigEndian(description[msgPos++..(msgPos+=3)-1])
        def dataType = Integer.parseInt(description[msgPos++..msgPos++], 16)
        def dataLen = DataType.getLength(dataType)
        
        if (dataLen == null || dataLen == -1) { // Probably variable length
            switch (dataType) {
                case DataType.STRING_OCTET:
                case DataType.STRING_CHAR:
			        dataLen = Integer.parseInt(description[msgPos++..msgPos++], 16)
                	break;
                case DataType.STRING_LONG_OCTET:
                case DataType.STRING_LONG_CHAR:
			        dataLen = toBigEndian(description[msgPos++..(msgPos+=3)-1])
                	break;
	            default:
    	            log.error("Unsupported data type in Xiaomi Report msg: attrID = 0x${Integer.toHexString(attrId)}, type = 0x${Integer.toHexString(dataType)}")
        	        return
            }
        }
        
        if (dataLen * 2 > msgLength - msgPos) {  // Yes, it happens with lumi.sensor_86sw2 (WXKG02LM)
            log.error("WTF Xiaomi, packet length received from you (${dataLen} bytes) is greater than the length of remaining data (${(msgLength - msgPos) / 2} bytes)!")
        	dataLen = (msgLength - msgPos) / 2
        }

        def dataPayload

        if (dataLen != 0)
        	dataPayload = description[msgPos++..(msgPos+=(dataLen * 2) - 1)-1]

        switch (attrId) {
            case 0xFF01:
             	manufacturerSpecificValues = parseXiaomiReport_FF01(dataPayload)
            	displayDebugLog(manufacturerSpecificValues);
               	break;
            case 0x0005:
             	modelId = parseXiaomiReport_0005(dataPayload)
               	break;
            default:
                log.warn("Unsupported attribute in Xiaomi Report msg: attrID = 0x${Integer.toHexString(attrId)}, type = 0x${Integer.toHexString(dataType)}, length = ${dataLen} bytes, payload = ${dataPayload}")
        }
    }

    def events = []
    
	/* Non battery-powered devices
    if (manufacturerSpecificValues?.containsKey("BatteryPct")) {
        events += [
			name: 'battery',
			value: manufacturerSpecificValues["BatteryPct"],
			unit: "%",
			isStateChange: true,
			descriptionText: "Battery level is ${manufacturerSpecificValues["BatteryPct"]}% (${manufacturerSpecificValues["BatteryVolts"]} Volts)"
		]
    }
	*/

    if (manufacturerSpecificValues?.containsKey("Temperature")) {
        events += [
			name: 'temperature',
			value: manufacturerSpecificValues["Temperature"],
			unit: "°C",
			isStateChange: true,
			descriptionText: "Temperature is ${manufacturerSpecificValues["Temperature"]}°C"
		]
    }

    if (manufacturerSpecificValues?.containsKey("Power")) {
        events += [
			name: 'power',
			value: manufacturerSpecificValues["Power"],
			unit: "W",
			isStateChange: true,
			descriptionText: "Actual power consumption is ${manufacturerSpecificValues["Power"]}W"
		]
    }

    if (manufacturerSpecificValues?.containsKey("Energy")) {
        events += [
			name: 'energy',
			value: manufacturerSpecificValues["Energy"],
			unit: "kWh",
			isStateChange: true,
			descriptionText: "Power consumption so far is ${manufacturerSpecificValues["Energy"]}kWh"
		]
    }

	if (manufacturerSpecificValues?.containsKey("RouterID")) {
		state.routerID = manufacturerSpecificValues["RouterID"].toUpperCase()
	}

	return events
}

private parseXiaomiReport_FF01(payload) {
	displayDebugLog("Xiaomi parse FF01 string = ${payload}")
    
    def values = [ : ]

    def msgPos = 0
	def msgLength = payload.size()

    while (msgPos < msgLength) {
        def dataTag = Integer.parseInt(payload[msgPos++..msgPos++], 16)
        def dataType = Integer.parseInt(payload[msgPos++..msgPos++], 16)
        def dataLen = DataType.getLength(dataType)

        if (dataLen == null || dataLen == -1) { // Probably variable length
            switch (dataType) {
                case DataType.STRING_OCTET:
                case DataType.STRING_CHAR:
			        dataLen = Integer.parseInt(payload[msgPos++..msgPos++], 16)
                	break;
                case DataType.STRING_LONG_OCTET:
                case DataType.STRING_LONG_CHAR:
			        dataLen = toBigEndian(payload[msgPos++..(msgPos+=3)-1])
                	break;
	            default:
    	            log.error("Unsupported data type in Xiaomi Report msg: attrID = 0x${Integer.toHexString(attrId)}, type = 0x${Integer.toHexString(dataType)}")
        	        return
            }
        }

        def dataPayload

        if (dataLen != 0)
        	dataPayload = payload[msgPos++..(msgPos+=(dataLen * 2) - 1)-1]
        
        switch (dataTag) {
            case 0x01: // Battery
                def rawVolts = toBigEndian(dataPayload) / 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))

            	values += [ BatteryPct : roundedPct ]
            	values += [ BatteryVolts : rawVolts ]
            	break;
            case 0x03: // Temperature
            	values += [ Temperature : Integer.parseInt(dataPayload, 16, ) -3]  // Just a guess :)
            	break;
            case 0x05: // RSSI
            	values += [ RSSI : (toBigEndian(dataPayload) / 10) - 90 ]
            	break;
            case 0x06: // LQI
            	values += [ LQI : 255 - toBigEndian(dataPayload) ]
            	break;
            case 0x0A: // router
            	values += [ RouterID : Integer.toHexString(toBigEndian(dataPayload)) ]
            	break;
            case 0x64: // switch 1 state
            	values += [ Switch1State : toBigEndian(dataPayload) ]
            	break;
            case 0x65: // switch 2 state
            	values += [ Switch2State : toBigEndian(dataPayload) ]
            	break;
			case 0x95: // energy
				long theValue = Long.parseLong(toBigEndianHexString(dataPayload), 16)
				float floatValue = Float.intBitsToFloat(theValue.intValue());
            	values += [ Energy : floatValue ]
            	break;
			case 0x98: // power
				long theValue = Long.parseLong(toBigEndianHexString(dataPayload), 16)
				float floatValue = Float.intBitsToFloat(theValue.intValue());
            	values += [ Power : floatValue ]
            	break;
            default:
		        displayDebugLog("Unsupported tag in Xiaomi Report msg: dataTag = 0x${Integer.toHexString(dataTag)}, type = 0x${Integer.toHexString(dataType)}, length = ${dataLen} bytes, payload = ${dataPayload}")
        }
    }
    
    return values;
}

private parseXiaomiReport_0005(payload) {
	displayDebugLog("Xiaomi parse 0005 string = ${payload}")
    
    return new String(payload.decodeHex())
}

def childOn(String deviceId) {
    def endpointId = endpointNumber(deviceId)
    def cmd = [
            "he cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 1 {}",
            "he rattr 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}","delay 200",  //light state
    ]
    return cmd
}

def childOff(String deviceId) {
    def endpointId = endpointNumber(deviceId)
    def cmd = [
            "he cmd 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}",
            "he rattr 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 {}","delay 200",  //light state
    ]
    return cmd
}

def configure() {
    log.warn "configure..."
    runIn(1800,logsOff)
    def cmds = []
    def children = getChildDevices()
    children?.each{
        def endpointId = endpointNumber(it.deviceNetworkId)
	    cmds += [
            //bindings
            "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0006 {${device.zigbeeId}} {}", "delay 200",
            //reporting
            "he cr 0x${device.deviceNetworkId} ${endpointId} 0x0006 0 0x10 0 0x3600 {}","delay 200",
    	]
    }
	
	cmds += zigbee.writeAttribute(0x0000, 0xFF22, DataType.UINT8, leftButtonDisconnect ? 0xFE : 0x12, [mfgCode: "0x115F"])
	cmds += zigbee.writeAttribute(0x0000, 0xFF23, DataType.UINT8, rightButtonDisconnect ? 0xFE : 0x22, [mfgCode: "0x115F"])
	
    cmds += refresh()
    
	sendEvent(name:"numberOfButtons", value: numButtons)

    return cmds
}

def installed() {
    createChildDevices()
	configure()
}

def updated() {
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${txtEnable == true}"
    if (logEnable) runIn(1800,logsOff)
    
    if (!childDevices) {
        createChildDevices()
    }
    else if (device.label != state.oldLabel) {
        childDevices.each {
            def newLabel = "$device.displayName (EP${endpointNumber(it.deviceNetworkId)})"
            it.setLabel(newLabel)
        }
  
        state.oldLabel = device.label
    }

    configure()
}

def recreateChildDevices() {
    log.debug "recreateChildDevices"
    deleteChildren()
    createChildDevices()
}

def deleteChildren() {
	log.debug "deleteChildren"
	def children = getChildDevices()
    
    children.each {child->
  		deleteChildDevice(child.deviceNetworkId)
    }
}

private void createChildDevices() {
    log.debug "createChildDevices"
    
    for (i in 1..numButtons.toInteger()) {
        def switchEPId = indexToSwitchEP(i)
        addChildDevice("guyee", "Generic Child Switch", "$device.zigbeeId-$switchEPId", [name: "EP$switchEPId", label: "$device.displayName $switchEPId", isComponent: false])
    }
}

def intTo16bitUnsignedHex(value) {
    def hexStr = zigbee.convertToHexString(value.toInteger(),4)
    return new String(hexStr.substring(2, 4) + hexStr.substring(0, 2))
}

def intTo8bitUnsignedHex(value) {
    return zigbee.convertToHexString(value.toInteger(), 2)
}

private endpointNumber(String deviceId) {
    return deviceId.split("-")[-1] as Integer
}

private int indexToSwitchEP(int index) {
    return 1 + index
}

private int indexToButtonEP(int index) {
    return 3 + index
}

private int buttonToSwitchEP(int EP) {
    return EP - 2
}

private int switchToButtonEP(int EP) {
    return EP + 2
}

private boolean isSwitchEP(int EP) {
    return (2..3).contains(EP)
}

private boolean isButtonEP(int EP) {
    return (4..5).contains(EP)
}

private int EPtoIndex(int EP)
{
    return (EP % 2) + 1
}

private def displayDebugLog(message) {
	if (logEnable) log.debug "${device.displayName}: ${message}"
}
1 Like

She has already made the children for me and they work perfect for me, thank you very much for the help, great community

1 Like