Missing generic zigbee windows shade

Hi at all, I just moved from Smartthings and I'm having some trouble with roller shutter controller. On smartthings I was using a preset ZigBee windows shade and it was working good, but I didn't find any working driver on hubitat, is anyone able to port that device handler or something similar. I've tried to copy the code but it's not working, maybe because does n4ot show the configure button. Please help me or I think I have to go back to ST :pensive:

It would be helpful if you can post more information on the type of shades you have. There are multiple existing community drivers out there for zigbee enabled shades. If we know the brand of your shades someone may be able to point you in the right direction.

This is a quick port of the ST Generic Zigbee Shade driver. It compiles but without a device I can’t tell you if it will work, but…..

ST Shade Driver Port
/**
 *
 *	Copyright 2019 SmartThings
 *
 *	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.
 */

import groovy.json.JsonOutput
import hubitat.zigbee.zcl.DataType

metadata {
	definition(name: "ZigBee Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") {
		capability "Actuator"
		capability "Configuration"
		capability "Refresh"
		capability "WindowShade"
//		capability "Window Shade Level"
		capability "SwitchLevel"
//		capability "Window Shade Preset"
		capability "HealthCheck"
		capability "SwitchLevel"

		command "pause"

		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0102", outClusters: "0019", model: "E2B0-KR000Z0-HA", deviceJoinName: "eZEX Window Treatment" // SY-IoT201-BD //SOMFY Blind Controller/eZEX
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.6", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.8", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "KG0001", deviceJoinName: "Window Treatment" //Smart Curtain Motor(BCM300D)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "DY0010", deviceJoinName: "Window Treatment" //Smart Curtain Motor(DT82TV)
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Glydea Ultra Curtain", deviceJoinName: "Somfy Window Treatment" //Somfy Glydea Ultra
		fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Sonesse 30 WF Roller", deviceJoinName: "Somfy Window Treatment" // Somfy Sonesse 30 Zigbee LI-ION Pack
	}

	preferences {
		input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false
	}


}

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}"

	if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) {
		sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%")
	}

	if (description?.startsWith("read attr -")) {
		Map descMap = zigbee.parseDescriptionAsMap(description)

		if (isBindingTableMessage(description)) {
			parseBindingTableMessage(description)
		} else 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)

				if (shouldInvertLiftPercentage()) {
					// some devices report % level of being closed (instead of % level of being opened)
					// inverting that logic is needed here to avoid a code duplication
					newLevel = 100 - newLevel
				}
				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)
		}
	}
}

def getLastLevel() {
	device.currentState("shadeLevel") ? device.currentValue("shadeLevel") : device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below
}

def levelEventHandler(currentLevel) {
	log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"

	if ((lastLevel == "undefined" || currentLevel == lastLevel) && state.invalidSameLevelEvent) { //Ignore invalid reports
		log.debug "Ignore invalid reports"
	} else {
		state.invalidSameLevelEvent = true

		sendEvent(name: "shadeLevel", value: currentLevel, unit: "%")
		sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false)

		if (currentLevel == 0 || currentLevel == 100) {
			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"])
			}
			runIn(1, "updateFinalState", [overwrite:true])
		}
	}
}

def updateFinalState() {
	def level = device.currentValue("shadeLevel")
	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(value, rate = null) {
	log.info "setLevel($value)"

	setShadeLevel(value)
}

def setShadeLevel(value) {
	log.info "setShadeLevel($value)"

	Integer level = Math.max(Math.min(value as Integer, 100), 0)
	def cmd

	if (isSomfy() && Math.abs(level - lastLevel) <= GLYDEA_MOVE_THRESHOLD) {
		state.invalidSameLevelEvent = false
	}

	if (supportsLiftPercentage()) {
		if (shouldInvertLiftPercentage()) {
			// some devices keeps % level of being closed (instead of % level of being opened)
			// inverting that logic is needed here
			level = 100 - level
		}
		cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2))
	} else {
		cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2))
	}

	return cmd
}

def pause() {
	log.info "pause()"
	def currentShadeStatus = device.currentValue("windowShade")

	if (currentShadeStatus == "open" || currentShadeStatus == "closed") {
		sendEvent(name: "windowShade", value: currentShadeStatus)
	} else {
		zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE)
	}
}

def presetPosition() {
	setShadeLevel(preset ?: 50)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 * */
def ping() {
	return refresh()
}

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 installed() {
	log.debug "installed"

	state.invalidSameLevelEvent = true

	sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false)
}

def configure() {
	def cmds

	log.info "configure()"

	// Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
	sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])

	log.debug "Configuring Reporting and Bindings."

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

	return refresh() + cmds
}

private def parseBindingTableMessage(description) {
	Integer groupAddr = getGroupAddrFromBindingTable(description)
	if (groupAddr) {
		List cmds = addHubToGroup(groupAddr)
		cmds?.collect { new hubitat.device.HubAction(it) }
	}
}

private Integer getGroupAddrFromBindingTable(description) {
	log.info "Parsing binding table - '$description'"
	def btr = zigbee.parseBindingTableResponse(description)
	def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 }

	log.info "Found ${groupEntry}"

	!groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16)
}

private List addHubToGroup(Integer groupAddr) {
	["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"]
}

private List readDeviceBindingTable() {
	["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"]
}

def shouldInvertLiftPercentage() {
	return isSomfy()
}

def isSomfy() {
	device.getDataValue("manufacturer") == "SOMFY"
}

private getGLYDEA_MOVE_THRESHOLD() { 3 }

@thebearmay Great it worked.. I've tried to do it but maybe I was doing something wrong. Now it works one the app and dashboard but it doesn't on Alexa, she recognise it but when I set up a percentage it doesn't do nothing and remain on the 0 without moving it :sweat_smile:

@matt9 it's brand Vimar model 14592, it's an Italian brand so I guess is difficult that someone got it...anyway it's a roller shutter controller with up and down button

ST had a Shade Level capability that HE doesn’t, could try adding the SwitchLevel capability to see if that lets the % work.

@thebearmay can you help me please? Since if it doesn't work with Alexa is actually useless :sweat_smile:

Added the SwitchLevel capability to the code above. May have to remove the device from the Alexa interface and re-add it to get a good test.

Unfortunately it didn't work :pensive: I was looking for some virtual device to mirror it through Alexa but the app mirror doesn't support this kind of device :sob:

Let me poke around to see if I can figure out how others are getting around this, sure it's something simple that I'm missing.

@thebearmay many thanks :blush: hopefully this is going to help even other users :grinning:

@thebearmay If this can help when I increase the percentage on Alexa to open or close it goes again to 0 without moving nothing

Made a few changes that may help. Try importing from:

https://raw.githubusercontent.com/thebearmay/hubitat/main/development/stZigbeeShade.groovy

1 Like

@thebearmay nothing...it would be good something even just opening and closing

Just noticed that the setPosition command wasn't pointing anywhere, so added some logic for it, also add the switch capability (mapped on->open, off->close). May or may not make a difference, but...

@thebearmay Man you're amazing :grin: finally it works :heart_eyes: the only problem is that when I say open Alexa close it and when I say close she opens it but at least I can manage it by voice

1 Like

I can try flipping the switch on/off to see if that helps if you like...

But by the app it works properly, probably this happen because 100% means closed and 0% is open, if it doesn't take long for you I would try to invert it

I added a switch to let you try it (turned off by default). Turn it on and see if it makes a difference, if it doesn't just turn it back off. Also commented out all of the debug logging so that it doesn't fill your log unnecessarily.

Nothing change, even by the app, if I press on it opens and vice versa