Aqara Roller Blind Motor (2019 new)

Has anyone had any luck connecting the new roller type aqara blind motor (https://s.click.aliexpress.com/e/lyeBYe4)? And if so what device type handler did you use?

I’m moving to a new build and am torn between this motor and the upcoming IKEA Fyrtur, but don’t like there inflexible dimensions of the Fyrtur.

I would wait for Ikea. The Aqara model doesn’t say which zigbee classification it is certified for. We know the Ikea blinds will be Zigbee 3.0

1 Like

anyone know the uk release date?

Presume you mean IKEA Fyrtur? Supposedly mid August, the battery packs are already on sale (BRAUNIT)

strange a quick google, but cant find much on them

There’s a bit out there, they delayed the release from May to give them time to make firmware upgrades, but are fairly sure it will be mid August (October for US).

A lot of good info here https://www.idealhome.co.uk/news/ikea-smart-blinds-231218

Just got this off Amazon UK for £60. It is indeed Zigbee 3.0. It paired OK as a thing and then had to edit the device in the IDE and change the type to ZigBee Window Shade. Works a treat now!

do you still have the DH for this please, as i cant seem to find one that works

I did actually tweak the SmartThings DH to make it work with this as the percentage levels were reversed eg. setting the level to 25% actually gave 75%. I’m afraid I couldn’t get the battery level to show in ST but the battery lasts for ages anyway tbh. Here’s the code

/**
 *
 *	Copyright 2019 SmartThings
 *
 *	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.
 */

import groovy.json.JsonOutput
import physicalgraph.zigbee.zcl.DataType

metadata {
	definition(name: "ZigBee Window Shade Battery", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") {
		capability "Actuator"
		capability "Battery"
		capability "Configuration"
		capability "Refresh"
		capability "Window Shade"
		capability "Window Shade Level"
		capability "Window Shade Preset"
		capability "Health Check"
		capability "Switch Level"

		command "pause"

		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0102", outClusters: "0019", model: "E2B0-KR000Z0-HA", deviceJoinName: "eZEX Window Treatment" // SY-IoT201-BD //SOMFY Blind Controller/eZEX
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.6", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.8", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "KG0001", deviceJoinName: "Window Treatment" //Smart Curtain Motor(BCM300D)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "DY0010", deviceJoinName: "Window Treatment" //Smart Curtain Motor(DT82TV)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Glydea Ultra Curtain", deviceJoinName: "Somfy Window Treatment" //Somfy Glydea Ultra
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Sonesse 30 WF Roller", deviceJoinName: "Somfy Window Treatment" // Somfy Sonesse 30 Zigbee LI-ION Pack
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Sonesse 40 Roller", deviceJoinName: "Somfy Window Treatment" // Somfy Sonesse 40
        fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "LUMI", model: "lumi.curtain.acn002", deviceJoinName: "Aqara Window Treatment" // Aqara Roller Shader E1
	}

	preferences {
		input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false
	}

	tiles(scale: 2) {
		multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) {
			tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") {
				attributeState "open", label: 'Open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing"
				attributeState "closed", label: 'Closed', action: "open", icon: "st.shades.shade-closed", backgroundColor: "#ffffff", nextState: "opening"
				attributeState "partially open", label: 'Partially open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing"
				attributeState "opening", label: 'Opening', action: "pause", icon: "st.shades.shade-opening", backgroundColor: "#00A0DC", nextState: "partially open"
				attributeState "closing", label: 'Closing', action: "pause", icon: "st.shades.shade-closing", backgroundColor: "#ffffff", nextState: "partially open"
			}
			tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") {
				attributeState "shadeLevel", action:"setShadeLevel"
			}
		}
		standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc"
		}
		standardTile("presetPosition", "device.presetPosition", width: 2, height: 2, decoration: "flat") {
			state "default", label: "Preset", action:"presetPosition", icon:"st.Home.home2"
		}
		standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
        valueTile("batteryLevel", "device.battery", decoration: "flat", width: 2, height: 2) {
            state "battery", label:'${currentValue}% battery', unit:""
        }

		main "windowShade"
		details(["windowShade", "contPause", "presetPosition", "refresh", "batteryLevel"])
	}
}

