[RELEASE] Zemismart Zigbee Blind Driver

Also, this is more of a general question about the Zemismart AM 43 rather than the driver itself. This won't work unless the pull cord/string that opens and closes the blinds is one, continuous loop, right?

Does anyone have experience trying to get this to work with blinds whose pull cords are not closed in a loop on the pulling end? Does simply cutting off their ends and tying them together in a loop work?

Look for minutes 4:10 to answer your question.

1 Like

Always a pleasure to help.

In hubitat there are tiles for shades, you can also try the HomeHabit app, it has a great UI, you can see it in the picture above. You can ask @igor if you need any help.

It only works with looped cords. I mean if you could rig it with trying to tie the ends together. If you're looking for a normal blinds controller (and not shades) you want this iblinds Kit v3

Hi! I'm sharing a driver I adapted to work with my Zemismart Zigbee Motor model TS0601, a battery powered motor. The driver is fully functional; the only problem is that when a voice command is sent through Alexa to adjust the curtain to a certain percentage, the command is executed correctly, but Alexa reports an error.

I hope it may be useful to somebody!

Driver code:

/**
 *  Tuya Window Shade (v.0.1.0)
 *	Copyright 2021 MarcusVR
 *
 *	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.
 https://github.com/iquix/Smartthings/blob/master/devicetypes/iquix/tuya-window-shade.src/tuya-window-shade.groovy
 */


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

metadata {
	definition(name: "ZemiSmart Battery Vertical Blind Motor", namespace: "MarcusVR", author: "MarcusVR", 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 "Switch"
    capability "Refresh"
	    capability "HealthCheck" 
    
		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
	}

}

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")
                    }
                	break
					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")
                	break
					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")
                    } 
                    else 
                    {
                        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))
                    break
					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")}
                    
                    break
                default: log.debug "abnormal case : $dp" 
                    break
				}
			}
		}
	}
}

private levelEventMoving(currentLevel) {
	def lastLevel = device.currentValue("level")
	if(logEnable) log.debug "levelEventMoving - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"
	if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports
		log.debug "Ignore invalid reports"
	} else {
		if (lastLevel < currentLevel) {
			sendEvent([name:"windowShade", value: "opening"])
		} else if (lastLevel > currentLevel) {
			sendEvent([name:"windowShade", value: "closing"])
		}
}
}

private levelEventArrived(level) {
	if (level == 0) {
	sendEvent(name: "windowShade", value: "closed")
} else if (level == 100) {
	sendEvent(name: "windowShade", value: "open")
} else if (level > 0 && level < 100) {
		sendEvent(name: "windowShade", value: "partially open")
} else {
	sendEvent(name: "windowShade", value: "unknown")
    //return
}
sendEvent(name: "level", value: (level))
sendEvent(name: "position", value: (level))
//To enable in GoggleHome
if (level < 100){ sendEvent(name: "switch", value: "on")}
else {sendEvent(name: "switch", value: "off")}
}

def close() {
	if(logEnable) log.info "close()"
	def currentLevel = device.currentValue("level")
if (currentLevel == 0) {
	sendEvent(name: "windowShade", value: "closed")
    return
}
	sendTuyaCommand("0104", "00", "0102")
}

def open() {
	if(logEnable) log.info "open()"
def currentLevel = device.currentValue("level")
if (currentLevel == 100) {
	sendEvent(name: "windowShade", value: "open")
    return
}
	sendTuyaCommand("0104", "00", "0100")
}

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

}

def setLevel(data, rate = null) {
	if(logEnable) log.info "setLevel("+data+")"
def currentLevel = device.currentValue("level")
if (currentLevel == data) {
	sendEvent(name: "level", value: currentLevel)
    sendEvent(name: "position", value: currentLevel) //HE capability attribute
    return
}
	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)
setLevel(position)
}
//mc

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

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

def updated() {
	def val = Direction
sendEvent([name:"Direction", value: (val == "00" ? "Forward" : "Reverse")])    
	DirectionSet(val)
}	

def DirectionSet(Dval) {
	if(logEnable) log.info "Direction set ${Dval} "
//zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + "05040001" + Dval)
sendTuyaCommand("05040001", Dval, "") //not tested
}

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


def poll() {
	null
}


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 off() {
	if(logEnable) log.info "open()"
def currentLevel = device.currentValue("level")
if (currentLevel == 100) {
	sendEvent(name: "windowShade", value: "open")
    return
}
	sendTuyaCommand("0104", "00", "0100")
}

def on() {
	if(logEnable) log.info "close()"
	def currentLevel = device.currentValue("level")
if (currentLevel == 0) {
	sendEvent(name: "windowShade", value: "closed")
    return
}
	sendTuyaCommand("0104", "00", "0102")
}

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}%"
			}
			break
		default:
			log.info descMap.cluster
			log.info "cluster1"
			break
	}

	log.info "OUT parseReportAttributeMessage()"
	return resultMap
}

