Porting existing custom Device Handlers to new app? What's VID selector used for?

I’ve spend the last couple of days trying to read up on custom device handlers. I’m familiar with the old method of using Github and Groovy IDE to get the necessary code pushed to your ST hub.

Has that process changed for the new app? Or do you can still use the old Groovy IDE web interface to edit existing or creative new code for custom devices?

For example, I’ve come across this new “VID selector” tool over on glitch.com but haven’t yet understood how it relates to the new app. Is it just a small database with example codes? Or what is the VID selector actually used for?

The VIDs define set of capabilities used by the DH and rendered in the new app.
The new app is capable to render some capabilities, but VIDs give a standardized order and look. In the selector you can select the capabilities what your DH needs and then it would give you a VID what you can include in the DH’s metadata part.
Look at the SmartThings GitHub repo, how the VIDs are used and where should be placed in the Groovy code.

Thanks for the explanation. So basically the old Github/Goovy IDE route is still being used and the VDI is just the additional code needed to have the capabilities of a given device show up in the new app, right?

If I want to port over an existing custom DH to work with the new app, can I leave code for the classic app untouched and just add the new VID part? Or do you have to make a clean break and delete the old code?

Adding the VID and/or ocfDeviceType usually improves things in the new mobile app, but a couple of my handlers work better without them.

There are also some capabilities that break when you add a VID. For example, all of the button related VIDs break the ability to setup automations from the device’s details screen.

The only way to get that automations feature in the new mobile app to work with all the specified supported button events is to not include a VID and use ocfDeviceType: “x.com.st.d.remotecontroller” instead.

That’s how it was a few weeks ago when I spent hours fighting with it so I’m assuming it’s still like that, but they’re constantly changing things.

You can make the change to an existing handler, but you often won’t see the changes made to the metadata immediately. It usually refreshes when the device list screen shows it as “checking status” (or something like that).

You can sometimes force that by re-opening the mobile app, opening and closing the device details, or changing it to a different handler and then changing it back.

Creating a new handler with a different name every time you want to test a change made to the metadata usually makes it easier to see the change, but sometimes you still have to do the things above to force it to refresh.

The most frustrating part about trying to get a device to work as expected in the new mobile app is that every once in a while ST makes a change that breaks it and then you have to go through all that again.

Everything should be a lot easier once ST releases the documentation for the new mobile app, but I highly doubt they’ll be doing that any time soon.

1 Like

Without any official documentation it is indeed very, very tough for a beginner like myself to get a handle of even where to start. It’s especially confusing, that it’s not always clear of a post around here is meant to refer to the old or new app. So I’m sorry in case I might come up as stupid :-/

My idea would be, to start out with an existing DH from the official Github repo and try to add the code needed to get my device working.

Since my device is a two buttoned Zigbee button, my guess would be that Zigbee Multi Button or Zigbee Button might be a good start.

What you be the best way forward?

I could find the following data in the logs or in the IDE data:

Zigbee Id: 00158D00027C082F
Device Network Id: 2B4C
Model: lumi.remote.b286acn01
Raw Description: 01 0104 5F01 01 05 0000 0003 0019 FFFF 0012 07 0000 0004 0003 0005 0019 FFFF 0012

fingerprint deviceId: “5F01”
outClusters: “0000,0003,0004,0005,0019,0012,FFFF”
inClusters: “0000,0003,0019,0012,FFFF”
Cluster: 0012 (Multistate input)
Attribute: 0055

When the buttons are pressed, this shows up:

description is read attr - raw: 2B4C0100120A5500210100, dni: 2B4C, endpoint: 01, cluster: 0012, size: 10, attrId: 0055, result: success, encoding: 21, value: 0001
description is read attr - raw: 2B4C0200120A5500210100, dni: 2B4C, endpoint: 02, cluster: 0012, size: 10, attrId: 0055, result: success, encoding: 21, value: 0001

I only work with Z-Wave devices so I can’t provide any suggestions, but if you get stuck on how to implement the button capabilities let me know…

