From 0c722637587b21aa0c7b891befc6664b4eadebf6 Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Wed, 25 Oct 2023 10:55:07 +0200 Subject: [PATCH] fix: device discovery Use the included SSDP device discovery in the denonavr library. --- driver.json | 16 +++++-- intg-denonavr/avr.py | 95 ++++------------------------------------- intg-denonavr/driver.py | 21 +++++---- tox.ini | 10 ++--- 4 files changed, 37 insertions(+), 105 deletions(-) diff --git a/driver.json b/driver.json index 8beb060..7f19e39 100644 --- a/driver.json +++ b/driver.json @@ -5,7 +5,9 @@ "name": { "en": "Denon AVR" }, "icon": "uc:integration", "description": { - "en": "Control your Denon or Marantz AVRs with Remote Two." + "en": "Control your Denon or Marantz AVRs with Remote Two.", + "de": "Steuere Denon oder Marantz AVRs mit Remote Two.", + "fr": "Contrôler Denon ou Marantz AVRs avec Remote Two." }, "developer": { "name": "Unfolded Circle ApS", @@ -15,18 +17,24 @@ "home_page": "https://www.unfoldedcircle.com", "setup_data_schema": { "title": { - "en": "Integration setup" + "en": "Integration setup", + "de": "Integrationssetup", + "fr": "Configuration de l'intégration" }, "settings": [ { "id": "info", "label": { - "en": "Setup process" + "en": "Setup process", + "de": "Setup Fortschritt", + "fr": "Progrès de la configuration" }, "field": { "label": { "value": { - "en": "The integration will discover your Denon or Marantz AVRs on your network." + "en": "The integration will discover your Denon or Marantz AVRs on your network.", + "de": "Diese Integration wird Denon und Marantz AVRs im Netzwerk finden.", + "fr": "Cette intégration trouvera des AVR Denon et Marantz dans le réseau." } } } diff --git a/intg-denonavr/avr.py b/intg-denonavr/avr.py index eaddef7..73568ca 100644 --- a/intg-denonavr/avr.py +++ b/intg-denonavr/avr.py @@ -9,7 +9,6 @@ import asyncio import logging import re -import socket from enum import IntEnum import denonavr @@ -17,18 +16,6 @@ from pyee import AsyncIOEventEmitter LOG = logging.getLogger(__name__) -LOG.setLevel(logging.DEBUG) - -MCAST_GRP = "239.255.255.250" -MCAST_PORT = 1900 -# is this correct? denonavr uses 2 -SSDP_MX = 3 - -SSDP_DEVICES = [ - "urn:schemas-upnp-org:device:MediaRenderer:1", - "urn:schemas-upnp-org:device:MediaServer:1", - "urn:schemas-denon-com:device:AiosDevice:1", -] class EVENTS(IntEnum): @@ -51,21 +38,6 @@ class STATES(IntEnum): PAUSED = 3 -def ssdp_request(ssdp_st: str, ssdp_mx: float = SSDP_MX) -> bytes: - """Return request bytes for given st and mx.""" - return "\r\n".join( - [ - "M-SEARCH * HTTP/1.1", - f"ST: {ssdp_st}", - f"MX: {ssdp_mx:d}", - 'MAN: "ssdp:discover"', - f"HOST: {MCAST_GRP}:{MCAST_PORT}", - "", - "", - ] - ).encode("utf-8") - - async def discover_denon_avrs(): """ Discover Denon AVRs on the network with SSDP. @@ -73,66 +45,15 @@ async def discover_denon_avrs(): :return: array of device information objects. """ LOG.debug("Starting discovery") - res = [] - - for ssdp_device in SSDP_DEVICES: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(3) - - try: - request = ssdp_request(ssdp_device) - sock.sendto(request, (MCAST_GRP, MCAST_PORT)) - - while True: - try: - data, addr = sock.recvfrom(1024) - LOG.info("Found SSDP device at %s: %s", addr, data.decode()) - # TODO pre-filter out known non-Denon devices. Check keys: hue-bridgeid, X-RINCON-HOUSEHOLD - # LOG.debug("-"*30) - - info = await get_denon_info(addr[0]) - if info: - LOG.info("Found Denon device %s", info) - res.append(info) - except socket.timeout: - break - finally: - sock.close() - - LOG.debug("Discovery finished") - return res - - -async def get_denon_info(ipaddress): - """ - Connect to the given IP address of a Denon AVR and retrieve model information. - - :param ipaddress: IP address of receiver to fetch information from. - :return: object with `id`, `manufacturer`, `model`, `name` and `ipaddress` - """ - LOG.debug("Trying to get device info for %s", ipaddress) - d = None - try: - d = denonavr.DenonAVR(ipaddress) - except denonavr.exceptions.DenonAvrError as e: - LOG.error("[%s] Failed to get device info. Maybe not a Denon device. %s", ipaddress, e) - return None + avrs = await denonavr.async_discover() + if not avrs: + LOG.info("No AVRs discovered") + return [] - try: - await d.async_setup() - await d.async_update() - except denonavr.exceptions.DenonAvrError as e: - LOG.error("[%s] Error initializing device: %s", ipaddress, e) - return None + LOG.info("Found AVR(s): %s", avrs) - return { - "id": d.serial_number, - "manufacturer": d.manufacturer, - "model": d.model_name, - "name": d.name, - "ipaddress": ipaddress, - } + return avrs class DenonAVR: @@ -273,7 +194,7 @@ async def _get_data(self): "artwork": self.artwork, }, ) - LOG.debug("Track data, artist: " + self.artist + " title: " + self.title + " artwork: " + self.artwork) + LOG.debug("Track data: artist: %s title: %s artwork: %s", self.artist, self.title, self.artwork) except denonavr.exceptions.DenonAvrError as e: LOG.error("Failed to get latest status information: %s", e) @@ -281,7 +202,7 @@ async def _get_data(self): LOG.debug("Getting track data done.") async def _update_callback(self, zone, event, parameter): - LOG.debug("Zone: " + zone + " Event: " + event + " Parameter: " + parameter) + LOG.debug("Zone: %s Event: %s Parameter: %s", zone, event, parameter) try: await self._avr.async_update() except denonavr.exceptions.DenonAvrError as e: diff --git a/intg-denonavr/driver.py b/intg-denonavr/driver.py index 8903ef5..1291e70 100644 --- a/intg-denonavr/driver.py +++ b/intg-denonavr/driver.py @@ -16,9 +16,8 @@ import ucapi.api as uc from ucapi import entities -LOG = logging.getLogger(__name__) +LOG = logging.getLogger("driver") # avoid having __main__ in log messages LOOP = asyncio.get_event_loop() -LOG.setLevel(logging.DEBUG) CFG_FILENAME = "config.json" # Global variables @@ -86,17 +85,13 @@ async def _on_setup_driver(websocket, req_id, _data): avrs = await avr.discover_denon_avrs() dropdown_items = [] - LOG.debug(avrs) - for a in avrs: - tv_data = {"id": a["ipaddress"], "label": {"en": a["name"] + " " + a["manufacturer"] + " " + a["model"]}} - + tv_data = {"id": a["host"], "label": {"en": f"{a['friendlyName']} ({a['modelName']}) [{a['host']}]"}} dropdown_items.append(tv_data) if not dropdown_items: LOG.warning("No AVRs found") await api.driverSetupError(websocket) - # TODO START AGAIN return await api.requestDriverSetupUserInput( @@ -106,7 +101,11 @@ async def _on_setup_driver(websocket, req_id, _data): { "field": {"dropdown": {"value": dropdown_items[0]["id"], "items": dropdown_items}}, "id": "choice", - "label": {"en": "Choose your Denon AVR"}, + "label": { + "en": "Choose your Denon AVR", + "de": "Wähle deinen Denon AVR", + "fr": "Choisissez votre Denon AVR", + }, } ], ) @@ -324,7 +323,7 @@ async def _handle_avr_update(entity_id, update): configured_entity = api.configuredEntities.getEntity(entity_id) - LOG.debug(update) + LOG.debug("AVR update: %s", update) if "state" in update: state = _get_media_player_state(update["state"]) @@ -452,6 +451,10 @@ async def main(): """Start the Remote Two integration driver.""" global CFG_FILE_PATH + level = os.getenv("UC_LOG_LEVEL", "DEBUG").upper() + logging.getLogger("avr").setLevel(level) + logging.getLogger("driver").setLevel(level) + path = api.configDirPath CFG_FILE_PATH = os.path.join(path, CFG_FILENAME) diff --git a/tox.ini b/tox.ini index 11c0f7e..cf9c78e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,22 +10,22 @@ basepython = python3.11 deps = -r{toxinidir}/test-requirements.txt commands = - python -m isort *.py --check --verbose - python -m black *.py --check --verbose + python -m isort intg-denonavr/. --check --verbose + python -m black intg-denonavr --check --verbose [testenv:pylint] basepython = python3.11 deps = -r{toxinidir}/test-requirements.txt -commands=python -m pylint *.py +commands=python -m pylint intg-denonavr [testenv:lint] basepython = python3.11 deps = -r{toxinidir}/test-requirements.txt commands = - python -m flake8 *.py -; python -m pydocstyle *.py + python -m flake8 intg-denonavr +; python -m pydocstyle intg-denonavr ;[testenv] ;setenv =