Dashboard tile:

1 Like

Did you confirm battery parsing works? Looks like your screenshot doesn't have a battery attribute set in the state. Also your rfresh code looks like it shouldn't work since description param is never set.

@amosyuen, please sorry! The battery status isn't reported, and I really don't know about the refresh code. I don't know nothing about programming, I took a driver that didn't fit my engine because it didn't recognize some commands - like percentage aperture - and tweaked it a little removing some slashes regarding the setLevel function. Now the commands open, close, pause and setLevel work, but this is all.

I appreciate if you can guide me in what should be changed so that it is fully functional, if you show me the way I will search to improve it, but it is accepting the commands sent.

I wasn't able to get battery or refresh to work as it didn't seem to be supported at the firmware level, which is why I was curious if you got it working. For everything else feel free to reference my code.

1 Like

What would be the best way to control groups of blinds each one controlled by the AM43, and in a way that Alexa understands that the control is for blinds? I have each one working properly more or less (more on that below). I noticed that I cannot create groups o these devices, which would have been handy for rooms with multiple windows and therefore multiple sets of blinds/ AM43 blinds motors.

I see there are virtual buttons including a Virtual Shade, but I can't find a way to make them trigger as "open" or "close" - I can only select digital switch in Rules Machine and select "On" or "Off" which I don't think would work for what I'm doing.

Also, another question. I installed two more of these in my Dining Room and everything went fine. However, in dashboard, using the Shades tile, I noticed that the open/close status does not reflect properly. I will open the shades and it will still say "opening" even when they are fully open, or "closing" even when they are fully closed. I checked to make sure they are both using the Zemismart Zigbee blind driver. My study room blinds use the same one and that one works fine. Is there something else I have to do to configure this properly? I already set the open and closed limits in the device so I don't think that's the problem.

1 Like

Virtual shade is probably the way to go for grouping. You can use Custom Attribute for the trigger in Rule Machine.

For the shades stuck in opening/closing, please enable logging in the device preferences and capture the logs.

1 Like

Thanks. Here is the log for Dining Room Blinds 1, which is currently closed and is currently reading in Dashboard reads as "opening" with the slider all the way to the right in the "open" position.

I clicked "open" in the device page and this happened:

However the blinds did not open.

I then cleared the log, clicked "close" in the device page, and this happened:

The blind opened, and the dashboard says it is "closing" with the slider all the way to the left in the closed position.

Possibly I did something wrong, but I later tried to enable the "reverse" direction, then save it, then hit "configure," but that didn't fix the problem.

Please enable all of "Enable debug logging", "Enable trace logging", and "Log unexpected messages".

Sounds like your blinds direction are reversed. Perhaps try playing with both "forward" and "reverse" values while watching the logs. You should make sure you get an ACK from the device on configure.

Did you wait for the blinds to fully open before sharing the logs? Assuming you did wait, looks like the motor isn't sending a message on stopping. Could you confirm that the blinds are the AM43 model?

Sorry, my mistake. I found the third option and enabled it as you mentioned. Now all three are enabled.

Yes, I can confirm that these are the AM43 model. That's what it says on the box and these are identical to the one that I got for my study room which so far works well.

I did indeed wait until the blinds fully opened before sharing the log. Let me try again.

Here is Dining Room Blinds 1, which is currently open, but which is read as "Closing" in my Dashboard and the slider is all the way to the left (tile type is Shades). When I slide it all the way to the right, the blind close, and once fully closed, here is the log:

Incidentally, after I took this shot, one more line appeared at the top which goes like this:

dev:20782021-07-14 09:51:58.666 pm [trace]checkForResponse: waitingForResponseSinceMillis=null}}

I don't know what that means or if it is relevant, but I included it.

The dashboard tile now says "Opening" and the slider is all the way to the right, but the blinds are in fact closed.

Now I slid it all the way to the left which should close the blinds. The blinds opened, and once completed the tile simply says "Closing" and here is the log:

I also tried changing the direction for this device to "reverse" saved it, then clicked "Configure." But I'm not sure where to see the ACK - will that be in the device page? Or the log page? And what does it say, exactly?

I decided to try it and wait a minute, although I'm not sure what sort of acknowledgement I'm looking for. Doing this reverse thing resulted in me being able to open/close the blinds one more time and then losing control completely - until I switch direction back to forward.

The ack for a direction change would be in the logs and would have start with updateDirection: .

But it seems like the blind is not behaving correctly since it is not sending a message on stopping. Maybe try resetting the device, setting limits, and re-pairing the device to hubitat.

1 Like

Has anyone successfully link the driver to Google Home?

I have done the following:

  • Added the Blind to the Hubitat Google Home app
  • Refresh Google Home Android App (by reconnecting Hubitat and selected the Blind and authorize it)
  • Once authorization is done, no new device / Blind is added to the Google Home android App
  • And when I went back to the Hubitat Google Home App, the Blind is unselected again