I’ve not worked on a Zigbee button device yet, so unfortunately I can only help a little bit. Since you’re working with Zigbee devices, keep this doc very handy: (page 3-99 talks about cluster 0012)

Also, using existing examples/code is super helpful!

Raw Description is very important, and it tells you what clusters are supported, and you use that info within that Spec doc to understand what values, attributes, and othe important info the device will send and can receive.

I’ve tried to adopt the original code which worked fine with the device in the old app.

Here’s what I’ve done so far:

/**
 *  Copyright 2019 SmartThings
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *	Author: SRPOL
 *	Date: 2019-02-18
 */

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

metadata {
	definition (name: "Zigbee Multi Button", namespace: "smartthings", author: "SmartThings", mcdSync: true, ocfDeviceType: "x.com.st.d.remotecontroller") {
		capability "Actuator"
		capability "Battery"
		capability "Button"
		capability "Holdable Button"
		capability "Configuration"
		capability "Sensor"
		capability "Health Check"

		fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L", deviceJoinName: "Iris KeyFob", mnmn: "SmartThings", vid: "generic-4-button"
		fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L2", deviceJoinName: "Iris KeyFob", mnmn: "SmartThings", vid: "generic-4-button"
		fingerprint profileId: "0104", inClusters: "0004", outClusters: "0000, 0001, 0003, 0004, 0005, 0B05", manufacturer: "HEIMAN", model: "SceneSwitch-EM-3.0", deviceJoinName: "HEIMAN Scene Keypad", vid: "generic-4-button"

		// Aqara Smart Light Switch - dual button - model WXKG02LM (2018 revision)
		fingerprint deviceId: "5F01", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.remote.b286acn01", deviceJoinName: "Aqara Switch WXKG02LM (2018)", mnmn: "TOMillr", vid: "generic-4-button"

		//AduroSmart
		fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", outClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", manufacturer: "AduroSmart Eria", model: "ADUROLIGHT_CSC", deviceJoinName: "Eria scene button switch V2.1", mnmn: "SmartThings", vid: "generic-4-button"
		fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", outClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", manufacturer: "ADUROLIGHT", model: "ADUROLIGHT_CSC", deviceJoinName: "Eria scene button switch V2.0", mnmn: "SmartThings", vid: "generic-4-button"
		fingerprint inClusters: "0000, 0003, 0008, FCCC, 1000", outClusters: "0003, 0004, 0006, 0008, FCCC, 1000", manufacturer: "AduroSmart Eria", model: "Adurolight_NCC", deviceJoinName: "Eria dimming button switch V2.1", mnmn: "SmartThings", vid: "generic-4-button"
		fingerprint inClusters: "0000, 0003, 0008, FCCC, 1000", outClusters: "0003, 0004, 0006, 0008, FCCC, 1000", manufacturer: "ADUROLIGHT", model: "Adurolight_NCC", deviceJoinName: "Eria dimming button switch V2.0", mnmn: "SmartThings", vid: "generic-4-button"
	}

	tiles {
		standardTile("button", "device.button", width: 2, height: 2) {
			state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
			state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC"
		}

		valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
			state "battery", label:'${currentValue}% battery', unit:""
		}

		standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
			state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		main (["button"])
		details(["button", "battery", "refresh"])
	}
}

def parse(String description) {
	def map = zigbee.getEvent(description)
	def result = map ? map : parseAttrMessage(description)
	if (result.name == "switch") {
		result = createEvent(descriptionText: "Wake up event came in", isStateChange: true)
	}
	log.debug "Description ${description} parsed to ${result}"
	return result
}

def parseAttrMessage(description) {
	def descMap = zigbee.parseDescriptionAsMap(description)
	def map = [:]
	if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap?.value) {
		map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16))
	} else if (isAduroSmartRemote()) {
		map = parseAduroSmartButtonMessage(descMap)
    	} else if (descMap?.clusterInt == zigbee.ONOFF_CLUSTER && descMap.isClusterSpecific) {
		map = getButtonEvent(descMap)
	} else if (descMap?.clusterInt == 0x0005) {
		def buttonNumber
		buttonNumber = buttonMap[device.getDataValue("model")][descMap.data[2]]
       
		log.debug "Number is ${buttonNumber}"
		def descriptionText = getButtonName() + " ${buttonNumber} was pushed"
		sendEventToChild(buttonNumber, createEvent(name: "button", value: "pushed", data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true))
		map = createEvent(name: "button", value: "pushed", data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)
   	}
	map
}

