Skip to content

Commit

Permalink
Improve roon integraton (#66000)
Browse files Browse the repository at this point in the history
* Update to new library, revise discovery to work with new library, specify port to work with new library.

* Move user gui to fallback.

* Revise tests.

* Handle old config.

* Improve debugging, refresh faster on load.

* Remove duplicate.

* Bump library version.

* Fix docstring per review.

* Review suggestion

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Review suggestion

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add check for duplicate host.

* Add error message to strings.

* Tidy.

* Review changes.

* Remove default.

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
2 people authored and balloob committed Apr 20, 2022
1 parent bd02895 commit 0b5b7d5
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 38 deletions.
9 changes: 6 additions & 3 deletions homeassistant/components/roon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from .const import DOMAIN
from .const import CONF_ROON_NAME, DOMAIN
from .server import RoonServer


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a roonserver from a config entry."""
hass.data.setdefault(DOMAIN, {})
host = entry.data[CONF_HOST]

# fallback to using host for compatibility with older configs
name = entry.data.get(CONF_ROON_NAME, entry.data[CONF_HOST])

roonserver = RoonServer(hass, entry)

if not await roonserver.async_setup():
Expand All @@ -23,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Roonlabs",
name=host,
name=f"Roon Core ({name})",
)
return True

Expand Down
53 changes: 40 additions & 13 deletions homeassistant/components/roon/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv

from .const import (
AUTHENTICATE_TIMEOUT,
CONF_ROON_ID,
CONF_ROON_NAME,
DEFAULT_NAME,
DOMAIN,
ROON_APPINFO,
)

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({vol.Required("host"): str})
DATA_SCHEMA = vol.Schema(
{
vol.Required("host"): cv.string,
vol.Required("port", default=9330): cv.port,
}
)

TIMEOUT = 120

Expand All @@ -45,7 +52,7 @@ def get_discovered_servers(discovery):
_LOGGER.debug("Servers = %s", servers)
return servers

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

def stop_apis(apis):
Expand All @@ -54,14 +61,15 @@ def stop_apis(apis):

token = None
core_id = None
core_name = None
secs = 0
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)]
apis = [RoonApi(ROON_APPINFO, None, host, port, 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.
Expand All @@ -71,14 +79,15 @@ def stop_apis(apis):
secs += AUTHENTICATE_TIMEOUT
if auth_api:
core_id = auth_api[0].core_id
core_name = auth_api[0].core_name
token = auth_api[0].token
break

await asyncio.sleep(AUTHENTICATE_TIMEOUT)

await self._hass.async_add_executor_job(stop_apis, apis)

return (token, core_id)
return (token, core_id, core_name)


async def discover(hass):
Expand All @@ -90,15 +99,21 @@ async def discover(hass):
return servers


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

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

return {CONF_HOST: host, CONF_ROON_ID: core_id, CONF_API_KEY: token}
return {
CONF_HOST: host,
CONF_PORT: port,
CONF_ROON_ID: core_id,
CONF_ROON_NAME: core_name,
CONF_API_KEY: token,
}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Expand All @@ -109,33 +124,45 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the Roon flow."""
self._host = None
self._port = None
self._servers = []

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

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()

return await self.async_step_fallback()

async def async_step_fallback(self, user_input=None):
"""Get host and port details from the user."""
errors = {}

if user_input is not None:
self._host = user_input["host"]
self._port = user_input["port"]
return await self.async_step_link()

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="fallback", data_schema=DATA_SCHEMA, errors=errors
)

async def async_step_link(self, user_input=None):
"""Handle linking and authenticting with the roon server."""

errors = {}
if user_input is not None:
# Do not authenticate if the host is already configured
self._async_abort_entries_match({CONF_HOST: self._host})

try:
info = await authenticate(self.hass, self._host, self._servers)
info = await authenticate(
self.hass, self._host, self._port, self._servers
)

