Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group_with function for multiroom audio / flareconnect #159

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions eiscp/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ def filter_for_message(getter_func, msg):
# It seems ISCP commands are always three characters.
if candidate and candidate[:3] == msg[:3]:
return candidate
elif candidate and candidate[:3] == 'MDI' and msg[:3] == 'MGS':
# the MGS command for grouping multiroom audio, returns an MDI message, not MGS
return candidate

# exception for HDMI-CEC commands (CTV) since they don't provide any response/confirmation
if "CTV" in msg[:3]:
Expand Down Expand Up @@ -564,6 +567,92 @@ def power_off(self):
"""Turn the receiver power off."""
return self.command('power', 'off')

def group_with(self, otherIDs=[]):
"""Create a multiroom audio / flareconnect group with the supplied device IDs.
Calling this without arguments or an empty list stops the multiroom audio / flareconnect group.
Calling this method twice with the same arguments does not generate a response from the receiver, thus causing a timeout on the message."""
if otherIDs:
# check if the supplied deviceIDs are all strings
for ID in otherIDs:
if type(ID) != str:
raise ValueError('group_with needs a list object, with each device identifier as a string')
# construct a MGS message with a list of the device IDs
message='MGS<mgs zone="1"><groupid>1</groupid><maxdelay>500</maxdelay><devices>' + \
'<device id="%s" zoneid="1"/>'%(self.identifier) + \
''.join(['<device id="%s" zoneid="1"/>'%(ID) for ID in otherIDs]) + \
'</devices></mgs>'
else:
# No other devices specified. Create an empty group, which stops the multiroom audio / flareconnect
message='MGS<mgs zone="1"><groupid>0</groupid></mgs>'
return self.raw(message)

def grouped_with(self, timeout=1):
"""Return a list of receiver objects we are currently grouped with and their role"""
group_list = []
# get our own group info
mygroups = self.get_groups()
mygroupids = []
if not mygroups:
# we are not part of a group, no need to waste time on discovering other receivers on the network
return None
# we are part of a group. Add ourselve to the group dict
for group in mygroups:
group_list.append({ "identifier" : self.identifier,
"host" : self.host,
"model_name" : self.model_name,
"zoneid" : group["id"],
"groupid" : group["groupid"],
"role" : group["role"],
"powerstate" : group["powerstate"]
})
mygroupids.append(group["groupid"])
# now let's find our group friends
receivers = self.discover(timeout=timeout)
for receiver in receivers:
if receiver.identifier == self.identifier:
# no need to parse ourselves
continue
receivergroups = receiver.get_groups()
for theirgroup in receivergroups:
# check if their groupid matches any of our groupids
if theirgroup["groupid"] in mygroupids:
# we have a match, append it to the group_list
group_list.append({ "identifier" : receiver.identifier,
"host" : receiver.host,
"model_name" : receiver.model_name,
"zoneid" : theirgroup["id"],
"groupid" : theirgroup["groupid"],
"role" : theirgroup["role"],
"powerstate" : theirgroup["powerstate"]
})
return group_list

def get_groups(self):
"""Show the current groups info for this receiver.
This returns a list of all zones in the receiver that are part of a multiroom audio group.
In most cases this will be an empty list (not grouped), or have a single entry.
In rare cases (e.g. receiver with both a main zone and a Zone2 that are participating in a group) you can get a multi-item list.
The items in the list are a dict with all the info returned by the receiver.
The interesting parts of this dict are (IMHO): "groupid", "role", "powerstate"
Determining which receivers are part of the group has to be done separately, by finding all receivers participating with the same groupid. One will have 'role' : 'src', all others will have 'role' : 'dst' (for source and destination)
"""
message = 'MDIQSTN'
data = self.raw(message)
grouped_zones=[]
if data:
# strip the "MDI" from the start of the reply
data = data.replace('MDI','')
# turn it into a dict
data = xmltodict.parse(data, attr_prefix="")
# Cast OrderedDict to dict
data = json.loads(json.dumps(data))
# the interesting part here is the ["mdi"]["zonelist"]["zone"] part
zonelist = data["mdi"]["zonelist"]["zone"]
for zone in zonelist:
if zone["groupid"] != '0' and zone["role"] != 'none':
grouped_zones.append(zone)
return grouped_zones

def get_nri(self):
"""Return NRI info as dict."""
data = self.command("dock.receiver-information=query")[1]
Expand Down
36 changes: 36 additions & 0 deletions eiscp/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
%(prog_n_space)s [--all] [--name <name>] [--id <identifier>]
%(prog_n_space)s [--verbose | -v]... [--quiet | -q]... <command>...
%(program_name)s --discover
%(program_name)s [--host <host>] [--group_with <otherhost> ...]
%(program_name)s [--host <host>] [--grouped_with]
%(program_name)s --help-commands [<zone> <command>]
%(program_name)s -h | --help

Expand All @@ -31,6 +33,11 @@
Turn receiver on, select "PC" source, set volume to 75.
%(program_name)s zone2.power=standby
To execute a command for zone that isn't the main one.
%(program_name)s --host 192.168.1.15 --group_with "0009B0E4B723" "0009B0E4B724"
Setup multiroom audio using Flareconnect. The <host> receiver is source, all receiver IDs supplied for <otherhost> join the <host> in a Flareconnect group.
Use the --discover option to get the receiver IDs.
%(program_name)s --host 192.168.1.15 --group_with
Stop the multiroom audio group / flareconnect.
'''

import sys
Expand Down Expand Up @@ -112,6 +119,35 @@ def main(argv=sys.argv):
print('No receivers found.')
return 1

if options['--group_with']:
receivers[0].group_with(options['<otherhost>'])

if options['--grouped_with']:
groupdata = receivers[0].grouped_with()
if groupdata:
# get a list of the active groups and their groupid. Normally this is only a single item
groups = set([dev["groupid"] for dev in groupdata])
for group in groups:
# find the source
source = [ dev for dev in groupdata if dev["groupid"] == group and dev["role"] == "src" ]
if source:
source = source[0]
else:
print("failed to detect the source for groupid %s" %(group))
print(groupdata)
return
destinations = [ dev for dev in groupdata if dev["groupid"] == group and dev["role"] == "dst" ]
if destinations:
pass
else:
print("failed to detect the destinations for groupid %s" %(group))
print(groupdata)
return
print("Multiroom audio group active with groupid %s" %(group))
print("source: %s on address: %s" %(source["model_name"], source["host"]))
for destination in destinations:
print("destination: %s on address: %s" %(destination["model_name"], destination["host"]))

# List of commands to execute - deal with special shortcut case
to_execute = options['<command>']

Expand Down