Skip to content
114 changes: 114 additions & 0 deletions Onkyo-homeassistant-flareconnect.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
Onkyo audio groups in home assistant
====================================

This is a crude guide on how to set up Onkyo flareconnect (aka multiroom audio / audio groups) with home assistant. For this we will replace the python-eiscp (pyeiscp) module used by the Onkyo integration in home assistant with a modified version. You can find the details on this modded version here: https://github.com/rlKoekie/python-eiscp/tree/onkyo-groups . The goal here is to create two simple buttons on your home assistant dashboard for easily starting and stopping the Onkyo audio group setup.

My own instance is a Home Assistant OS, running under libvirt on linux (virsh). Things might be slightly different depending on your own setup (e.g. docker). In these cases you will need to make sure you connect to the right docker container command line.
You will of course need two (or more) Onkyo devices with multiroom audio support (flareconnect), you can use the “Onkyo Controller” app on android ( https://play.google.com/store/apps/details?id=com.onkyo.jp.onkyocontroller ) to check and test this, or you can just YOLO it. The app has this feature under “edit group”.

Preparations
------------

Home Assistant installed addons:
- Advanced SSH & Web Terminal (protection mode is disabled, might not be needed)
- File editor

I am assuming that you already have your onkyo hardware configured under the home assistant “devices” under settings.

To make your own life a lot easier, you should configure your onkyo devices to have a fixed IP address. This is done in your router settings, usually in the DHCP configuration part.

Installation
------------

First we will install the modified python-eiscp module and then try the commands in the terminal, that makes it easier to debug things. After this we will define aliasses for the commands in the configuration yaml file, and create two buttons on the dashboard. Try to copy-paste the commands, it is very easy to make a mistake in the order of single quotes, brackets, double quotes and slashes. Stuff will not work if you mistype them.

Open up your HA terminal. This should show you a bash shell with a bunch of text and a $ sign. Welcome to Linux! If this is all new for you, you might want to read up a bit first about command lines, or you can just hope I included all needed steps in this writeup :-) Home Assistant is quite cleverly set up, with the actual homeassistant running in a virtual (docker) environment. So the shell we are in now, is not the same place as where things are executing when we use home assistant the normal way. So we first need to get into the docker environment. Run the following command to get in:

::

docker exec -it homeassistant /bin/bash

This will change your shell to something like: “homeassistant:/config#”, indicating you are now inside the “homeassistant” docker container, in folder “/config”, as “root” user (the # symbol). Now install the modified python-eiscp package with the following command:

::

pip install -I git+https://github.com/rlKoekie/python-eiscp.git@onkyo-groups

This should print some interesting text, and at the end there should be a line reading “Successfully installed netifaces-0.11.0 pyeiscp-0.0.7”. Feel free to ignore any warnings about running pip as ‘root’.

**This "pip install" step will most likely have to be repeated when you update home assistant! I hope that the Onkyo plugin for home assistant itself will get the changes I made to python-eiscp integrated at some point, that will remove the need for these command line moves.**

Now we are ready to test! Let’s see if we can talk to our Onkyo devices. Run the following command, after changing the IP address (the 10.0.0.100 part) to the address of your own onkyo hardware (you wrote that down right?)

::

eiscp_sender --host 10.0.0.100 'multiroom-status query'

This spits back some info about your Onkyo device. Write down the <deviceid>0123456DDEEFF</deviceid> somewhere (you can select it in the terminal, and then copy it with your right mouse button), repeat for your other Onkyo device(s).

Now we create the grouping command. It should look something like this:

::

/usr/local/bin/eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>1</groupid><maxdelay>500</maxdelay><devices><device id="012356AABBCC" zoneid="1"/><device id="0123456DDEEFF" zoneid="1"/></devices></mgs>'

Make sure to:

