From 31f2939f7e6a3407d4de59eb24fd89f71704f7c1 Mon Sep 17 00:00:00 2001 From: scarface-4711 Date: Sun, 15 Oct 2017 23:54:54 +0200 Subject: [PATCH] Added support for Denon AVR-4810. --- README.md | 2 +- README.rst | 2 +- denonavr/__init__.py | 2 +- denonavr/denonavr.py | 148 ++++++++---------- setup.py | 2 +- tests/test_denonavr.py | 2 +- tests/xml/AVR-4810-AppCommand.xml | 4 + tests/xml/AVR-4810-Deviceinfo.xml | 4 + .../xml/AVR-4810-formMainZone_MainZoneXml.xml | 67 ++++++++ ...VR-4810-formMainZone_MainZoneXmlStatus.xml | 4 + tests/xml/AVR-4810-formNetAudio_StatusXml.xml | 103 ++++++++++++ tests/xml/AVR-4810-formTuner_HdXml.xml | 86 ++++++++++ tests/xml/AVR-4810-formTuner_TunerXml.xml | 76 +++++++++ .../xml/AVR-4810-formZone2_Zone2XmlStatus.xml | 4 + .../xml/AVR-4810-formZone3_Zone3XmlStatus.xml | 4 + 15 files changed, 425 insertions(+), 85 deletions(-) create mode 100644 tests/xml/AVR-4810-AppCommand.xml create mode 100644 tests/xml/AVR-4810-Deviceinfo.xml create mode 100644 tests/xml/AVR-4810-formMainZone_MainZoneXml.xml create mode 100644 tests/xml/AVR-4810-formMainZone_MainZoneXmlStatus.xml create mode 100644 tests/xml/AVR-4810-formNetAudio_StatusXml.xml create mode 100644 tests/xml/AVR-4810-formTuner_HdXml.xml create mode 100644 tests/xml/AVR-4810-formTuner_TunerXml.xml create mode 100644 tests/xml/AVR-4810-formZone2_Zone2XmlStatus.xml create mode 100644 tests/xml/AVR-4810-formZone3_Zone3XmlStatus.xml diff --git a/README.md b/README.md index 822f2dc..723ce3c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # denonavr [![Build Status](https://travis-ci.org/scarface-4711/denonavr.svg?branch=master)](https://travis-ci.org/scarface-4711/denonavr) -Automation Library for Denon AVR receivers - current version 0.5.3 +Automation Library for Denon AVR receivers - current version 0.5.4 ## Installation diff --git a/README.rst b/README.rst index ada5997..94610b7 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ denonavr |Build Status| -Automation Library for Denon AVR receivers - current version 0.5.3 +Automation Library for Denon AVR receivers - current version 0.5.4 Installation ------------ diff --git a/denonavr/__init__.py b/denonavr/__init__.py index 58259fa..150c082 100644 --- a/denonavr/__init__.py +++ b/denonavr/__init__.py @@ -18,7 +18,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) __title__ = "denonavr" -__version__ = "0.5.3" +__version__ = "0.5.4" def discover(): diff --git a/denonavr/denonavr.py b/denonavr/denonavr.py index eb6b593..29004f7 100644 --- a/denonavr/denonavr.py +++ b/denonavr/denonavr.py @@ -40,6 +40,10 @@ "Internet Radio", "Favorites", "SpotifyConnect", "Flickr", "NET/USB", "Music Server", "NETWORK", "NET") +# Image URLs +STATIC_ALBUM_URL = "http://{host}/img/album%20art_S.png" +ALBUM_COVERS_URL = "http://{host}/NetAudio/art.asp-jpg?{time}" + # General URLs APPCOMMAND_URL = "/goform/AppCommand.xml" DEVICEINFO_URL = "/goform/Deviceinfo.xml" @@ -169,8 +173,7 @@ class DenonAVR(object): """Representing a Denon AVR Device.""" - # pylint: disable=too-many-instance-attributes - # pylint: disable=too-many-public-methods + # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self, host, name=None, show_all_inputs=False, timeout=2.0, add_zones=NO_ZONES): @@ -210,8 +213,7 @@ def __init__(self, host, name=None, show_all_inputs=False, timeout=2.0, self._favorite_func_list = [] self._state = None self._power = None - self._image_url = ( - "http://{host}/img/album%20art_S.png".format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) self._title = None self._artist = None self._album = None @@ -289,17 +291,18 @@ def update(self): Method queries device via HTTP and updates instance attributes. Returns "True" on success and "False" on fail. """ - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches,too-many-statements # If name is not set yet, get it from Main Zone URL if self._name is None and self._urls.mainzone is not None: name_tag = {"FriendlyName": None} try: root = self.get_status_xml(self._urls.mainzone) + except (ValueError, + requests.exceptions.RequestException): + _LOGGER.error("Receiver name could not be determined.") + else: # Get the tags from this XML name_tag = self._get_status_from_xml_tags(root, name_tag) - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Receiver name could not be determined") # Set all tags to be evaluated relevant_tags = {"Power": None, "InputFuncSelect": None, "Mute": None, @@ -308,15 +311,28 @@ def update(self): # Get status XML from Denon receiver via HTTP try: root = self.get_status_xml(self._urls.status) - # Get the tags from this XML - relevant_tags = self._get_status_from_xml_tags(root, relevant_tags) except ValueError: pass - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except requests.exceptions.RequestException: # On timeout and connection error, the device is probably off self._power = POWER_OFF + else: + # Get the tags from this XML + relevant_tags = self._get_status_from_xml_tags(root, relevant_tags) + # Second option to update variables from different source + if relevant_tags and self._power != POWER_OFF: + try: + root = self.get_status_xml(self._urls.mainzone) + except (ValueError, + requests.exceptions.RequestException): + pass + else: + # Get the tags from this XML + relevant_tags = self._get_status_from_xml_tags(root, + relevant_tags) + + # Error message if still some variables are not updated yet if relevant_tags and self._power != POWER_OFF: _LOGGER.error("Missing status information from XML of %s for: %s", self._zone, ", ".join(relevant_tags.keys())) @@ -339,10 +355,7 @@ def update(self): self._band = None self._frequency = None self._station = None - if self._image_url != ("http://{host}/img/album%20art_S.png" - .format(host=self._host)): - self._image_url = ("http://{host}/img/album%20art_S.png" - .format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) else: self._state = STATE_OFF self._title = None @@ -377,8 +390,7 @@ def _update_input_func_list(self): # For structural information of the variables please see the methods try: receiver_sources = self._get_receiver_sources() - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except (ValueError, requests.exceptions.RequestException): # If connection error occurred, update failed _LOGGER.error("Connection error: Receiver sources list empty. " "Please check if device is powered on.") @@ -484,8 +496,7 @@ def _get_renamed_deleted_sources(self): root = self.get_status_xml(self._urls.mainzone) else: return (renamed_sources, deleted_sources) - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except (ValueError, requests.exceptions.RequestException): return (renamed_sources, deleted_sources) # Get the relevant tags from XML structure @@ -568,8 +579,7 @@ def _get_renamed_deleted_sourcesapp(self): try: res = self.send_post_command( self._urls.appcommand, body.getvalue()) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except requests.exceptions.RequestException: _LOGGER.error("No connection to host %s. Renamed and deleted " "sources could not be determined.", self._host) body.close() @@ -641,6 +651,7 @@ def _get_receiver_sources(self): # In this case there is no AVR-X device self._avr_x = False + # Not an AVR-X device, start determination of sources if self._avr_x is False: # Sources list is equal to list of renamed sources. non_x_sources, deleted_non_x_sources, status_success = ( @@ -660,8 +671,8 @@ def _get_receiver_sources(self): non_x_sources.pop("SOURCE", None) return non_x_sources + # Following source determination of AVR-X receivers else: - # Following source determination of AVR-X receivers # receiver_sources is of type dict with "FuncName" as key and # "DefaultName" as value. receiver_sources = {} @@ -694,14 +705,12 @@ def _update_media_data(self): Internal method which queries device via HTTP to update media information (title, artist, etc.) and URL of cover image. """ - # pylint: disable=too-many-branches - # pylint: disable=too-many-statements + # pylint: disable=too-many-branches,too-many-statements # Use different query URL based on selected source if self._input_func in self._netaudio_func_list: try: root = self.get_status_xml(self._urls.netaudiostatus) - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except (ValueError, requests.exceptions.RequestException): return False # Get the relevant tags from XML structure @@ -715,10 +724,8 @@ def _update_media_data(self): child[4].text is not None) else None): # Refresh cover with a new time stamp for media URL # when track is changing - self._image_url = ( - "http://{host}/NetAudio/art.asp-jpg?{time}" - .format(host=self._host, time=int(time.time())) - ) + self._image_url = (ALBUM_COVERS_URL.format( + host=self._host, time=int(time.time()))) # On track change assume device is PLAYING self._state = STATE_PLAYING self._title = html.unescape(child[1].text) if ( @@ -734,8 +741,7 @@ def _update_media_data(self): elif self._input_func == "Tuner" or self._input_func == "TUNER": try: root = self.get_status_xml(self._urls.tunerstatus) - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except (ValueError, requests.exceptions.RequestException): return False # Get the relevant tags from XML structure @@ -754,15 +760,12 @@ def _update_media_data(self): self._state = STATE_PLAYING # No special cover, using a static one - if self._image_url != ("http://{host}/img/album%20art_S.png" - .format(host=self._host)): - self._image_url = ("http://{host}/img/album%20art_S.png" - .format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) + elif self._input_func == "HD Radio" or self._input_func == "HDRADIO": try: root = self.get_status_xml(self._urls.hdtunerstatus) - except (ValueError, requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except (ValueError, requests.exceptions.RequestException): return False # Get the relevant tags from XML structure @@ -790,10 +793,8 @@ def _update_media_data(self): self._state = STATE_PLAYING # No special cover, using a static one - if self._image_url != ("http://{host}/img/album%20art_S.png" - .format(host=self._host)): - self._image_url = ("http://{host}/img/album%20art_S.png" - .format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) + # No behavior implemented, so reset all variables for that source else: self._band = None @@ -805,8 +806,7 @@ def _update_media_data(self): # Assume PLAYING_DEVICE is always PLAYING self._state = STATE_PLAYING # No special cover, using a static one - self._image_url = ( - "http://{host}/img/album%20art_S.png".format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) # Finished return True @@ -1019,9 +1019,8 @@ def set_input_func(self, input_func): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: input function %s not set", + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: input function %s not set.", input_func) return False @@ -1048,9 +1047,8 @@ def _play(self): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: play command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: play command not sent.") return False def _pause(self): @@ -1067,9 +1065,8 @@ def _pause(self): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: pause command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: pause command not sent.") return False def previous_track(self): @@ -1082,10 +1079,9 @@ def previous_track(self): try: return bool(self.send_post_command( self._urls.command_netaudio_post, body)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): + except requests.exceptions.RequestException: _LOGGER.error( - "Connection error: previous track command not sent") + "Connection error: previous track command not sent.") return False def next_track(self): @@ -1098,9 +1094,8 @@ def next_track(self): try: return bool(self.send_post_command( self._urls.command_netaudio_post, body)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: next track command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: next track command not sent.") return False def power_on(self): @@ -1112,9 +1107,8 @@ def power_on(self): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: power on command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: power on command not sent.") return False def power_off(self): @@ -1126,27 +1120,24 @@ def power_off(self): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: power off command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: power off command not sent.") return False def volume_up(self): """Volume up receiver via HTTP get command.""" try: return bool(self.send_get_command(self._urls.command_volume_up)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: volume up command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: volume up command not sent.") return False def volume_down(self): """Volume down receiver via HTTP get command.""" try: return bool(self.send_get_command(self._urls.command_volume_down)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: volume down command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: volume down command not sent.") return False def set_volume(self, volume): @@ -1162,9 +1153,8 @@ def set_volume(self, volume): try: return bool(self.send_get_command( self._urls.command_set_volume % volume)) - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: set volume command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: set volume command not sent.") return False def mute(self, mute): @@ -1182,9 +1172,8 @@ def mute(self, mute): return True else: return False - except (requests.exceptions.ConnectionError, - requests.exceptions.Timeout): - _LOGGER.error("Connection error: mute command not sent") + except requests.exceptions.RequestException: + _LOGGER.error("Connection error: mute command not sent.") return False @@ -1235,8 +1224,7 @@ def __init__(self, parent_avr, zone, name): self._favorite_func_list = self._parent_avr._favorite_func_list self._state = None self._power = None - self._image_url = ( - "http://{host}/img/album%20art_S.png".format(host=self._host)) + self._image_url = (STATIC_ALBUM_URL.format(host=self._host)) self._title = None self._artist = None self._album = None diff --git a/setup.py b/setup.py index b59b7c8..2cfdaaf 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup(name='denonavr', - version='0.5.3', + version='0.5.4', description='Automation Library for Denon AVR receivers', long_description='Automation Library for Denon AVR receivers', url='https://github.com/scarface-4711/denonavr', diff --git a/tests/test_denonavr.py b/tests/test_denonavr.py index 69ecb14..f444ebf 100644 --- a/tests/test_denonavr.py +++ b/tests/test_denonavr.py @@ -24,7 +24,7 @@ "M-RC610": NO_ZONES, "AVR-X2100W-2": NO_ZONES, "AVR-X2000": ZONE2_ZONE3, "AVR-X2000-2": NO_ZONES, "SR5008": NO_ZONES, "M-CR603": NO_ZONES, - "NR1604": ZONE2_ZONE3} + "NR1604": ZONE2_ZONE3, "AVR-4810": NO_ZONES} APPCOMMAND_URL = "/goform/AppCommand.xml" STATUS_URL = "/goform/formMainZone_MainZoneXmlStatus.xml" diff --git a/tests/xml/AVR-4810-AppCommand.xml b/tests/xml/AVR-4810-AppCommand.xml new file mode 100644 index 0000000..a72ef64 --- /dev/null +++ b/tests/xml/AVR-4810-AppCommand.xml @@ -0,0 +1,4 @@ +Document Error: Data follows +

