diff --git a/README.rst b/README.rst index f6bc959..09dd67f 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,7 @@ run those other applications. Requirements ------------ -- Python 3.4 or newer with asyncio +- Python 3.5 or newer with asyncio - An Anthem MRX or AVM receiver or processor Known Issues diff --git a/anthemav/__init__.py b/anthemav/__init__.py index 9add9dc..cc2c678 100644 --- a/anthemav/__init__.py +++ b/anthemav/__init__.py @@ -3,5 +3,5 @@ This module provides a unified asyncio network handler for interacting with home A/V receivers and processors made by Anthem ( http://www.anthemav.com/ ) """ -from .connection import Connection # noqa: F401 -from .protocol import AVR # noqa: F401 +from .connection import Connection # noqa: F401 +from .protocol import AVR # noqa: F401 diff --git a/anthemav/connection.py b/anthemav/connection.py index d8cc8d3..70d341f 100644 --- a/anthemav/connection.py +++ b/anthemav/connection.py @@ -3,12 +3,12 @@ import logging from .protocol import AVR -__all__ = ('Connection') +__all__ = "Connection" try: ensure_future = asyncio.ensure_future except: - ensure_future = getattr(asyncio, 'async') + ensure_future = getattr(asyncio, "async") class Connection: @@ -19,10 +19,15 @@ def __init__(self): self.log = logging.getLogger(__name__) @classmethod - @asyncio.coroutine - def create(cls, host='localhost', port=14999, - auto_reconnect=True, loop=None, protocol_class=AVR, - update_callback=None): + async def create( + cls, + host="localhost", + port=14999, + auto_reconnect=True, + loop=None, + protocol_class=AVR, + update_callback=None, + ): """Initiate a connection to a specific device. Here is where we supply the host and port and callback callables we @@ -50,7 +55,7 @@ def create(cls, host='localhost', port=14999, :type update_callback: callable """ - assert port >= 0, 'Invalid port value: %r' % (port) + assert port >= 0, "Invalid port value: %r" % (port) conn = cls() conn.host = host @@ -68,10 +73,12 @@ def connection_lost(): ensure_future(conn._reconnect(), loop=conn._loop) conn.protocol = protocol_class( - connection_lost_callback=connection_lost, loop=conn._loop, - update_callback=update_callback) + connection_lost_callback=connection_lost, + loop=conn._loop, + update_callback=update_callback, + ) - yield from conn._reconnect() + await conn._reconnect() return conn @@ -93,48 +100,53 @@ def _reset_retry_interval(self): def _increase_retry_interval(self): self._retry_interval = min(300, 1.5 * self._retry_interval) - @asyncio.coroutine - def _reconnect(self): + async def _reconnect(self): while True: try: if self._halted: - yield from asyncio.sleep(2, loop=self._loop) + await asyncio.sleep(2, loop=self._loop) else: - self.log.info('Connecting to Anthem AVR at %s:%d', - self.host, self.port) - yield from self._loop.create_connection( - lambda: self.protocol, self.host, self.port) + self.log.debug( + "Connecting to Anthem AVR at %s:%d", self.host, self.port + ) + await self._loop.create_connection( + lambda: self.protocol, self.host, self.port + ) self._reset_retry_interval() return except OSError: self._increase_retry_interval() interval = self._get_retry_interval() - self.log.warning('Connecting failed, retrying in %i seconds', - interval) - yield from asyncio.sleep(interval, loop=self._loop) + self.log.warning("Connecting failed, retrying in %i seconds", interval) + if not self._auto_reconnect or self._closing: + raise + await asyncio.sleep(interval, loop=self._loop) + + if not self._auto_reconnect or self._closing: + break def close(self): """Close the AVR device connection and don't try to reconnect.""" - self.log.warning('Closing connection to AVR') + self.log.debug("Closing connection to AVR") self._closing = True if self.protocol.transport: self.protocol.transport.close() def halt(self): """Close the AVR device connection and wait for a resume() request.""" - self.log.warning('Halting connection to AVR') + self.log.warning("Halting connection to AVR") self._halted = True if self.protocol.transport: self.protocol.transport.close() def resume(self): """Resume the AVR device connection if we have been halted.""" - self.log.warning('Resuming connection to AVR') + self.log.warning("Resuming connection to AVR") self._halted = False @property def dump_conndata(self): """Developer tool for debugging forensics.""" attrs = vars(self) - return ', '.join("%s: %s" % item for item in attrs.items()) + return ", ".join("%s: %s" % item for item in attrs.items()) diff --git a/anthemav/protocol.py b/anthemav/protocol.py index 0ccd180..a4639a2 100755 --- a/anthemav/protocol.py +++ b/anthemav/protocol.py @@ -3,68 +3,112 @@ import logging import time -__all__ = ('AVR') +__all__ = "AVR" # In Python 3.4.4, `async` was renamed to `ensure_future`. try: ensure_future = asyncio.ensure_future except AttributeError: - ensure_future = getattr(asyncio, 'async') + ensure_future = getattr(asyncio, "async") # These properties apply even when the AVR is powered off -ATTR_CORE = {'Z1POW', 'IDM'} +ATTR_CORE = {"Z1POW", "IDM", "IDN"} LOOKUP = {} -LOOKUP['Z1POW'] = {'description': 'Zone 1 Power', - '0': 'Off', '1': 'On'} -LOOKUP['FPB'] = {'description': 'Front Panel Brightness', - '0': 'Off', '1': 'Low', '2': 'Medium', '3': 'High'} -LOOKUP['Z1VOL'] = {'description': 'Zone 1 Volume'} -LOOKUP['IDR'] = {'description': 'Region'} -LOOKUP['IDM'] = {'description': 'Model'} -LOOKUP['IDS'] = {'description': 'Software version'} -LOOKUP['IDB'] = {'description': 'Software build date'} -LOOKUP['IDH'] = {'description': 'Hardware version'} -LOOKUP['IDN'] = {'description': 'MAC address'} -LOOKUP['ECH'] = {'description': 'Tx status', - '0': 'Off', '1': 'On'} -LOOKUP['SIP'] = {'description': 'Standby IP control', - '0': 'Off', '1': 'On'} -LOOKUP['ICN'] = {'description': 'Active input count'} -LOOKUP['Z1INP'] = {'description': 'Zone 1 current input'} -LOOKUP['Z1MUT'] = {'description': 'Zone 1 mute', - '0': 'Unmuted', '1': 'Muted'} -LOOKUP['Z1ARC'] = {'description': 'Zone 1 ARC', - '0': 'Off', '1': 'On'} -LOOKUP['Z1VIR'] = {'description': 'Video input resolution', - '0': 'No video', '1': 'Other', '2': '1080p60', '3': '1080p50', - '4': '1080p24', '5': '1080i60', '6': '1080i50', '7': '720p60', - '8': '720p50', '9': '576p50', '10': '576i50', '11': '480p60', - '12': '480i60', '13': '3D', '14': '4K'} -LOOKUP['Z1IRH'] = {'description': 'Active horizontal video resolution (pixels)'} -LOOKUP['Z1IRV'] = {'description': 'Active vertical video resolution (pixels)'} -LOOKUP['Z1AIC'] = {'description': 'Audio input channels', - '0': 'No audio', '1': 'Other', '2': 'Mono (center channel)', - '3': '2 channel', '4': '5.1 channel', '5': '6.1 channel', - '6': '7.1 channel', '7': 'Atmos'} -LOOKUP['Z1AIF'] = {'description': 'Audio input format', - '0': 'No audio', '1': 'Analog', '2': 'PCM', '3': 'Dolby', - '4': 'DSD', '5': 'DTS', '6': 'Atmos'} -LOOKUP['Z1BRT'] = {'description': 'Audio input bitrate (kbps)'} -LOOKUP['Z1SRT'] = {'description': 'Audio input sampling rate (hKz)'} -LOOKUP['Z1AIN'] = {'description': 'Audio input name'} -LOOKUP['Z1AIR'] = {'description': 'Audio input rate name'} -LOOKUP['Z1ALM'] = {'description': 'Audio listening mode', - '00': 'None', '01': 'AnthemLogic Movie', '02': 'AnthemLogic Music', - '03': 'PLIIx Movie', '04': 'PLIIx Music', '05': 'Neo:6 Cinema', - '06': 'Neo:6 Music', '07': 'All Channel Stereo', - '08': 'All Channel Mono', '09': 'Mono', '10': 'Mono-Academy', - '11': 'Mono (L)', '12': 'Mono (R)', '13': 'High Blend', - '14': 'Dolby Surround', '15': 'Neo:X Cinema', '16': 'Neo:X Music'} -LOOKUP['Z1DYN'] = {'description': 'Dolby digital dynamic range', - '0': 'Normal', '1': 'Reduced', '2': 'Late Night'} -LOOKUP['Z1DIA'] = {'description': 'Dolby digital dialog normalization (dB)'} +LOOKUP["Z1POW"] = {"description": "Zone 1 Power", "0": "Off", "1": "On"} +LOOKUP["FPB"] = { + "description": "Front Panel Brightness", + "0": "Off", + "1": "Low", + "2": "Medium", + "3": "High", +} +LOOKUP["Z1VOL"] = {"description": "Zone 1 Volume"} +LOOKUP["IDR"] = {"description": "Region"} +LOOKUP["IDM"] = {"description": "Model"} +LOOKUP["IDS"] = {"description": "Software version"} +LOOKUP["IDB"] = {"description": "Software build date"} +LOOKUP["IDH"] = {"description": "Hardware version"} +LOOKUP["IDN"] = {"description": "MAC address"} +LOOKUP["ECH"] = {"description": "Tx status", "0": "Off", "1": "On"} +LOOKUP["SIP"] = {"description": "Standby IP control", "0": "Off", "1": "On"} +LOOKUP["ICN"] = {"description": "Active input count"} +LOOKUP["Z1INP"] = {"description": "Zone 1 current input"} +LOOKUP["Z1MUT"] = {"description": "Zone 1 mute", "0": "Unmuted", "1": "Muted"} +LOOKUP["Z1ARC"] = {"description": "Zone 1 ARC", "0": "Off", "1": "On"} +LOOKUP["Z1VIR"] = { + "description": "Video input resolution", + "0": "No video", + "1": "Other", + "2": "1080p60", + "3": "1080p50", + "4": "1080p24", + "5": "1080i60", + "6": "1080i50", + "7": "720p60", + "8": "720p50", + "9": "576p50", + "10": "576i50", + "11": "480p60", + "12": "480i60", + "13": "3D", + "14": "4K", +} +LOOKUP["Z1IRH"] = {"description": "Active horizontal video resolution (pixels)"} +LOOKUP["Z1IRV"] = {"description": "Active vertical video resolution (pixels)"} +LOOKUP["Z1AIC"] = { + "description": "Audio input channels", + "0": "No audio", + "1": "Other", + "2": "Mono (center channel)", + "3": "2 channel", + "4": "5.1 channel", + "5": "6.1 channel", + "6": "7.1 channel", + "7": "Atmos", +} +LOOKUP["Z1AIF"] = { + "description": "Audio input format", + "0": "No audio", + "1": "Analog", + "2": "PCM", + "3": "Dolby", + "4": "DSD", + "5": "DTS", + "6": "Atmos", +} +LOOKUP["Z1BRT"] = {"description": "Audio input bitrate (kbps)"} +LOOKUP["Z1SRT"] = {"description": "Audio input sampling rate (hKz)"} +LOOKUP["Z1AIN"] = {"description": "Audio input name"} +LOOKUP["Z1AIR"] = {"description": "Audio input rate name"} +LOOKUP["Z1ALM"] = { + "description": "Audio listening mode", + "00": "None", + "01": "AnthemLogic Movie", + "02": "AnthemLogic Music", + "03": "PLIIx Movie", + "04": "PLIIx Music", + "05": "Neo:6 Cinema", + "06": "Neo:6 Music", + "07": "All Channel Stereo", + "08": "All Channel Mono", + "09": "Mono", + "10": "Mono-Academy", + "11": "Mono (L)", + "12": "Mono (R)", + "13": "High Blend", + "14": "Dolby Surround", + "15": "Neo:X Cinema", + "16": "Neo:X Music", +} +LOOKUP["Z1DYN"] = { + "description": "Dolby digital dynamic range", + "0": "Normal", + "1": "Reduced", + "2": "Late Night", +} +LOOKUP["Z1DIA"] = {"description": "Dolby digital dialog normalization (dB)"} # pylint: disable=too-many-instance-attributes, too-many-public-methods @@ -95,16 +139,16 @@ def __init__(self, update_callback=None, loop=None, connection_lost_callback=Non self.log = logging.getLogger(__name__) self._connection_lost_callback = connection_lost_callback self._update_callback = update_callback - self.buffer = '' + self.buffer = "" self._input_names = {} self._input_numbers = {} self._poweron_refresh_successful = False self.transport = None for key in LOOKUP: - setattr(self, '_'+key, '') + setattr(self, "_" + key, "") - self._Z1POW = '0' + self._Z1POW = "0" def refresh_core(self): """Query device for all attributes that exist regardless of power state. @@ -115,8 +159,11 @@ def refresh_core(self): This does not return any data, it just issues the queries. """ - self.log.info('Sending out mass query for all attributes') + self.log.info("Sending out mass query for all attributes") for key in ATTR_CORE: + if self.transport is None: + self.log.warning("Lost connection to receiver while refreshing device") + break self.query(key) def poweron_refresh(self): @@ -129,13 +176,12 @@ def poweron_refresh(self): values have been returned for at least one input name (this seems to be the laggiest of all the attributes) """ - if self._poweron_refresh_successful: + if self._poweron_refresh_successful or self.transport is None: return else: self.refresh_all() self._loop.call_later(2, self.poweron_refresh) - def refresh_all(self): """Query device for all attributes that are known. @@ -145,39 +191,41 @@ def refresh_all(self): This does not return any data, it just issues the queries. """ - self.log.info('refresh_all') + self.log.info("refresh_all") for key in LOOKUP: + if self.transport is None: + self.log.warning("Lost connection to receiver while refreshing device") + break self.query(key) - # # asyncio network functions # def connection_made(self, transport): """Called when asyncio.Protocol establishes the network connection.""" - self.log.info('Connection established to AVR') + self.log.info("Connection established to AVR") self.transport = transport - #self.transport.set_write_buffer_limits(0) + # self.transport.set_write_buffer_limits(0) limit_low, limit_high = self.transport.get_write_buffer_limits() - self.log.debug('Write buffer limits %d to %d', limit_low, limit_high) + self.log.debug("Write buffer limits %d to %d", limit_low, limit_high) - self.command('ECH1') + self.command("ECH1") self.refresh_core() def data_received(self, data): """Called when asyncio.Protocol detects received data from network.""" self.buffer += data.decode() - self.log.debug('Received %d bytes from AVR: %s', len(self.buffer), self.buffer) + self.log.debug("Received %d bytes from AVR: %s", len(self.buffer), self.buffer) self._assemble_buffer() def connection_lost(self, exc): """Called when asyncio.Protocol loses the network connection.""" - if exc is None: - self.log.warning('eof from receiver?') - else: - self.log.warning('Lost connection to receiver: %s', exc) + self.log.warning("Lost connection to receiver") + + if exc is not None: + self.log.debug(exc) self.transport = None @@ -195,9 +243,9 @@ def _assemble_buffer(self): """ self.transport.pause_reading() - for message in self.buffer.split(';'): - if message != '': - self.log.debug('assembled message '+message) + for message in self.buffer.split(";"): + if message != "": + self.log.debug("assembled message " + message) self._parse_message(message) self.buffer = "" @@ -213,7 +261,7 @@ def _populate_inputs(self, total): """ total = total + 1 for input_number in range(1, total): - self.query('ISN'+str(input_number).zfill(2)) + self.query("ISN" + str(input_number).zfill(2)) def _parse_message(self, data): """Interpret each message datagram from device and do the needful. @@ -226,17 +274,17 @@ def _parse_message(self, data): recognized = False newdata = False - if data.startswith('!I'): - self.log.warning('Invalid command: %s', data[2:]) + if data.startswith("!I"): + self.log.warning("Invalid command: %s", data[2:]) recognized = True - elif data.startswith('!R'): - self.log.warning('Out-of-range command: %s', data[2:]) + elif data.startswith("!R"): + self.log.warning("Out-of-range command: %s", data[2:]) recognized = True - elif data.startswith('!E'): - self.log.warning('Cannot execute recognized command: %s', data[2:]) + elif data.startswith("!E"): + self.log.debug("Cannot execute recognized command: %s", data[2:]) recognized = True - elif data.startswith('!Z'): - self.log.warning('Ignoring command for powered-off zone: %s', data[2:]) + elif data.startswith("!Z"): + self.log.debug("Ignoring command for powered-off zone: %s", data[2:]) recognized = True else: @@ -244,69 +292,84 @@ def _parse_message(self, data): if data.startswith(key): recognized = True - value = data[len(key):] - oldvalue = getattr(self, '_'+key) + value = data[len(key) :] + oldvalue = getattr(self, "_" + key) if oldvalue != value: - changeindicator = 'New Value' + changeindicator = "New Value" newdata = True else: - changeindicator = 'Unchanged' + changeindicator = "Unchanged" if key in LOOKUP: - if 'description' in LOOKUP[key]: + if "description" in LOOKUP[key]: if value in LOOKUP[key]: - self.log.info('%s: %s (%s) -> %s (%s)', - changeindicator, - LOOKUP[key]['description'], key, - LOOKUP[key][value], value) + self.log.info( + "%s: %s (%s) -> %s (%s)", + changeindicator, + LOOKUP[key]["description"], + key, + LOOKUP[key][value], + value, + ) else: - self.log.info('%s: %s (%s) -> %s', - changeindicator, - LOOKUP[key]['description'], key, - value) + self.log.info( + "%s: %s (%s) -> %s", + changeindicator, + LOOKUP[key]["description"], + key, + value, + ) else: - self.log.info('%s: %s -> %s', changeindicator, key, value) + self.log.info("%s: %s -> %s", changeindicator, key, value) - setattr(self, '_'+key, value) + setattr(self, "_" + key, value) - if key == 'Z1POW' and value == '1' and oldvalue == '0': - self.log.info('Power on detected, refreshing all attributes') + if key == "Z1POW" and value == "1" and oldvalue == "0": + self.log.info("Power on detected, refreshing all attributes") self._poweron_refresh_successful = False self._loop.call_later(1, self.poweron_refresh) - if key == 'Z1POW' and value == '0' and oldvalue == '1': + if key == "Z1POW" and value == "0" and oldvalue == "1": self._poweron_refresh_successful = False + if self._Z1POW == "0" and all( + coreKey not in key for coreKey in ATTR_CORE + ): + # AVR doesn't send Power State ON + # force refresh power state when receiving any command from a potential powered on AVR + self.log.debug("Force refresh Power State") + self.query("Z1POW") + break - if data.startswith('ICN'): - self.log.warning('ICN update received') + if data.startswith("ICN"): + self.log.debug("ICN update received") recognized = True self._populate_inputs(int(value)) - if data.startswith('ISN'): + if data.startswith("ISN"): recognized = True self._poweron_refresh_successful = True input_number = int(data[3:5]) value = data[5:] - oldname = self._input_names.get(input_number, '') + oldname = self._input_names.get(input_number, "") if oldname != value: self._input_numbers[value] = input_number self._input_names[input_number] = value - self.log.info('New Value: Input %d is called %s', input_number, value) + self.log.info("New Value: Input %d is called %s", input_number, value) newdata = True if newdata: if self._update_callback: self._loop.call_soon(self._update_callback, data) else: - self.log.debug('no new data encountered') + self.log.debug("no new data encountered") if not recognized: - self.log.warning('Unrecognized response: %s', data) + self.log.warning("Unrecognized response: %s", data) def query(self, item): """Issue a raw query to the device for an item. @@ -328,7 +391,7 @@ def query(self, item): >>> query('Z1VOL') """ - item = item+'?' + item = item + "?" self.command(item) def command(self, command): @@ -348,7 +411,7 @@ def command(self, command): >>> command('Z1VOL-50') """ - command = command+';' + command = command + ";" self.formatted_command(command) def formatted_command(self, command): @@ -370,12 +433,12 @@ def formatted_command(self, command): command = command command = command.encode() - self.log.debug('> %s', command) + self.log.debug("> %s", command) try: self.transport.write(command) time.sleep(0.01) except: - self.log.warning('No transport found, unable to send command') + self.log.warning("No transport found, unable to send command") # # Volume and Attenuation handlers. The Anthem tracks volume internally as @@ -443,8 +506,8 @@ def attenuation(self): @attenuation.setter def attenuation(self, value): if isinstance(value, int) and -90 <= value <= 0: - self.log.debug('Setting attenuation to '+str(value)) - self.command('Z1VOL'+str(value)) + self.log.debug("Setting attenuation to " + str(value)) + self.command("Z1VOL" + str(value)) @property def volume(self): @@ -493,7 +556,7 @@ def volume_as_percentage(self, value): # def _get_boolean(self, key): - keyname = '_'+key + keyname = "_" + key try: value = getattr(self, keyname) return bool(int(value)) @@ -504,9 +567,9 @@ def _get_boolean(self, key): def _set_boolean(self, key, value): if value is True: - self.command(key+'1') + self.command(key + "1") else: - self.command(key+'0') + self.command(key + "0") # # Boolean properties and corresponding setters @@ -518,12 +581,12 @@ def power(self): Returns and expects a boolean value. """ - return self._get_boolean('Z1POW') + return self._get_boolean("Z1POW") @power.setter def power(self, value): - self._set_boolean('Z1POW', value) - self._set_boolean('Z1POW', value) + self._set_boolean("Z1POW", value) + self.query("Z1POW") @property def txstatus(self): @@ -531,7 +594,7 @@ def txstatus(self): When enabled, all commands, status changes, and control information are reported through the Ethernet and RS-232 connections. Do not - disable this setting, the anthemav pacakge requires it. + disable this setting, the anthemav package requires it. It is explicitly set to True whenever this module connects to the AVR, but I'll still let you disable it though, because I believe in aiming @@ -540,11 +603,11 @@ def txstatus(self): :param arg1: setting :type arg1: boolean """ - return self._get_boolean('ECH') + return self._get_boolean("ECH") @txstatus.setter def txstatus(self, value): - self._set_boolean('ECH', value) + self._set_boolean("ECH", value) @property def standby_control(self): @@ -558,29 +621,31 @@ def standby_control(self): :param arg1: setting :type arg1: boolean """ - return self._get_boolean('SIP') + return self._get_boolean("SIP") @standby_control.setter def standby_control(self, value): - self._set_boolean('SIP', value) + self._set_boolean("SIP", value) @property def arc(self): """Current ARC (Anthem Room Correction) on or off (read/write).""" - return self._get_boolean('Z1ARC') + return self._get_boolean("Z1ARC") @arc.setter def arc(self, value): - self._set_boolean('Z1ARC', value) + self._set_boolean("Z1ARC", value) @property def mute(self): """Mute on or off (read/write).""" - return self._get_boolean('Z1MUT') + return self._get_boolean("Z1MUT") @mute.setter def mute(self, value): - self._set_boolean('Z1MUT', value) + self._set_boolean("Z1MUT", value) + # Query mute because the AVR doesn't always return back the state (eg: after power on without changing the volume first) + self.query("Z1MUT") # # Read-only text properties @@ -631,7 +696,7 @@ def audio_input_ratename(self): # def _get_integer(self, key): - keyname = '_'+key + keyname = "_" + key if hasattr(self, keyname): value = getattr(self, keyname) try: @@ -645,17 +710,17 @@ def dolby_dialog_normalization(self): Returns value in dB of normalization (if applicable). """ - return self._get_integer('Z1DIA') + return self._get_integer("Z1DIA") @property def horizontal_resolution(self): """Query active horizontal video resolution (in pixels).""" - return self._get_integer('Z1IRH') + return self._get_integer("Z1IRH") @property def vertical_resolution(self): """Query active vertical video resolution (in pixels).""" - return self._get_integer('Z1IRV') + return self._get_integer("Z1IRV") @property def audio_input_bitrate(self): @@ -664,19 +729,19 @@ def audio_input_bitrate(self): For Analog/PCM inputs this is equal to the sample rate multiplied by the bit depth and the number of channels. """ - return self._get_integer('Z1BRT') + return self._get_integer("Z1BRT") @property def audio_input_samplerate(self): """Query audio input sampling rate (kHz).""" - return self._get_integer('Z1SRT') + return self._get_integer("Z1SRT") # # Helper functions for working with raw/text multi-property items # # - def _get_multiprop(self, key, mode='raw'): - keyname = '_'+key + def _get_multiprop(self, key, mode="raw"): + keyname = "_" + key if hasattr(self, keyname): rawvalue = getattr(self, keyname) @@ -686,7 +751,7 @@ def _get_multiprop(self, key, mode='raw'): if rawvalue in LOOKUP[key]: value = LOOKUP[key][rawvalue] - if mode == 'raw': + if mode == "raw": return rawvalue else: return value @@ -703,19 +768,19 @@ def panel_brightness(self): 0=off, 1=low, 2=medium, 3=high """ - return self._get_multiprop('FPB', mode='raw') + return self._get_multiprop("FPB", mode="raw") @property def panel_brightness_text(self): """Current front panel brighness value (str) (read-only).""" - return self._get_multiprop('FPB', mode='text') + return self._get_multiprop("FPB", mode="text") @panel_brightness.setter def panel_brightness(self, number): if isinstance(number, int): if 0 <= number <= 3: - self.log.info('Switching panel brightness to '+str(number)) - self.command('FPB'+str(number)) + self.log.info("Switching panel brightness to " + str(number)) + self.command("FPB" + str(number)) @property def audio_listening_mode(self): @@ -730,19 +795,19 @@ def audio_listening_mode(self): Some options are not available in all models or under all circumstances. """ - return self._get_multiprop('Z1ALM', mode='raw') + return self._get_multiprop("Z1ALM", mode="raw") @property def audio_listening_mode_text(self): """Current audio listening mode (str) (read-only).""" - return self._get_multiprop('Z1ALM', mode='text') + return self._get_multiprop("Z1ALM", mode="text") @audio_listening_mode.setter def audio_listening_mode(self, number): if isinstance(number, int): if 0 <= number <= 16: - self.log.info('Switching audio listening mode to '+str(number)) - self.command('Z1ALM'+str(number).zfill(2)) + self.log.info("Switching audio listening mode to " + str(number)) + self.command("Z1ALM" + str(number).zfill(2)) @property def dolby_dynamic_range(self): @@ -752,19 +817,19 @@ def dolby_dynamic_range(self): 0=Normal, 1=Reduced, 2=Late Night. """ - return self._get_multiprop('Z1DYN', mode='raw') + return self._get_multiprop("Z1DYN", mode="raw") @property def dolby_dynamic_range_text(self): """Current Dolby Dynamic Range setting (str) (read-only).""" - return self._get_multiprop('Z1DYN', mode='text') + return self._get_multiprop("Z1DYN", mode="text") @dolby_dynamic_range.setter def dolby_dynamic_range(self, number): if isinstance(number, int): if 0 <= number <= 2: - self.log.info('Switching Dolby dynamic range to '+str(number)) - self.command('Z1DYN'+str(number)) + self.log.info("Switching Dolby dynamic range to " + str(number)) + self.command("Z1DYN" + str(number)) # # Read-only properties with raw and text options @@ -778,12 +843,12 @@ def video_input_resolution(self): 6=1080i50, 7=720p60, 8=720p50, 9=576p50, 10=576i50, 11=480p60, 12=480i60, 13=3D, 14=4k """ - return self._get_multiprop('Z1VIR', mode='raw') + return self._get_multiprop("Z1VIR", mode="raw") @property def video_input_resolution_text(self): """Current video input resolution (str) (read-only).""" - return self._get_multiprop('Z1VIR', mode='text') + return self._get_multiprop("Z1VIR", mode="text") @property def audio_input_channels(self): @@ -792,12 +857,12 @@ def audio_input_channels(self): 0=no input, 1=other, 2=mono (center channel only), 3=2-channel, 4=5.1-channel, 5=6.1-channel, 6=7.1-channel, 7=Atmos """ - return self._get_multiprop('Z1AIC', mode='raw') + return self._get_multiprop("Z1AIC", mode="raw") @property def audio_input_channels_text(self): """Current audio input channels (str) (read-only).""" - return self._get_multiprop('Z1AIC', mode='text') + return self._get_multiprop("Z1AIC", mode="text") @property def audio_input_format(self): @@ -805,12 +870,12 @@ def audio_input_format(self): 0=no input, 1=Analog, 2=PCM, 3=Dolby, 4= DSD, 5=DTS, 6=Atmos. """ - return self._get_multiprop('Z1AIF', mode='raw') + return self._get_multiprop("Z1AIF", mode="raw") @property def audio_input_format_text(self): """Current audio input format (str) (read-only).""" - return self._get_multiprop('Z1AIF', mode='text') + return self._get_multiprop("Z1AIF", mode="text") # # Input number and lists @@ -835,14 +900,14 @@ def input_name(self, value): @property def input_number(self): """Number of currently active input (read-write).""" - return self._get_integer('Z1INP') + return self._get_integer("Z1INP") @input_number.setter def input_number(self, number): if isinstance(number, int): if 1 <= number <= 99: - self.log.info('Switching input to '+str(number)) - self.command('Z1INP'+str(number)) + self.log.info("Switching input to " + str(number)) + self.command("Z1INP" + str(number)) # # Miscellany @@ -851,11 +916,11 @@ def input_number(self, number): @property def dump_rawdata(self): """Return contents of transport object for debugging forensics.""" - if hasattr(self, 'transport'): + if hasattr(self, "transport"): attrs = vars(self.transport) - return ', '.join("%s: %s" % item for item in attrs.items()) + return ", ".join("%s: %s" % item for item in attrs.items()) @property def test_string(self): """I really do.""" - return 'I like cows' + return "I like cows" diff --git a/anthemav/tools.py b/anthemav/tools.py index 0e603c0..28c7641 100644 --- a/anthemav/tools.py +++ b/anthemav/tools.py @@ -5,11 +5,10 @@ import anthemav -__all__ = ('console', 'monitor') +__all__ = ("console", "monitor") -@asyncio.coroutine -def console(loop, log): +async def console(loop, log): """Connect to receiver and show events as they occur. Pulls the following arguments from the command line (not method arguments): @@ -22,9 +21,9 @@ def console(loop, log): Show debug logging. """ parser = argparse.ArgumentParser(description=console.__doc__) - parser.add_argument('--host', default='127.0.0.1', help='IP or FQDN of AVR') - parser.add_argument('--port', default='14999', help='Port of AVR') - parser.add_argument('--verbose', '-v', action='count') + parser.add_argument("--host", default="192.168.3.16", help="IP or FQDN of AVR") + parser.add_argument("--port", default="14999", help="Port of AVR") + parser.add_argument("--verbose", "-v", action="count") args = parser.parse_args() @@ -37,24 +36,25 @@ def console(loop, log): def log_callback(message): """Receives event callback from Anthem Protocol class.""" - log.info('Callback invoked: %s' % message) + log.info("Callback invoked: %s" % message) host = args.host port = int(args.port) - log.info('Connecting to Anthem AVR at %s:%i' % (host, port)) + log.info("Connecting to Anthem AVR at %s:%i" % (host, port)) - conn = yield from anthemav.Connection.create( - host=host, port=port, loop=loop, update_callback=log_callback) + conn = await anthemav.Connection.create( + host=host, port=port, loop=loop, update_callback=log_callback + ) - log.info('Power state is '+str(conn.protocol.power)) + log.info("Power state is " + str(conn.protocol.power)) conn.protocol.power = True - log.info('Power state is '+str(conn.protocol.power)) + log.info("Power state is " + str(conn.protocol.power)) - yield from asyncio.sleep(10, loop=loop) + await asyncio.sleep(10, loop=loop) - log.info('Panel brightness (raw) is '+str(conn.protocol.panel_brightness)) - log.info('Panel brightness (text) is '+str(conn.protocol.panel_brightness_text)) + log.info("Panel brightness (raw) is " + str(conn.protocol.panel_brightness)) + log.info("Panel brightness (text) is " + str(conn.protocol.panel_brightness_text)) def monitor(): diff --git a/example.py b/example.py index 676e455..c99a6f2 100755 --- a/example.py +++ b/example.py @@ -7,12 +7,13 @@ log = logging.getLogger(__name__) + @asyncio.coroutine def test(): parser = argparse.ArgumentParser(description=test.__doc__) - parser.add_argument('--host', default='127.0.0.1', help='IP or FQDN of AVR') - parser.add_argument('--port', default='14999', help='Port of AVR') - parser.add_argument('--verbose', '-v', action='count') + parser.add_argument("--host", default="127.0.0.1", help="IP or FQDN of AVR") + parser.add_argument("--port", default="14999", help="Port of AVR") + parser.add_argument("--verbose", "-v", action="count") args = parser.parse_args() @@ -24,30 +25,41 @@ def test(): logging.basicConfig(level=level) def log_callback(message): - log.info('Callback invoked: %s' % message) + log.info("Callback invoked: %s" % message) host = args.host port = int(args.port) - log.info('Connecting to Anthem AVR at %s:%i' % (host, port)) + log.info("Connecting to Anthem AVR at %s:%i" % (host, port)) - conn = yield from anthemav.Connection.create(host=host,port=port,loop=loop,update_callback=log_callback) + conn = yield from anthemav.Connection.create( + host=host, port=port, loop=loop, update_callback=log_callback + ) - log.info('Power state is '+str(conn.protocol.power)) + log.info("Power state is " + str(conn.protocol.power)) conn.protocol.power = True - log.info('Power state is '+str(conn.protocol.power)) + log.info("Power state is " + str(conn.protocol.power)) yield from asyncio.sleep(2, loop=loop) - log.info('Panel brightness (raw) is '+str(conn.protocol.panel_brightness)) - log.info('Panel brightness (text) is '+str(conn.protocol.panel_brightness_text)) + log.info("Panel brightness (raw) is " + str(conn.protocol.panel_brightness)) + log.info("Panel brightness (text) is " + str(conn.protocol.panel_brightness_text)) + + log.info( + "Video resolution (text) is " + str(conn.protocol.video_input_resolution_text) + ) + log.info( + "Audio input channels (text) is " + str(conn.protocol.audio_input_channels_text) + ) + log.info( + "Audio input format (text) is " + str(conn.protocol.audio_input_format_text) + ) + log.info( + "Audio listening mode (text) is " + str(conn.protocol.audio_listening_mode_text) + ) - log.info('Video resolution (text) is '+str(conn.protocol.video_input_resolution_text)) - log.info('Audio input channels (text) is '+str(conn.protocol.audio_input_channels_text)) - log.info('Audio input format (text) is '+str(conn.protocol.audio_input_format_text)) - log.info('Audio listening mode (text) is '+str(conn.protocol.audio_listening_mode_text)) -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() asyncio.async(test()) diff --git a/setup.py b/setup.py index 26f52c3..4ba4ac2 100644 --- a/setup.py +++ b/setup.py @@ -2,34 +2,37 @@ """Setup for anthemav module.""" from setuptools import setup + def readme(): """Return README file as a string.""" - with open('README.rst', 'r') as f: + with open("README.rst", "r") as f: return f.read() + setup( - name='anthemav', - version='1.1.10', - author='David McNett', - author_email='nugget@macnugget.org', - url='https://github.com/nugget/python-anthemav', + name="anthemav", + version="1.1.10", + author="David McNett", + author_email="nugget@macnugget.org", + url="https://github.com/nugget/python-anthemav", license="LICENSE", - packages=['anthemav'], + packages=["anthemav"], scripts=[], - description='Python API for controlling Anthem Receivers', + description="Python API for controlling Anthem Receivers", long_description=readme(), classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", ], include_package_data=True, zip_safe=True, - entry_points={ - 'console_scripts': [ 'anthemav_monitor = anthemav.tools:monitor', ] - } + "console_scripts": [ + "anthemav_monitor = anthemav.tools:monitor", + ] + }, ) diff --git a/tests/fulltests.py b/tests/fulltests.py index 49b61e6..c76d434 100755 --- a/tests/fulltests.py +++ b/tests/fulltests.py @@ -6,20 +6,21 @@ import anthemav -@asyncio.coroutine -def test(): + +async def test(): log = logging.getLogger(__name__) def log_callback(message): - log.info('Callback invoked: %s' % message) + log.info("Callback invoked: %s" % message) - host = '127.0.0.1' + host = "127.0.0.1" port = 14999 - log.info('Connecting to Anthem AVR at %s:%i' % (host, port)) + log.info("Connecting to Anthem AVR at %s:%i" % (host, port)) + + # conn = await anthemav.Connection.create(host=host,port=port,loop=loop,update_callback=log_callback,auto_reconnect=False) - # conn = yield from anthemav.Connection.create(host=host,port=port,loop=loop,update_callback=log_callback,auto_reconnect=False) -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(test())