- Change the 10.0.0.100 IP address into the IP address of your main Onkyo device (the one sharing its audio to the secondary devices).
- Change 0123456DDEEFF into the mac address of the main (sending) onkyo device
- Change 012356AABBCC into the mac address of the secondary (receiving) onkyo device. (you can also setup multiroom audio with more than two devices, just repeat the <device id="012356AABBCC" zoneid="1"/> block with the mac address for another secondary onkyo device.

If you run the moddified command on the homeassistant docker shell, it should change your onkyo devices to run in flareconnect mode. Congratulations!

You can stop the audio grouping by telling the main device to switch to an empty audio group. The command should look something like this:

::

/usr/local/bin//eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>0</groupid></mgs>'

Make sure to change the IP address to your own main (sending) device IP!

If everything works: nice!

- Copy the full commands you executed to start and stop the audio grouping to a text file.
- Shut down the docker terminal by typing “exit” and hitting enter
- Shut down the home assistant terminal by again typing “exit” and hitting enter


Now use the file editor to modify your /homeassistant/configuration.yaml
Add the following section:

::

shell_command:
stop_flareconnect: /usr/local/bin//eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>0</groupid></mgs>'
start_flareconnect: /usr/local/bin/eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>1</groupid><maxdelay>500</maxdelay><devices><device id="012356AABBCC" zoneid="1"/><device id="0123456DDEEFF" zoneid="1"/></devices></mgs>'

Of course make sure to change the start and stop commands to the ones you succesfully used in the terminal app!

After these changes to configuration.yaml, restart homeassistant (settings > system > top right menu > reboot) and wait for HA to return.

Now we are going to create the buttons on your home assistant dashboard:

- Go to settings > devices and services > select the “helpers” tab at the top
- Hit the “create helper” button on the bottom right, select “button”
- Give the button a name (e.g. “start flareconnect button”, and find a nice icon (e.g. mdi:speaker-multiple )
- Repeat to create a second button, now for “stop flareconnect button”
- Go to your home assistant dashboard, hit the “edit” button on the top right
- Select “by entities” at the top, and search for the name of your button (e.g. “flareconnect”). Select both, and proceed. Click on “add to dashboard”.

Now edit each button with the following:

- Appearance> Give it a name, tick the “Name” box, disable the “status” box.
- Interactions> behaviour: run action (my translation might be off, my instance is in Dutch).
- Action: Shell command: start_flareconnect (or stop_flareconnect for the stop button).
- Hit “Save”.

Select the “done editing” button on your dashboard (top right), and test your new buttons.

Problems?
---------

So your new stuff is not working? The first step is to go back to the docker command line, and try your commands again. If these work, then make sure to check for typos in the configuration.yaml file.
Now go check the home assistant log files: Settings > system > logs > 3-dot menu > Show full log. Maybe there are some hints in there.

24 changes: 24 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ your receiver and its configured port:

eiscp_monitor --host 10.0.0.100 --port 60128

A secondary command line tool allows for easy sending of commands to
the receiver. For example, to query the multiroom audio status ("groups")
on an Onkyo receiver:

::

eiscp_sender --host 10.0.0.100 'multiroom-status query'

The eiscp_sender command can also be used to enable/disable multiroom audio
aka (flareconnect). For this you will need the mac address of the secondary
device, you can obtain this from the <deviceid> field returned from the
'multiroom-status query' command. To start a multiroom audio group with
10.0.0.100 (0123456DDEEFF) as the host, and 012356AABBCC as secondary device:

::

eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>1</groupid><maxdelay>500</maxdelay><devices><device id="012356AABBCC" zoneid="1"/><device id="0123456DDEEFF" zoneid="1"/></devices></mgs>'

To stop this grouping:

::

eiscp_sender --host 10.0.0.100 'raw-message=<mgs zone="1"><groupid>0</groupid></mgs>'


Credits
-------
Expand Down
20 changes: 19 additions & 1 deletion pyeiscp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# at 2019-06-16 18:16:40.906934

from collections import OrderedDict
from .utils import ValueRange
from .utils import ValueRange, RawMessage

COMMANDS = OrderedDict([('main', OrderedDict([('PWR', {'values': OrderedDict([('00', {'name': ('standby',
'off'),
Expand All @@ -17,6 +17,20 @@
'description': 'gets the System Power Status'})]),
'name': 'system-power',
'description': 'System Power Command'}),
('MDI', {
'values': OrderedDict([
('QSTN', {'name': 'query', 'description': 'query the multiroom zones status'}),
]),
'name': 'multiroom-status',
'description': 'Get details on the multiroom zones this device is a part of'
}),
('MGS', {
'values' : OrderedDict([
('<xml>', {'name': '<xml>', 'description': 'some xml formatted message'})
]),
'name': 'raw-message',
'description': 'Send a pre-formatted message to the receiver, Onkyo devices use this for setting up multiroom audio (flareconnect)'
}),
('AMT', {'values': OrderedDict([('00', {'name': 'off',
'description': 'sets Audio Muting Off'}),
('01', {'name': 'on', 'description': 'sets Audio Muting On'}),
Expand Down Expand Up @@ -2343,6 +2357,8 @@
'source': 'SLI',
'battery-charge-status': 'BCS',
'system-power': 'PWR',
'multiroom-status': 'MDI',
'raw-message': 'MGS',
'dialog-control-enabled': 'DCE',
'current-folder-status-no': 'CFS',
'xm-category': 'XCT',
Expand Down Expand Up @@ -3428,6 +3444,8 @@
'b-xx': 'B{xx}',
'query': 'QSTN',
't-xx': 'T{xx}'},
'MDI': {'query': 'QSTN'},
'MGS': {RawMessage('<xml>'): '<xml>'},
'PNR': {'on': '01', 'toggle': 'TG', 'off': '00', 'query': 'QSTN'},
'MCC': {'query': 'QSTN', '00': '00', '01': '01'},
'ARC': {'auto': '01', 'off': '00', 'up': 'UP', 'query': 'QSTN'},
Expand Down
17 changes: 13 additions & 4 deletions pyeiscp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import struct
import re
from pyeiscp import commands
from pyeiscp.utils import ValueRange
from pyeiscp.utils import ValueRange, RawMessage
from collections import namedtuple

__all__ = "AVR"
Expand Down Expand Up @@ -177,15 +177,15 @@ def command_to_iscp(command, arguments=None, zone=None):
if arguments is None and zone is None:
# Separating command and args with colon allows multiple args
if ":" in command or "=" in command:
base, arguments = re.split(r"[:=]", command, 1)
base, fullarguments = re.split(r"[:=]", command, 1)
parts = [norm(c) for c in re.split(command_sep, base)]
if len(parts) == 2:
zone, command = parts
else:
zone = default_zone
command = parts[0]
# Split arguments by comma or space
arguments = [norm(a) for a in re.split(r"[ ,]", arguments)]
arguments = [norm(a) for a in re.split(r"[ ,]", fullarguments)]
else:
# Split command part by space or dot
parts = [norm(c) for c in re.split(command_sep, command)]
Expand Down Expand Up @@ -229,6 +229,14 @@ def command_to_iscp(command, arguments=None, zone=None):
# We need to send the format "FF", hex() gives us 0xff
value = hex(int(argument))[2:].zfill(2).upper()
break
else:
if isinstance(possible_arg, RawMessage):
# This is a raw string message that we should just pass as-is to the receiver
if fullarguments:
value = fullarguments
else:
value = argument
break

# TODO: patterns not yet supported
else:
Expand Down Expand Up @@ -338,7 +346,8 @@ def command(self, command, arguments=None, zone=None):
self.log.error(f"Invalid message. {error}")
return

self.log.debug("> %s", command)
self.log.debug("> command: %s", command)
self.log.debug("> iscp_message: %s", iscp_message)
try:
self.transport.write(command_to_packet(iscp_message))
except:
Expand Down
22 changes: 17 additions & 5 deletions pyeiscp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import argparse
import asyncio
import logging
import time

import pyeiscp

__all__ = ("console", "monitor")


async def console(loop, log):
async def console(loop, log, timeout=False):
"""Connect to receiver and show events as they occur.

Pulls the following arguments from the command line (not method arguments):
Expand Down Expand Up @@ -54,11 +55,22 @@ def connect_callback(host):

for message in args.messages:
conn.send(message)

if timeout:
await asyncio.sleep(timeout)
loop.stop()

def monitor():
"""Wrapper to call console with a loop."""
log = logging.getLogger(__name__)
loop = asyncio.get_event_loop()
asyncio.ensure_future(console(loop, log))
loop.run_forever()
async def main():
loop = asyncio.get_running_loop()
await console(loop, log)
asyncio.run(main())

def sender():
"""Wrapper to call console with a loop that stops after 2 seconds"""
log = logging.getLogger(__name__)
async def main():
loop = asyncio.get_running_loop()
await console(loop, log, timeout=1)
asyncio.run(main())
11 changes: 11 additions & 0 deletions pyeiscp/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import string
class ValueRange(object):
"""Some command values are defined as a range of possible
values, such as from 1 to 100. We use a custom type to represen
Expand All @@ -14,3 +15,13 @@ def __init__(self, start, end):

def __contains__(self, value):
return value in self._range

class RawMessage(object):
"""Some command values are plain strings that get send as-is.
For now we use this for the MSG message type, used on Onkyo
receivers for setting up multi-room audio (flareconnect).
"""
def __init__(self, content):
self.content = content
# currently not used, but we might want to check messages for having valid chars
self.allowedchars = string.printable
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ def readme():
"netifaces",
],
zip_safe=True,
entry_points={"console_scripts": ["eiscp_monitor = pyeiscp.tools:monitor",]},
entry_points={"console_scripts": ["eiscp_monitor = pyeiscp.tools:monitor","eiscp_sender = pyeiscp.tools:sender",]},
)