hass-core/homeassistant/components/squeezebox/config_flow.py
rajlaud 3f427602ba
Squeezebox config flow (#35669)
* Squeezebox add config flow and player discovery

* Fixes to config flow

* Unavailable player detection and recovery

* Improved error message for auth failure

* Testing for squeezebox config flow

* Import configuration.yaml

* Support for discovery integration

* Internal server discovery

* Fix bug restoring previously detected squeezebox player

* Tests for user and edit steps in config flow

* Tests for import config flow

* Additional config flow tests and fixes

* Linter fixes

* Check that players are found before iterating them

* Remove noisy logger message

* Update requirements_all after rebase

* Use asyncio.Event in discovery task

* Use common keys in strings.json

* Bump pysqueezebox to v0.2.2 for fixed server discovery using python3.7

* Bump pysqueezebox version to v0.2.3

* Don't trap AbortFlow exception

Co-authored-by: J. Nick Koston <nick@koston.org>

* Refactor validate_input

* Update squeezebox tests

* Build data flow schema using function

* Fix linter error

* Updated en.json

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update .coveragerc for squeezebox config flow test

* Mock TIMEOUT for faster testing

* More schema de-duplication and testing improvements

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Testing and config flow improvements

* Remove unused exceptions

* Remove deprecated logger message

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Implement suggestions from code review

* Add async_unload_entry

* Use MockConfigEntry in squeezebox tests

* Remove unnecessary config schema

* Stop server discovery task when last config entry unloaded

* Improvements to async_unload_entry

* Fix bug in _discovery arguments

* Do not await server discovery in async_setup_entry

* Do not await start server discovery in async_setup

* Do not start server discovery from async_setup_entry until homeassistant running

* Re-detect players when server removed and re-added without restart

* Use entry.entry_id instead of unique_id

* Update unittests to avoid patching homeassistant code

Co-authored-by: J. Nick Koston <nick@koston.org>
2020-06-22 09:29:01 -05:00

189 lines
6.3 KiB
Python

"""Config flow for Logitech Squeezebox integration."""
import asyncio
import logging
from pysqueezebox import Server, async_discover
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
HTTP_UNAUTHORIZED,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
# pylint: disable=unused-import
from .const import DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
TIMEOUT = 5
def _base_schema(discovery_info=None):
"""Generate base schema."""
base_schema = {}
if discovery_info and CONF_HOST in discovery_info:
base_schema.update(
{
vol.Required(
CONF_HOST,
description={"suggested_value": discovery_info[CONF_HOST]},
): str,
}
)
else:
base_schema.update({vol.Required(CONF_HOST): str})
if discovery_info and CONF_PORT in discovery_info:
base_schema.update(
{
vol.Required(
CONF_PORT,
default=DEFAULT_PORT,
description={"suggested_value": discovery_info[CONF_PORT]},
): int,
}
)
else:
base_schema.update({vol.Required(CONF_PORT, default=DEFAULT_PORT): int})
base_schema.update(
{vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str}
)
return vol.Schema(base_schema)
class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Logitech Squeezebox."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize an instance of the squeezebox config flow."""
self.data_schema = _base_schema()
self.discovery_info = None
async def _discover(self, uuid=None):
"""Discover an unconfigured LMS server."""
self.discovery_info = None
discovery_event = asyncio.Event()
def _discovery_callback(server):
if server.uuid:
# ignore already configured uuids
for entry in self._async_current_entries():
if entry.unique_id == server.uuid:
return
self.discovery_info = {
CONF_HOST: server.host,
CONF_PORT: server.port,
"uuid": server.uuid,
}
_LOGGER.debug("Discovered server: %s", self.discovery_info)
discovery_event.set()
discovery_task = self.hass.async_create_task(
async_discover(_discovery_callback)
)
await discovery_event.wait()
discovery_task.cancel() # stop searching as soon as we find server
# update with suggested values from discovery
self.data_schema = _base_schema(self.discovery_info)
async def _validate_input(self, data):
"""
Validate the user input allows us to connect.
Retrieve unique id and abort if already configured.
"""
server = Server(
async_get_clientsession(self.hass),
data[CONF_HOST],
data[CONF_PORT],
data.get(CONF_USERNAME),
data.get(CONF_PASSWORD),
)
try:
status = await server.async_query("serverstatus")
if not status:
if server.http_status == HTTP_UNAUTHORIZED:
return "invalid_auth"
return "cannot_connect"
except Exception: # pylint: disable=broad-except
return "unknown"
if "uuid" in status:
await self.async_set_unique_id(status["uuid"])
self._abort_if_unique_id_configured()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input and CONF_HOST in user_input:
# update with host provided by user
self.data_schema = _base_schema(user_input)
return await self.async_step_edit()
# no host specified, see if we can discover an unconfigured LMS server
try:
await asyncio.wait_for(self._discover(), timeout=TIMEOUT)
return await self.async_step_edit()
except asyncio.TimeoutError:
errors["base"] = "no_server_found"
# display the form
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Optional(CONF_HOST): str}),
errors=errors,
)
async def async_step_edit(self, user_input=None):
"""Edit a discovered or manually inputted server."""
errors = {}
if user_input:
error = await self._validate_input(user_input)
if error:
errors["base"] = error
else:
return self.async_create_entry(
title=user_input[CONF_HOST], data=user_input
)
return self.async_show_form(
step_id="edit", data_schema=self.data_schema, errors=errors
)
async def async_step_import(self, config):
"""Import a config flow from configuration."""
error = await self._validate_input(config)
if error:
return self.async_abort(reason=error)
return self.async_create_entry(title=config[CONF_HOST], data=config)
async def async_step_discovery(self, discovery_info):
"""Handle discovery."""
_LOGGER.debug("Reached discovery flow with info: %s", discovery_info)
if "uuid" in discovery_info:
await self.async_set_unique_id(discovery_info.pop("uuid"))
self._abort_if_unique_id_configured()
else:
# attempt to connect to server and determine uuid. will fail if password required
error = await self._validate_input(discovery_info)
if error:
await self._async_handle_discovery_without_unique_id()
# update schema with suggested values from discovery
self.data_schema = _base_schema(discovery_info)
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}})
return await self.async_step_edit()