private getCLUSTER_WINDOW_COVERING() { 0x0102 }
private getCOMMAND_OPEN() { 0x00 }
private getCOMMAND_CLOSE() { 0x01 }
private getCOMMAND_PAUSE() { 0x02 }
private getCOMMAND_GOTO_LIFT_PERCENTAGE() { 0x05 }
private getATTRIBUTE_POSITION_LIFT() { 0x0008 }
private getATTRIBUTE_CURRENT_LEVEL() { 0x0000 }
private getCOMMAND_MOVE_LEVEL_ONOFF() { 0x04 }
private getBATTERY_PERCENTAGE_REMAINING() { 0x0021 }

private List<Map> collectAttributes(Map descMap) {
	List<Map> descMaps = new ArrayList<Map>()

	descMaps.add(descMap)

	if (descMap.additionalAttrs) {
		descMaps.addAll(descMap.additionalAttrs)
	}

	return descMaps
}

// Parse incoming device messages to generate events
def parse(String description) {
	//log.debug "description:- ${description}"

	if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) {
		sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%")
	}

	if (description?.startsWith("read attr -")) {
		Map descMap = zigbee.parseDescriptionAsMap(description)

		if (isBindingTableMessage(description)) {
			parseBindingTableMessage(description)
		} else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) {
			// log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}, ${BATTERY_PERCENTAGE_REMAINING}"
			List<Map> descMaps = collectAttributes(descMap)
			def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT }

			if (liftmap && liftmap.value) {
				def newLevel = zigbee.convertHexToInt(liftmap.value)

				if (shouldInvertLiftPercentage()) {
					// some devices report % level of being closed (instead of % level of being opened)
					// inverting that logic is needed here to avoid a code duplication
					newLevel = 100 - newLevel
				}
				levelEventHandler(newLevel)
			}
            if (reportsBatteryPercentage() && descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && zigbee.convertHexToInt(descMap?.attrId) == BATTERY_PERCENTAGE_REMAINING && descMap.value) {
                def batteryLevel = zigbee.convertHexToInt(descMap.value)

                batteryPercentageEventHandler(batteryLevel)
            }
		} else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) {
			def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100)

			levelEventHandler(valueInt)
		}
	}
}

def getLastLevel() {
	device.currentState("shadeLevel") ? device.currentValue("shadeLevel") : device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below
}

def levelEventHandler(currentLevel) {
	def priorLevel = lastLevel
    if (Math.round(currentLevel) != Math.round(priorLevel)) {
		log.debug "levelEventHandle - currentLevel: ${currentLevel} priorLevel: ${priorLevel}"
    }

	if ((priorLevel == "undefined" || currentLevel == priorLevel) && state.invalidSameLevelEvent) { //Ignore invalid reports
		// log.debug "Ignore invalid reports"
	} else {
		state.invalidSameLevelEvent = true

		sendEvent(name: "shadeLevel", value: currentLevel, unit: "%")
		sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false)

		if (currentLevel == 0 || currentLevel == 100) {
			sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open")
		} else {
			if (priorLevel < currentLevel) {
				sendEvent([name:"windowShade", value: "opening"])
			} else if (priorLevel > currentLevel) {
				sendEvent([name:"windowShade", value: "closing"])
			}
			runIn(1, "updateFinalState", [overwrite:true])
		}
	}
}

def updateFinalState() {
	def level = device.currentValue("shadeLevel")
	log.debug "updateFinalState: ${level}"

	if (level > 0 && level < 100) {
		sendEvent(name: "windowShade", value: "partially open")
	}
}

def batteryPercentageEventHandler(batteryLevel) {
	if (batteryLevel != null) {
		batteryLevel = Math.min(100, Math.max(0, batteryLevel))
		sendEvent([name: "battery", value: batteryLevel, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%"])
	}
}

def supportsLiftPercentage() {
	device.getDataValue("manufacturer") != "Feibit Co.Ltd"
}

def close() {
	log.info "close()"
	zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE)
}

def open() {
	log.info "open()"
	zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN)
}

def setLevel(value, rate = null) {
	log.info "setLevel($value)"

	setShadeLevel(shouldInvertLiftPercentage() ? 100 - value : value)
}

def setShadeLevel(value) {
	log.info "setShadeLevel($value)"

	Integer level = Math.max(Math.min(value as Integer, 100), 0)
	def cmd

	if (isSomfy() && Math.abs(level - lastLevel) <= GLYDEA_MOVE_THRESHOLD) {
		state.invalidSameLevelEvent = false
	}

	if (supportsLiftPercentage()) {
		if (shouldInvertLiftPercentage()) {
			// some devices keeps % level of being closed (instead of % level of being opened)
			// inverting that logic is needed here
			level = 100 - level
		}
		cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2))
	} else {
		cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2))
	}

	return cmd
}

