diff --git a/anthemav/protocol.py b/anthemav/protocol.py index ca5097f..2bb02be 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -46,6 +46,13 @@ "All Channel Mono": 8, } +ALM_NUMBER_STR = { + "Stereo": 7, + "Mono": 9, + "Both=Left": 11, + "Both=Right": 12, +} + # 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,8 @@ MODEL_X40 = "x40" MODEL_X20 = "x20" MODEL_MDX = "mdx" +MODEL_STR_SEP = "str_pa" +MODEL_STR_INT = "str_ia" # pylint: disable=too-many-instance-attributes, too-many-public-methods @@ -242,6 +270,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] = {} @@ -394,7 +423,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 +441,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): @@ -420,6 +455,10 @@ def _populate_inputs(self, total): """ total = total + 1 for input_number in range(1, total): + if self._model_series == MODEL_STR_INT: + # 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") @@ -692,6 +731,20 @@ 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 PA" in model: + self.log.debug("Set Command to Model STR Pre-Amp") + self._ignored_commands = COMMANDS_STR_IGNORE + 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") else: self.log.debug("Set Command to Model x20") self._ignored_commands = COMMANDS_X40 + COMMANDS_MDX @@ -709,6 +762,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 in [MODEL_STR_SEP, MODEL_STR_INT]: + number_of_zones = 1 else: number_of_zones = 2 @@ -765,6 +820,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.""" @@ -785,7 +852,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: @@ -1248,14 +1315,23 @@ def get_current_input_value(self, command: str) -> str: 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: """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_INT, MODEL_STR_SEP] # # 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). + # 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: @@ -1265,23 +1341,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 @@ -1290,12 +1366,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: @@ -1361,22 +1437,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_float("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}")