Skip to content

Commit

Permalink
fix: device discovery (#12)
Browse files Browse the repository at this point in the history
Use the included SSDP device discovery in the denonavr library.
  • Loading branch information
zehnm authored Oct 25, 2023
1 parent 4fcb7a0 commit fae29ee
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 105 deletions.
16 changes: 12 additions & 4 deletions driver.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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."
}
}
}
Expand Down
95 changes: 8 additions & 87 deletions intg-denonavr/avr.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,13 @@
import asyncio
import logging
import re
import socket
from enum import IntEnum

import denonavr
import denonavr.exceptions
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):
Expand All @@ -51,88 +38,22 @@ 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.
: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:
Expand Down Expand Up @@ -273,15 +194,15 @@ 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)

self.getting_data = False
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:
Expand Down
21 changes: 12 additions & 9 deletions intg-denonavr/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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",
},
}
],
)
Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit fae29ee

Please sign in to comment.