except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/roon/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DOMAIN = "roon"

CONF_ROON_ID = "roon_server_id"
CONF_ROON_NAME = "roon_server_name"

DATA_CONFIGS = "roon_configs"

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/roon/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "RoonLabs music player",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roon",
"requirements": ["roonapi==0.0.38"],
"requirements": ["roonapi==0.1.1"],
"codeowners": ["@pavoni"],
"iot_class": "local_push",
"loggers": ["roonapi"]
Expand Down
42 changes: 30 additions & 12 deletions homeassistant/components/roon/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import asyncio
import logging

from roonapi import RoonApi
from roonapi import RoonApi, RoonDiscovery

from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.dt import utcnow

from .const import CONF_ROON_ID, ROON_APPINFO

_LOGGER = logging.getLogger(__name__)
INITIAL_SYNC_INTERVAL = 5
FULL_SYNC_INTERVAL = 30
PLATFORMS = [Platform.MEDIA_PLAYER]

Expand All @@ -33,23 +34,38 @@ def __init__(self, hass, config_entry):

async def async_setup(self, tries=0):
"""Set up a roon server based on config parameters."""

def get_roon_host():
host = self.config_entry.data.get(CONF_HOST)
port = self.config_entry.data.get(CONF_PORT)
if host:
_LOGGER.debug("static roon core host=%s port=%s", host, port)
return (host, port)

discover = RoonDiscovery(core_id)
server = discover.first()
discover.stop()
_LOGGER.debug("dynamic roon core core_id=%s server=%s", core_id, server)
return (server[0], server[1])

def get_roon_api():
token = self.config_entry.data[CONF_API_KEY]
(host, port) = get_roon_host()
return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True)

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]
# 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 = await self.hass.async_add_executor_job(get_roon_api)

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
self.roon_id = (
core_id if core_id is not None else self.config_entry.data[CONF_HOST]
)

# initialize media_player platform
hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS)
Expand Down Expand Up @@ -98,13 +114,14 @@ def roonapi_state_callback(self, event, changed_zones):
async def async_do_loop(self):
"""Background work loop."""
self._exit = False
await asyncio.sleep(INITIAL_SYNC_INTERVAL)
while not self._exit:
await self.async_update_players()
# await self.async_update_playlists()
await asyncio.sleep(FULL_SYNC_INTERVAL)

async def async_update_changed_players(self, changed_zones_ids):
"""Update the players which were reported as changed by the Roon API."""
_LOGGER.debug("async_update_changed_players %s", changed_zones_ids)
for zone_id in changed_zones_ids:
if zone_id not in self.roonapi.zones:
# device was removed ?
Expand All @@ -127,6 +144,7 @@ async def async_update_changed_players(self, changed_zones_ids):
async def async_update_players(self):
"""Periodic full scan of all devices."""
zone_ids = self.roonapi.zones.keys()
_LOGGER.debug("async_update_players %s", zone_ids)
await self.async_update_changed_players(zone_ids)
# check for any removed devices
all_devs = {}
Expand Down
8 changes: 5 additions & 3 deletions homeassistant/components/roon/strings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"config": {
"step": {
"user": {
"description": "Could not discover Roon server, please enter your the Hostname or IP.",
"user": {},
"fallback": {
"description": "Could not discover Roon server, please enter your Hostname and Port.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"link": {
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2073,7 +2073,7 @@ rokuecp==0.16.0
roombapy==1.6.5

# homeassistant.components.roon
roonapi==0.0.38
roonapi==0.1.1

# homeassistant.components.rova
rova==0.3.0
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,7 @@ rokuecp==0.16.0
roombapy==1.6.5

# homeassistant.components.roon
roonapi==0.0.38
roonapi==0.1.1

# homeassistant.components.rpi_power
rpi-bad-power==0.1.0
Expand Down
Loading

0 comments on commit 0b5b7d5

Please sign in to comment.