[OBSOLETE] Device Handler for Aqara Wired Wall Switch

Thanks to information from veeceeoh & aonghusmor & zigbee2mqtt and based on code from Smartthings ZigBee Switch & Zooz Power Strip Outlet & Diego Schich, I made this DHT for Aqara Switch QBKG12LM.


The way is a little difference. We have 1 main DHT for general information and child for each switch.

  • Main: easy to turn on / off both switches -> easy set automation mode. If we turn on / off each switch separately: main switch will ON when any switch is ON and OFF when both switches are OFF. It also has measurement for voltage, temperature, power & energy for whole QBKG12LM.
  • Child: main will create 2 childs for 2 switches like Zooz Power Strip Outlet.

You can use and modify them for your convenient

Main DHT

/*
 *  for Aqara Switch QBKG12LM
 *
 *  use information from Smartthings ZigBee Switch & Diego Schich & Zooz Power Strip Outlet & Koenkk zigbee2mqtt
 *                     & aonghusmor & veeceeoh
 *
 *  Leza, 10-Aug-2019
 */

metadata {
	definition (name: "Aqara QBKG12LM Main", namespace: "Leza", author: "LPT", vid:"generic-switch-power-energy") {
		capability "Actuator"
		capability "Switch"
		capability "Temperature Measurement"
		capability "Voltage Measurement"
		capability "Power Meter"
		capability "Energy Meter"
		capability "Sensor"
		capability "Configuration"
		capability "Refresh"
		capability "Health Check"
		
		(1..2).each {
			//attribute "sw${it}Power", "number"
			attribute "sw${it}Switch", "string"
			attribute "sw${it}Name", "string"
			command "sw${it}On"
			command "sw${it}Off"
		}

		fingerprint	profileId: "0104", deviceId: "0051", inClusters: "0000,0001,0002,0003,0004,0005,0006,0010,000A", outClusters: "0019,000A",
					manufacturer: "LUMI", model: "lumi.ctrl_ln2.aq1", deviceJoinName: "Aqara Switch QBKG12LM"
	}	
	
	preferences {
		// Log Debug
		getBoolInput("debugOutput", "Enable Debug Logging", true)
	}

	tiles(scale: 2) {
		multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
			}
			tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
				attributeState("default", label:'Last Update: ${currentValue}')
			}
		}
		
		valueTile("sw1Name", "device.sw1Name", decoration:"flat", width:5, height: 1) {
			state "default", label:'${currentValue}'
		}
		standardTile("sw1Switch", "device.sw1Switch", width:1, height: 1) {
			state "on", label:'ON', action:"sw1Off", backgroundColor: "#00A0DC"
			state "off", label:'OFF', action:"sw1On", backgroundColor:"#ffffff"
		}
		valueTile("sw2Name", "device.sw2Name", decoration:"flat", width:5, height: 1) {
			state "default", label:'${currentValue}'
		}
		standardTile("sw2Switch", "device.sw2Switch", width:1, height: 1) {
			state "on", label:'ON', action:"sw2Off", backgroundColor: "#00A0DC"
			state "off", label:'OFF', action:"sw2On", backgroundColor:"#ffffff"
		}
		
		valueTile("voltage", "device.voltage", width: 2, height: 2) {
			state "voltage", label:'${currentValue} V', icon:"st.Appliances.appliances17"
		}
		valueTile("temperature", "device.temperature", width: 2, height: 2) {
			state "temperature", label:'${currentValue} °C', icon:"st.Weather.weather2"
		}
		standardTile("refresh", "device.refresh", width: 2, height: 2) {
			state "default", label:'Refresh', action: "refresh", icon:"st.secondary.refresh-icon"
		}
		valueTile("power", "device.power", width: 3, height: 2) {
			state "power", label:'${currentValue} W', icon:"st.Appliances.appliances5"
		}
		valueTile("energy", "device.energy", width: 3, height: 2) {
			state "energy", label:'${currentValue} kWh', icon:"st.Health & Wellness.health7"
		}
		
		main (["switch"])
		details (detailsTiles)
	}
}

private getDetailsTiles() {
	def tiles = ["switch"]
	(1..2).each {
		tiles << "sw${it}Name"
		tiles << "sw${it}Switch"
	}
    tiles << "voltage" << "temperature" << "refresh" << "power" << "energy"
	return tiles
}

