I have found a "work around" for the issue...
Once the device is paired to the hub, it does not report events to the hub until the hub binds to it. If a driver binds to the zigbee.ON_OFF_CLUSTER or zigbee.LEVEL_CONTROL_CLUSTER, the device will send two reports for every press/twist of the device... BUT If the driver binds to the zigbee.IAS_ZONE_CLUSTER, the device then sends single reports for the events.
I have created a driver, based off an ST DTH, that exposes the device as a 6 button device...
The controller does not support "hold" events, but does support:
- Single Tap - mapped to Button 1 pushed
- Double Tap - mapped to Button 2 pushed
- Triple Tap - mapped to Button 3 pushed
- rotate clockwise - mapped to Button 4 pushed
- rotate counter-clockwise - mapped to Button 5 pushed
- rotate stopped - mapped to button 6 pushed
Additionally, the rotate CW/CCW are also mapped to level events, where the value is a proportional representation of the rotation from -100(CCW) to 100(CW) - I do not know if this will be useful, it is a holdover from the original ST DTH.
/**
* Copyright 2019 Juha Tanskanen
*
* 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.
*
* Sonos Speaker Control
*
* Version Author Note
* 0.9 Juha Tanskanen Initial release
* 0.9HE CybrMage Hubitat version Initial release
*
*/
import hubitat.zigbee.zcl.DataType
metadata {
definition (name: "SYMFONISK Sound Controller", namespace: "cybr", author: "Juha Tanskanen / CybrMage") {
capability "Actuator"
capability "Battery"
capability "PushableButton"
capability "Configuration"
fingerprint inClusters: "0000, 0001, 0003, 0020, 1000", outClusters: "0003, 0004, 0006, 0008, 0019, 1000", manufacturer: "IKEA of Sweden", model: "SYMFONISK Sound Controller"
}
preferences {
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
}
}
private getVERSION() { "v0.9HE" }
private getCLUSTER_GROUPS() { 0x0004 }
private getIkeaSoundControlNames() {
[
"singleTap", // "single tap Control button",
"doubleTap", // "double tap Control button"
"tripleTap", // "triple tap Control button"
"levelUp", // "increase volume knob"
"levelDown", // "decrease volume button"
"levelStop", // "increase volume button"
]
}
private getDeviceInfoAttributeName() {
[
"ZCLVersion",
"ApplicationVersion",
"StackVersion",
"HWVersion",
"ManufacturerName",
"ModelIdentifier",
"DateCode",
"PowerSource",
]
}
def INFO(String msg) { if (logEnable) log.info(msg)}
def DEBUG(String msg) { if (logEnable) log.debug(msg)}
//def getZCLDataValue(String Encoding, String Value) {
// switch(Encoding) {
// case 0x08:
// case 0x09:
// case 0x0a:
// case 0x0b:
// case 0x0c:
// case 0x0d:
// case 0x0e:
// case 0x0f:
// case 0x20:
// return Value as Integer
// break
// case 0x10:
// return (Value as Integer)? true : false
// break
// case 0x42:
// return new String(hubitat.helper.HexUtils.hexStringToByteArray(Value.substring(2)))
// break
// }
//}
// parse events into attributes
def parse(String description) {
DEBUG("Parsing message from device: '$description'")
def event = ""
try {
event = zigbee.getEvent(description)
} catch(e) {}
if (event) {
DEBUG(" Creating event: ${event}")
sendEvent(event)
} else {
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
//DEBUG(" Catch all: $description")
def descMap = zigbee.parseDescriptionAsMap(description)
if (descMap.clusterInt == 0 && descMap.command == "01") {
// DEBUG(" Received Basic Device Info Response - ${DeviceInfoAttributeName[(descMap.attrId as Integer)]} = ${getZCLDataValue(descMap.encoding, descMap.value)}")
DEBUG(" Received Basic Device Info Response")
} else if (descMap.clusterInt == 1 && descMap.command == "07") {
DEBUG(" Received Configure Reporting Response")
} else if (descMap.clusterInt == zigbee.IDENTIFY_CLUSTER && descMap.command == "01") {
DEBUG(" Received IDENTIFY report")
} else if (descMap.clusterInt == 6 && descMap.command == "00") {
DEBUG(" Received Descriptor Match Request")
} else if (descMap.clusterInt == 0x13 && descMap.command == "00" && descMap.profileId == "0000") {
def data = descMap.data
def id16 = "${data[2]}${data[1]}"
def id64 = "${data[10]}${data[9]}${data[8]}${data[7]}${data[6]}${data[5]}${data[4]}${data[3]}"
def caps = "${data[11]}"
DEBUG(" Received DEVICE DISCOVERY BROADCAST: ${id16} ${id64} ${caps}")
} else if (descMap.clusterInt == 0x8005) {
DEBUG(" Received Active endpoints response")
} else if (descMap.clusterInt == 0x8021) {
DEBUG(" Received BIND RESPONSE")
} else if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrId == "0021") {
DEBUG(" Processing Battery Event")
event = getBatteryEvent(zigbee.convertHexToInt(descMap.value))
} else if (descMap.clusterInt == CLUSTER_SCENES || descMap.clusterInt == zigbee.ON_OFF_CLUSTER || descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
DEBUG(" Received button or volume Event")
event = getButtonEvent(descMap)
} else {
DEBUG(" UNHANDLED REPORT: \ndescription: ${description}\ndescMap: ${descMap}")
}
}
def result = []
if (event) {
DEBUG(" Creating event: ${event}")
result = createEvent(event)
}
return result
}
}
def installed() {
log.debug "installed() called - Driver ${VERSION}"
sendEvent(name: "numberOfButtons", value: 6, isStateChange: true)
state.start = now()
}
def updated() {
log.debug "Updated() called - Driver ${VERSION}"
sendEvent(name: "numberOfButtons", value: 6, isStateChange: true)
}
def configure() {
log.debug "Configuring device ${device.getDataValue("model")} - Driver ${VERSION}"
sendEvent(name: "numberOfButtons", value: 6, isStateChange: true)
state.lastButtonEvent = 0
def cmds = zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, DataType.UINT8, 30, 21600, 0x01) +
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21) +
["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x500 {${device.zigbeeId}} {}"] +
// ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 6 {${device.zigbeeId}} {}"] +
// ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 8 {${device.zigbeeId}} {}"] +
// zigbee.enrollResponse() +
readDeviceBindingTable() // Need to read the binding table to see what group it's using
cmds
}
private Map getBatteryEvent(value) {
def result = [:]
result.value = value
result.name = 'battery'
result.descriptionText = "${device.displayName} battery was ${result.value}%"
return result
}
private Map getButtonEvent(Map descMap) {
def buttonState = ""
def buttonNumber = 0
Map result = [:]
DEBUG " Processing Button Event: Cluster = ${descMap.clusterInt} command: ${descMap.command}"
if (descMap.clusterInt == zigbee.ON_OFF_CLUSTER) {
if (descMap.command as Integer == 0x02) {
buttonState = "pushed"
buttonNumber = 1
}
} else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
switch (descMap.command as Integer) {
case 0x02:
if (descMap.data[0] == "00") {
buttonState = "pushed"
buttonNumber = 2
} else {
buttonState = "pushed"
buttonNumber = 3
}
//buttonNumber = REMOTE_BUTTONS.CONTROL_BUTTON
break;
case 0x01:
if (descMap.data[0] == "00") {
buttonState = "levelUp"
buttonNumber = 4
} else {
buttonState = "levelDown"
buttonNumber = 5
}
break;
case 0x03:
buttonState = "levelStop"
buttonNumber = 6
break;
default:
break;
}
}
if (buttonNumber != 0) {
DEBUG(" Button - STATE: ${buttonState} NUMBER: ${buttonNumber}")
// Create and send component event
sendButtonEvent(buttonNumber, buttonState)
sendLevelEvent(buttonNumber, buttonState)
}
result
}
private sendButtonEvent(buttonNumber, buttonState) {
if (buttonNumber > 0) {
def descriptionText = "$device.displayName button ${buttonNumber} (${getButtonName(buttonNumber)}) was pushed" // TODO: Verify if this is needed, and if capability template already has it handled
sendEvent(name: "pushed", value: buttonNumber, descriptionText: descriptionText, isStateChange: true)
state.lastButtonEvent = now()
INFO(" sendButtonEvent: sendEvent(name: \"pushed\", value: ${buttonNumber}, descriptionText: \"${descriptionText}\", isStateChange: true)")
}
}
private sendLevelEvent(buttonNumber, buttonState) {
if (buttonNumber > 3) {
switch (buttonState) {
case "levelUp":
state.start = now()
state.direction = 1
break
case "levelDown":
state.start = now()
state.direction = 0
break
case "levelStop":
long iTime = now() - state.start
def iChange = 0
// Ignore turns over 5 seconds, probably a lag issue
if (iTime > 5000) {
iTime = 0
}
// Change based on 5 seconds for full 0-100 change in brightness
iChange = iTime/5000 * 100
def volumeChange = state.direction ? ((BigInteger)iChange).intValue() : ((BigInteger)(0 - iChange)).intValue()
def descriptionText = "Switch Level was changed $volumeChange"
sendEvent(name: "level", value: volumeChange, descriptionText: descriptionText, isStateChange: true)
INFO(" sendLevelEvent: sendEvent(name: \"level\", value: \"${volumeChange}\", descriptionText: \"${descriptionText}\", isStateChange: true)")
break
}
}
}
private getButtonLabel(buttonNum) {
def label = ikeaSoundControlNames[buttonNum - 1]
return label
}
private getButtonName(buttonNum) {
// return "${device.displayName} " + getButtonLabel(buttonNum)
return getButtonLabel(buttonNum)
}
private List addHubToGroup(Integer groupAddr) {
["he cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"]
}
private List readDeviceBindingTable() {
log.info "readDeviceBindingTable called..."
["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] +
["zdo active 0x${device.deviceNetworkId}"]
}