Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pyroon discovery #44811

Merged
merged 9 commits into from
Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 69 additions & 25 deletions homeassistant/components/roon/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import asyncio
import logging

from roonapi import RoonApi
from roonapi import RoonApi, RoonDiscovery
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_HOST

from .const import ( # pylint: disable=unused-import
AUTHENTICATE_TIMEOUT,
CONF_ROON_ID,
DEFAULT_NAME,
DOMAIN,
ROON_APPINFO,
Expand All @@ -25,36 +26,79 @@
class RoonHub:
"""Interact with roon during config flow."""

def __init__(self, host):
"""Initialize."""
self._host = host
def __init__(self, hass):
"""Initialise the RoonHub."""
self._hass = hass

async def discover(self):
"""Try and discover roon servers."""

def get_discovered_servers(discovery):
servers = discovery.all()
discovery.stop()
return servers

discovery = RoonDiscovery(None)
servers = await self._hass.async_add_executor_job(
get_discovered_servers, discovery
)
_LOGGER.debug("Servers = %s", servers)
return servers

async def authenticate(self, host, servers):
"""Authenticate with one or more roon servers."""

def stop_apis(apis):
for api in apis:
api.stop()

async def authenticate(self, hass) -> bool:
"""Test if we can authenticate with the host."""
token = None
core_id = None
secs = 0
roonapi = RoonApi(ROON_APPINFO, None, self._host, blocking_init=False)
while secs < TIMEOUT:
token = roonapi.token
if host is None:
apis = [
RoonApi(ROON_APPINFO, None, server[0], server[1], blocking_init=False)
for server in servers
]
else:
apis = [RoonApi(ROON_APPINFO, None, host, blocking_init=False)]

while secs <= TIMEOUT:
# Roon can discover multiple devices - not all of which are proper servers, so try and authenticate with them all.
# The user will only enable one - so look for a valid token
auth_api = [api for api in apis if api.token is not None]

secs += AUTHENTICATE_TIMEOUT
if token:
if auth_api:
core_id = auth_api[0].core_id
token = auth_api[0].token
break

await asyncio.sleep(AUTHENTICATE_TIMEOUT)

token = roonapi.token
roonapi.stop()
return token
await self._hass.async_add_executor_job(stop_apis, apis)

return (token, core_id)


async def authenticate(hass: core.HomeAssistant, host):
async def discover(hass):
"""Connect and authenticate home assistant."""

hub = RoonHub(host)
token = await hub.authenticate(hass)
hub = RoonHub(hass)
servers = await hub.discover()

return servers


async def authenticate(hass: core.HomeAssistant, host, servers):
"""Connect and authenticate home assistant."""

hub = RoonHub(hass)
(token, core_id) = await hub.authenticate(host, servers)
if token is None:
raise InvalidAuth

return {CONF_HOST: host, CONF_API_KEY: token}
return {CONF_HOST: host, CONF_ROON_ID: core_id, CONF_API_KEY: token}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Expand All @@ -66,20 +110,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the Roon flow."""
self._host = None
self._servers = []

async def async_step_user(self, user_input=None):
"""Handle getting host details from the user."""

errors = {}
self._servers = await discover(self.hass)

# We discovered one or more roon - so skip to authentication
if self._servers:
return await self.async_step_link()

if user_input is not None:
self._host = user_input["host"]
existing = {
entry.data[CONF_HOST] for entry in self._async_current_entries()
}
if self._host in existing:
errors["base"] = "duplicate_entry"
return self.async_show_form(step_id="user", errors=errors)

return await self.async_step_link()

return self.async_show_form(
Expand All @@ -92,7 +136,7 @@ async def async_step_link(self, user_input=None):
errors = {}
if user_input is not None:
try:
info = await authenticate(self.hass, self._host)
info = await authenticate(self.hass, self._host, self._servers)
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/roon/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

DOMAIN = "roon"

CONF_ROON_ID = "roon_server_id"

DATA_CONFIGS = "roon_configs"

DEFAULT_NAME = "Roon Labs Music Player"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/roon/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def device_info(self):
"name": self.name,
"manufacturer": "RoonLabs",
"model": dev_model,
"via_hub": (DOMAIN, self._server.host),
"via_hub": (DOMAIN, self._server.roon_id),
}

def update_data(self, player_data=None):
Expand Down
29 changes: 17 additions & 12 deletions homeassistant/components/roon/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.dt import utcnow

from .const import ROON_APPINFO
from .const import CONF_ROON_ID, ROON_APPINFO

_LOGGER = logging.getLogger(__name__)
FULL_SYNC_INTERVAL = 30
Expand All @@ -22,28 +22,33 @@ def __init__(self, hass, config_entry):
self.config_entry = config_entry
self.hass = hass
self.roonapi = None
self.roon_id = None
self.all_player_ids = set()
self.all_playlists = []
self.offline_devices = set()
self._exit = False
self._roon_name_by_id = {}

@property
def host(self):
"""Return the host of this server."""
return self.config_entry.data[CONF_HOST]

async def async_setup(self, tries=0):
"""Set up a roon server based on host parameter."""
host = self.host
"""Set up a roon server based on config parameters."""
hass = self.hass
# Host will be None for configs using discovery
host = self.config_entry.data[CONF_HOST]
token = self.config_entry.data[CONF_API_KEY]
_LOGGER.debug("async_setup: %s %s", token, host)
self.roonapi = RoonApi(ROON_APPINFO, token, host, blocking_init=False)
# Default to None for compatibility with older configs
core_id = self.config_entry.data.get(CONF_ROON_ID)
_LOGGER.debug("async_setup: host=%s core_id=%s token=%s", host, core_id, token)

self.roonapi = RoonApi(
ROON_APPINFO, token, host, blocking_init=False, core_id=core_id
)
self.roonapi.register_state_callback(
self.roonapi_state_callback, event_filter=["zones_changed"]
)

# Default to 'host' for compatibility with older configs without core_id
self.roon_id = core_id if core_id is not None else host

# initialize media_player platform
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
Expand Down Expand Up @@ -152,11 +157,11 @@ async def async_create_player_data(self, zone, output):
new_dict = zone.copy()
new_dict.update(output)
new_dict.pop("outputs")
new_dict["host"] = self.host
new_dict["roon_id"] = self.roon_id
new_dict["is_synced"] = len(zone["outputs"]) > 1
new_dict["zone_name"] = zone["display_name"]
new_dict["display_name"] = output["display_name"]
new_dict["last_changed"] = utcnow()
# we don't use the zone_id or output_id for now as unique id as I've seen cases were it changes for some reason
new_dict["dev_id"] = f"roon_{self.host}_{output['display_name']}"
new_dict["dev_id"] = f"roon_{self.roon_id}_{output['display_name']}"
return new_dict
5 changes: 2 additions & 3 deletions homeassistant/components/roon/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "Please enter your Roon server Hostname or IP.",
"description": "Could not discover Roon server, please enter your the Hostname or IP.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
Expand All @@ -14,8 +14,7 @@
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"duplicate_entry": "That host has already been added."
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
Expand Down
Loading