private getBoolInput(name, title, defaultVal) {
	input "${name}", "bool", 
	title: "${title}?", 
	defaultValue: defaultVal, 
	required: false
}

// Globals - Key IDs
private Key_TEMPERATURE()	{ 0x03 }
private Key_RSSI()			{ 0x05 }
private Key_SWITCH_1()		{ 0x64 }
private Key_SWITCH_2()		{ 0x65 }
private Key_ENERGY_METER()	{ 0x95 }
private Key_POWER_METER()	{ 0x98 }

// ZigBee - Data Types
private ZB_Data_BOOLEAN()	{ 0x10 }
private ZB_Data_UINT8()		{ 0x20 }
private ZB_Data_UINT16()	{ 0x21 }
private ZB_Data_UINT64()	{ 0x27 }
private ZB_Data_INT8()		{ 0x28 }
private ZB_Data_FLOAT4()	{ 0x39 }

def installed() { 
	// Make sure the switch get created if using the new mobile app.
	runIn(30, createChildDevices)
}

def updated() {	
	if (!isDuplicateCommand(state.lastUpdated, 3000)) {
		state.lastUpdated = new Date().time
		
		def cmds = []
		
		if (childDevices?.size() != 2)	cmds += createChildDevices()
		
		cmds += configure()
		return cmds ? response(cmds) : []
	}
}

def createChildDevices() {
	(1..2).each { endPoint ->
		if (!findChildByEndPoint(endPoint)) {			
			def dni = "${getChildDeviceNetworkId(endPoint)}"
			
			addChildSwitch(dni, endPoint)
			childUpdated(dni)
		}
	}
}

private addChildSwitch(dni, endPoint) {
	logDebug "Creating SW${endPoint} Child Device"
	addChildDevice("Leza", "Aqara QBKG12LM Child Switch", dni, null, 
		[
			completedSetup: true,
			isComponent: false,
			label: "${device.displayName}-SW${endPoint}",
			componentLabel: "SW ${endPoint}",
			componentName: "SW${endPoint}"
		]
	)
}

def configure() {	
	updateHealthCheckInterval()
}

private updateHealthCheckInterval() {
	// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
	def minReportingInterval = (1 * 60 * 60)
	
	if (state.minReportingInterval != minReportingInterval) {
		state.minReportingInterval = minReportingInterval
		logDebug "Configured health checkInterval when updated()"
		
		// Set the Health Check interval so that it can be skipped twice plus 2 minutes.
		def checkInterval = ((minReportingInterval * 2) + (2 * 60))
		sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
	}
}

def childUpdated(dni) {
	logDebug "childUpdated(${dni})"
	def child = findChildByDeviceNetworkId(dni)
	def endPoint = getEndPoint(dni)
	def nameAttr = "sw${endPoint}Name"
	logDebug "${child.displayName} vs ${device.currentValue(nameAttr)}"
	if (child && "${child.displayName}" != "${device.currentValue(nameAttr)}") {
		sendEvent(name: nameAttr, value: child.displayName, displayed: false)
	}
}

def ping() {
	logDebug "ping()..."
	return zigbee.onOffRefresh()
}

def on() {
	logDebug "on()..."
	sw1On()
	sw2On()
}

def off() {
	logDebug "off()..."
	sw1Off()
	sw2Off()
}

def sw1On() { childOn(getChildDeviceNetworkId(1)) }
def sw2On() { childOn(getChildDeviceNetworkId(2)) }

def childOn(dni) {
	logDebug "childOn(${dni})..."
    
	def endPoint = getEndPoint(dni)
	
	if (endPoint == 1) {
		logDebug "On(${endPoint})..."
		//zigbee.on()
		def actions = [new physicalgraph.device.HubAction("st cmd 0x${device.deviceNetworkId} 0x01 0x0006 0x01 {}")]    
		sendHubCommand(actions)
	}
	else if (endPoint == 2) {
		logDebug "On(${endPoint})..."
		def actions = [new physicalgraph.device.HubAction("st cmd 0x${device.deviceNetworkId} 0x02 0x0006 0x01 {}")]    
		sendHubCommand(actions)
	}
}

def sw1Off() { childOff(getChildDeviceNetworkId(1)) }
def sw2Off() { childOff(getChildDeviceNetworkId(2)) }

