In my soon to be never-ending quest to get my pool controls over to Hubitat, my first task is to convert the ST Multichannel Driver over to Hubitat. The purpose of the Driver is to take the single Device discovered on the network (Z-Wave Device) and convert it over to the 10 devices occupied by the pool controls (10 switches).
/**
* Copyright 2015 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.
*
*/
metadata {
definition (name: "Z-Wave Device Multichannel", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Switch"
capability "Switch Level"
capability "Refresh"
capability "Configuration"
capability "Sensor"
capability "Zw Multichannel" // deprecated
fingerprint inClusters: "0x60"
fingerprint inClusters: "0x60, 0x25"
fingerprint inClusters: "0x60, 0x26"
fingerprint inClusters: "0x5E, 0x59, 0x60, 0x8E"
}
simulator {
status "on": "command: 2003, payload: FF"
status "off": "command: 2003, payload: 00"
reply "8E010101,delay 800,6007": "command: 6008, payload: 4004"
reply "8505": "command: 8506, payload: 02"
reply "59034002": "command: 5904, payload: 8102003101000000"
reply "6007": "command: 6008, payload: 0002"
reply "600901": "command: 600A, payload: 10002532"
reply "600902": "command: 600A, payload: 210031"
}
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.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
}
childDeviceTiles("endpoints")
/*standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}*/
}
}
def installed() {
def queryCmds = []
def delay = 200
def zwInfo = getZwaveInfo()
def endpointCount = zwInfo.epc as Integer
def endpointDescList = zwInfo.ep ?: []
// This is needed until getZwaveInfo() parses the 'ep' field
if (endpointCount && !zwInfo.ep && device.hasProperty("rawDescription")) {
try {
def matcher = (device.rawDescription =~ /ep:(\[.*?\])/) // extract 'ep' field
endpointDescList = util.parseJson(matcher[0][1].replaceAll("'", '"'))
} catch (Exception e) {
log.warn "couldn't extract ep from rawDescription"
}
}
if (zwInfo.zw.contains("s")) {
// device was included securely
state.sec = true
}
if (endpointCount > 1 && endpointDescList.size() == 1) {
// This means all endpoints are identical
endpointDescList *= endpointCount
}
endpointDescList.eachWithIndex { desc, i ->
def num = i + 1
if (desc instanceof String && desc.size() >= 4) {
// desc is in format "1001 AA,BB,CC" where 1001 is the device class and AA etc are the command classes
// supported by this endpoint
def parts = desc.split(' ')
def deviceClass = parts[0]
def cmdClasses = parts.size() > 1 ? parts[1].split(',') : []
def typeName = typeNameForDeviceClass(deviceClass)
def componentLabel = "${typeName} ${num}"
log.debug "EP #$num d:$deviceClass, cc:$cmdClasses, t:$typeName"
if (typeName) {
try {
String dni = "${device.deviceNetworkId}-ep${num}"
addChildDevice(typeName, dni, device.hub.id,
[completedSetup: true, label: "${device.displayName} ${componentLabel}",
isComponent: true, componentName: "ch${num}", componentLabel: "${componentLabel}"])
// enabledEndpoints << num.toString()
log.debug "Endpoint $num ($desc) added as $componentLabel"
} catch (e) {
log.warn "Failed to add endpoint $num ($desc) as $typeName - $e"
}
} else {
log.debug "Endpoint $num ($desc) ignored"
}
def cmds = cmdClasses.collect { cc -> queryCommandForCC(cc) }.findAll()
if (cmds) {
queryCmds += encapWithDelay(cmds, num) + ["delay 200"]
}
}
}
response(queryCmds)
}
private typeNameForDeviceClass(String deviceClass) {
def typeName = null
switch (deviceClass[0..1]) {
case "10":
case "31":
typeName = "Switch Endpoint"
break
case "11":
typeName = "Dimmer Endpoint"
break
case "08":
//typeName = "Thermostat Endpoint"
//break
case "21":
typeName = "Multi Sensor Endpoint"
break
case "20":
case "A1":
typeName = "Sensor Endpoint"
break
}
return typeName
}
private queryCommandForCC(cc) {
switch (cc) {
case "30":
return zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0xFF).format()
case "71":
return zwave.notificationV3.notificationSupportedGet().format()
case "31":
return zwave.sensorMultilevelV4.sensorMultilevelGet().format()
case "32":
return zwave.meterV1.meterGet().format()
case "8E":
return zwave.multiChannelAssociationV2.multiChannelAssociationGroupingsGet().format()
case "85":
return zwave.associationV2.associationGroupingsGet().format()
default:
return null
}
}
def parse(String description) {
def result = null
if (description.startsWith("Err")) {
result = createEvent(descriptionText:description, isStateChange:true)
} else if (description != "updated") {
def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.debug("'$description' parsed to $result")
return result
}
def uninstalled() {
sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices")
}
def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) {
[ createEvent(descriptionText: "${device.displayName} woke up", isStateChange:true),
response(["delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) ]
}
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
if (cmd.value == 0) {
createEvent(name: "switch", value: "off")
} else if (cmd.value == 255) {
createEvent(name: "switch", value: "on")
} else {
[ createEvent(name: "switch", value: "on"), createEvent(name: "switchLevel", value: cmd.value) ]
}
}
private List loadEndpointInfo() {
if (state.endpointInfo) {
state.endpointInfo
} else if (device.currentValue("epInfo")) {
util.parseJson((device.currentValue("epInfo")))
} else {
[]
}
}
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd) {
updateDataValue("endpoints", cmd.endPoints.toString())
if (!state.endpointInfo) {
state.endpointInfo = loadEndpointInfo()
}
if (state.endpointInfo.size() > cmd.endPoints) {
cmd.endpointInfo
}
state.endpointInfo = [null] * cmd.endPoints
//response(zwave.associationV2.associationGroupingsGet())
[ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""),
response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ]
}
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) {
def result = []
def cmds = []
if(!state.endpointInfo) state.endpointInfo = []
state.endpointInfo[cmd.endPoint - 1] = cmd.format()[6..-1]
if (cmd.endPoint < getDataValue("endpoints").toInteger()) {
cmds = zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: cmd.endPoint + 1).format()
} else {
log.debug "endpointInfo: ${state.endpointInfo.inspect()}"
}
result << createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:"")
if(cmds) result << response(cmds)
result
}
def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) {
state.groups = cmd.supportedGroupings
if (cmd.supportedGroupings > 1) {
[response(zwave.associationGrpInfoV1.associationGroupInfoGet(groupingIdentifier:2, listMode:1))]
}
}
def zwaveEvent(hubitat.zwave.commands.associationgrpinfov1.AssociationGroupInfoReport cmd) {
def cmds = []
/*for (def i = 0; i < cmd.groupCount; i++) {
def prof = cmd.payload[5 + (i * 7)]
def num = cmd.payload[3 + (i * 7)]
if (prof == 0x20 || prof == 0x31 || prof == 0x71) {
updateDataValue("agi$num", String.format("%02X%02X", *(cmd.payload[(7*i+5)..(7*i+6)])))
cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:num, nodeId:zwaveHubNodeId))
}
}*/
for (def i = 2; i <= state.groups; i++) {
cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:i, nodeId:zwaveHubNodeId))
}
cmds
}
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1])
if (encapsulatedCommand) {
def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join()
if (state.enabledEndpoints.find { it == cmd.sourceEndPoint }) {
createEvent(name: "epEvent", value: "$cmd.sourceEndPoint:$formatCmd", isStateChange: true, displayed: false, descriptionText: "(fwd to ep $cmd.sourceEndPoint)")
}
def childDevice = getChildDeviceForEndpoint(cmd.sourceEndPoint)
if (childDevice) {
log.debug "Got $formatCmd for ${childDevice.name}"
childDevice.handleEvent(formatCmd)
}
}
}
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1])
if (encapsulatedCommand) {
state.sec = 1
def result = zwaveEvent(encapsulatedCommand)
result = result.collect {
if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) {
response(cmd.CMD + "00" + it.toString())
} else {
it
}
}
result
}
}
def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) {
def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2]
// def encapsulatedCommand = cmd.encapsulatedCommand(versions)
def version = versions[cmd.commandClass as Integer]
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
if (encapsulatedCommand) {
zwaveEvent(encapsulatedCommand)
}
}
def zwaveEvent(hubitat.zwave.Command cmd) {
createEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true)
}
def on() {
commands([zwave.basicV1.basicSet(value: 0xFF), zwave.basicV1.basicGet()])
}
def off() {
commands([zwave.basicV1.basicSet(value: 0x00), zwave.basicV1.basicGet()])
}
def refresh() {
command(zwave.basicV1.basicGet())
}
def setLevel(value) {
commands([zwave.basicV1.basicSet(value: value as Integer), zwave.basicV1.basicGet()], 4000)
}
def configure() {
commands([
zwave.multiChannelV3.multiChannelEndPointGet()
], 800)
}
// epCmd is part of the deprecated Zw Multichannel capability
def epCmd(Integer ep, String cmds) {
def result
if (cmds) {
def header = state.sec ? "988100600D00" : "600D00"
result = cmds.split(",").collect { cmd -> (cmd.startsWith("delay")) ? cmd : String.format("%s%02X%s", header, ep, cmd) }
}
result
}
// enableEpEvents is part of the deprecated Zw Multichannel capability
def enableEpEvents(enabledEndpoints) {
state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger()
null
}
// sendCommand is called by endpoint child device handlers
def sendCommand(endpointDevice, commands) {
def result
if (commands instanceof String) {
commands = commands.split(',') as List
}
def endpoint = deviceEndpointNumber(endpointDevice)
if (endpoint) {
log.debug "${endpointDevice.deviceNetworkId} cmd: ${commands}"
result = commands.collect { cmd ->
if (cmd.startsWith("delay")) {
new hubitat.device.HubAction(cmd)
} else {
new hubitat.device.HubAction(encap(cmd, endpoint))
}
}
sendHubCommand(result, 0)
}
}
private deviceEndpointNumber(device) {
String dni = device.deviceNetworkId
if (dni.size() >= 5 && dni[2..3] == "ep") {
// Old format: 01ep2
return device.deviceNetworkId[4..-1].toInteger()
} else if (dni.size() >= 6 && dni[2..4] == "-ep") {
// New format: 01-ep2
return device.deviceNetworkId[5..-1].toInteger()
} else {
log.warn "deviceEndpointNumber() expected 'XX-epN' format for dni of $device"
}
}
private getChildDeviceForEndpoint(Integer endpoint) {
def children = childDevices
if (children && endpoint) {
return children.find{ it.deviceNetworkId.endsWith("ep$endpoint") }
}
}
private command(hubitat.zwave.Command cmd) {
if (state.sec) {
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
} else {
cmd.format()
}
}
private commands(commands, delay=200) {
delayBetween(commands.collect{ command(it) }, delay)
}
private encap(cmd, endpoint) {
if (endpoint) {
if (cmd instanceof hubitat.zwave.Command) {
command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd))
} else {
// If command is already formatted, we can't use the multiChannelCmdEncap class
def header = state.sec ? "988100600D00" : "600D00"
String.format("%s%02X%s", header, endpoint, cmd)
}
} else {
command(cmd)
}
}
private encapWithDelay(commands, endpoint, delay=200) {
delayBetween(commands.collect{ encap(it, endpoint) }, delay)
}
When I find replace physical graph out of the code, I receive the following message upon trying to save.
No signature of method: Script1.childDeviceTiles() is applicable for argument types: (java.lang.String) values: [endpoints]
The ST Composite Device Handler (aka Paraent/Child Device Handler) feature does not yet exist on Hubitat. I faced the same issue with my HubDuino (ST_Anything) code.
So, I broke the Parent Device Handler code into what I call a HubDuino Bridge Driver + a HubDuino Service Manager App. The Srv Mgr app is repsonsible for creating the Child Devices and keeping them up to date. I was able to move most of my code from the original Parent DTH to my Service Manager App without too much trouble,
OK, I found a couple of issues with that driver.
the MultiInstanceReport command on line 873 is implemented in the multiinstancev1 class, not multichannelv3
same goes for MultiInstanceCmdEncap on line 909, itās implemented in multiinstancev1
This isnāt some bizarre hubitat implementation, this is per Z-Wave command class spec.
Hiā¦ with the more recent updates I am able to port over an app that will take this driver and create 10 switchesā¦ however, I am still stuck on the driver itself.
So, I managed to get the device up and running by commenting out the lines with the error message.
Two things:
When running the multi-channel app on the device, the app seems to complete by letting me select āDoneā but no new devices are created whereas on ST 10 devices were created;
The only buttons that appear to work are on/off and pool/spa.
I will post screenshots when I am back on the LAN.
So got the driver installed. It now shows a mish-mosh of information that would need to be broken apart into child devices by way of the ST Multi-Channel Appā¦ also installed (but I donāt think working correctly).
When I go to install the ST Multi-Channel App, I run through the template and all appears to āinstallā however no child devices are created.
This is the log:
2018-03-11 16:04:02.325:errorDevice type 'vTile_ms' in namespace 'ms_w_vts' not found on line 169
[app:269](http://192.168.7.201/logs#app269)2018-03-11 16:04:02.310:debugLabel Filter Pump
[app:269](http://192.168.7.201/logs#app269)2018-03-11 16:04:00.636:errorDevice type 'vTile_ms' in namespace 'ms_w_vts' not found on line 169
[app:269](http://192.168.7.201/logs#app269)2018-03-11 16:04:00.618:debugLabel Filter Pump
I believe this causes the other errors when I try to use the device.
For instance, if I select the button for VSP1, nothing happens and I receive the following log:
2018-03-11 16:05:29.900:warnNo signature of method: java.util.LinkedHashMap.putAll() is applicable for argument types: (java.util.LinkedHashMap$Entry) values: [1=No] Possible solutions: putAll(java.util.Map), putAll(java.util.Map), putAll(java.util.Collection), putAt(java.lang.Object, java.lang.Object), putAt(java.lang.String, java.lang.Object), findAll()
[dev:257](http://192.168.7.201/logs#dev257)2018-03-11 16:05:29.082:debug+++++ getVSPSpeed()
[dev:257](http://192.168.7.201/logs#dev257)2018-03-11 16:05:29.060:debug+++++ setVSPSpeedAndGet() speed=1
[dev:257](http://192.168.7.201/logs#dev257)2018-03-11 16:05:20.186:warnNo signature of method: java.util.LinkedHashMap.putAll() is applicable for argument types: (java.util.LinkedHashMap$Entry) values: [1=No] Possible solutions: putAll(java.util.Map), putAll(java.util.Map), putAll(java.util.Collection), putAt(java.lang.Object, java.lang.Object), putAt(java.lang.String, java.lang.Object), findAll()
The device, DOES however appear to work in very limited capacity as On/Off works (turns on/off the pool light) and spa/pool will actually switch the actuator.
I simply think the error within the ST Multi Channel app is holding me back.
This error
vTile_msā in namespace āms_w_vtsā not foundā¦
Is telling you it canāt create the device.
In order for an app to create a child device, the specified driver (noted above) must exist in the drivers code page, or within the drivers provided on the hub.
So, unlike ST, I must first create the driver code for the device (ie. VSP1)?
Lets assume VSP1 is nothing more than a virtual on/off switch.
What sort of naming convention would I use so that my app knows/understands that my virtual switch corresponds because as far as I can tell the app should create 10 on/off switches.__
So I am really close on this one and would appreciate help.
I have installed a working driver for the Intermatic PE653 and the app ST Multi Channel
However when I run the ST Multichannel app I receive an error ****
vTile_msā in namespace āms_w_vtsā not foundā¦
This error means the App will not create the device because no driver for the device exists.
The driver, in this case would be an on/off switch.
I can use your github and see you have a child switch device type for hubitat but my question is how do I tell my ST Multichannel app to associate the switch child device?
It would see from reading what you have done, you created the virtual device (ie. virtual switch) and your app ?somehow? connected to that child device.
My HubDuino/ST_Anything Child Devices are specifically written to work with my Service Manager App, but they are a good starting point.
One thing that was not obvious to me originally, is that you will need to change the following line in the Child Device Driver so the Parent App can successfully find the Child Driver code.
In the above line, the ānamespaceā field will need to be changed to the same ānamespaceā of your Parent Appās definition. Otherwise, if the two do not match, the Parent wonāt be able to create the Child Device.
This namespace is also referenced when Adding the Child Device to the Parent App. Make sure they match. Read through my HubDuino Service Manager App to see how everything is done. It is the Glue that ties everything together.
So, the Parent needs a reference to the physical device, as it will perform all of the communications with the real-world physical device.
The Parent creates virtual Child Devices and keeps them up to date with any physical device updates.
The Child Devices implement the correct capabilities to allow other Apps to subscribe to their events AND to send commands if appropriate. If a command is received from an App, the Child needs to make a call into the Parent App, who will then call the Physical Deviceās appropriate function to communicate with the real-world device.
Hope this helps.
Note: Some of my code is a little unique, especially in how my Parent App updates its children through a custom command. This was done to allow the Child device code an opportunity to tweak the incoming data per user defined preferences (e.g. converting temperatures from F to C.)