Tuya Zigbee Roller Shade Blind Motor - Hubitat Issues

@kkossev those are the issues I have. Is your modified driver available anywhere? Thanks. Mine is this model:

Hmmm - not sure at the moment; I've been doing the setup in spare moments in evenings this week, and while the driver definitely lets you set the position ("open", "close", "25%", etc...) via Hubitat and Alexa I haven't had chance to check much beyond that.

Will give it more of a look over the weekend. To echo @johnwill1, if that does turn out to be an issue with the code I have, is your fix for it available?

I will publish a temporary link to the mods later tonight when home.
If it works for all of us, will contact Amos to merge the fixes into his driver.


This is the temporary link to the updated driver.

  • The 3 new manufacturers fingerprints are added to the driver, now the driver should be automatically selected when a new device is paired.

  • The blind position should be reported correctly now.

1 Like

Thanks! Have taken a local copy, and will do a before/after comparison of position reporting ASAP.

just given that revised code a go, it does indeed seem to report the blind position correctly. i set the blind to 82 (closed in my case) and it incremented to 82 in the 'position' state below before stopping.

Current States

  • numberOfButtons : 5
  • position : 82
  • presence : present
  • windowShade : opening

When you send an Open command, what is the final position - 0 or 100?

I have some strange invertion of the the Close/Open direction that drives me crazy... :frowning:
The `windowShade state never gets "open" or "closed"..

MarcusVR modification ( the "ZemiSmart Battery Vertical Blind Motor" link that I posted yesterday) works correctly with my device - Opening goes down to 0 and the final state is 'open'. Closing goes up to 100 and the final state is 'closed'. However this driver has other problems... Will have to analyze the differences.

If everything is configured and works correctly, a HE dashboard should look like this:

EDIT 04/30/2022 :

1 Like

Open is 0, closed is 100 in my case. However the status when opening the blind (to 0) shows 'closing', even when it gets to fully open (and you would hope it says 'closed'). Conversely, fully closed (100) reports as 'opening' in the status. I can live with such quirks for my purposes!

1 Like

That is what happens on mine and always has to be fair. It seems to reach its position after opening and instead of reporting "open" reports "closing". It does the same after closing - reports "opening"

At the moment I've only switched to the new driver. I'll delete and re pair the blind when I get chance. I also have to have mine set to "tilt" instead of "lift" or I have issues.

I can now reproduce the same issues and I think that I see where the problems are. It will take a couple of days for the fixes, as I will be traveling during the weekend.

Mode *

  • lift - motor moves until button pressed again
  • tilt - pressing button < 1.5s, movement stops on release; pressing button > 1.5s, motor moves until button pressed again

The above is not correct for our models, it is probably true for some older models.
Our mode setting must be 'lift', otherwise the open/close commands are inverted in the code.

It is not needed to re-pair your device at the moment, when I have a newer version that works as expected with my device, will notify you for another test.

Thank you for the feedback, @johnwill1 .

That makes sense. When I first got the blind I had a nightmare switching back and forth between forward and reverse trying to get it working. The only way it seemed to work correctly in Hubitat was when the buttons on the motor were inverted. I think that's why I had to use "tilt" instead of lift.

The upshot is that it has been working in automations but I couldn't send open or close, I had to send position 0 or 100 and couldn't get it the state to end on open or closed (though it did report reaching its position)

thanks I look forward to it.

1 Like

Dude you are a programming animal and I'm sure other will agree, we appreciate all you have done with these little rogue devices. Like I said, if you ever get to Pittsburgh, we are definitely hitting a few breweries.


Working from home today, so was able to give your updated driver code a go sooner than expected; details below.

First, and unexpectedly (to me!), I found that the "Direction" setting in the driver's Preferences section didn't just affect how Hubitat interacted with the motor, it also changed the way the physical buttons on the motor and remote worked. Switch between "forward" and "reverse", and those buttons move the blind in the opposite direction.

With my setup the direction needed to be set to "reverse" for the blind's movement to match the buttons.

With your driver installed and all settings other than "Direction" left as their default:

  • The blind's position is continuously updated in Hubitat (in device details and Dashboard) as it moves. This happens whatever initiated the movement; Hubitat, Alexa or blind button.
  • Fully open corresponds to position = 0 (sometimes 1)
  • Fully closed corresponds to position = 100
  • The "Open" and "Close" buttons on the Hubitat device page move the blind into the correct state.
  • "Open" and "Close" commands given via Alexa move the blind into the wrong state ("Alexa open spare room blind" closes it, and vice versa).
  • The "windowShade" value is the opposite of what it should be; "opening" when the blind's actually closing, and vice versa.
  • The "windowShade" value stays as "opening" or "closing" when the blind stops moving, whether that's because the stop button was pressed or because it's reached an upper/lower limit.

If there are any more tests you'd like me to run, please let me know.

1 Like

At the risk of muddying the waters, now things are moving in the right direction with the not-AM43 external motor, I'll quickly refer back to the AM15 I mentioned in the original post on this thread.

This is an in-the-blind-cylinder motor, and comes with a USB Zigbee/RF convertor that sends commands from whatever Zigbee hub it's paired to on to the blind's RF receiver.

Even within the Tuya app this communication seems to be very basic, and is entirely unidirectional - hub --> convertor --> blind. As far as I can see there's zero feedback from the blind on its position/status.

The operations it supports, initiated from the app, are:

  • "Up" - blind moves to top limit; app switches to 100%/open blind graphic.
  • "Down" - blind moves to bottom limit; app switches to 0%/closed blind graphic.
  • "Stop" - blind stops moving (if moving in the first place); app switches to 50%/half-open graphic. The app switches to this position and graphic even if the blind's already at a limit.

In summary, it looks like the app starts or stops the blind moving according to the button press, and takes a best guess as to the state it'll finish up in.

The blind's state in the app doesn't change if the blind's moved with the remote.

Unhelpfully the blind detects as "zemismart zigbee curtain" in the app (which it definitely isn't!) and the Hubitat Data settings are:

  • model: TS0601
  • manufacturer: _TZE200_iossyxra

Amos Yuen's driver sort of works with this; open and close commands are obeyed, but intermittently.

Does anyone know if there's a more appropriate driver for this available?

Edit: Reading the code in the existing driver I think I can see what the problem is; there's an entirely appropriate check for requested blind positions differing from the actual position, and if the two match it's assumed nothing needs to be done and no command is sent. That'd work fine for everything except the AM15, which doesn't have a reliable actual position for Hubitat to read. When time permits I might have a go a writing a cut-down driver with those checks, and anything else that assumes the blind's sending back to the hub, taken out.

1 Like

The driver I'm using ( Manufacturer _TZE200_xuzcvlku) and that I made some changes is working the same as the image of your Dashboard. Open, close and set level commands are working.

 *  Tuya Window Shade (v.0.1.0)
 *	Copyright 2020 iquix
 *	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.
 This DTH is coded based on iquix's tuya-window-shade DTH.

import groovy.json.JsonOutput
//mc // import physicalgraph.zigbee.zcl.DataType
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

metadata {
	definition(name: "ZemiSmart Vertical Blind", namespace: "ShinJjang", author: "ShinJjang-iquix", ocfDeviceType: "oic.d.blind", vid: "generic-shade") {
		capability "Actuator"
		capability "Configuration"
		capability "Window Shade"
// mc not supported in HE		capability "Window Shade Preset"
		capability "Switch Level"
		capability "Sensor"
        capability "Battery"
        capability "Light" //GH
		command "pause"
		command "levelOpenClose"
        command "presetPosition"
        attribute "Direction", "enum", ["Reverse","Forward"]
        attribute "OCcommand", "enum", ["Replace","Original"]
        attribute "stapp", "enum", ["Reverse","Forward"]
        attribute "remote", "enum", ["Reverse","Forward"]

		fingerprint endpointId: "01", profileId: "0104", inClusters: "0000 0004 0005 EF00", outClusters: "0019, 000A", manufacturer: "_TZE200_xuzcvlku", model: "TS0601", deviceJoinName: "Zemismart Vertical Blind" 
        //mc changeed endpointId from 0x01 to 01

	preferences {
		input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "0..100", required: false, displayDuringSetup: false
        input name: "Direction", type: "enum", title: "Direction Set", options:["01": "Reverse", "00": "Forward"], required: true, displayDuringSetup: true
        input name: "OCcommand", type: "enum", title: "Replace Open and Close commands", options:["2": "Replace", "0": "Original"], required: true, displayDuringSetup: true
        input name: "stapp", type: "enum", title: "app opening,closing Change", options:["2": "Reverse", "0": "Forward"], required: true, displayDuringSetup: true
        input name: "remote", type: "enum", title: "RC opening,closing Change", options:["1": "Reverse", "0": "Forward"], required: true, displayDuringSetup: true
        input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: true
/* removed tiles section as not used in Hubitat
	tiles(scale: 2) {
		multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) {
			tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") {
				attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing"
				attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening"
				attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing"
				attributeState "opening", label: 'Opening', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing"
				attributeState "closing", label: 'Closing', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening"
		standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc"
		standardTile("presetPosition", "device.presetPosition", width: 2, height: 2, decoration: "flat") {
			state "default", label: "Preset", action:"presetPosition", icon:"st.Home.home2"
		standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		valueTile("shadeLevel", "device.level", width: 4, height: 1) {
			state "level", label: 'Shade is ${currentValue}% up', defaultState: true
		controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) {
			state "level", action:"switch level.setLevel"

		main "windowShade"
		details(["windowShade", "contPause", "presetPosition", "shadeLevel", "levelSliderControl", "refresh"])

private getCLUSTER_TUYA() { 0xEF00 }
private getSETDATA() { 0x00 }

// Parse incoming device messages to generate events
def parse(String description) {
log.debug description
	if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) {
		Map descMap = zigbee.parseDescriptionAsMap(description)      
		if (descMap?.clusterInt==CLUSTER_TUYA) {
			if ( descMap?.command == "01" || descMap?.command == "02" ) {
				def dp = zigbee.convertHexToInt(descMap?.data[3]+descMap?.data[2])
                if(logEnable) log.debug "dp = " + dp
				switch (dp) {
					case 1025: // 0x04 0x01: Confirm opening/closing/stopping (triggered from Zigbee)
                        def parData = descMap.data[6] as int
                        if(parData != 1){
                        def stappVal = (stapp ?:"0") as int
                        def data = Math.abs(parData - stappVal)
						sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
                        log.debug "App control=" + (data == 0 ? "opening":"closing")
					case 1031: // 0x04 0x07: Confirm opening/closing/stopping (triggered from remote)
                        def parData = descMap.data[6] as int
                        def remoteVal = remote as int
                        def data = Math.abs(parData - remoteVal)
						sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
                        log.debug "Remote control=" + (data == 0 ? "opening":"closing")
					case 514: // 0x02 0x02: Started moving to position (triggered from Zigbee)
                    	def setLevel = zigbee.convertHexToInt(descMap.data[9])
                        def lastLevel = device.currentValue("level")
						//sendEvent([name:"windowShade", value: (setLevel >= lastLevel ? "opening":"closing"), displayed: true])
                        log.debug "Remote control=" + (setLevel >= lastLevel ? "opening":"closing")
                        log.debug "setLevel : $setLevel"
                        log.debug "lastLevel : $lastLevel"
                    	if (setLevel > 0 && setLevel <100) 
                        	sendEvent(name: "windowShade", value: "partially open")
                            if (setLevel == 0) 
                            sendEvent([name:"windowShade", value: "open", displayed: true])
                            if (setLevel == 100)
                            sendEvent([name:"windowShade", value: "closed", displayed: true])                           
                    	//log.debug "arrived at position :"+pos
                        sendEvent(name: "level", value: (setLevel))
					case 515: // 0x02 0x03: Arrived at position
                    	def pos = zigbee.convertHexToInt(descMap.data[9])
                    	log.debug "arrived at position :"+pos
                    	if (pos > 0 && pos <100) {
                        	sendEvent(name: "windowShade", value: "partially open")
                        } else {
                        	sendEvent([name:"windowShade", value: (pos == 100 ? "open":"closed"), displayed: true])
                        sendEvent(name: "level", value: (pos))
                        //To enable in GoggleHome
                        if (pos < 100){ sendEvent(name: "switch", value: "on")}
                        else {sendEvent(name: "switch", value: "off")}
                    default: log.debug "abnormal case : $dp" 

def close() {
	log.info "close()"
    def cm = (OCcommand ?:"0") as int
    log.debug "cm=${cm}"
    def val = Math.abs(2 - cm)
	sendTuyaCommand("0104", "00", "010" + val)

def open() {
	log.info "open()"
    def cm = (OCcommand ?:"0") as int
    def val = Math.abs(0 - cm)
	sendTuyaCommand("0104", "00", "010" + val)

def pause() {
	log.info "pause()"
	sendTuyaCommand("0104", "00", "0101")

def setLevel(data, rate = null) {
	log.info "setLevel("+data+")"
    def currentLevel = device.currentValue("level")
    if (currentLevel == data) {
    	sendEvent(name: "level", value: currentLevel, displayed: true)
	sendTuyaCommand("0202", "00", "04000000"+zigbee.convertToHexString(data.intValue(), 2))

def refresh() {
	zigbee.readAttribute(CLUSTER_TUYA, 0x00, )
    //Nivel da Bateria
                        map = parseReportAttributeMessage(description)
                        listResult << createEvent(map)

//mc new for HE Commands
def setPosition(position){
    log.info "setLevel("+$position+")"
    //setLevel(data = position, rate = null)

def presetPosition() {
    setLevel(preset ?: 50)

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

def updated() {
	log.debug "val(${Direction}),valC(${OCcommand}),valR(${remote})"
	DirectionSet(Direction ?:"00")

def DirectionSet(Dval) {
	log.info "Dset(${Dval})"
   sendHubCommand(zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + "05040001" + Dval))

def configure() {
	log.info "configure()"

private sendTuyaCommand(dp, fn, data) {
	log.info "${zigbee.convertToHexString(rand(256), 2)}=${dp},${fn},${data}"
	zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + dp + fn + data)

private rand(n) {
	return (new Random().nextInt(n))

def on (){log.warn "$device - some thing thinks im a switch  and is turning me on"}
def off () {log.warn "$device - some thing thinks im a switch and is turning me off"}

private Map parseReportAttributeMessage(String description) {
	Map descMap = (description - "read attr - ").split(",").inject([:]) {
		map, param -> def nameAndValue = param.split(":")
		map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	Map resultMap = [:]

	log.info "IN parseReportAttributeMessage()"
	log.debug "descMap ${descMap}"

	switch(descMap.cluster) {
		case "0001":
			log.debug "Battery status reported"

			if(descMap.attrId == "0021") {
				resultMap.name = 'battery'
                resultMap.value = (convertHexToInt(descMap.value) / 2)
                log.debug "Battery Percentage convert to ${resultMap.value}%"
			log.info descMap.cluster
			log.info "cluster1"

	log.info "OUT parseReportAttributeMessage()"
	return resultMap


1 Like

Did you ever get anywhere with the fixes you mentioned @kkossev? (TS0601 _TZE200_rddyvrci) I've been holding off buying a few more of these (though there are that many different versions I'd be lucky to get the same again!)

Sorry @johnwill1 , there was no progress on this in the past 2-3 weeks, got busy with other projects.

1 Like

No problem, thanks - just making sure I'd not missed a post

1 Like

Download the Hubitat app