def childOff(dni) {
	logDebug "childOff(${dni})..."
	
	def endPoint = getEndPoint(dni)
	
	if (endPoint == 1) {
		logDebug "Off(${endPoint})..."
		def actions = [new physicalgraph.device.HubAction("st cmd 0x${device.deviceNetworkId} 0x01 0x0006 0x00 {}")]
		sendHubCommand(actions)
	}
	else if (endPoint == 2) {
		logDebug "Off(${endPoint})..."
		def actions = [new physicalgraph.device.HubAction("st cmd 0x${device.deviceNetworkId} 0x02 0x0006 0x00 {}")]
		sendHubCommand(actions)
	}
}

def refresh() {
	logDebug "Refresh..."	
	def Cmds = zigbee.readAttribute(0x0001, 0x0000, [destEndpoint: 0x01]) +		// voltage
			   zigbee.readAttribute(0x0002, 0x0000, [destEndpoint: 0x01]) +		// temperature
			   zigbee.readAttribute(0x0006, 0x0000, [destEndpoint: 0x01]) +		// switch 1 state
			   zigbee.readAttribute(0x0006, 0x0000, [destEndpoint: 0x02]) +		// switch 2 state
			   zigbee.readAttribute(0x000C, 0x0055, [destEndpoint: 0x03])		// power
	return Cmds
}

def childRefresh(dni) {
	logDebug "child(${dni})Refresh..."
	
	def endPoint = getEndPoint(dni)
	
	def Cmds = zigbee.readAttribute(0x0001, 0x0000, [destEndpoint: 0x01]) +		// voltage
			   zigbee.readAttribute(0x0002, 0x0000, [destEndpoint: 0x01]) +		// temperature
			   zigbee.readAttribute(0x0006, 0x0000, [destEndpoint: endPoint])	// switch state
	
	def actions = []
	
    Cmds?.each {
		actions << new physicalgraph.device.HubAction(it)
	}
	sendHubCommand(actions, 100)
	return []
}

