Ikea Symfonisk remote! On French Ikea. Zigbee!

Managed to pair the sonos remote to HE. Looking at the parse logs it definitely supports 1, 2, and 3 button presses, as well as moving the jog wheel seems to give cluster 8 messages, but it looks like it spams the messages a bit. Not sure if @mike.maxwell would be willing to code up a driver for it, but they're certainly a nice cheap button dimmer.

I did manage to touchlink it to some hue/ikea bulbs as well.

Found Devices:
Device
Label:
Please Enter a Device Label
ID: 1FEF
Manufacturer: IKEA of Sweden
Product Name:
Model Number: SYMFONISK Sound Controller
deviceTypeId: 125
manufacturer : IKEA of Sweden
idAsInt : 1
inClusters : 0000,0001,0003,0020,1000
endpointId : 01
profileId : 0104
application : 21
outClusters : 0003,0004,0006,0008,0019,1000
initialized : true
model : SYMFONISK Sound Controller
stage : 4

1 Like

looks promising from the fingerprint, I'll see if I can order one...

1 Like

Yeah that's what I thought. Because it's a jog wheel for the dimmer it might be a bit of pain to filter rubbish messages for dimming, but if anyone can get it working... it be you buddy. :grin:

The key to pairing is to press the pair button under the back cover 4 times quickly, it will then detect.

I also noticed that it is apart of the Zigbee 3.0 range of devices that the tradfri are coming out with now, so more of their stuff will likely be compatible in the future.

2 Likes

I got a remote paired to HE easily (four quick clicks on the pair button and voila!) without the Ikea hub in the house. Is there anything I can do with it yet? Device driver just says device.

endpointId: 01
application: 21
softwareBuild: 2.1.022
inClusters: 0000,0001,0003,0020,1000
outClusters: 0003,0004,0006,0008,0019,1000
model: SYMFONISK Sound Controller
manufacturer: IKEA of Sweden

Mike is going to get one and see if a driver is feasible for it. Haven't found a driver that works currently. You can touchlink it to Hue bulbs to control directly but that doesn't add anything of use to the hub.

OK, this is why I freaking hate Ikea devices...
Writing a driver for this thing should have been a one hour, maybe two hour job tops.

so here we go...
Join, nice!
Broadcast messaging disabled when clusters are bound to?, yup, nice!
Payload decoding?, simple and done...
Duplicate frames sent for every command 15ms apart?, WTH...

OK, I get wanting to make sure the frame gets to the hub over the customers potentially crap Zigbee mesh, but that's why there's such a thing in Zigbee world as a command acknowledgment request, but no we won't request one of those because then we would have to wait for the response and send the command after some timeout if we don't get a response from the hub, and that sounds like way too much work, so lets just send the exact same freaking frame twice...

It takes HE about 25ms to commit an event to the db, so the net effect of this issue is this thing would always fire off two events for everything, which could wreck all sorts of havoc...

So back in the box and on the shelf it goes...

2 Likes

Have to say Mike, I knew exactly what was going to happen when I saw the 4 duplicate messages in the log lol. Thanks a bunch for taking a look though, I assumed that would be too much of a pain to code around when I saw that. IKEA really have made some odd choices with their zigbee stuff.

They do work ok as a spammy touchlink control directly to a bulb though, so I'll use em as dirty quick dimmer for side lamps.

Thanks mate.

2 Likes

That's just it, it can't be coded around currently, we would have to intentionally slow down fetching frames from our zigbee receive queue by around 30ms so these could be detected before being sent to the driver, and we're not about to do that...

Don't blame ya lol. The coding on the tradfri gateway must be a right royal mess.

I suspect they don't care, just send it to the device twice, for the same lame reason the controller sends it twice...

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}"]
}
6 Likes

Nice work. Is that info something that might help @mike.maxwell with other troublesome IKEA devices?

The other problematic Ikea devices don't produce double reports, so this won't help with them.
Based on what @cybrmage has discovered I'll give this device another shot.
This is completly aberrant behavior by the way...

2 Likes

Maybe Sonos helped with this one. I bought one to play with and it’s much smoother than the puck dimmer at controlling bulbs when you pair it via touch link.

Yes, yes it is...

Wait!!! Are we talking about me or the device???

8-}

Doubtful, what does sonos know of zigbee?

1 Like

Aparently that's not the criteria to produce Zigbee products for IKEA. :wink:

This works very well for Hue lights. Extremely smooth dimming if you use buttons 4 & 5 for start raising/lowering and 6 for stop changing. Have not tried with Sonos yet, but with this DTH and your changes, it is one of my new favourite dimmers. Nothing like the experience with the IKEA puck dimmer. @bertabcd1234, I wonder if this same method would work with the puck?

Thanks for this. I did find that it was necessary to be very patient for the pairing to complete, and then I had to pair it twice to get the battery level to appear, and a few moments later for the pushed state to appear. I tested the pairing on two different hubs, with the latest and a previous platform build, and it acted the same on each.

I was planning to return this, but now I really like it. I don't have a Lutron Aurora dimmer, but I have just ordered one to compare.

Thanks again. Much appreciated!

How have you set up the rules to control your lights using the controller?

Button controller. I’m still using an older platform version for my primary hub, so this example is button controller 3, but the principal is the same for RM4