def getButtonEvent(descMap) {
	if (descMap.commandInt == 1) {
		getButtonResult("press")
	}
	else if (descMap.commandInt == 0) {
		def button = buttonMap[device.getDataValue("model")][descMap.sourceEndpoint]
		getButtonResult("release", button)
	}
}

def getButtonResult(buttonState, buttonNumber = 1) {
	def event = [:]
	if (buttonState == 'release') {
		def timeDiff = now() - state.pressTime
		if (timeDiff > 10000) {
			return event
		} else {
			buttonState = timeDiff < holdTime ? "pushed" : "held"
			def descriptionText = getButtonName() + " ${buttonNumber} was ${buttonState}"
			event = createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)
			sendEventToChild(buttonNumber, event)
			return createEvent(descriptionText: descriptionText)
		}
	} else if (buttonState == 'press') {
		state.pressTime = now()
		return event
	}
}

def sendEventToChild(buttonNumber, event) {
	String childDni = "${device.deviceNetworkId}:$buttonNumber"
	def child = childDevices.find { it.deviceNetworkId == childDni }
	child?.sendEvent(event)
}

def getBatteryPercentageResult(rawValue) {
	log.debug 'Battery'
	def volts = rawValue / 10
	if (volts > 3.0 || volts == 0 || rawValue == 0xFF) {
		[:]
	} else {
		def result = [
				name: 'battery'
		]
		def minVolts = 2.1
		def maxVolts = 3.0
		def pct = (volts - minVolts) / (maxVolts - minVolts)
		result.value = Math.min(100, (int)(pct * 100))
		def linkText = getLinkText(device)
		result.descriptionText = "${linkText} battery was ${result.value}%"
		createEvent(result)
	}
}

def refresh() {
	return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage) +
			zigbee.readAttribute(zigbee.ONOFF_CLUSTER, switchType)
			zigbee.enrollResponse()
}

def ping() {
	refresh()
}

def configure() {
	def bindings = getModelBindings(device.getDataValue("model"))
	def cmds = zigbee.onOffConfig() +
			zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage, DataType.UINT8, 30, 21600, 0x01) +
			zigbee.enrollResponse() +
			zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage) + bindings
	if (isHeimanButton())
		cmds += zigbee.writeAttribute(0x0000, 0x0012, DataType.BOOLEAN, 0x01) +
		addHubToGroup(0x000F) + addHubToGroup(0x0010) + addHubToGroup(0x0011) + addHubToGroup(0x0012)
	return cmds
}

def installed() {
	sendEvent(name: "button", value: "pushed", isStateChange: true, displayed: false)
	sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false)
	
	initialize()
}

def updated() {
	runIn(2, "initialize", [overwrite: true])
}

def initialize() {
	def numberOfButtons = modelNumberOfButtons[device.getDataValue("model")]
	sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false)
	sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
    
	if(!childDevices) {
		addChildButtons(numberOfButtons)
	}
	if(childDevices) {
		def event
		for(def endpoint : 1..device.currentValue("numberOfButtons")) {
			event = createEvent(name: "button", value: "pushed", isStateChange: true)
			sendEventToChild(endpoint, event)
		}
	}
}

private addChildButtons(numberOfButtons) {
	for(def endpoint : 1..numberOfButtons) {
		try {
			String childDni = "${device.deviceNetworkId}:$endpoint"
			def componentLabel = getButtonName() + "${endpoint}"

			if (isAduroSmartRemote()) {
				componentLabel = device.displayName + " - ${endpoint}"
			}
			def child = addChildDevice("Child Button", childDni, device.getHub().getId(), [
					completedSetup: true,
					label         : componentLabel,
					isComponent   : true,
					componentName : "button$endpoint",
					componentLabel: "Button $endpoint"
			])
			child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false)
		} catch(Exception e) {
			log.debug "Exception: ${e}"
		}
	}
}

