Zemismart Roller Blind Motor

Have you tried the original driver in this post

Yes but I am having battery issues with the motors (I have 2 motors that are both needing to be recharged after a week).
I was thinking that perhaps the other handler is making lots of ;check-in' type requests to the motor as other motors are mains powered and that might be draining the battery. The driver that I posted above is specifically for my motor. I started to convert it to Hubitat last night. Got the open and close working but still working on the set position part.

They make a solar charger you can attach discreetly to the window so you drink worry about charging

No solar charger for this model

If the plug fits it will work

I have a very similar motor. Just received it today. There's a driver I found that I'm using but the code is all over the place. Open and close attributes are inverted and the guy that edited the tuya driver made some other weird changes. Going to clean up the code in the next couple of days. Let's keep each other posted on our progress.

Here is my initial go at converting the driver. I'm no Java developer so it was a bit hit and miss but seems to mostly work. Hubitat - Tuya Window Shade Driver

Nice work... I'm done with mine but would send tomorrow. I worked off a different codebase and cleaned a bunch of things in it. Might have made sense to start with what they provided you actually.

Question for you, does your motor ever send an arrived message i.e. do you ever get into the "levelEventArrived" function? Mine never does so I had to use the "levelEventMoving" to determine when it's partially opened, opened or closed (I basically got rid of "opening" and "closing").

Not sure but the opening and closing seems to work ok. Do you know what the difference between "level" and "position" is?

There's no difference. They mean the same thing. In your code, an event is never sent for position so you won't have a position attribute. Also, setPosition() just calls the setLevel() function. In my code, I got rid of level completely and just used position for everything.

There are a few issues with your code. The direction doesn't work. Also, your code throws some errors. I'm going to stick to mine and add any good bits from yours. Mine's a bit more specific though to the zigbee battery motor.

My code is below.

