Custom Device Capabilities: How To?

The documentation implies that it’s possible to define custom device capabilities, but no detail are given:

http://docs.smartthings.com/en/latest/introduction/what-can-developers-do/capability-taxonomy.html

Custom Capabilities
If you are integrating or building a device that requires a capability that isn’t listed here, no problem! You can add a custom capability to the platform yourself, and we will then look for common threads across new custom capabilities in order to determine which ones should be made part of the standard taxonomy.

If I just add capability to metadata definition section, I get an exception:

definition (name:"Launch Pad", namespace:"VirtuconIndustries", author:"Dr. Evil") {
    capability "Missile Launcher"

java.lang.RuntimeException: Metadata Error: Capability 'Missile Launcher' not found. @ line 31

So what’s the proper syntax? Throw me a bone here, it’s not like I’m trying to take over the world. :smile:

Why then, when I push that button… The pretty candy like RED button, the I see rockets launching? Why I ask you? Why?

And sorry I do not have the answer to your question. However a good one to look at for answers if the AEON HEMV2 device in the IDE. If I remember right it uses custom variables as you want to do.

I for one welcome my new overlord!

I can only see “Aeon Home Energy Meter” in the IDE. Where is HEMV2? I know I can add custom attributes and commands. My question was about custom capability. Thanks anyway…

I took a look at the Aeon Home Energy Meter and it leverages capabilities that already exist in the standard capability taxonomy: https://graph.api.smartthings.com/ide/doc/capabilities

Oh drats, let me look around here… I know I had an application that used custom variables…

Sorry for the mis-direct guys,.

Found it… CUstom Lock by bigpunk6

Copy code here, but also he has a Github page.

/** 
 * This is a custom version of the Z-Wave Lock Device Type code. This code adds support for user codes with the use of a Smartapp.
 *
 * This Device Type was designed for the Kwikset Deadbolt so might not be compatible with other locks.
 *
 * Installation
 *
 * Create a new device type (https://graph.api.smartthings.com/ide/devices)
 *    Capabilities:
 *        Configuration
 *        Battery
 *        Polling
 *        Lock
 *    Custom Attribute
 *        user1
 *        code1
 *    Custom Command
 *        usercodechange
 */
metadata {
	definition (name: "Custom Lock", author: "bigpunk6") {
		capability "Configuration"
		capability "Lock"
        capability "Lock Codes"
		capability "Polling"
		capability "Battery"

		attribute "user1", "string"
		attribute "code1", "string"

		command "usercodechange"
	}
	simulator {
		status "locked": "command: 9881, payload: 00 62 03 FF 00 00 FE FE"
		status "unlocked": "command: 9881, payload: 00 62 03 00 00 00 FE FE"

		reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 00 FE FE"
		reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 00 FE FE"
	}

	tiles {
		standardTile("toggle", "device.lock", width: 2, height: 2) {
			state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
			state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
			state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
			state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
		}
		standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") {
			state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
		}
		standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") {
			state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
		}
		valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
            state "battery", label:'${currentValue}% battery', action:"batteryupdate", unit:""
        }
        valueTile("usercode", "device.usercode", inactiveLabel: false, decoration: "flat") {
			state "usercode", label:'${currentValue}', unit:""
		}
		standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") {
			state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
			state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		}

		main "toggle"
		details(["toggle", "lock", "unlock", "battery", "usercode", "configure", "refresh"])
	}
}

import physicalgraph.zwave.commands.doorlockv1.*

//parse
def parse(String description) {
	def result = null
	if (description.startsWith("Err")) {
	    result = createEvent(descriptionText:description, displayed:true)
	} else {
		def cmd = zwave.parse(description, [ 0x98: 1])
		if (cmd) {
			result = zwaveEvent(cmd)
		}
	}
	log.debug "\"$description\" parsed to ${result.inspect()}"
    log.debug "Parse result $result"
	result
}