private getBatteryVoltage() { 0x0020 }
private getSwitchType() { 0x0000 }
private getHoldTime() { 1000 }
private getButtonMap() {[
		"3450-L" : [
				"01" : 4,
				"02" : 3,
				"03" : 1,
				"04" : 2
		],
		"3450-L2" : [
				"01" : 4,
				"02" : 3,
				"03" : 1,
				"04" : 2
		],
		"SceneSwitch-EM-3.0" : [
				"01" : 1,
				"02" : 2,
				"03" : 3,
				"04" : 4
		],
		"lumi.remote.b286acn01" : [
				"01" : 1,
				"02" : 2,
		]
]}

private getSupportedButtonValues() {
	def values
	if (device.getDataValue("model") == "SceneSwitch-EM-3.0") {
		values = ["pushed"]
	} else if (isAduroSmartRemote()) {
		values = ["pushed"]
	} else {
		values = ["pushed", "held"]
	}
	return values
}

private getModelNumberOfButtons() {[
		"3450-L" : 4,
		"3450-L2" : 4,
		"SceneSwitch-EM-3.0" : 4, 
		"ADUROLIGHT_CSC" : 4,
		"Adurolight_NCC" : 4,
        "lumi.remote.b286acn01" : 2
]}

private getModelBindings(model) {
	def bindings = []
	for(def endpoint : 1..modelNumberOfButtons[model]) {
		bindings += zigbee.addBinding(zigbee.ONOFF_CLUSTER, ["destEndpoint" : endpoint])
	}
	if (isAduroSmartRemote()) {
		bindings += zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER, ["destEndpoint" : 2]) + 
			zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER, ["destEndpoint" : 3])
	}
	bindings
}

private getButtonName() {
	def values = device.displayName.endsWith(' 1') ? "${device.displayName[0..-2]}" : "${device.displayName}"
	return values
}

private Map parseAduroSmartButtonMessage(Map descMap){
	def buttonState = "pushed"
	def buttonNumber = 0
	if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) {
		if (descMap.command == "01") {
		    buttonNumber = 1
		} else if (descMap.command == "00") {
		    buttonNumber = 4
		}
	} else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
		if (descMap.command == "02") {
		    def data = descMap.data
		    def d0 = data[0]
		    if (d0 == "00") {
			buttonNumber = 2
		    } else if (d0 == "01") {
			buttonNumber = 3
		    }
		}
	} else if (descMap.clusterInt == ADUROSMART_SPECIFIC_CLUSTER) {
		def list2 = descMap.data
		buttonNumber = (list2[1] as int) + 1
	}
	if (buttonNumber != 0) {
		def childevent = createEvent(name: "button", value: "pushed", data: [buttonNumber: 1], isStateChange: true)
		sendEventToChild(buttonNumber, childevent)
		def descriptionText = "$device.displayName button $buttonNumber was $buttonState"
		return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)
        } else {
		return [:]
	}
}

def isAduroSmartRemote(){
	((device.getDataValue("model") == "Adurolight_NCC") || (device.getDataValue("model") == "ADUROLIGHT_CSC"))
}

def getADUROSMART_SPECIFIC_CLUSTER() {0xFCCC}

private getCLUSTER_GROUPS() { 0x0004 }

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

def isHeimanButton(){
	device.getDataValue("model") == "SceneSwitch-EM-3.0"
}

After you saved and published this, and then changed the device to use it, did you force a Configuration process? You can send that command to the device, but since it’a a sleepy device, you’ll need to “wake” it right away by using a button. It should then accept the config. OR, it should run through a config as soon as you change to the DTH. You can check that this works via the device’s event log or Live Logging.

If you did that and everything worked within the DTH, you should see within the new app Button 1 and Button 2 with Held and Pressed options.

You can also strip out a lot of the code that’s not relevant to your device, like the AduroSmartButton checks and Iris and Heiman stuff. It will simplify things a lot and make reading through the code and logic a lot easier.