Access Error: Data follows

+

Form AppCommand.xml is not defined

+ diff --git a/tests/xml/AVR-4810-Deviceinfo.xml b/tests/xml/AVR-4810-Deviceinfo.xml new file mode 100644 index 0000000..ab384ac --- /dev/null +++ b/tests/xml/AVR-4810-Deviceinfo.xml @@ -0,0 +1,4 @@ +Document Error: Data follows +

Access Error: Data follows

+

Form Deviceinfo.xml is not defined

+ diff --git a/tests/xml/AVR-4810-formMainZone_MainZoneXml.xml b/tests/xml/AVR-4810-formMainZone_MainZoneXml.xml new file mode 100644 index 0000000..2bc7b17 --- /dev/null +++ b/tests/xml/AVR-4810-formMainZone_MainZoneXml.xml @@ -0,0 +1,67 @@ + + +ON +ON + +SOURCE +TUNER +PHONO +CD +DVD +HDP +TV +SAT/CBL +VCR +DVR +V.AUX +NET/USB +XM +SIRIUS +HDRADIO + + + +TUNER +PHONO +CD +PhonoPre +HDP +TV +WeTek +XBox One +AirPlay +V.AUX +NET/USB + + + + +MAIN ZONE + + +DEL +DEL +USE +USE +USE +USE +USE +USE +USE +USE +USE + + + + +ON +3 +1 +DVD + +DVD +Relative +-25.0 +off +OFF + diff --git a/tests/xml/AVR-4810-formMainZone_MainZoneXmlStatus.xml b/tests/xml/AVR-4810-formMainZone_MainZoneXmlStatus.xml new file mode 100644 index 0000000..b89dab8 --- /dev/null +++ b/tests/xml/AVR-4810-formMainZone_MainZoneXmlStatus.xml @@ -0,0 +1,4 @@ +Document Error: Data follows +