/**
 *  Tuya Window Shade (v.0.1.0)
 *	Copyright 2021 YoYoToGbLo
 *
 *	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
import groovy.transform.Field
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

metadata {
	definition(name: "ZemiSmart Battery Blind Motor", namespace: "yoyotogblo", author: "Ro Odun", ocfDeviceType: "oic.d.blind", vid: "generic-shade") {
		capability "Actuator"
		capability "Battery"
		capability "Configuration"
		capability "Window Shade"
    
		command "pause"
		command "presetPosition"
    
		attribute "direction", "enum", ["Reverse","Forward"]
		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 Battery Blind Motor")
	}

	preferences {
		input("preset", "number", title: "Preset position", 
			description: "Set the window shade preset position", 
			defaultValue: 50, range: "0..100", required: false, 
			displayDuringSetup: false)
		input("direction", "enum", title: "Direction Set", 
			options:["01": "Reverse", "00": "Forward"], 
			required: true, defaultValue: "01", displayDuringSetup: true)
		input("stapp", "enum", title: "app opening,closing Change", 
			options:["2": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("remote", "enum", title: "RC opening,closing Change", 
			options:["1": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("enableInfoLog", "bool", title: "Enable info logging", required: true, defaultValue: false)
		input("enableDebugLog", "bool", title: "Enable debug logging", required: true, defaultValue: false)
	}

}

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

// Parse incoming device messages to generate events
def parse(String description) {
	logDebug(description)
	if (description == null || (!description.startsWith('catchall:') && !description.startsWith('read attr -'))) {
		logDebug("parse: Unhandled description=${description}")
		return
	}
	
	Map descMap = zigbee.parseDescriptionAsMap(description)  
	if (descMap?.clusterInt != CLUSTER_TUYA) {
		logDebug("parse: Not a Tuya Message descMap=${descMap}")
		return
	}
	if (descMap?.command == "01" || descMap?.command == "02" ) {
		def dp = zigbee.convertHexToInt(descMap?.data[3]+descMap?.data[2])
		logDebug("dp = " + dp)
		switch (dp) {
			case 1025: // 0x04 0x01: Confirm opening/closing/stopping (triggered from Zigbee)
				def parData = descMap.data[6] as int
                logInfo("par data is " + parData)
				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])
					logDebug("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				
                logInfo("par data is " + parData)
				def remoteVal = remote as int
				def data = Math.abs(parData - remoteVal)
				sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
				logDebug("Remote control=" + (data == 0 ? "opening":"closing"))
				break
				
			case 514: // 0x02 0x02: Started moving to position (triggered from Zigbee)
				def setPosition = zigbee.convertHexToInt(descMap.data[9])
				def lastPosition = device.currentValue("position")
				//sendEvent([name:"windowShade", value: (setPosition >= lastPosition ? "opening":"closing"), displayed: true])
				logDebug("Remote control=" + (setPosition >= lastPosition ? "opening":"closing"))
				logDebug("setPosition : $setPosition")
				logDebug("lastPosition : $lastPosition")
				if (setPosition > 0 && setPosition <100) 
				{
					sendEvent(name: "windowShade", value: "partially open")
				} 
				else 
				{
					if (setPosition == 100) {
						sendEvent([name:"windowShade", value: "open", displayed: true])
					}
					if (setPosition == 0) {
						sendEvent([name:"windowShade", value: "closed", displayed: true])   
                    }
				}
				sendEvent(name: "position", value: (setPosition))
				break
				/**
 *  Tuya Window Shade (v.0.1.0)
 *	Copyright 2021 YoYoToGbLo
 *
 *	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
import groovy.transform.Field
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

metadata {
	definition(name: "ZemiSmart Battery Blind Motor", namespace: "yoyotogblo", author: "Ro Odun", ocfDeviceType: "oic.d.blind", vid: "generic-shade") {
		capability "Actuator"
		capability "Battery"
		capability "Configuration"
		capability "Window Shade"
    
		command "pause"
		command "presetPosition"
    
		attribute "direction", "enum", ["Reverse","Forward"]
		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 Battery Blind Motor")
	}

	preferences {
		input("preset", "number", title: "Preset position", 
			description: "Set the window shade preset position", 
			defaultValue: 50, range: "0..100", required: false, 
			displayDuringSetup: false)
		input("direction", "enum", title: "Direction Set", 
			options:["01": "Reverse", "00": "Forward"], 
			required: true, defaultValue: "01", displayDuringSetup: true)
		input("stapp", "enum", title: "app opening,closing Change", 
			options:["2": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("remote", "enum", title: "RC opening,closing Change", 
			options:["1": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("enableInfoLog", "bool", title: "Enable info logging", required: true, defaultValue: false)
		input("enableDebugLog", "bool", title: "Enable debug logging", required: true, defaultValue: false)
	}

}

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

// Parse incoming device messages to generate events
def parse(String description) {
	logDebug(description)
	if (description == null || (!description.startsWith('catchall:') && !description.startsWith('read attr -'))) {
		logDebug("parse: Unhandled description=${description}")
		return
	}
	
	Map descMap = zigbee.parseDescriptionAsMap(description)  
	if (descMap?.clusterInt != CLUSTER_TUYA) {
		logDebug("parse: Not a Tuya Message descMap=${descMap}")
		return
	}
	if (descMap?.command == "01" || descMap?.command == "02" ) {
		def dp = zigbee.convertHexToInt(descMap?.data[3]+descMap?.data[2])
		logDebug("dp = " + dp)
		switch (dp) {
			case 1025: // 0x04 0x01: Confirm opening/closing/stopping (triggered from Zigbee)
				def parData = descMap.data[6] as int
                logInfo("par data is " + parData)
				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])
					logDebug("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				
                logInfo("par data is " + parData)
				def remoteVal = remote as int
				def data = Math.abs(parData - remoteVal)
				sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
				logDebug("Remote control=" + (data == 0 ? "opening":"closing"))
				break
				
			case 514: // 0x02 0x02: Started moving to position (triggered from Zigbee)
				def setPosition = zigbee.convertHexToInt(descMap.data[9])
				def lastPosition = device.currentValue("position")
				//sendEvent([name:"windowShade", value: (setPosition >= lastPosition ? "opening":"closing"), displayed: true])
				logDebug("Remote control=" + (setPosition >= lastPosition ? "opening":"closing"))
				logDebug("setPosition : $setPosition")
				logDebug("lastPosition : $lastPosition")
				if (setPosition > 0 && setPosition <100) 
				{
					sendEvent(name: "windowShade", value: "partially open")
				} 
				else 
				{
					if (setPosition == 100) {
						sendEvent([name:"windowShade", value: "open", displayed: true])
					}
					if (setPosition == 0) {
						sendEvent([name:"windowShade", value: "closed", displayed: true])   
                    }
				}
				sendEvent(name: "position", value: (setPosition))
				break
				
			case 515: // 0x02 0x03: Arrived at position
				def pos = zigbee.convertHexToInt(descMap.data[9])
				logDebug("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: "position", value: (pos))
				
			default: 
				logDebug("abnormal case : $dp")
				break
		}
	}
}

/* private positionEventMoving(currentPosition) {
	def lastPosition = device.currentValue("position")
	logDebug("positionEventMoving - currentPosition: ${currentPosition} lastPosition: ${lastPosition}")
	if (lastPosition == "undefined" || currentPosition == lastPosition) { //Ignore invalid reports
		logDebug("Ignore invalid reports")
	} 
	else {
		if (lastPosition < currentPosition) {
			sendEvent([name:"windowShade", value: "opening"])
		} 
		else if (lastPosition > currentPosition) {
			sendEvent([name:"windowShade", value: "closing"])
		}
	}
} */

/* private positionEventArrived(position) {
	if (position == 0) {
		sendEvent(name: "windowShade", value: "closed")
	} 
	else if (position == 100) {
		sendEvent(name: "windowShade", value: "open")
	} 
	else if (position > 0 && position < 100) {
		sendEvent(name: "windowShade", value: "partially open")
	} 
	else {
		sendEvent(name: "windowShade", value: "unknown")
	}
	sendEvent(name: "position", value: (position))
	sendEvent(name: "position", value: (position))
} */

def close() {
	logInfo("close()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 0) {
		sendEvent(name: "windowShade", value: "closed")
		return
	}
	sendTuyaCommand("0104", "00", "0100")
}

def open() {
	logInfo("open()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 100) {
		sendEvent(name: "windowShade", value: "open")
		return
	}
	sendTuyaCommand("0104", "00", "0102")
}

def off() {
	logInfo("off()")
	close()
}

def on() {
	logInfo("on()")
	open()
}

def startPositionChange(state) {
	logInfo("startPositionChange")
	switch (state) {
		case "close":
			close()
			return
		case "open":
			open()
			return
		default:
			throw new Exception("Unsupported startPositionChange state \"${state}\"")
	}
}

def stopPositionChange() {
	logInfo("stopPositionChange")
	pause()
}

def pause() {
	logInfo("pause()")
	sendTuyaCommand("0104", "00", "0101")
}

def setPosition(data, rate = null) {
	logInfo("setPosition("+data+")")
	def currentPosition = device.currentValue("position")
	if (currentPosition == data) {
		logInfo("setting position from "+currentPosition+"to "+data)
		sendEvent(name: "position", value: currentPosition)
		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)
}

def presetPosition() {
	setPosition(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) {
	logInfo("Direction set ${dVal} ")
	//zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + "05040001" + Dval)
	sendTuyaCommand("05040001", dVal, "") //not tested
}

def configure() {
	logInfo("configure()")
}

def poll() {
	null
}

private sendTuyaCommand(dp, fn, data) {
	logInfo("${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))
}

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 = [:]

	logInfo("IN parseReportAttributeMessage()")
	logDebug("descMap ${descMap}")

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

			if(descMap.attrId == "0021") {
				resultMap.name = 'battery'
				resultMap.value = (convertHexToInt(descMap.value) / 2)
				logDebug("Battery Percentage convert to ${resultMap.value}%")
			}
			break
			
		default:
			logInfo(descMap.cluster)
			logInfo("cluster1")
			break
	}

	logInfo("OUT parseReportAttributeMessage()")
	return resultMap
}

private logInfo(text) {
	if (!enableInfoLog) {
		return
	}
	log.info(text)
}

private logDebug(text) {
	if (!enableDebugLog) {
		return
	}
	log.debug(text)
}
			case 515: // 0x02 0x03: Arrived at position
				def pos = zigbee.convertHexToInt(descMap.data[9])
				logDebug("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: "position", value: (pos))
				
			default: 
				logDebug("abnormal case : $dp")
				break
		}
	}
}

/* private positionEventMoving(currentPosition) {
	def lastPosition = device.currentValue("position")
	logDebug("positionEventMoving - currentPosition: ${currentPosition} lastPosition: ${lastPosition}")
	if (lastPosition == "undefined" || currentPosition == lastPosition) { //Ignore invalid reports
		logDebug("Ignore invalid reports")
	} 
	else {
		if (lastPosition < currentPosition) {
			sendEvent([name:"windowShade", value: "opening"])
		} 
		else if (lastPosition > currentPosition) {
			sendEvent([name:"windowShade", value: "closing"])
		}
	}
} */

/* private positionEventArrived(position) {
	if (position == 0) {
		sendEvent(name: "windowShade", value: "closed")
	} 
	else if (position == 100) {
		sendEvent(name: "windowShade", value: "open")
	} 
	else if (position > 0 && position < 100) {
		sendEvent(name: "windowShade", value: "partially open")
	} 
	else {
		sendEvent(name: "windowShade", value: "unknown")
	}
	sendEvent(name: "position", value: (position))
	sendEvent(name: "position", value: (position))
} */

def close() {
	logInfo("close()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 0) {
		sendEvent(name: "windowShade", value: "closed")
		return
	}
	sendTuyaCommand("0104", "00", "0100")
}

def open() {
	logInfo("open()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 100) {
		sendEvent(name: "windowShade", value: "open")
		return
	}
	sendTuyaCommand("0104", "00", "0102")
}

def off() {
	logInfo("off()")
	close()
}

def on() {
	logInfo("on()")
	open()
}

def startPositionChange(state) {
	logInfo("startPositionChange")
	switch (state) {
		case "close":
			close()
			return
		case "open":
			open()
			return
		default:
			throw new Exception("Unsupported startPositionChange state \"${state}\"")
	}
}

def stopPositionChange() {
	logInfo("stopPositionChange")
	pause()
}

def pause() {
	logInfo("pause()")
	sendTuyaCommand("0104", "00", "0101")
}

def setPosition(data, rate = null) {
	logInfo("setPosition("+data+")")
	def currentPosition = device.currentValue("position")
	if (currentPosition == data) {
		logInfo("setting position from "+currentPosition+"to "+data)
		sendEvent(name: "position", value: currentPosition)
		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)
}

def presetPosition() {
	setPosition(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) {
	logInfo("Direction set ${dVal} ")
	//zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + "05040001" + Dval)
	sendTuyaCommand("05040001", dVal, "") //not tested
}

def configure() {
	logInfo("configure()")
}

def poll() {
	null
}

private sendTuyaCommand(dp, fn, data) {
	logInfo("${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))
}

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 = [:]

	logInfo("IN parseReportAttributeMessage()")
	logDebug("descMap ${descMap}")

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

			if(descMap.attrId == "0021") {
				resultMap.name = 'battery'
				resultMap.value = (convertHexToInt(descMap.value) / 2)
				logDebug("Battery Percentage convert to ${resultMap.value}%")
			}
			break
			
		default:
			logInfo(descMap.cluster)
			logInfo("cluster1")
			break
	}

	logInfo("OUT parseReportAttributeMessage()")
	return resultMap
}

private logInfo(text) {
	if (!enableInfoLog) {
		return
	}
	log.info(text)
}

private logDebug(text) {
	if (!enableDebugLog) {
		return
	}
	log.debug(text)
}

That code you pasted is a bit messed up. Looks like 2 drivers together. Copy and paster error i think :slight_smile:
Does your driver report battery level?

Ah sorry, copy and paste error. Mine doesn't report battery. I don't believe the motors report battery at all via zigbee.

