From 5daaed9e12a53f0e9e74c0a6b4eef07fd94ee6ac Mon Sep 17 00:00:00 2001 From: Hans van Schoot Date: Tue, 9 Apr 2024 20:30:53 +0200 Subject: [PATCH 1/2] add group_with function to the eISCP class. This function sets up multiroom audio (a.k.a. flareconnect) with other receivers. The filter_for_message function needed a modification, as the multiroom setup MGS message gets an MDI response instead. Also added the group_with functionality to the standalone script. --- eiscp/core.py | 22 ++++++++++++++++++++++ eiscp/script.py | 9 +++++++++ 2 files changed, 31 insertions(+) diff --git a/eiscp/core.py b/eiscp/core.py index 806cb6a..fd94ef0 100644 --- a/eiscp/core.py +++ b/eiscp/core.py @@ -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]: @@ -564,6 +567,25 @@ 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='MGS1500' + \ + ''%(self.identifier) + \ + ''.join([''%(ID) for ID in otherIDs]) + \ + '' + else: + # No other devices specified. Create an empty group, which stops the multiroom audio / flareconnect + message='MGS0' + return self.raw(message) + def get_nri(self): """Return NRI info as dict.""" data = self.command("dock.receiver-information=query")[1] diff --git a/eiscp/script.py b/eiscp/script.py index f55ccf2..b017c27 100644 --- a/eiscp/script.py +++ b/eiscp/script.py @@ -5,6 +5,7 @@ %(prog_n_space)s [--all] [--name ] [--id ] %(prog_n_space)s [--verbose | -v]... [--quiet | -q]... ... %(program_name)s --discover + %(program_name)s [--host ] [--group_with ...] %(program_name)s --help-commands [ ] %(program_name)s -h | --help @@ -31,6 +32,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 receiver is source, all receiver IDs supplied for join the 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 @@ -112,6 +118,9 @@ def main(argv=sys.argv): print('No receivers found.') return 1 + if options['--group_with']: + receivers[0].group_with(options['']) + # List of commands to execute - deal with special shortcut case to_execute = options[''] From 1496382cbdeb6099f94d16052c416db18fdeb98c Mon Sep 17 00:00:00 2001 From: Hans van Schoot Date: Fri, 17 May 2024 15:33:48 +0200 Subject: [PATCH 2/2] Created two functions in eiscp/core.py. get_groups is used for extracting multiroom audio group data from the Onkyo receiver through an 'MDIQSTN' message. The grouped_with function uses this function to generate a list of dicts, one dict for every receiver participating in the groups active on the selected receiver. Added a --grouped_with argument to the command line utility. This function prints the result of the grouped_with function in core.py in a human-readable format. --- eiscp/core.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ eiscp/script.py | 27 ++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/eiscp/core.py b/eiscp/core.py index fd94ef0..605d412 100644 --- a/eiscp/core.py +++ b/eiscp/core.py @@ -586,6 +586,73 @@ def group_with(self, otherIDs=[]): message='MGS0' 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] diff --git a/eiscp/script.py b/eiscp/script.py index b017c27..32fd51c 100644 --- a/eiscp/script.py +++ b/eiscp/script.py @@ -6,6 +6,7 @@ %(prog_n_space)s [--verbose | -v]... [--quiet | -q]... ... %(program_name)s --discover %(program_name)s [--host ] [--group_with ...] + %(program_name)s [--host ] [--grouped_with] %(program_name)s --help-commands [ ] %(program_name)s -h | --help @@ -121,6 +122,32 @@ def main(argv=sys.argv): if options['--group_with']: receivers[0].group_with(options['']) + 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['']