// Parse incoming device messages to generate events
def parse(String description) {
	logDebug "description is $description"
	def cluster = zigbee.parseDescriptionAsMap(description)
	logDebug "descMap is $cluster"
	
	if (description?.startsWith("read attr -")) {		
		// Switch state
		if ((cluster.clusterInt == 0x0006) && (cluster.attrInt == 0x0000)) {
			if(cluster.endpoint == "01") {
				state.sw1 = (cluster.value == "01" ? "on" : "off")
				executeSendEvent(findChildByEndPoint(1), createEventMap("switch", state.sw1))
				executeSendEvent(null, createEventMap("sw1Switch", state.sw1))
				executeSendEvent(null, createEventMap("switch", ((state.sw1 == "on") || (state.sw2 == "on")) ? "on" : "off"))
			}
        	else if (cluster.endpoint == "02") {
				state.sw2 = (cluster.value == "01" ? "on" : "off")
				executeSendEvent(findChildByEndPoint(2), createEventMap("switch", state.sw2))
				executeSendEvent(null, createEventMap("sw2Switch", state.sw2))
				executeSendEvent(null, createEventMap("switch", ((state.sw1 == "on") || (state.sw2 == "on")) ? "on" : "off"))
			}
		}
		// Voltage
		else if (cluster.clusterInt == 0x0001 && cluster.attrInt == 0x000) {
			if (cluster.endpoint == "01") {
				def value = (int) (Integer.parseInt(cluster.value, 16) / 10)
				executeSendEvent(null, createEventMap("voltage", value, null, "V"))
				executeSendEvent(findChildByEndPoint(1), createEventMap("voltage", value, null, "V"))
				executeSendEvent(findChildByEndPoint(2), createEventMap("voltage", value, null, "V"))
			}
		}
		// Temperature
		else if (cluster.clusterInt == 0x0002 && cluster.attrInt == 0x000) {
			if (cluster.endpoint == "01") {
				def value = Integer.parseInt(cluster.value, 16)
				if (value > 127)	value = value - 256
				executeSendEvent(null, createEventMap("temperature", value, null, "C"))
				executeSendEvent(findChildByEndPoint(1), createEventMap("temperature", value, null, "C"))
				executeSendEvent(findChildByEndPoint(2), createEventMap("temperature", value, null, "C"))
			}
		}
		// Power meter
		else if ((cluster.clusterInt == 0x000C) && (cluster.attrInt == 0x0055)) {
			if (cluster.endpoint == "03") {
				def value = Float.intBitsToFloat(Long.parseLong(cluster.value, 16).intValue())
				executeSendEvent(null, createEventMap("power", roundTwoPlaces(value), null, "W"))
			}
		}
	}
	
	if (description?.startsWith("catchall:")) {
		if ((cluster.clusterInt == 0x0000) && (cluster.attrInt == 0xFF01)) {
			Map key = [:]
			int i = 4
			int j
			
			// get keys
			while (i < cluster.data.size()) {
				// save start index
				j = i
			
				// get key type
				key.type = Integer.parseInt(cluster.data[i], 16)
				if      (key.type == Key_TEMPERATURE())		key.name = "temperature"        	
				else if (key.type == Key_RSSI())			key.name = "RSSI" 
				else if (key.type == Key_SWITCH_1())		key.name = "switch1" 
				else if (key.type == Key_SWITCH_2())		key.name = "switch2" 
				else if (key.type == Key_ENERGY_METER())	key.name = "energy" 
				else if (key.type == Key_POWER_METER())		key.name = "power" 
				else										key.name = "unknown (" + cluster.data[i] + ")"
			
				// get key value
				i++
				key.datatype = Integer.parseInt(cluster.data[i++], 16)
				if (key.datatype == ZB_Data_BOOLEAN()) {
					key.value = ((cluster.data[i] == "01") ? "on" : "off")
				}
				else if (key.datatype == ZB_Data_UINT8()) {
					key.value = Integer.parseInt(cluster.data[i], 16)
				}
				else if (key.datatype == ZB_Data_UINT16()) {
					key.value = Integer.parseInt(cluster.data[i+1] + cluster.data[i], 16)
					i += 1
				}
				else if (key.datatype == ZB_Data_UINT64()) {
					key.value = Long.parseLong(cluster.data[i+7] + cluster.data[i+6] + cluster.data[i+5] + cluster.data[i+4] +
											   cluster.data[i+3] + cluster.data[i+2] + cluster.data[i+1] + cluster.data[i], 16)
					i += 7
				}
				else if (key.datatype == ZB_Data_INT8()) {
					key.value = Integer.parseInt(cluster.data[i], 16)
					if (key.value > 127) {
						key.value = key.value - 256
					}
				}
				else if (key.datatype == ZB_Data_FLOAT4()) {
					key.value = Float.intBitsToFloat(Long.parseLong(cluster.data[i+3] + cluster.data[i+2] +
																	cluster.data[i+1] + cluster.data[i], 16).intValue())
					i += 3
				}
				logDebug "index: $j, key: $key.name, value: $key.value"
			
				// send event
				if (key.type == Key_TEMPERATURE()) {
					executeSendEvent(null, createEventMap("temperature", key.value, null, "C"))
					executeSendEvent(findChildByEndPoint(1), createEventMap("temperature", key.value, null, "C"))
					executeSendEvent(findChildByEndPoint(2), createEventMap("temperature", key.value, null, "C"))
				}
				else if (key.type == Key_SWITCH_1()) {
					state.sw1 = key.value
					executeSendEvent(findChildByEndPoint(1), createEventMap("switch", state.sw1))
					executeSendEvent(null, createEventMap("sw1Switch", state.sw1))
					executeSendEvent(null, createEventMap("switch", ((state.sw1 == "on") || (state.sw2 == "on")) ? "on" : "off"))
				}
				else if (key.type == Key_SWITCH_2()) {
					state.sw2 = key.value
					executeSendEvent(findChildByEndPoint(2), createEventMap("switch", state.sw2))
					executeSendEvent(null, createEventMap("sw2Switch", state.sw2))
					executeSendEvent(null, createEventMap("switch", ((state.sw1 == "on") || (state.sw2 == "on")) ? "on" : "off"))
				}
				else if (key.type == Key_ENERGY_METER()) {
					executeSendEvent(null, createEventMap("energy", roundTwoPlaces(key.value), null, "kWh"))
				}
				else if (key.type == Key_POWER_METER()) {
					executeSendEvent(null, createEventMap("power", roundTwoPlaces(key.value), null, "W"))
				}
			
				i++
			}
		}
	}
    
	// send event for heartbeat
	def now = new Date().format("dd-MM-yyyy HH:mm:ss", location.timeZone)
	sendEvent(name: "lastCheckin", value: now, displayed: false)
	executeSendEvent(findChildByEndPoint(1), createEventMap("lastCheckin", now))
	executeSendEvent(findChildByEndPoint(2), createEventMap("lastCheckin", now))
}

