[RELEASE] IKEA window roller blinds

Just a little off topic, did you use HubConnect 1.6.4 or 2.0 (there are a lot of posts in that thread to read. lol) I am in the same boat as you moving all my Smartthings devices to Hubitat but slowly finding our some devices not compatible with Hubitat. My intention was to ditch smarthings but to use these inexpensive Ikea buttons for the blinds and other things looks like I need to keep Smartthings hub also.

Thank you for your work here. I too have noticed and appreciate the improvements in your driver over those previously available.

I am no programmer but am a fan so I tried to tweak your code to include an On/Off switch for the Logs. My tweak didn't work but it was based on this post by @csteele. I'm sure I had a comma or quote out of place and just ran out of time to mess with it.

Anyway, an On/Off switch would be nice to have if you're in there one day and see it as a valuable addition to your work.

@mavrantzas and @a4refillpad - if you are interested, I did end up getting the on/off switches working with HE using a zigbee stick plugged into my raspberry pi and a community driver for DeCONZ. Let me know if you are interested and I can post some more details.

2 Likes

Thanks but I got it running with Hubconnect and my smartthings hub!! Took a while to figure it out but with the help of others I managed to get it.

1 Like

Count me as interested. I've got one of these and a Pi up and running that i could run DeCONZ with. (I hope this is the right thing anyways)

Screen Shot 2020-12-03 at 6.56.45 PM

1 Like

The main thing would be you got it working with DeCONZ, that's what the drivers integrate with. I used the Conbee2, but I wouldn't expect the hardware used would change anything. If you search for DeCONZ on the community you should find the drivers, otherwise I can track it down later.

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.