From 219ab99d26fdbc5461d46d36395b9ddbeb17be46 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Sun, 29 Jan 2023 14:10:48 +0100 Subject: [PATCH 1/8] Guard against missing transport in assemble_buffer(). --- anthemav/protocol.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index ca5097f..387b4a1 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -394,7 +394,10 @@ async def _assemble_buffer(self): disassembles the chain of datagrams into individual messages which are then passed on for interpretation. """ - self.transport.pause_reading() + try: + self.transport.pause_reading() + except AttributeError: + self.log.warning('Lost connection to receiver while assembling buffer') for message in self.buffer.split(";"): if message != "": @@ -409,7 +412,10 @@ async def _assemble_buffer(self): self.buffer = "" - self.transport.resume_reading() + try: + self.transport.resume_reading() + except AttributeError: + self.log.warning('Lost connection to receiver while assembling buffer') return def _populate_inputs(self, total): From 55d2dd723f213bdbc388a82cb470a8bd27993c3d Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Sun, 29 Jan 2023 14:11:47 +0100 Subject: [PATCH 2/8] Abstract attenuation range. Don't hardcode the supported attenuation range as -90dB to 0dB. Instead make it a property of the AVR class. Note that we now treat the attenuation value as a float with 0.5dB precision. --- anthemav/protocol.py | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index 387b4a1..d9b8bdc 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -242,6 +242,7 @@ def __init__( self._deviceinfo_received = asyncio.Event() self._alm_number = {"None": 0} self._available_input_numbers = [] + self._attenuation_range = [-90.0, 0] self.zones: Dict[int, Zone] = {1: Zone(self, 1)} self.values: Dict[str, str] = {} @@ -771,6 +772,18 @@ def formatted_command(self, command: str): "No transport found, unable to send command. error: %s", str(error) ) + @property + def min_attenuation(self): + return self._attenuation_range[0] + + @property + def max_attenuation(self): + return self._attenuation_range[1] + + @property + def attenuation_range(self): + return self.max_attenuation - self.min_attenuation + @property def support_audio_listening_mode(self) -> bool: """Return true if the zone support audio listening mode.""" @@ -791,7 +804,7 @@ def attenuation(self): """Current volume attenuation in dB (read/write). You can get or set the current attenuation value on the device with this - property. Valid range from -90 to 0. + property. Valid range usually from -90 to 0. :Examples: @@ -1261,7 +1274,7 @@ def support_attenuation(self) -> bool: # # Volume and Attenuation handlers. The Anthem tracks volume internally as - # an attenuation level ranging from -90dB (silent) to 0dB (bleeding ears) + # an attenuation level usually ranging from -90dB (silent) to 0dB (bleeding ears). # # We expose this in three methods for the convenience of downstream apps # which will almost certainly be doing things their own way: @@ -1271,23 +1284,23 @@ def support_attenuation(self) -> bool: # - volume_as_percentage (0-1 floating point) # - def attenuation_to_volume(self, value: int) -> int: + def attenuation_to_volume(self, value: float) -> int: """Convert a native attenuation value to a volume value. - Takes an attenuation in dB from the Anthem (-90 to 0) and converts it + Takes an attenuation in dB from the Anthem (usually -90 to 0) and converts it into a normal volume value (0-100). - :param arg1: attenuation in dB (negative integer from -90 to 0) - :type arg1: int + :param arg1: attenuation in dB + :type arg1: float returns an integer value representing volume """ try: - return round((90.00 + int(value)) / 90 * 100) + return int((round(2 * (float(value) - self._avr.min_attenuation)) / 2) / self._avr.attenuation_range * 100) except ValueError: return 0 - def volume_to_attenuation(self, value: int): + def volume_to_attenuation(self, value: int) -> float: """Convert a volume value to a native attenuation value. Takes a volume value and turns it into an attenuation value suitable @@ -1296,12 +1309,12 @@ def volume_to_attenuation(self, value: int): :param arg1: volume (integer from 0 to 100) :type arg1: int - returns a negative integer value representing attenuation in dB + returns a float value representing attenuation in dB """ try: - return round((value / 100) * 90) - 90 + return round(2 * (self._avr.min_attenuation + ((float(value) / 100) * self._avr.attenuation_range))) / 2 except ValueError: - return -90 + return self._avr.min_attenuation @property def power(self) -> bool: @@ -1367,22 +1380,22 @@ def volume_as_percentage(self, value: float): self.volume = value @property - def attenuation(self) -> int: + def attenuation(self) -> float: """Current volume attenuation in dB (read/write). You can get or set the current attenuation value on the device with this - property. Valid range from -90 to 0. + property. Valid range usually from -90 to 0. :Examples: >>> attvalue = attenuation >>> attenuation = -50 """ - return self._get_integer("VOL", -90) + return self._get_integer("VOL", self._avr.min_attenuation) @attenuation.setter def attenuation(self, value: int): - if -90 <= value <= 0: + if self._avr.min_attenuation <= value <= self._avr.max_attenuation: self._avr.log.debug("Setting attenuation to %s", str(value)) self.command(f"VOL{value}") From 2349ea9247015cbbf381644d6c97a86f63d641f1 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Sun, 29 Jan 2023 14:13:06 +0100 Subject: [PATCH 3/8] Support STR pre-amplifier. Only one zone. Only four listening modes. Attenuation range from -96dB to +7dB in 0.5dB steps. The BRT command, which means query bitrate in most devices, is changed to move the left/right balance to the right. Z1BRT0 is equivalent to moving balance 0.5dB to the right, Z1BRT50 moves it by 2.5dB and Z1BRT100 by 5dB. When we send Z1BRT? to query bitrate, we actually shift the balance 0.5dB to the right, which is probably a bug on the device. Simply using this API would inadvertently adjust the balance fully to the right over time! --- anthemav/protocol.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index d9b8bdc..92d6543 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -46,6 +46,13 @@ "All Channel Mono": 8, } +ALM_NUMBER_STR = { + "Stereo": 0, + "Mono": 1, + "Both=Left": 2, + "Both=Right": 3, +} + # Some models (eg:MRX 520) provide a limited list of listening mode ALM_RESTRICTED = ["00", "01", "02", "03", "04", "05", "06", "07"] @@ -187,6 +194,25 @@ "Z1VIR", ] COMMANDS_MDX = ["MAC"] +COMMANDS_STR_IGNORE = [ + "ECH", + "EMAC", + "GCFPB", + "GCTXS", + "IDR", + "MAC", + "SIP", + "WMAC", + "Z1AIC", + "Z1AIN", + "Z1AIR", + "Z1BRT", + "Z1DIA", + "Z1DYN", + "Z1IRH", + "Z1IRV", + "Z1VIR", +] EMPTY_MAC = "00:00:00:00:00:00" UNKNOWN_MODEL = "Unknown Model" @@ -194,6 +220,7 @@ MODEL_X40 = "x40" MODEL_X20 = "x20" MODEL_MDX = "mdx" +MODEL_STR = "str" # pylint: disable=too-many-instance-attributes, too-many-public-methods @@ -699,6 +726,13 @@ def set_model_command(self, model: str): self._ignored_commands = COMMANDS_X20 + COMMANDS_X40 + COMMANDS_MDX_IGNORE self._model_series = MODEL_MDX self.query("MAC") + elif "STR" in model: + self.log.debug("Set Command to Model STR") + self._ignored_commands = COMMANDS_STR_IGNORE + self._model_series = MODEL_STR + self._alm_number = ALM_NUMBER_STR + self._attenuation_range = [-96.0, 7.0] + self.query("IDN") else: self.log.debug("Set Command to Model x20") self._ignored_commands = COMMANDS_X40 + COMMANDS_MDX @@ -716,6 +750,8 @@ def set_zones(self, model: str): number_of_zones = 4 # MDX 16 input number range is 1 to 12, but MDX 8 only have 1 to 4 and 9 self._available_input_numbers = [1, 2, 3, 4, 9] + elif self._model_series == MODEL_STR: + number_of_zones = 1 else: number_of_zones = 2 @@ -1270,11 +1306,12 @@ def get_current_input_value(self, command: str) -> str: @property def support_attenuation(self) -> bool: """Return true if the zone support sound mode and sound mode list.""" - return self._avr._model_series == MODEL_X20 + return self._avr._model_series in [MODEL_X20, MODEL_STR] # # Volume and Attenuation handlers. The Anthem tracks volume internally as # an attenuation level usually ranging from -90dB (silent) to 0dB (bleeding ears). + # Note that STR pre-amplifiers have a range from -96dB to +7dB # # We expose this in three methods for the convenience of downstream apps # which will almost certainly be doing things their own way: From a27752f9a1bafd85099b35556c679b6bb9048402 Mon Sep 17 00:00:00 2001 From: mattlathrop Date: Wed, 28 Jun 2023 21:34:43 -0500 Subject: [PATCH 4/8] Add HTB and parse volume as float --- anthemav/protocol.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index 92d6543..ba9785e 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -454,6 +454,10 @@ def _populate_inputs(self, total): """ total = total + 1 for input_number in range(1, total): + if self._model_series == MODEL_STR: + # Manually add HTB input + self._input_numbers["HTB"] = 32 + self._input_names[32] = "HTB" if self._model_series == MODEL_X40: self.query(f"IS{input_number}IN") self.query(f"IS{input_number}ARC") @@ -475,6 +479,8 @@ async def _parse_message(self, data: str): recognized = False newdata = False + logging.info(f"Message:{data}") + if data.startswith("!I"): self.log.warning("Invalid command: %s", data[2:]) recognized = True @@ -1302,6 +1308,14 @@ def get_current_input_value(self, command: str) -> str: if self.input_number > 0 and self._avr._model_series == MODEL_X40: return self._avr.values.get(f"IS{self.input_number}{command}") return None + + def _get_float(self, key, default: float = 0.0) -> float: + if key not in self.values: + return default + try: + return float(self.values[key]) + except ValueError: + return default @property def support_attenuation(self) -> bool: @@ -1428,7 +1442,7 @@ def attenuation(self) -> float: >>> attvalue = attenuation >>> attenuation = -50 """ - return self._get_integer("VOL", self._avr.min_attenuation) + return self._get_float("VOL", self._avr.min_attenuation) @attenuation.setter def attenuation(self, value: int): From 6f9bb4c11abceeb5a514df97360dedcfbe10fe53 Mon Sep 17 00:00:00 2001 From: mattlathrop Date: Wed, 28 Jun 2023 22:41:12 -0500 Subject: [PATCH 5/8] Remove logging --- anthemav/protocol.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index ba9785e..d6a3533 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -479,8 +479,6 @@ async def _parse_message(self, data: str): recognized = False newdata = False - logging.info(f"Message:{data}") - if data.startswith("!I"): self.log.warning("Invalid command: %s", data[2:]) recognized = True From 7104aae1ca69469aaee9f74e5d39083a232af607 Mon Sep 17 00:00:00 2001 From: mattlathrop Date: Thu, 29 Jun 2023 09:52:44 -0500 Subject: [PATCH 6/8] Split out STR Integrated and Pre-Amp --- anthemav/protocol.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index d6a3533..4f20734 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -220,7 +220,8 @@ MODEL_X40 = "x40" MODEL_X20 = "x20" MODEL_MDX = "mdx" -MODEL_STR = "str" +MODEL_STR_SEP = "str_pa" +MODEL_STR_INT = "str_ia" # pylint: disable=too-many-instance-attributes, too-many-public-methods @@ -454,7 +455,7 @@ def _populate_inputs(self, total): """ total = total + 1 for input_number in range(1, total): - if self._model_series == MODEL_STR: + if self._model_series == MODEL_STR_INT: # Manually add HTB input self._input_numbers["HTB"] = 32 self._input_names[32] = "HTB" @@ -730,10 +731,17 @@ def set_model_command(self, model: str): self._ignored_commands = COMMANDS_X20 + COMMANDS_X40 + COMMANDS_MDX_IGNORE self._model_series = MODEL_MDX self.query("MAC") - elif "STR" in model: - self.log.debug("Set Command to Model STR") + elif "STR PA" in model: + self.log.debug("Set Command to Model STR Pre-Amp") self._ignored_commands = COMMANDS_STR_IGNORE - self._model_series = MODEL_STR + self._model_series = MODEL_STR_SEP + self._alm_number = ALM_NUMBER_STR + self._attenuation_range = [-96.0, 7.0] + self.query("IDN") + elif "STR IA" in model: + self.log.debug("Set Command to Model STR Integrated") + self._ignored_commands = COMMANDS_STR_IGNORE + self._model_series = MODEL_STR_INT self._alm_number = ALM_NUMBER_STR self._attenuation_range = [-96.0, 7.0] self.query("IDN") @@ -754,7 +762,7 @@ def set_zones(self, model: str): number_of_zones = 4 # MDX 16 input number range is 1 to 12, but MDX 8 only have 1 to 4 and 9 self._available_input_numbers = [1, 2, 3, 4, 9] - elif self._model_series == MODEL_STR: + elif self._model_series == MODEL_STR_SEP or self._model_series == MODEL_STR_INT: number_of_zones = 1 else: number_of_zones = 2 @@ -1318,7 +1326,7 @@ def _get_float(self, key, default: float = 0.0) -> float: @property def support_attenuation(self) -> bool: """Return true if the zone support sound mode and sound mode list.""" - return self._avr._model_series in [MODEL_X20, MODEL_STR] + return self._avr._model_series in [MODEL_X20, MODEL_STR_INT, MODEL_STR_SEP] # # Volume and Attenuation handlers. The Anthem tracks volume internally as From 73f97368016b63bd54e7a178571cb5ff02546c35 Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Fri, 30 Jun 2023 08:20:49 +0200 Subject: [PATCH 7/8] Formatting. --- anthemav/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index 4f20734..ea0a04c 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -762,7 +762,7 @@ def set_zones(self, model: str): number_of_zones = 4 # MDX 16 input number range is 1 to 12, but MDX 8 only have 1 to 4 and 9 self._available_input_numbers = [1, 2, 3, 4, 9] - elif self._model_series == MODEL_STR_SEP or self._model_series == MODEL_STR_INT: + elif self._model_series in [MODEL_STR_SEP, MODEL_STR_INT]: number_of_zones = 1 else: number_of_zones = 2 @@ -1314,7 +1314,7 @@ def get_current_input_value(self, command: str) -> str: if self.input_number > 0 and self._avr._model_series == MODEL_X40: return self._avr.values.get(f"IS{self.input_number}{command}") return None - + def _get_float(self, key, default: float = 0.0) -> float: if key not in self.values: return default From 773cdc4de8441f4e66a2eae9edf4b548f30ce03e Mon Sep 17 00:00:00 2001 From: Iain Patterson Date: Mon, 2 Oct 2023 15:03:46 +0200 Subject: [PATCH 8/8] Correct listening modes for STR. --- anthemav/protocol.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index ea0a04c..2bb02be 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -47,10 +47,10 @@ } ALM_NUMBER_STR = { - "Stereo": 0, - "Mono": 1, - "Both=Left": 2, - "Both=Right": 3, + "Stereo": 7, + "Mono": 9, + "Both=Left": 11, + "Both=Right": 12, } # Some models (eg:MRX 520) provide a limited list of listening mode