[RELEASE] Aqara (B1) Smart Curtain Motor (ZNCLDJ11LM & ZNCLDJ12LM) Driver

I'll fix the state update, I think it has to do with a change in how some zigbee packets are being parsed now vs 2months ago in my drivers. I might a need a debug log from you, but I should be fine. Will look at this during the weekend.

Try this IKEA driver:

/**
 *
 *
 *	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.
 *
 *  
 *  first release for IKEA smart window blinds for hubitat adapted from the driver for ST by Wayne Man
 */
import hubitat.zigbee.zcl.DataType

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

        command "pause"
        
       	attribute "lastCheckin", "String"
		attribute "lastOpened", "String"

		fingerprint inClusters: "0000,0001,0003,0004", manufacturer: "IKEA of Sweden", model: "FYRTUR block-out roller blind"
    }
}

private getCLUSTER_BATTERY_LEVEL() { 0x0001 }
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 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}"
    def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
    //  send event for heartbeat    
    sendEvent(name: "lastCheckin", value: now)
    if (description?.startsWith("read attr -")) {
        Map descMap = zigbee.parseDescriptionAsMap(description)
        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")}"
            List<Map> descMaps = collectAttributes(descMap)
            def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT }
            if (liftmap && liftmap.value) {
                def newLevel = zigbee.convertHexToInt(liftmap.value)
                levelEventHandler(newLevel)
            }
        } else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) {
            def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100)

            levelEventHandler(valueInt)
        }
		if (descMap?.clusterInt == CLUSTER_BATTERY_LEVEL && descMap.value) {
            log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}"
            sendEvent(name: "battery", value: Integer.parseInt(descMap.value, 16))
        }
    }
}

def levelEventHandler(currentLevel) {
    def lastLevel = device.currentValue("level")
    log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"
    if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports
        log.debug "Ignore invalid reports"
    } else {
        currentLevel = 100 - currentLevel
        sendEvent(name: "level", value: currentLevel)
        if (currentLevel == 0 || currentLevel == 100) {
            sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed": "open" )
        } else {
            if (lastLevel > currentLevel) {
                sendEvent([name:"windowShade", value: "closing"])
            } else {
                sendEvent([name:"windowShade", value: "opening"])
            }
            runIn(1, "updateFinalState", [overwrite:true])
        }
    }
												  
}

def updateFinalState() {
    def level = device.currentValue("level")
    log.debug "updateFinalState: ${level}"
    if (level > 0 && level < 100) {
        sendEvent(name: "windowShade", value: "partially open")
    }
}

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(data, rate = null) {
						   
    log.info "setLevel()"
    def cmd
	data = data.toInteger()
    if (supportsLiftPercentage()) {
        cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2))
    } else {
        cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2))
    }

    return cmd
}

def pause() {
    log.info "pause()"
    zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 * */

def ping() {
       return zigbee.readAttribute(CLUSTER_BATTERY_LEVEL, 0x0021) // Read the Battery Level
}

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 configure() {
    // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
    log.info "configure()"
    sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
    log.debug "Configuring Reporting and Bindings."

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

def setPosition(value){
	setLevel(value)
}

Sorry for the delay but better late than never! Here's where I bought my Aqara B1 motors that have the battery. You can leave them plugged in to a power outlet 24/7 or just run on battery. So far my battery has lasted 2 months opening and closing once per day and it still has 47% life left. So I expect it to last ~4 months. Here's where I bought them but I paid only $127 each + $8 for DHL shipping (which took a full month :confused:)

And I bought these tracks from this exact seller. I bought two sections exactly 2m long (I had two windows that each needed a 2m track). My tracks opened one direction (you have to specify what length and type of opening when ordering). You can also make one motor on one track open two curtain panels from the middle of the window but I did not do that. The tracks were a perfect fit, almost fully assembled and a white powder coat finish. They are mostly silent but we do hear the motors a bit. But it hardly ever wakes me and they surround our bed.... I paid ~$16 per meter + $59 for DHL shipping of two tracks each 2m long (4 meters total). Figuring out how to order the tracks was the hardest part. But this seller had good instructions. When I received the tracks they had no instructions but he referred me to his video in the product photos which shows what to do:

If you get them let us know. Make sure to thank Markus as he got the driver working. I helped with the testing of the B1 motors and they work. You really need to decide if you want the battery style. If you are just going to leave them connected to power 24/7 buying the B1 version with the battery will be a waste of money unless you think you may move them in the future...

Thank you so much for the details. Much appreciated. I was looking at my curtains today and thinking I really must do something about them. I'm already indebted to @markus for something else he got working for me (camera related) so yeah, you guys and this forum rocks and thanks for all the guidance and assistance. I will order soon I think and keep you posted on how it goes.

Cheers.

I might look at other vendors and compare prices. The prices from these two went up unless they are now including free shipping (but maybe that is the case?) I would shop around

Ok, thanks yeah will shop around.
Cheers.

I bought the old Aqara curtain motor (the version without the battery) because I knew I wanted to leave it plugged in so cheaper option was good. Bought both for about $140. It's more expensive from the vendor I bought from now but you can shop around. (Aqara Smart Curtain Motor Intelligent Curtain Zigbee Mi Home APP Remote Control Wireless Timing Electric Curtain Motor|Building Automation| - AliExpress).

Also, my wife doesn't like the typical tracks that come with it so I bought grommet tracks from Rooms Beautiful (Motorized Curtain with Decorative Tracks/Rods - RoomsBeautiful.com). Other than looking much nicer than the typical track, the grommet track also hides the wires behind the curtains so no wires dangling around. I didn't buy their motor because it's way more expensive than the Aqara motor for essentially the same motor.

It performs great and looks good also so best of both worlds.

1 Like

I have the same thing happen with only one of my curtain motors (I have the non battery version). It never checks in and at times, get's stuck in its status. My 2nd motor works great. Not sure if the issue is with the driver or the motor but since they both use this driver, I had assumed it was the motor. @markus is looking into it though so will find out soon enough!

There is a new version with more verbose Info logging around the positioning data, please update to that driver. I don't see this issue with any of my ZNCLDJ11LM curtains, so would need to see the issue from someone else. One thing to do could be to re-pair the curtain without deleting the device from HE. There are no binding commands sent at pairing, but maybe something is not 100% with the curtain setup. Before trying the re-pairing, please try to log positioning when it fails to update and PM me the logs as text (not a screenshot).

When I posted last I was on an older driver version 1.0.1.xxxxxx. So I updated to the latest driver linked at the very top of this thread (v1.0.2.0701b). That's all I did. Then I opened and closed both of my curtains a few times and watched the dashboard tile. The status indicated on the tile updated correctly during and after each open and close action so it seems to be working correctly now. Could have been that my driver was just old (during dev) and I didn't have the updated version. At any rate the latest driver seems to work properly. Thanks @markus!

1 Like

So excited. I finally got around to buying a set (motor plus rails). I have stacks of patio doors and windows I'd like to automate, but decided to start out with 1 set to check it out and test it. Hurry up AliExpress and deliver my Xiaomi Smart curtain rail already :grin:

1 Like

Ok, so I got my first Xiaomi motor (11LM non battery model) and track. Now, how to set up the driver? So many options I'm confused and way too lazy to spend 80 minutes reading this entire thread! Any tips on the best way to set up? Or point me to the right post (I don't see setup instructions in the initial post).

Finally got my Aqara curtain and set it up. Fantastic! Works beautifully. Thanks to "@markus" for his work on the driver (we miss him). I'm going to get quite a few of these I think around the house. Now I'm working out how to put controls on my custom dashboard. It's gonna be so cool :smiley:

Yeehah!

This popup opens after clicking on the curtain icon in the 3D floorplan (the icon indicates the 3 states of the curtain driven from the eventsocket - open/half/closed - where the half images is used between 10-90 position). Click any of the 3 icons on the popup to drive to 0/50/100 or use the slider. Click x to close the popup. Beautiful :joy:

What's the 3d floor plan and the pop-up?

It's a custom HE dashboard I've built, documented a little elsewhere on this forum. I need to do a full write-up but I'm waiting for my 3D floorplan to be finished as part of our renovation project. I control most things using RM and Node-RED but I like to have a nice cool dashboard too so I built this using html/javascript/CSS and using the eventsocket. It means you can basically create exactly what you want. So this particular popup controls the curtain position.

1 Like

Anyone using this driver with Google Home? I have added my curtains through the official Hubitat integration with GH, all my curtains are recognized as lights. I wonder how to fix that. I am reading that GH looks into the name and the capabilities of the driver. So I have removed the sensor and light capabilities, but still it comes as light in GH. Any Ideas?

I didn't set up my curtain controller on GH yet. But you could try the community GH driver because I believe it has more device type options.

This driver works well for me! :ok_hand:

About to take the plunge on some of these.

Searched the thread but don't see anyone talk about pairing.

Guessing it's the standard press a button somewhere on the device and search for ZigBee in hubitat?

Correct. Load the driver first of course. These pair quite readily.

Download the Hubitat app