def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
	def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1,0x62: 1, 0x63: 1, 0x70: 1, 0x71: 2, 0x75: 1, 0x80:1, 0x85: 2, 0x4E: 2, 0x4C: 1, 0x8B: 1, 0x5D: 2])
	log.debug "encapsulated: $encapsulatedCommand"
	if (encapsulatedCommand) {
		zwaveEvent(encapsulatedCommand)
	}
}

def zwaveEvent(DoorLockOperationReport cmd) {
	def map = [ name: "lock" ]
	if (cmd.doorLockMode == 0xFF) {
		map.value = "locked"
	} else if (cmd.doorLockMode >= 0x40) {
		map.value = "unknown"
	} else if (cmd.doorLockMode & 1) {
		map.value = "unlocked with timeout"
	} else {
		map.value = "unlocked"
	}
	createEvent(map)
}

def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
	def map = null
	if (cmd.zwaveAlarmType == 6) {
		if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) {
			map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ]
		}
		switch(cmd.zwaveAlarmEvent) {
			case 1:
				map.descriptionText = "$device.displayName was manually locked"
				break
			case 2:
				map.descriptionText = "$device.displayName was manually unlocked"
				break
			case 5:
				if (cmd.eventParameter) {
					map.descriptionText = "$device.displayName was locked with code ${cmd.eventParameter.first()}"
				}
				break
			case 6:
				if (cmd.eventParameter) {
					map.descriptionText = "$device.displayName was unlocked with code ${cmd.eventParameter.first()} zwaveAlarmEvent"
				}
				break
			case 9:
				map.descriptionText = "$device.displayName was autolocked"
				break
			case 7:
			case 8:
			case 0xA:
				map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ]
				break
			case 0xB:
				map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed" ]
				break
			case 0xC:
				map = [ descriptionText: "$device.displayName: all user codes deleted", display: true ]
				break
			case 0xD:
				map = [ descriptionText: "$device.displayName: user code deleted", display: true ]
				break
			case 0xE:
				map = [ descriptionText: "$device.displayName: user code added", display: true ]
				break
			default:
				map = map ?: [ descriptionText: "$device.displayName: alarm event $cmd.zwaveAlarmEvent", display: false ]
				break
		}
	} else switch(cmd.alarmType) {
        case 17:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "$device.displayName Secured at Keypad – Bolt Jammed"
			break
        case 18:
			map = [ name: "lock", value: "locked" ]
            if(cmd.alarmLevel) {
				map.descriptionText = "$device.displayName Secured by User ${cmd.alarmLevel} at Keypad"
                map = [ name: "code", value: ${cmd.alarmLevel} ]
            }
			break
		case 19:
			map = [ name: "lock", value: "unlocked" ]
			if(cmd.alarmLevel) {
				map.descriptionText = "$device.displayName Un-Secured by User ${cmd.alarmLevel} at Keypad"
			}
			break
        case 21:
			map = [ name: "lock", value: "locked" ]
				map.descriptionText = "$device.displayName Secured using Keyed cylinder or inside thumb-turn"
			break
        case 22:
			map = [ name: "lock", value: "unlocked" ]
				map.descriptionText = "$device.displayName Un-Secured using Keyed cylinder or inside thumb-turn"
			break
        case 23:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "$device.displayName Secured by Controller – Bolt Jammed"
			break
        case 24:
			map = [ name: "lock", value: "locked" ]
				map.descriptionText = "$device.displayName Secured by Controller"
			break
        case 25:
			map = [ name: "lock", value: "unlocked" ]
				map.descriptionText = "$device.displayName Un-Secured by Controller"
			break
        case 26:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "$device.displayName Lock Auto Secured – Bolt Jammed"
			break
        case 27:
			map = [ name: "lock", value: "locked" ]
				map.descriptionText = "$device.displayName Lock Auto Secured"
			break
        case 32:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "All User Codes deleted from $device.displayName"
			break
        case 33:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "User ${cmd.alarmLevel} deleted from $device.displayName"
			break
       case 112:
			map = [ name: "lock", value: "unknown" ]
			if(cmd.alarmLevel) {
				map.descriptionText = "New User: ${cmd.alarmLevel} added to $device.displayName"
			}
			break
       case 161:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "Failed User Code attempt at Keypad on $device.displayName"
			break
       case 162:
			map = [ name: "lock", value: "unknown" ]
			if(cmd.alarmLevel) {
				map.descriptionText = "Attempted access by user ${cmd.alarmLevel} outside of scheduled on $device.displayName"
			}
			break
        case 167:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "Low battery level on $device.displayName"
			break
       case 168:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "Critical battery level on $device.displayName"
			break
       case 169:
			map = [ name: "lock", value: "unknown" ]
				map.descriptionText = "Battery level too low to operate $device.displayName"
			break
		default:
			map = [ displayed: false, descriptionText: "$device.displayName: $cmd" ]
	}
	createEvent(map)
}