Access Error: Data follows

+

Form formMainZone_MainZoneXmlStatus.xml is not defined

+ diff --git a/tests/xml/AVR-4810-formNetAudio_StatusXml.xml b/tests/xml/AVR-4810-formNetAudio_StatusXml.xml new file mode 100644 index 0000000..c8a2f2c --- /dev/null +++ b/tests/xml/AVR-4810-formNetAudio_StatusXml.xml @@ -0,0 +1,103 @@ + + + +0 +8 +0 +0 +0 +0 +0 +0 +0 +0 + + +Denon AVR +Favorites +Internet Radio +Media Server +Napster + + + + [ 1/4 ] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +0 +0 +0 +0 +0 +0 +0 +3 +1 +DVD + +DVD +Relative +-25.0 +off +OFF + diff --git a/tests/xml/AVR-4810-formTuner_HdXml.xml b/tests/xml/AVR-4810-formTuner_HdXml.xml new file mode 100644 index 0000000..68dbf9d --- /dev/null +++ b/tests/xml/AVR-4810-formTuner_HdXml.xml @@ -0,0 +1,86 @@ + + + + +<value></value> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.00 +EUR + +3 +1 +DVD + +DVD +Relative +-25.0 +off +OFF + diff --git a/tests/xml/AVR-4810-formTuner_TunerXml.xml b/tests/xml/AVR-4810-formTuner_TunerXml.xml new file mode 100644 index 0000000..025d9f4 --- /dev/null +++ b/tests/xml/AVR-4810-formTuner_TunerXml.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.00 +EUR + +3 +1 +DVD + +DVD +Relative +-25.0 +off +OFF + diff --git a/tests/xml/AVR-4810-formZone2_Zone2XmlStatus.xml b/tests/xml/AVR-4810-formZone2_Zone2XmlStatus.xml new file mode 100644 index 0000000..0d85218 --- /dev/null +++ b/tests/xml/AVR-4810-formZone2_Zone2XmlStatus.xml @@ -0,0 +1,4 @@ +Document Error: Data follows +

Access Error: Data follows

+

Form formZone2_Zone2XmlStatus.xml is not defined

+ diff --git a/tests/xml/AVR-4810-formZone3_Zone3XmlStatus.xml b/tests/xml/AVR-4810-formZone3_Zone3XmlStatus.xml new file mode 100644 index 0000000..f8acf3d --- /dev/null +++ b/tests/xml/AVR-4810-formZone3_Zone3XmlStatus.xml @@ -0,0 +1,4 @@ +Document Error: Data follows +

Access Error: Data follows

+

Form formZone3_Zone3XmlStatus.xml is not defined

+