[RELEASE] IKEA window roller blinds

Just a quick note of thanks to @csteele for his code tweak on this driver to add on/off switch for Logs...

"I took a stab at adding the log reduction code..."
Revised Code

Can you link me to the Revised Code?

Just click on the words that are colored in blue.

When I click the “Revised Code” link in your post I get “Oops! That page doesn’t exist or is private”.

https://community.hubitat.com/t/re-release-ikea-window-roller-blinds/57538

It was a Private Message (PM) so, it's not visible to anyone else... but here's the code:

Revised Code
/**
 *
 *
 *	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
 *  Modified for hubitat use
 *  
 *  added fallback refreshing calls if blinds decide not to update status
 *  added position event updates to track realtime open percentage in dashboard
 *  added max open percentage preference (optional)
 *  added hardOpen function in case you need to reset blind height occasionally esp useful with above function
 *  added fix to execute the open function if "setposition 100" occurs - this allows Dashboard and Alexa to acknowledge max open level
 *  fixed bug with final state of blinds, now always ending with either open or closed state
 *  
 *  known issue: fingerprint does not seem to be working right now just manually assign the driver after device is added 
 *
 *  IMPORTANT: remember to hit configure button after device is added.
 */
import hubitat.zigbee.zcl.DataType

metadata {
    definition(name: "IKEA Window Blinds", namespace: "a4refillpad", author: "Wayne Man", 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"
        command "hardOpen"
        
       	attribute "lastCheckin", "String"
        attribute "lastOpened", "String"

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

        preferences {

            input name: "openLevel", type: "number", defaultValue: 0, range: "0..100", title: "Max open level",
                description: "Max percentage open when Open function is called\n" +
                             "(delete or set value to 0 to disable this)"
		input name: "debugOutput", type: "bool", title: "Enable debug logging?", defaultValue: true
		input name: "descTextOutput", type: "bool", title: "Enable descriptionText logging?", defaultValue: true
        }
  
}

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) {
    if (debugOutput) 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 (descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) {
            if (debugOutput) 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 = 100 - zigbee.convertHexToInt(liftmap.value)
                levelEventHandler(newLevel)
            }
        } 
        if (descMap?.clusterInt == CLUSTER_BATTERY_LEVEL && descMap.value) {
            if (debugOutput) 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")
    if (debugOutput) log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"
    if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports
        if (debugOutput) log.debug "undefined lastLevel"
        runIn(3, "updateFinalState", [overwrite:true])
    } else {
        sendEvent(name: "level", value: currentLevel)
        sendEvent(name: "position", value: currentLevel)
        if (currentLevel == 0 || currentLevel >= 97) {
            sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open")
        } else {
            if (lastLevel < currentLevel) {
                sendEvent([name:"windowShade", value: "opening"])
            } else if (lastLevel > currentLevel) {
                sendEvent([name:"windowShade", value: "closing"])
            }
        }
    }
    if (lastLevel != currentLevel) {
        if (debugOutput) log.debug "newlevel: ${newLevel} currentlevel: ${currentLevel} lastlevel: ${lastLevel}"
        runIn(1, refresh)
    }
}

def updateFinalState() {
    def level = device.currentValue("level")
    if (debugOutput) log.debug "updateFinalState: ${level}"
    sendEvent(name: "windowShade", value: level == 0 ? "closed" : "open")
}

def close() {
    if (descTextOutput) log.info "close()"
    runIn(5, refresh)
    zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE)
}

def open() {
    if (descTextOutput) log.info "open()"
    runIn(5, refresh)
    if (openLevel) {
        setLevel(openLevel)
    } else {
        hardOpen()
    }
}

def hardOpen() {
    if (descTextOutput) log.info "hardOpen()"
    runIn(5, refresh)
    zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN)
    
}

def setLevel(data, rate = null) {
    runIn(5, refresh)
    data = data.toInteger()
    if (descTextOutput) log.info "setLevel()"
    if (data == 100) {
        open()
    } else {
        def cmd
        cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(100 - data, 2))
        return cmd
    }
}

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


