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

Yes, it is possible and is an indication of a possibly not so very healthy mesh. I would try to pair the curtains again without deleting them from HE. It would also be a good idea to put a good repeater like the IKEA Repeater in the same room as the curtains.

Yes, though they make for rather lousy repeaters. Again, a good repeater or two like the IKEA ones keeps my devices happy. The downside could be dropped devices, but it could also go without issues if the rest of the mesh is very strong.

Do let me know how it goes, I'm looking at some changes to how I handle these curtains in the driver and might find something that can make them happier in the mesh. Which version is it that you have?

The Iris 3210-Ls are actually meant to be very good repeaters. And the cool thing was I was using them as both zigbee and z-wave repeaters (although I only have wired z-wave devices right now so don't need repeaters). With these curtains, looks like I might unfortunately need to replace all of them with the Ikea Tradfris.

How do you ensure your devices go through the IKEAs rather than the curtains? Also, what do you mean by the downside? Are you saying adding the IKEAs might lead to some dropped devices? Would have thought it was the opposite.

ZNCLDJ11LM

You shouldn't need to get rid of them, just try first with adding an Ikea repeater in the same room as the Curtains.

Sorry, late here, so not very clear, the downside to the curtains being repeaters are you could get dropped devices, but I've not personally seen this.

Ok, same as all that I have.

I got two of the “Aqara B1” motors which have the battery. And I helped with testing/reworking the driver above. I am very pleased and they have performed almost flawlessly for me. It has had a great Wife Approval Factor which should get me a lot of leeway for smarthome tech moving forward! The seller I found with the best price on the motors didn’t sell the tracks at all. So I bought them separate which seems to be somewhat normal. I ordered both on AliExpress. The motors were around $125 each and the 2-meter white silent tracks we got came mostly assembled for ~$70 each. Tracking info for the shipments was absolute shit (it never updated) and both the motors and tracks took all of 30-40 days for them to arrive in Atlanta, GA (end of March 2020). But they did meet the initial delivery estimate given when I ordered. Shipping times can vary drastically so check the different sellers. It’s all taken patience and work. And been a bit of a learning curve... but now that they are up and running it is totally worth it. They open every morning and close every evening like clockwork. And when they open on schedule and we then decide we want to sleep more it is really awesome to say “Alexa close curtains” and not lift a finger while she darkens the room for us :slight_smile:

2 Likes

So cool. Thanks for sharing your experience!

1 Like

Hi Markus - I just got two Ikea Fyrtur window shades and used this driver from @Ryan780: Ikea Blinds

The Ikea shades work perfectly. So perfect that it's made me realize we might have some work to do on the Aqara curtain driver. It all relates to the dashboard tiles, status updates and one's idea of what 0 or 100 means. The two main "issues" are this:

  1. Status isn't updating correctly with my Aqara B1 motors and our driver. I added two tiles to my dashboard for the ikea shades and set them as a "shade". Those tiles update status and it is always correct. For some reason it is not updating on our Aqara driver for my curtains. After normal operation this morning I checked and both curtains showed the following. They always say "opening":

When I hit the refresh button on one of them it updated and I got this:

  1. When I set the Ikea shades up as a "shade" in the dashboard the tiles show the fully open state = 0 and the fully closed state = 100. The Aqara curtains show the opposite which I think is more confusing. I realize this may be a matter of semantics but here's what I'm showing on my dashboard with everything fully open (both ikea shades and both aqara curtains are fully open; ignore Bedroom-Curtain-Wall as it is suffering from the status update issue):

Is there an easy way to make the Aqara curtains act like the Ikea blinds where 0 = fully open and 100 = fully closed? That will make them match the Ikea driver and will also make my brain happier lol!

Issue 2 is not as critial as issue 1 obviously. But can you let me know your thoughts?

That would be a bug that needs to be sorted, I will look at that.

This for me would be backwards. The reason is that if tied to for example a dimmer level, 0=off, 100=full brightness. I have button controllers that work exactly this way and that I tie to my curtains. I'm probably not the only one.

I would be possible to add an option to invert this behavior, though I', not sure it is the "correct" behavior. I just worked on another curtain driver for a WiFi based controller, there 0=closed, 100=fully open.

To keep it the same I'd rather change the IKEA driver to function the way mine do than the other way around. With that said, if you really, really prefer it inverted I can probably add an option...

Yea I get it. If you think of it as a dimmer light switch then 100 is fully open and sunny. I guess it's just hard for me to get in the habit of thinking it's a light lol. I can just live with it for now I guess. They've already discussed this "issue" in the Ikea thread. Seems everyone disagrees lol. But can you please try to get the curtains to update status? That is the most important. If it's super easy to reverse the Ikea driver we can give it a whirl too. Your choice...

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:)
https://www.aliexpress.com/item/4000499176673.html?spm=a2g0s.9042311.0.0.3b664c4dpzVSRe

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:
https://www.aliexpress.com/item/33036042205.html?spm=a2g0s.9042311.0.0.3b664c4dpzVSRe

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: