Generic Device handler for 2-Gang Zigbee dimmer?

I am completely new to ST, but I have still in a few days solved most of my issues, but I can’t find a working Device Handler for a dimmer puck (2 gang dimmer) with manufacturer ID _TYZB01_v8gtiaed model TS110F

One channel is working with the dimmer device handler and I manage to get on/off working with the “integrated ZigBee Switch” device handler:

I can’t find any device handler for 2-gang dimmers at all, are there any?

The raw description of the dimmer puck is:
01 0104 0101 01 05 0000 0004 0005 0006 0008 02 0019 000A

Does anyone know how I solve this?

1 Like

I think I have figured out a few things, the problem seems to be that there are no device handlers for dimmer child-devices in the standard device handlers. So I must create a child device for dimmers (which should be quite easy based on the code I have parsed), but then I also must take a copy of “zigbee-multi-switch” & “zigbee-dimmer” and merge these figuring out the zigbee clusters that I need to write to to set the dimmer level. That is not as easy for a newbie.

Is there a good template that I can use for child-devices in dimmers? I have found a few devices that may be possible to use, but I have no clue which one to use as a template for this. None of them has worked when I have tried them “out of the box”.

I guess I just have to print out the code for several of them (as well as the device handlers above), compare and use a highlight pen as I used to do back in the days when I was actively coding…

Any help in the right direction would be very helpful. Any help in acknowledging that I am on the right track would also be appreciated.

That dimmer is made by Tuya and sold under many different brand names, including Losonho, yagusmart, and Zemismart. (Zemismart also offers a Z wave version, so make sure the DTH you find is for Zigbee.)

Smartthings has never been very good at multi endpoint devices, and the child device construct was a way around that. However, the way that multi button devices, which is what a two gang looks like in many DTHs, are handled changed significantly a few months ago, so many DTHs older than October 2020 won’t work correctly with the new V3 app. Even if they worked just fine with the classic app.

The Number of Buttons Value

In particular, the old generation used “number of buttons” for multi button devices, and this was also commonly used for a device that had one button with multiple tap patterns like double tap, triple tap, etc. where the DTH would spoof individual buttons for the different tap patterns.

The newest architecture went too far the other way and now you’ll see DTHs for a triple gang switch with a DTH that says there is only one button, but is using 5 tap for the third gang, etc.

It’s all very confusing. so if you are going to use code from a different device, you might want to look up a picture of the device before you start so you understand what the code author was trying to work with.

Anyway… The new platform doesn’t use the “number of buttons“ value, but the official SmartLighting feature and Webcore still do, so you even see some brand new custom DTHs now which are using “number of buttons“ but then only work with smartlighting and Webcore, they don’t work with other automations. :scream:

The most common symptom is the only one button works and the others are ignored. But there could be other symptoms as well

So that’s the first thing to be aware of before you start researching, or you might be bringing in bits of code that no longer work. :disappointed_relieved:

None of this is clearly documented in any of the official documentation yet, at least not that I’ve seen. Individual community members have worked it out over time, so quite a few DTHs have been updated. But not all, and not even all of the official versions.

Doing research

And if it all sounds very confusing, it is. So I just wanted to make sure you knew a bit of the history before you get out that yellow highlighter.

I suggest you check the forum for threads on the Tuya or Zemismart Multi gang Zigbee devices and look for posts since October 2020. You should find something useful there. Just be sure to read carefully to determine if the code you were going to look at only works with smartlighting/Webcore or if it has been fully updated for the new automations. :sunglasses:

Tagging @johnconstantelo just because he uses a lot of zigbee devices and might have something to add.

1 Like

And here’s one for a multi gang switch (not dimmer) from this line that just went up today. However, it looks like it’s using number of buttons, so I would expect it to work with Webcore but not with automations using the new rules API. But I might be wrong on that, I’ve asked the author to clarify.

1 Like

Actually you nailed it. It’s doable, but since there’s no child device for a dimmer, you’ll have to develop that one. I used “zigbee-multi-switch” when developing the DTH for Aqara’s double rocker to include power reporting.

EDIT:

Look at this example for a child dimmer:

1 Like

Also, you probably already know this, but just for clarity, smartthings adopted some of the Zigbee terminology and applied it for Z wave devices and some of the Z wave terminology and applied it for Zigbee devices, which can add more confusion. :thinking:

Anyway, in smartthings a “multilevel switch“ is a dimmer. The levels are the dim percentages like 10%, 50%, 75%, etc. up to 100%. But for many devices are expressed as 0 to 256 to allow for conversion to hex.

I went through the code of the standard zigbee-dimmers, switch child-devices and multi-switch devices yesterday and I think that the codes for these should be possible to merge. It will take me a day or two, but the dimmer child-device is actually already done, that part was simple.

The main thing that worries me is if the child-device commands are going to work as they are supposed to, but I will tell you after the weekend.

2 Likes

It is so difficult to code in a run-time typed programming language.

New errors popping up here and there.

It is not simpler when many of the examples are not working.

Documentation is not helping much either. I am currently stuck at sending a command or writeattribute to the secondary channel for setting the level, I can’t get that working. If it is not complaints about null pointers, it is complaining about the wrong datatype.

Is it possible to get a reference to something similar? I have looked around but I haven’t found any working code doing exactly that. Any pointers? Anyone?

Any good news ? Did you successfully activated the two channels dimmer module ?
Did you mange to finalize the device handler ?

I still have issues. It is now working as a dimmer on both channels, but I lost the possibility to turn it on and off without dimming to 0%. I will continue working on it…

Give me a few more days…

1 Like

I am now very close to have a working solution. It seems to work, I just need to test it a bit more.

Once I figured out what was wrong, it was so much easier… :wink:

1 Like

OK, so here is the code that seems to be working. I have some issues with Automations that can’t find the child device. I have no clue why, but I think it is a bug in the SmaretThings app rather than in my DH (as Automations shows a previous version of the child device than the one that is installed).

Note that there are two sets of code below, the “Zigbee Multi Dimmer” (the parent) and the “Child Switch Dimmer” (the child device). Both of them needs to be created.

I have based these on multiple other DH, but I guess most of it comes from the original Multi Switch, Dimmer, and Child Switch devices on the SmartThings GitHub.

So if anyone is interesting to try these, to find issues, and perhaps even solve issues, please do. Anyway, these DH are for the Lonsonho 2-Gang Tuya Dimmers (QS-Zigbee-D02-TRIAC-2C-LN) that looks like this:

/////////////////////////
// Zigbee Multi Dimmer
/////////////////////////
    
import physicalgraph.zigbee.zcl.DataType

metadata {
	definition (name: "ZigBee Multi Dimmer", namespace: "jhb", author: "jhb", ocfDeviceType: "oic.d.light", runLocally: false, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") {
		capability "Actuator"
		capability "Configuration"
		capability "Refresh"
		capability "Switch"
		capability "Switch Level"
		capability "Health Check"
		capability "Light"
        
		command "childOn", ["string"]
		command "childOff", ["string"]
		command "childSetLevel", ["string", "string"]

		// Generic
		fingerprint profileId: "0104", deviceId: "0101", inClusters: "0006, 0008", deviceJoinName: "Light" //Generic Dimmable Light
	}

	tiles(scale: 2) {
		multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
			}
			tileAttribute ("device.level", key: "SLIDER_CONTROL") {
				attributeState "level", action:"switch level.setLevel"
			}
		}
		standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		main "switch"
		details(["switch", "refresh"])
	}
}

def installed() {
	createChildDevices()
	updateDataValue("onOff", "catchall")
	refresh()
}

// Parse incoming device messages to generate events
def parse(String description) {

	Map eventMap = zigbee.getEvent(description)
	Map eventDescMap = zigbee.parseDescriptionAsMap(description)

	log.debug "description is $description"

	if (eventMap) {
		if (eventDescMap && eventDescMap?.attrId == "0000") {//0x0000 : OnOff attributeId
			if (eventDescMap?.sourceEndpoint == "01" || eventDescMap?.endpoint == "01") {
				sendEvent(eventMap)
			} else {
				def childDevice = childDevices.find {
					it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}" || it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.endpoint}"
				}
				if (childDevice) {
					childDevice.sendEvent(eventMap)
				} else {
					log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found"
				}
			}
		}
	} else {
		def descMap = zigbee.parseDescriptionAsMap(description)
		if (descMap && descMap.clusterInt == 0x0006 && descMap.commandInt == 0x07) {
			if (descMap.data[0] == "00") {
				log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
				sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
			} else {
				log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
			}
		} else {
			log.warn "DID NOT PARSE MESSAGE for description : $description"
			log.debug "${descMap}"
		}
	}
}

private getChildEndpoint(String dni) {
	dni.split(":")[-1] as Integer
}

def off() {
	log.debug("off")
    zigbee.off()
}

def on() {
	log.debug("on")
    zigbee.on()
}

def childOn(String dni) {
	def childEndpoint = getChildEndpoint(dni)
    log.debug(" child on ${dni} ${childEndpoint} ")
	zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: childEndpoint])
}

def childOff(String dni) {
	def childEndpoint = getChildEndpoint(dni)
    log.debug(" child off ${dni} ${ childEndpoint} ")
	zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: childEndpoint])
}

def setOnLevel (endpoint, level) {
	log.debug "setOnLevel($endpoint, $level)"
	zigbee.writeAttribute(zigbee.LEVEL_CONTROL_CLUSTER, 0x0011, DataType.UINT8, level, [destEndpoint: endpoint])
}

def moveToLevel (endpoint, level, transitionTime) {
	log.debug "moveToLevel($endpoint, $level, $transitionTime)"
    def p1 = DataType.pack(level, DataType.UINT8, false)
    def p2 = DataType.pack(transitionTime, DataType.UINT16, false)
    def dataString = [p1, p2].join(" ")
    //"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${scaledLevel} ${transitionTime}}"
    zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x0000, dataString, [destEndpoint: endpoint]) +
    zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x0004, dataString, [destEndpoint: endpoint])
}

def childSetLevel (String dni, Integer value) {
	def endpoint = getChildEndpoint(dni)
    def rate = 5;
	log.debug "setLevel($endpoint, $value, $rate)"
    def level = Math.round(Math.floor(value * 2.54))
    setOnLevel(endpoint, level) + moveToLevel(endpoint, level, rate)
}

def setLevel(value, rate = null) {
	def additionalCmds = []
	zigbee.setLevel(value) + additionalCmds
}
/**
 * PING is used by Device-Watch in attempt to reach the Device
 * */
def ping() {
	return zigbee.onOffRefresh()
}

def refresh() {
	zigbee.onOffRefresh() + zigbee.levelRefresh()
}

private void createChildDevices() {
	if (!childDevices) {
		def x = getChildCount()
       	log.debug(" childCount ${x}")

		for (i in 2..x) {
			addChildDevice("Child Switch Dimmer", "${device.deviceNetworkId}:0${i}", device.hubId,
				[completedSetup: true, label: "${device.displayName[0..-2]}${i}", isComponent: false])
           	log.debug(" Device ${i} created")

		}
	}
}

def updated() {
	log.debug "updated()"
	updateDataValue("onOff", "catchall")
	for (child in childDevices) {
		if (!child.deviceNetworkId.startsWith(device.deviceNetworkId) || //parent DNI has changed after rejoin
				!child.deviceNetworkId.split(':')[-1].startsWith('0')) {
			child.setDeviceNetworkId("${device.deviceNetworkId}:0${getChildEndpoint(child.deviceNetworkId)}")
		}
	}
	refresh()
}

def configureHealthCheck() {
	Integer hcIntervalMinutes = 12
	if (!state.hasConfiguredHealthCheck) {
		log.debug "Configuring Health Check, Reporting"
		unschedule("healthPoll")
		runEvery5Minutes("healthPoll")
		def healthEvent = [name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]]
		// Device-Watch allows 2 check-in misses from device
		sendEvent(healthEvent)
		childDevices.each {
			it.sendEvent(healthEvent)
		}
		state.hasConfiguredHealthCheck = true
	}
}

def configure() {
	log.debug "configure()"
	configureHealthCheck()
	   
    def cmds = zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
	def x = getChildCount()
	for (i in 2..x) {
		cmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: i])
	}
	cmds += refresh()
	return cmds
}

private getChildCount() {
	return 2
}

/////////////////////////
// Child Switch Dimmer
/////////////////////////
    
metadata {
	definition(name: "Child Switch Dimmer", namespace: "jhb", author: "jhb", ocfDeviceType: "oic.d.light", runLocally: false, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") {
		capability "Actuator"
		capability "Refresh"
		capability "Switch"
		capability "Switch Level"
		capability "Light"
	}

	tiles(scale: 2) {
		multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
			}
			tileAttribute ("device.level", key: "SLIDER_CONTROL") {
				attributeState "level", action:"switch level.setLevel"
			}
		}
		standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		main "switch"
		details(["switch", "refresh"])
     
	}
}

def installed() {
    sendEvent(name: "checkInterval", value: 30 * 60, displayed: false, data: [protocol: "zigbee"])
    log.debug("installed")
}

// Parse incoming device messages to generate events
def parse(String description) {
    log.debug("parse")
}

def ping() {
    log.debug("ping")
}

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

def setLevel(value, rate = null) {
    log.debug("level ${value}  ")
	parent.childSetLevel(device.deviceNetworkId, value)
}


void on() {
    log.debug("Local child on ${device.deviceNetworkId}")
	parent.childOn(device.deviceNetworkId)
}

void off() {
    log.debug("Local child off ${device.deviceNetworkId}")
	parent.childOff(device.deviceNetworkId)
}

void refresh() {
    log.debug("Local child refresh ${device.deviceNetworkId}")
}
2 Likes

It tokk over 12h, but now the child device is correctly displayed also in automations.

2 Likes