[RELEASE] IKEA window roller blinds

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