def refresh() {
    if (descTextOutput) log.info "refresh()"
    
    def cmds
    cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) + zigbee.readAttribute(CLUSTER_BATTERY_LEVEL, 0x0021) 

    return cmds
}

def configure() {
    if (descTextOutput) log.info "configure()"
    sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
    if (debugOutput) log.debug "Configuring Reporting and Bindings."

    def cmds
    cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, 0x01) + zigbee.configureReporting(CLUSTER_BATTERY_LEVEL, 0x0021, DataType.UINT8, 600, 21600, 0x01)

    return refresh() + cmds
}

def setPosition(value){
	setLevel(value)
}

def updated() {
	unschedule()
	if (debugOutput) runIn(1800,logsOff)
}

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

Thank you! :sunglasses:

Haha... I guess I left a lot of debug lines in unnecessarily when I was trying to work things out. There's no real need to leave them in there to be honest but I like the toggle.

@sburke781 @morningz another option for these is Zigbee2MQTT. Doing the binding for the button in Deconz is a little complicated and was a pain in my ■■■. Z2M is as simple as creating the group in the GUI using dropdowns and then sending an MQTT command to bind the button to the group. Plus, Z2M is adding more devices and features at a much faster pace than Deconz. Now that there's an MQTT app developped for HE, this would be my route of choice for getting these into HE with a working button.

1 Like

Yes, I should have mentioned that as well, I was aware of that but just chose what felt like a simpler option at the time for me, but there are a few options in this space. I do agree deCONZ can be problematic (not the HE app, the one on my rpi).

I didn't have "issues" to say with deconz. But the phoscon GUI was terrible and literally every single device got classified as a "light". And I felt like I needed a PhD to use the actual deCONZ GUI. That's why I looked at z2m, initially with z2m assistant for a GUI. Then they launched their own GUI which is very slick. And it's very easy to spin up in a docker container.

2 Likes

Do I read it right that one still needs to use a virtual dimmer/shades switch to get Google Assistant control?

Unsure of google, but with Alexa the native shade device works. For Alexa, think of it as a dimmer and the amount of light a dimmer provides - 90% is 9/10ths fully open (lots of light) and 10% is almost fully closed (little light). Once you get into that mindset, you get used to it.

1 Like

Or simply say "Alexa open kitchen blinds' or "close kitchen blinds" of course.

Or set to 50% and you will always be right! LOL! Is the shade half empty or half full?

1 Like

Has anyone had any troubles getting their ALL their blinds to respond to automations? I have 15 blinds and only about half will trigger on sunrise/sunset as I have them setup. They all executed perfectly on ST.

Yes, but not since I migrated them to my new hub. As part of that migration I used a different channel. Which was much less congested by WiFi signals. That made a huge difference for several devices that would have minor issues from time to time, including my blinds. Now it’s rock solid.

On my old hub the blinds would sometimes not respond or report incorrectly. Since the migration I have not had a single problem.

It might be worth looking into the signal strength and congestion on the Zigbee channel you’re using. Maybe that might help.... But at the same time I don’t have as many blinds as you, and they are really chatty when they are opening and closing. If it’s not the channel, then it could be the chatter. Might also try staggering them so you only close a few at a time. Just a thought.

I've got 9 of them along 1 wall and found that often there'd be a couple that wouldn't respond when executing group commands.

What I ended up doing is putting varying delays between them.
Blind 1 fires right away
Blind 2 fires after a 1s delay
Blind 7 fires after 10s delay
etc..

I thought I'd hate it not having them move together, but it actually creates kind of a cool wave effect that I actually think I like it more this way

1 Like

How many zigbee repeaters do you have? What does the zigbee routing table look like? How many total zigbee devices are there? Those all factor in.
I only have 2 blinds, every once in a while one will miss - maybe 1-2 times a month. I have 5 zigbee repeaters spread through the house (Iris smart plugs) I'm giving the repeaters the credit for how stable my zigbee devices are.

I don't have that many, but when I have an issue like that i just make a automation to do something, for just a few bulbs for example, wait 10 seconds, and do the same to the rest .. just an idea if things like that cause issues.