I made some more updates to the code. I added switch, switchLevel and changeLevel capabilities. So if the shade is closed, it also reports as a switch that it's off. Vice versa for if it's opened. This is useful in apps that only support switches. BTW, that's the difference between level and position. Level is used for switches, position is used for curtains and shades.

ChangeLevel is useful if you have a button device or switch that you can hold (to start opening/closing the curtain) and open release, it'd automatically stop opening/closing the curtain.

Here's the updated code:

/**
 *  Tuya Window Shade (v.0.1.0)
 *	Copyright 2021 YoYoToGbLo
 *
 *	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
import groovy.transform.Field
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

metadata {
	definition(name: "ZemiSmart Battery Blind Motor", namespace: "yoyotogblo", author: "Ro Odun", ocfDeviceType: "oic.d.blind", vid: "generic-shade") {
		capability "Actuator"
		capability "Battery"
		capability "Configuration"
		capability "Window Shade"
    
		command "pause"
		command "presetPosition"
		
        capability "Switch"
        capability "SwitchLevel"
        capability "ChangeLevel"
    
		attribute "direction", "enum", ["Reverse","Forward"]
		attribute "hubDirection", "enum", ["Reverse","Forward"]
		attribute "remoteDirection", "enum", ["Reverse","Forward"]

		fingerprint(endpointId: "01", profileId: "0104", 
					inClusters: "0000 0004 0005 EF00", outClusters: "0019, 000A", 
					manufacturer: "_TZE200_xuzcvlku", model: "TS0601", 
					deviceJoinName: "Zemismart Battery Blind Motor")
		fingerprint(profileId: "0104", 
					manufacturer: "_TZE200_5sbebbzs", model: "TS0601", 
					deviceJoinName: "Zemismart Battery Blind Motor") // Zemismart Blind with Battery *

	}

	preferences {
		input("preset", "number", title: "Preset position", 
			description: "Set the window shade preset position", 
			defaultValue: 50, range: "0..100", required: false, 
			displayDuringSetup: false)
		input("direction", "enum", title: "Direction Set", 
			options:["01": "Reverse", "00": "Forward"], 
			required: true, defaultValue: "00", displayDuringSetup: true)
		input("hubDirection", "enum", title: "app opening,closing Change", 
			options:["2": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("remoteDirection", "enum", title: "RC opening,closing Change", 
			options:["1": "Reverse", "0": "Forward"], 
			required: true, defaultValue: "0", displayDuringSetup: true)
		input("fixPercent", "enum", title: "Fix Percent", 
			description: "Set 'Fix Percent' option unless open is 100% and close is 0%. In Smartthings, 'Open' should be 100% in level and 'Close' should be 0% in level. If it is reversed, then set this option to 'Fix Percent'.", 
			options: ["Default", "Fix Percent"], 
			defaultValue: "Default", required: false, displayDuringSetup: false)
		input("enableInfoLog", "bool", title: "Enable info logging", required: true, defaultValue: false)
		input("enableDebugLog", "bool", title: "Enable debug logging", required: true, defaultValue: false)
	}

}

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

// Parse incoming device messages to generate events
def parse(String description) {
	logDebug(description)
	if (description == null || (!description.startsWith('catchall:') && !description.startsWith('read attr -'))) {
		logDebug("parse: Unhandled description=${description}")
		return
	}
	
	Map descMap = zigbee.parseDescriptionAsMap(description)  
	logDebug("parse: descMap=${descMap}")
	if (descMap?.clusterInt != CLUSTER_TUYA) {
		logDebug("parse: Not a Tuya Message descMap=${descMap}")
		return
	}
	if (descMap?.command == "01" || descMap?.command == "02" ) {
		def dp = zigbee.convertHexToInt(descMap?.data[3]+descMap?.data[2])
		logDebug("dp = " + dp)
		switch (dp) {
			case 1025: // 0x04 0x01: Confirm opening/closing/stopping (triggered from Zigbee)
				def parData = descMap.data[6] as int
                logInfo("par data is " + parData)
				if(parData != 1){
					def hubDirectionVal = (hubDirection ?:"0") as int
					def data = Math.abs(parData - hubDirectionVal)
					sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
					logDebug("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				
                logInfo("par data is " + parData)
				def remoteDir = remoteDirection as int
				def data = Math.abs(parData - remoteDir)
				sendEvent([name:"windowShade", value: (data == 0 ? "opening":"closing"), displayed: true])
				logDebug("Remote control=" + (data == 0 ? "opening":"closing"))
				break
				
			case 514: // 0x02 0x02: Started moving to position (triggered from Zigbee)
				def setPosition = levelVal(zigbee.convertHexToInt(descMap.data[9]))
				if (!supportRealTimePosition()) {
					positionEventMoving(setPosition)
				}
				else {
					positionEventArrived(setPosition)
				}
				break
				
			case 515: // 0x02 0x03: Arrived at position
				def setPosition = levelVal(zigbee.convertHexToInt(descMap.data[9]))
				positionEventArrived(setPosition)
				break
				
			default: 
				logDebug("abnormal case : $dp")
				break
		}
	}
}

private positionEventMoving(setPosition) {
	def lastPosition = device.currentValue("position")
	logDebug("Remote control=" + (setPosition >= lastPosition ? "opening":"closing"))
	logDebug("positionEventMoving: setPosition=${setPosition}, lastPosition=${lastPosition}")
	
	sendEvent(name: "position", value: setPosition)
	sendEvent(name: "level", value: setPosition)
	
	if (lastPosition < setPosition) {
		sendEvent(name:"windowShade", value: "opening")
		sendEvent(name:"switch", value: 'on')
	} 
	else if (lastPosition > setPosition) {
		sendEvent(name:"windowShade", value: "closing")
		sendEvent(name:"switch", value: 'off')
	}
}

private positionEventArrived(setPosition) {
	logDebug("positionEventArrived: arrived at setPosition :${setPosition}")
	
	sendEvent(name: "position", value: setPosition)
	sendEvent(name: "level", value: setPosition)
	
	if (setPosition == 0) {
		sendEvent(name: "windowShade", value: "closed")
		sendEvent(name:"switch", value: 'off')
	} 
	else if (setPosition == 100) {
		sendEvent(name: "windowShade", value: "open")
		sendEvent(name:"switch", value: 'on')
	} 
	else if (setPosition > 0 && setPosition < 100) {
		sendEvent(name: "windowShade", value: "partially open")
		sendEvent(name:"switch", value: 'on')
	} 
	else {
		sendEvent(name: "windowShade", value: "unknown")
		sendEvent(name:"switch", value: 'off')
	}
}

private isWithinOne(setPosition) {
	def lastPosition = device.currentValue("position")
	logDebug("isWithinOne: setPosition=${setPosition}, lastPosition=${lastPosition}")
	
	if (lastPosition != "undefined" && Math.abs(setPosition - lastPosition) <= 1) {
		return true
	}
	return false
}

def close() {
	logInfo("close()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 0) {
		sendEvent(name: "windowShade", value: "closed")
		sendEvent(name:"switch", value: 'off')
		return
	}
	sendTuyaCommand("0104", "00", "0102")
}

def open() {
	logInfo("open()")
	def currentPosition = device.currentValue("position")
	if (currentPosition == 100) {
		sendEvent(name: "windowShade", value: "open")
		sendEvent(name:"switch", value: 'on')
		return
	}
	sendTuyaCommand("0104", "00", "0100")
}

def pause() {
	logInfo("pause()")
	sendTuyaCommand("0104", "00", "0101")
}

def off() {
	logInfo("off()")
	close()
}

def on() {
	logInfo("on()")
	open()
}

def startPositionChange(openOrClose) {
	logInfo("startPositionChange")
	switch (openOrClose) {
		case "close":
			close()
			break
		case "open":
			open()
			break
		default:
			throw new Exception("Unsupported startPositionChange state \"${openOrClose}\"")
	}
}

def stopPositionChange() {
	logInfo("stopPositionChange")
	pause()
}

def setPosition(data, rate = null) {
	logInfo("setPosition("+data+")")
	def currentPosition = device.currentValue("position")
	if (currentPosition == data) {
		logInfo("setting position from "+currentPosition+" to "+data)
		sendEvent(name: "position", value: currentPosition)
		sendEvent(name: "level", value: currentPosition)
		return
	}
	sendTuyaCommand("0202", "00", "04000000"+zigbee.convertToHexString(levelVal(data).intValue(), 2))
}

def setLevel(data, rate = null) {
	logInfo("setPosition("+data+")")
	setPosition(data, rate)
}

def startLevelChange(levelChangeDirection) {
    logInfo("startLevelChange(direction=$levelChangeDirection)")
	if(levelChangeDirection == "up") {
		open()
    } 
	else {
        close()
    }
}

def stopLevelChange() {
    logInfo("stopLevelChange()")
    pause()
}


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

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

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

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

def directionSet(dVal) {
	logInfo("Direction set ${dVal} ")
	//zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + "05040001" + Dval)
	sendTuyaCommand("05040001", dVal, "") //not tested
}

def configure() {
	logInfo("configure()")
}

def poll() {
	null
}

private levelVal(n) {
	return (int)((fixPercent == "Fix Percent") ? 100 - n : n)	
}

private sendTuyaCommand(dp, fn, data) {
	logInfo("${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))
}

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 = [:]

	logInfo("IN parseReportAttributeMessage()")
	logDebug("descMap ${descMap}")

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

			if(descMap.attrId == "0021") {
				resultMap.name = 'battery'
				resultMap.value = (convertHexToInt(descMap.value) / 2)
				logDebug("Battery Percentage convert to ${resultMap.value}%")
			}
			break
			
		default:
			logInfo(descMap.cluster)
			logInfo("cluster1")
			break
	}

	logInfo("OUT parseReportAttributeMessage()")
	return resultMap
}

private getProductId() {
	return device.getDataValue("manufacturer")[-7..-1]
}

private supportRealTimePosition() {
    logDebug("supportRealTimePosition: product ID is ${productId}") 
	return (productId == "ueqqe6k" || productId == "sbebbzs")
}

private logInfo(text) {
	if (!enableInfoLog) {
		return
	}
	log.info(text)
}

private logDebug(text) {
	if (!enableDebugLog) {
		return
	}
	log.debug(text)
}
3 Likes

I am having a hard time setting the start and end position of the blind. I have the model: TS0601 of Zemismart Battery Blind Roller. Does anyone knows if we can set these position through HE using this driver. I tried to set the starting and ending blind position directly on the device and using HE @yototogblo driver both without any success so far. This is not the easiest component to properly configure. Thanks for your help!

I have a different model, but for mine I needed to set the top/bottom with the included remote. It was not possible via Hubitat unfortunately.

I'm getting the window shade getting stuck on closing which is messing with my webcore automations, anyone else had similar?

Yea, fixed it now, I'll update my git driver and re post

edit, try this
https://raw.githubusercontent.com/Mark-C-uk/Hubitat/master/ZemiSmart%20Zigbee%20Blind

1 Like

Damn just found this post. I originally had this Zemismart bead chain motor but couldn't get it working with the driver I found. I returned it and bought the AM43 (with solar panel) from aliexpress. Unfortunately that one also has issues such as showing "closing" permanently when reaching its upper limit. I'm using the driver by amosyuen for it.

Strange I don't have that issue with my AM43. Try setting the upper limit a couple of notches down. The biggest issue I had was getting the tension right on the chain so it wouldn't slip. Other than that mine has been working flawlessly..

Yeah I detailed it ages ago on the other thread. Limit is fine, it's already set well below the blind top out/physical limit. I can either get it working at full speed in lift mode but the position reporting is all to pot OR I can switch it to tilt mode and the position reporting works fine but it permanently shows "closing" or "opening" incorrectly. I wondered if it's down to the forward/reverse setting. Obviously you can switch that to suit whether the cord is on the right or the left, but mine is a Roman Blind not a roller and I wondered if it's perceivable that it needs to rotate in the opposite direction to lift or lower. Whatever I did it always seemed to be wrong. Anyway it's fine for now as I've just got it raising and lowering fully in a sunset/sunrise rule.

Thanks. Probably should say I’m using the roller blinds, will this driver work for that as well?