It works perfectly with Amazon Echo Skill app though

See Google Home Integration section in first post

1 Like

D'oh! sorry I totally missed it.

I wonder why it uses Curtain, instead of Blind? consider Blinds will allow you to change its height.

Unpairing, resetting, and re-pairing seems to have fixed the problem. I had to reverse the direction and never saw the updateDirection: line in logs, but... it worked.

I also suddenly lost control over the other device - Dining Room Blinds 2. Didn't know what to make of that, but I removed/reset/repaired it and now it's working fine. Thanks!

Next question. I'm trying to create devices that can control multiple shades and I did what you had suggested. I have two rules configured as below. Main problems I see with this are two: (1) When I open/close the device, there is a delay before the blinds open/close. (2) (more annoying), the individual blinds and the overall blinds control do not sync. I.e. if the individual blinds are closed, the overall control does not read as closed - i have to close it again manually. Any suggests for fixing this so the virtual shades device gives correct updated information on the blinds position? Let's assume I want the virtual shades device to read as "open" if either of them are open, and "closed" if both are closed.

Thanks in advance,

I seem to be having some issues with my Zemismart AM43.
I can do a set position, but nothing other than that.
The positions are set and works as expected.
This is what I get when trying to, well anything other than setPosition:

dev:705 2021-07-18 19:33:01.873 errororg.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'null' with class 'null' to class 'int'. Try 'java.lang.Integer' instead on line 204 (setDirection)
dev:705 2021-07-18 19:33:00.867 errororg.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'null' with class 'null' to class 'int'. Try 'java.lang.Integer' instead on line 210 (setMode)
dev:705 2021-07-18 19:32:59.900 errororg.codehaus.groovy.runtime.InvokerInvocationException: java.lang.Exception: Invalid maxClosedPosition "null" should be between 0 and 100 inclusive. (configure)
dev:705 2021-07-18 19:30:08.140 errorjava.lang.NullPointerException: Cannot invoke method negative() on null object on line 516 (stepClose)
dev:705 2021-07-18 19:30:07.125 errorgroovy.lang.GroovyRuntimeException: Ambiguous method overloading for method java.math.BigDecimal#plus.
Cannot resolve which method to invoke for [null] due to overlapping prototypes between:
	[class java.lang.Character]
	[class java.lang.String]
	[class java.lang.Number]
	[class java.math.MathContext] on line 524 (stepOpen)
dev:705 2021-07-18 19:30:06.350 errorgroovy.lang.GroovyRuntimeException: Ambiguous method overloading for method java.math.BigDecimal#plus.
Cannot resolve which method to invoke for [null] due to overlapping prototypes between:
	[class java.lang.Character]
	[class java.lang.String]
	[class java.lang.Number]
	[class java.math.MathContext] on line 524 (stepOpen)
dev:705 2021-07-18 19:30:05.655 errorjava.lang.NullPointerException: Cannot invoke method negative() on null object on line 516 (stepClose)
dev:705 2021-07-18 19:30:05.060 errorjava.lang.NullPointerException: Cannot invoke method negative() on null object on line 516 (stepClose)
dev:705 2021-07-18 19:30:02.436 errorjava.lang.NullPointerException: Cannot invoke method negative() on null object on line 516 (stepClose)
dev:705 2021-07-18 19:16:05.037 errororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_amosyuen_ZemiSmart_Zigbee_Blind_693.ping() is applicable for argument types: () values: []
Possible solutions: find(), print(java.lang.Object), find(groovy.lang.Closure), print(java.io.PrintWriter), print(java.lang.Object), run() (ping)
dev:705 2021-07-18 18:57:06.050 errororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_amosyuen_ZemiSmart_Zigbee_Blind_693.checkEventInterval() is applicable for argument types: () values: [] (checkEventInterval)
dev:705 2021-07-18 18:45:05.039 errororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_amosyuen_ZemiSmart_Zigbee_Blind_693.ping() is applicable for argument types: () values: []
Possible solutions: find(), print(java.lang.Object), find(groovy.lang.Closure), print(java.io.PrintWriter), print(java.lang.Object), run() (ping)
dev:705 2021-07-18 18:36:44.547 errorgroovy.lang.GroovyRuntimeException: Ambiguous method overloading for method java.math.BigDecimal#plus.
Cannot resolve which method to invoke for [null] due to overlapping prototypes between:
	[class java.lang.Character]
	[class java.lang.String]
	[class java.lang.Number]
	[class java.math.MathContext] on line 515 (stepOpen)
dev:705 2021-07-18 18:36:43.146 errorjava.lang.NullPointerException: Cannot invoke method negative() on null object on line 507 (stepClose)

I'm also getting this one regularly:

dev:7052021-07-18 19:45:05.040     errororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_amosyuen_ZemiSmart_Zigbee_Blind_693.ping() is applicable for argument types: () values: []

Download the Hubitat app