Ikea Symfonisk remote! On French Ikea. Zigbee!

A zigbee Sonos (and hopefully other stuff?) Remote

Have we seen this yet? I don't speak French, but apparently this thing is zigbee and is designed to control Sonos.

Some days I hate that I sold all my Sonos stuff to go with Chromecast audios on much better amps/speakers... But that's the route I went. There's no question the Sonos stuff was a better user experience though.
I'm still holding out hope that Google will continue to improve Google cast for us.

In the meantime... Looks like we can expect to see these before too long. I know I would have LOVED them when I had my Sonos stuff, and I'd love them now.

What do we think the odds are we will be able to get a driver for these to control our Google cast stuff? :thinking:

1 Like
1 Like

I've purchased a few of these remotes and have placed one in the shower where i can't have alexa/google home for voice control of sonos - it's awesome! I'm using their hub at present.

1 Like

I just bought 2 of the bookshelf speakers at the Norfolk, VA store. These replaced Google Minis in order to go local for TTS. There were remotes available at $15, but I really couldn't think of a use for them. Good thing, too, as I don't want a gateway.

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