def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
	def result = []
	state.associationQuery = null
	if (cmd.nodeId.any { it == zwaveHubNodeId }) {
		log.debug "$device.displayName is associated to $zwaveHubNodeId"
		state.assoc = zwaveHubNodeId
	} else {
		result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
	}
	result
}

//battery
def batteryupdate() {
    def result = secure(zwave.batteryV1.batteryGet())
    log.debug "battery $result"
    result
}

def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {
		map.value = 1
		map.descriptionText = "$device.displayName has a low battery"
	} else {
		map.value = cmd.batteryLevel
	}
	createEvent(map)
}

//User
def usercodechange(user1, code1, idstatus1) {
    log.debug "Set $code1 for User $user1"
    log.debug "Set User ID Status to $idstatus1"
    secureSequence([
        zwave.userCodeV1.userCodeSet(userIdentifier: user1, userIdStatus: idstatus1, code: code1),
        zwave.userCodeV1.userCodeGet(userIdentifier: user1)
    ], 4200)
}

def zwaveEvent(physicalgraph.zwave.commands.usercodev1.UserCodeReport cmd) {
    log.debug "$cmd"
    def map = [ name: "usercode" ]
    map.value = cmd.code
    createEvent(map)
}

//Configuration
def configupdate() {
    secure(zwave.configurationV1.configurationGet(parameterNumber: 1))
}

def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
    log.debug "$cmd"
    def map = [ name: "config", value:  cmd.configurationValue]
    if (cmd.configurationValue) {
        switch(cmd.configurationValue) {
			case "[1]":
				map.descriptionText = "User $cmd.parameterNumber is Type: Owner (Default)"
                log.debug "Owner"
				break
			case "[3]":
				map.descriptionText = "User $cmd.parameterNumber is Type: Guest (Required for Year Day Schedules)"
				break
            case "[4]":
				map.descriptionText = "User $cmd.parameterNumber is Type: Worker (Required for Week Day Schedules)"
				break
            case "[255]":
				map.descriptionText = "User $cmd.parameterNumber not found"
				break
            default:
			    map = [ displayed: false, descriptionText: "$device.displayName: $cmd" ]
            }
    }
    createEvent(map)
}


def zwaveEvent(physicalgraph.zwave.Command cmd) {
    log.warn "Unexpected zwave command $cmd"
    createEvent(displayed: false, descriptionText: "$device.displayName: $cmd")
}

def lockAndCheck(doorLockMode) {
    secureSequence([
        zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
        zwave.doorLockV1.doorLockOperationGet()
    ], 4200)
}

def lock() {
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
}

def unlock() {
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
}

def unlockwtimeout() {
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
}

def refresh() {
	def result = [secure(zwave.doorLockV1.doorLockOperationGet())]
	if (state.assoc == zwaveHubNodeId) {
		//log.debug "$device.displayName is associated to ${state.assoc}"
	} else if (!state.associationQuery) {
		log.debug "checking association"
		result << "delay 4200"
		result << zwave.associationV1.associationGet(groupingIdentifier:2).format()  // old Schlage locks use group 2 and don't secure the Association CC
		result << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = new Date().time
	} else if (new Date().time - state.associationQuery.toLong() > 9000) {
		log.debug "setting association"
		result << "delay 6000"
		result << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		result << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		result << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		result << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = new Date().time
	}
	result
}