private executeSendEvent(child, evt) {
	if (evt.displayed == null)	evt.displayed = (getAttrVal(evt.name, child) != evt.value)
    
	if (evt) {
		if (child) {
			if (evt.descriptionText) {
				evt.descriptionText = evt.descriptionText.replace(device.displayName, child.displayName)
			}
            logDebug "${evt}"
			child.sendEvent(evt)						
		}
		else {
			logDebug "${evt}"
            sendEvent(evt)
		}		
	}
}

private createEventMap(name, value, displayed=null, unit=null) {	
	def eventMap = [
		name: name,
		value: value,
		displayed: displayed,
		isStateChange: true,
		descriptionText: "${device.displayName} - ${name} is ${value}"
	]
	
	if (unit) {
		eventMap.unit = unit
		eventMap.descriptionText = "${eventMap.descriptionText} ${unit}"
	}
	return eventMap
}

private getAttrVal(attrName, child=null) {
	try {
		if (child) {
			return child?.currentValue("${attrName}")
		}
		else {
			return device?.currentValue("${attrName}")
		}
	}
	catch (ex) {
		//logTrace "$ex"
		return null
	}
}

private findChildByEndPoint(endPoint) {
	def dni = "${getChildDeviceNetworkId(endPoint)}"
	return findChildByDeviceNetworkId(dni)
}

private findChildByDeviceNetworkId(dni) {
	return childDevices?.find { it.deviceNetworkId == dni }
}

private getEndPoint(childDeviceNetworkId) {
	return safeToInt("${childDeviceNetworkId}".reverse().take(1))
}

private getChildDeviceNetworkId(endPoint) {
	return "${device.deviceNetworkId}-SW${endPoint}"
}

private safeToInt(val, defaultVal=0) {
	return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}

private safeToDec(val, defaultVal=0) {
	return "${val}"?.isBigDecimal() ? "${val}".toBigDecimal() : defaultVal
}

private roundTwoPlaces(val) {
	return Math.round(safeToDec(val) * 100) / 100
}

private isDuplicateCommand(lastExecuted, allowedMil) {
	!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) 
}

private logDebug(msg) {
	if (settings?.debugOutput != false) {
		log.debug "$msg"
	}
}

Child DHT

/*
 *  for Aqara Switch QBKG12LM - Child Switch
 *
 *  modify code from Smartthings ZigBee Switch & Diego Schich & Zooz Power Strip Outlet
 *
 *  Leza, 10-Aug-2019
 */

metadata {
	definition (name: "Aqara QBKG12LM Child Switch", namespace: "Leza", author: "LPT", vid:"generic-switch-power-energy") {
        capability "Actuator"
		capability "Switch"
        capability "Temperature Measurement"
        capability "Voltage Measurement"
		capability "Sensor"
		capability "Refresh"
	}
    
    simulator { }

	tiles(scale: 2) {
		multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
			}
			tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
				attributeState("default", label:'Last Update: ${currentValue}')
			}
		}
		
		valueTile("voltage", "device.voltage", width: 2, height: 2) {
			state "voltage", label:'${currentValue} V', icon:"st.Appliances.appliances17"
		}
		valueTile("temperature", "device.temperature", width: 2, height: 2) {
			state "temperature", label:'${currentValue} °C', icon:"st.Weather.weather2"
		}
		standardTile("refresh", "device.refresh", width: 2, height: 2) {
			state "default", label:'Refresh', action: "refresh", icon:"st.secondary.refresh-icon"
		}

		main (["switch"])
		details(["switch", "voltage", "temperature", "refresh"])
	}
}

def installed() {}

def updated() {	
	parent.childUpdated(device.deviceNetworkId)
}

def on() {
	parent.childOn(device.deviceNetworkId)	
}

def off() {
	parent.childOff(device.deviceNetworkId)	
}

def refresh() {
	parent.childRefresh(device.deviceNetworkId)
}
5 Likes