def pause() {
	log.info "pause()"
	def currentShadeStatus = device.currentValue("windowShade")

	if (currentShadeStatus == "open" || currentShadeStatus == "closed") {
		sendEvent(name: "windowShade", value: currentShadeStatus)
	} else {
		zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE)
	}
}

def presetPosition() {
	setShadeLevel(preset ?: 50)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 * */
def ping() {
	return refresh()
}

def refresh() {
	log.info "refresh()"
	def cmds

	if (supportsLiftPercentage()) {
		cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT)
	} else {
		cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL)
	}

	return cmds
}

def installed() {
	log.debug "installed"

	state.invalidSameLevelEvent = true

	sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false)
}

def configure() {
	def cmds

	log.info "configure()"

	// Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
	sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])

	log.debug "Configuring Reporting and Bindings."

	if (supportsLiftPercentage()) {
		cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null)
	} else {
		cmds = zigbee.levelConfig()
	}

	if (reportsBatteryPercentage()) {
		cmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 30, 21600, 0x01)
	}
	return refresh() + cmds
}

private def parseBindingTableMessage(description) {
	Integer groupAddr = getGroupAddrFromBindingTable(description)
	if (groupAddr) {
		List cmds = addHubToGroup(groupAddr)
		cmds?.collect { new physicalgraph.device.HubAction(it) }
	}
}

private Integer getGroupAddrFromBindingTable(description) {
	log.info "Parsing binding table - '$description'"
	def btr = zigbee.parseBindingTableResponse(description)
	def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 }

	log.info "Found ${groupEntry}"

	!groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16)
}

private List addHubToGroup(Integer groupAddr) {
	["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"]
}

private List readDeviceBindingTable() {
	["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"]
}

def shouldInvertLiftPercentage() {
	return isSomfy() || isLumi()
}

def reportsBatteryPercentage() {
	return isLumi()
}

def isSomfy() {
	device.getDataValue("manufacturer") == "SOMFY"
}

def isLumi() {
	device.getDataValue("manufacturer") == "LUMI"
}

private getGLYDEA_MOVE_THRESHOLD() { 3 }
3 Likes

amazing thank you paul

Anyone that could advise me on how to setup minimum and maximum shade height? Ive got the Aqara roller blind motor paired in SmartThings, but all I can see in the app under the device is “Preset position” and I cant wrap my head around it. If i press “close” in the app it will never stop lowering the blinds, and vice versa for “open”. Help would be greatly appreciated!

Hi Paul, thanks for posting this - I’m looking at it now as a possible solution to my problem.

Can you confirm you’ve paired it direct to smartthings without the Aquara Hub?

Thanks!

Hi @pau1phi11ips, sorry, realised I hadn’t tagged you in my question.

Thanks in advance!

Yeah, straight to the SmartThings hub, if you add the Device Handler to the IDE first it should be recognised as the Shade straight away. Otherwise it just appears as a Thing and you need to assign it the correct type in the IDE

Thanks very much. Very helpful.

Crobbie

Hi HaaBo.
I was in the same situation with this blind motor (aqara) ,and i not found any help for the max and min setup , sooo i buyed a aqara m2 hub , its work great , of course same brand, but the funny thing is when i seted up everything together in IFTTT too for smartthings goodnight etc. i funded one video on youtube , what shows , how you can setup the maximum and minimum positions on the motor itself , without the aqara hub. Link bellow :

I hope its help to you. Write a messege pls after you try.

1 Like

Thank you, i found a similar video - it’s now working as it should! Really appreciate the response!:ok_hand:

@pau1phi11ips Thanks for this great Device Handler. It works well on my Aqara roller shade driver and the smartthings hub.

I still have one issue. Choosing open or close in the app results in 20 seconds ‘waiting wheel’ followed by an error message ‘network or server error, try again later’. Anyone a solution for this issue?

1 Like

@Ronald_van_Pijkeren Never seen that issue myself. Searched for it here and this has a solution HELP! A network or server error occurred. Try again later - #9 by jkp

@pau1phi11ips wondering if you can help I have purchased the Aqara roller shade driver e1, tried to connect directly to my Smartthings hub, I added your DH and Smartthings finds the device as Aqara Window Treatment and also shows all the controls, but it keeps saying “Checking” as if it cant communicate with the hub and gives a network error when I try carrying out any operations on the blind. Any advice on getting this working, I have literally moved my Smartthings hub to be next to the device to eliminate it not getting a signal. Thanks would appreciate any advice on getting this working.