def updated() {
}

def secure(physicalgraph.zwave.Command cmd) {
    def result = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    log.debug "Result $result"
    result
}

def secureSequence(commands, delay=4200) {
	delayBetween(commands.collect{ secure(it) }, delay)
}

I hope that helps… I am still learning Groozy, so always double check me when I post (SMILE)

Sorry, these are not custom capabilities. This device has custom attributes and commands, but this is not what I’m looking for.

Sorry… I was trying to help… Like I said still learning… and well… Feeling foolish now.

No worries… We’re all learning here. :smile:

@geko think you are overthinking it…

standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { state "default", label:'reset kWh', action:"reset"

this second option “device.energy” can be anything and then you can access it via the tile “reset” and get the value.

then in a smartapp you can subscribe to the device and get the device.latestValue(“energy”)

Hope that helps…

1 Like

@pstuart, I respectfully disagree. Your example just creates a custom attribute, not custom capability. Capability is a class that binds a name to a collection of commands and attributes as defined here: https://graph.api.smartthings.com/ide/doc/capability

The reason to have a capability is that a SmartApp can bind itself to devices providing particular capability:

input "myDevice", "capability.missileLauncher"

I understand and have asked for the same thing. Additional capabilities or the ability to define our own. This is not something they want us community folks to have access to via core api.

Ultimately for what I needed I could subscribe to the device and get the custom attributes so that was that for my logging or triggering.

Just don’t really know your use case that makes it different.

That’s too bad and it contradicts the official document, which says in plain English that

You can add a custom capability to the platform yourself

The use case, for example, would be a SmartApp that tracks atmospheric pressure. Since there’s no capability.atmosphericPressure, I would have to implement device handler with generic capability.sensor and a custom attribute atmosphericPressure. The app then would be very confusing then because choosing the input would list all the devices with capability.sensor and the app would have to filter out somehow devices that don’t provide atmosphericPressure attribute. That’s ugly.

From what I can tell, there is no way to add custom capability at this moment. The docs are misleading. I tries to create several difference device types with the same capability, but it’s not possible at this moment from what I can tell.

Hopefully this gets enabled at some point. It helps to achieve a higher level of obstruction.

I’d prefer a lower level of obstruction. :slight_smile: I hate autocorrection sometimes. :smile:

@geko You’re correct and the docs are wrong on this point. Your device type can define custom commands and custom attributes, but you don’t currently have the ability to define custom capabilities.

We do want to allow custom capabilities, but we’re not there yet. I don’t expect that we’ll be able to give you the ability to create custom capabilities in 2014. To be clear, there’s no core api method to do this today either. It’s not that we don’t want you to be able to do this, we just haven’t had time to implement the full vision yet.

For the example use case you pointed out, capability.atmosphericPressure, you are correct that today, you wouldn’t be able to have an input that asked for all atmosphericPressure device types - if there existed a large number of different device types that provided this information.

I realize it’s not great, but as a potential short-term solution - if you had a short list of device types (1-3 or so seems reasonable) - your SmartApp could ask for each of the separate device types, rather than asking by capability (e.g. “Select your XYZ atmosphericPressure device type devices” followed by “and any ABC atmosphericPressure sensors” … etc). If that’s not feasible for your situation, then you’re right that you’d need to ask for capability.sensor and filter the selection as you pointed out.

I hope that helps.

Hey Scott, Thank you for clarification. It’s not a critical issue, I understand and I am looking forward to SmartThings getting more feature complete in the future. Keep up good work! :smile:

Thanks for the answer, but please keep the docs correct, as I can now add myself to the list of people misled and spent an hour trying to sort it out. :wink:

we really need this or at least an endpoint parameter to capabilities so for instance we can get energy reports and set and check switch states for multiple switch devices etc.

how about something link
capability Switch Multi

and capability Energy Meter Mutil

etc

I am looking for something similar as well, being able to create a new type of contact sensor where th status is updated via a an MQTT bridge. Has this issue been resolved? Can we now define our own custom capabilities?