From 235298a1b28ea3a8ecdfd0055ea4d3d31410f093 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jul 2020 14:12:24 +0200 Subject: [PATCH] Add Hue manual bridge config flow + options flow (#37268) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 +- homeassistant/components/hue/__init__.py | 88 +++++++--- homeassistant/components/hue/bridge.py | 47 ++++- homeassistant/components/hue/config_flow.py | 137 ++++++++++----- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/strings.json | 18 +- tests/components/hue/test_bridge.py | 22 ++- tests/components/hue/test_config_flow.py | 180 ++++++++++++++++---- tests/components/hue/test_init.py | 10 +- 9 files changed, 389 insertions(+), 117 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9ea7de5c2c1..fb071afaeee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -184,7 +184,7 @@ homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis -homeassistant/components/hue/* @balloob +homeassistant/components/hue/* @balloob @frenck homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 675fe3ae5e1..a466405094a 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -31,26 +31,25 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - vol.Optional( - CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE - ): cv.boolean, - vol.Optional( - CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS - ): cv.boolean, + vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, + vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean, vol.Optional("filename"): str, } ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], - ) - } - ) - }, + vol.All( + cv.deprecated(DOMAIN, invalidation_version="0.115.0"), + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_BRIDGES): vol.All( + cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], + ) + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -64,7 +63,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DATA_CONFIGS] = {} - # User has configured bridges + # User has not configured bridges if CONF_BRIDGES not in conf: return True @@ -105,16 +104,55 @@ async def async_setup_entry( host = entry.data["host"] config = hass.data[DATA_CONFIGS].get(host) - if config is None: - allow_unreachable = entry.data.get( - CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE - ) - allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS) - else: - allow_unreachable = config[CONF_ALLOW_UNREACHABLE] - allow_groups = config[CONF_ALLOW_HUE_GROUPS] + # Migrate allow_unreachable from config entry data to config entry options + if ( + CONF_ALLOW_UNREACHABLE not in entry.options + and CONF_ALLOW_UNREACHABLE in entry.data + and entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE + ): + options = { + **entry.options, + CONF_ALLOW_UNREACHABLE: entry.data[CONF_ALLOW_UNREACHABLE], + } + data = entry.data.copy() + data.pop(CONF_ALLOW_UNREACHABLE) + hass.config_entries.async_update_entry(entry, data=data, options=options) - bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) + # Migrate allow_hue_groups from config entry data to config entry options + if ( + CONF_ALLOW_HUE_GROUPS not in entry.options + and CONF_ALLOW_HUE_GROUPS in entry.data + and entry.data[CONF_ALLOW_HUE_GROUPS] != DEFAULT_ALLOW_HUE_GROUPS + ): + options = { + **entry.options, + CONF_ALLOW_HUE_GROUPS: entry.data[CONF_ALLOW_HUE_GROUPS], + } + data = entry.data.copy() + data.pop(CONF_ALLOW_HUE_GROUPS) + hass.config_entries.async_update_entry(entry, data=data, options=options) + + # Overwrite from YAML configuration + if config is not None: + options = {} + if CONF_ALLOW_HUE_GROUPS in config and ( + CONF_ALLOW_HUE_GROUPS not in entry.options + or config[CONF_ALLOW_HUE_GROUPS] != entry.options[CONF_ALLOW_HUE_GROUPS] + ): + options[CONF_ALLOW_HUE_GROUPS] = config[CONF_ALLOW_HUE_GROUPS] + + if CONF_ALLOW_UNREACHABLE in config and ( + CONF_ALLOW_UNREACHABLE not in entry.options + or config[CONF_ALLOW_UNREACHABLE] != entry.options[CONF_ALLOW_UNREACHABLE] + ): + options[CONF_ALLOW_UNREACHABLE] = config[CONF_ALLOW_UNREACHABLE] + + if options: + hass.config_entries.async_update_entry( + entry, options={**entry.options, **options}, + ) + + bridge = HueBridge(hass, entry) if not await bridge.async_setup(): return False diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 977a6717f1b..546b0368c8d 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -14,7 +14,14 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN, LOGGER +from .const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN, + LOGGER, +) from .errors import AuthenticationRequired, CannotConnect from .helpers import create_config_flow from .sensor_base import SensorManager @@ -33,12 +40,10 @@ _LOGGER = logging.getLogger(__name__) class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry, allow_unreachable, allow_groups): + def __init__(self, hass, config_entry): """Initialize the system.""" self.config_entry = config_entry self.hass = hass - self.allow_unreachable = allow_unreachable - self.allow_groups = allow_groups self.available = True self.authorized = False self.api = None @@ -46,12 +51,27 @@ class HueBridge: # Jobs to be executed when API is reset. self.reset_jobs = [] self.sensor_manager = None + self.unsub_config_entry_listner = None @property def host(self): """Return the host of this bridge.""" return self.config_entry.data["host"] + @property + def allow_unreachable(self): + """Allow unreachable light bulbs.""" + return self.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) + + @property + def allow_groups(self): + """Allow groups defined in the Hue bridge.""" + return self.config_entry.options.get( + CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS + ) + async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" host = self.host @@ -105,6 +125,10 @@ class HueBridge: 3 if self.api.config.modelid == "BSB001" else 10 ) + self.unsub_config_entry_listner = self.config_entry.add_update_listener( + _update_listener + ) + self.authorized = True return True @@ -160,6 +184,9 @@ class HueBridge: while self.reset_jobs: self.reset_jobs.pop()() + if self.unsub_config_entry_listner is not None: + self.unsub_config_entry_listner() + # If setup was successful, we set api variable, forwarded entry and # register service results = await asyncio.gather( @@ -244,8 +271,18 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired - except (asyncio.TimeoutError, client_exceptions.ClientOSError): + except ( + asyncio.TimeoutError, + client_exceptions.ClientOSError, + client_exceptions.ServerDisconnectedError, + client_exceptions.ContentTypeError, + ): raise CannotConnect except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") raise AuthenticationRequired + + +async def _update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 77c24caa389..63cd5ec80b4 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,6 +1,6 @@ """Config flow to configure Philips Hue.""" import asyncio -from typing import Dict, Optional +from typing import Any, Dict, Optional from urllib.parse import urlparse import aiohue @@ -10,12 +10,14 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge from .const import ( # pylint: disable=unused-import CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, DOMAIN, LOGGER, ) @@ -23,6 +25,7 @@ from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] +HUE_MANUAL_BRIDGE_ID = "manual" class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -31,7 +34,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return HueOptionsFlowHandler(config_entry) def __init__(self): """Initialize the Hue flow.""" @@ -57,6 +64,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init(self, user_input=None): """Handle a flow start.""" + # Check if user chooses manual entry + if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID: + return await self.async_step_manual() + if ( user_input is not None and self.discovered_bridges is not None @@ -64,9 +75,9 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): self.bridge = self.discovered_bridges[user_input["id"]] await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) - # We pass user input to link so it will attempt to link right away - return await self.async_step_link({}) + return await self.async_step_link() + # Find / discover bridges try: with async_timeout.timeout(5): bridges = await discover_nupnp( @@ -75,34 +86,50 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except asyncio.TimeoutError: return self.async_abort(reason="discover_timeout") - if not bridges: - return self.async_abort(reason="no_bridges") + if bridges: + # Find already configured hosts + already_configured = self._async_current_ids(False) + bridges = [ + bridge for bridge in bridges if bridge.id not in already_configured + ] + self.discovered_bridges = {bridge.id: bridge for bridge in bridges} - # Find already configured hosts - already_configured = self._async_current_ids(False) - bridges = [bridge for bridge in bridges if bridge.id not in already_configured] - - if not bridges: - return self.async_abort(reason="all_configured") - - if len(bridges) == 1: - self.bridge = bridges[0] - await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) - return await self.async_step_link() - - self.discovered_bridges = {bridge.id: bridge for bridge in bridges} + if not self.discovered_bridges: + return await self.async_step_manual() return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Required("id"): vol.In( - {bridge.id: bridge.host for bridge in bridges} + { + **{bridge.id: bridge.host for bridge in bridges}, + HUE_MANUAL_BRIDGE_ID: "Manually add a Hue Bridge", + } ) } ), ) + async def async_step_manual( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle manual bridge setup.""" + if user_input is None: + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + ) + + if any( + user_input["host"] == entry.data["host"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + self.bridge = self._async_get_bridge(user_input[CONF_HOST]) + return await self.async_step_link() + async def async_step_link(self, user_input=None): """Attempt to link with the Hue bridge. @@ -118,35 +145,30 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await authenticate_bridge(self.hass, bridge) - - # Can happen if we come from import. - if self.unique_id is None: - await self.async_set_unique_id( - normalize_bridge_id(bridge.id), raise_on_progress=False - ) - - return self.async_create_entry( - title=bridge.config.name, - data={ - "host": bridge.host, - "username": bridge.username, - CONF_ALLOW_HUE_GROUPS: False, - }, - ) except AuthenticationRequired: errors["base"] = "register_failed" - except CannotConnect: LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) - errors["base"] = "linking" - + return self.async_abort(reason="cannot_connect") except Exception: # pylint: disable=broad-except LOGGER.exception( "Unknown error connecting with Hue bridge at %s", bridge.host ) errors["base"] = "linking" - return self.async_show_form(step_id="link", errors=errors) + if errors: + return self.async_show_form(step_id="link", errors=errors) + + # Can happen if we come from import or manual entry + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge.id), raise_on_progress=False + ) + + return self.async_create_entry( + title=bridge.config.name, + data={CONF_HOST: bridge.host, CONF_USERNAME: bridge.username}, + ) async def async_step_ssdp(self, discovery_info): """Handle a discovered Hue bridge. @@ -211,3 +233,38 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = self._async_get_bridge(import_info["host"]) return await self.async_step_link() + + +class HueOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Hue options.""" + + def __init__(self, config_entry): + """Initialize Hue options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Manage Hue options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_HUE_GROUPS, + default=self.config_entry.options.get( + CONF_ALLOW_HUE_GROUPS, False + ), + ): bool, + vol.Optional( + CONF_ALLOW_UNREACHABLE, + default=self.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, False + ), + ): bool, + } + ), + ) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 687e0a7330e..caa008de408 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -21,6 +21,6 @@ "homekit": { "models": ["BSB002"] }, - "codeowners": ["@balloob"], + "codeowners": ["@balloob", "@frenck"], "quality_scale": "platinum" } diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 5d56a787448..90deb950935 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -7,6 +7,12 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "manual": { + "title": "Manual configure a Hue bridge", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, "link": { "title": "Link Hub", "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" @@ -47,5 +53,15 @@ "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Allow Hue groups", + "allow_unreachable": "Allow unreachable bulbs to report their state correctly" + } + } + } } -} \ No newline at end of file +} diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 385097514f8..19b642b0283 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -2,6 +2,10 @@ import pytest from homeassistant.components.hue import bridge, errors +from homeassistant.components.hue.const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, +) from homeassistant.exceptions import ConfigEntryNotReady from tests.async_mock import AsyncMock, Mock, patch @@ -12,7 +16,8 @@ async def test_bridge_setup(hass): entry = Mock() api = Mock(initialize=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch("aiohue.Bridge", return_value=api), patch.object( hass.config_entries, "async_forward_entry_setup" @@ -29,7 +34,8 @@ async def test_bridge_setup_invalid_username(hass): """Test we start config flow if username is no longer whitelisted.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch.object( bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired @@ -44,7 +50,8 @@ async def test_bridge_setup_timeout(hass): """Test we retry to connect if we cannot connect.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch.object( bridge, "authenticate_bridge", side_effect=errors.CannotConnect @@ -56,7 +63,8 @@ async def test_reset_if_entry_had_wrong_auth(hass): """Test calling reset when the entry contained wrong auth.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch.object( bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired @@ -72,7 +80,8 @@ async def test_reset_unloads_entry_if_setup(hass): """Test calling reset while the entry has been setup.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( "aiohue.Bridge", return_value=Mock() @@ -95,7 +104,8 @@ async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" entry = Mock(async_setup=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} - hue_bridge = bridge.HueBridge(hass, entry, False, False) + entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + hue_bridge = bridge.HueBridge(hass, entry) with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( "aiohue.Bridge", return_value=Mock() diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 4ba4ecb06a6..06a0bcedc81 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -57,6 +57,13 @@ async def test_flow_works(hass): const.DOMAIN, context={"source": "user"} ) + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": mock_bridge.id} + ) + assert result["type"] == "form" assert result["step_id"] == "link" @@ -76,21 +83,104 @@ async def test_flow_works(hass): assert result["data"] == { "host": "1.2.3.4", "username": "home-assistant#test-home", - "allow_hue_groups": False, } assert len(mock_bridge.initialize.mock_calls) == 1 -async def test_flow_no_discovered_bridges(hass, aioclient_mock): +async def test_manual_flow_works(hass, aioclient_mock): + """Test config flow discovers only already configured bridges.""" + mock_bridge = get_mock_bridge() + + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[mock_bridge], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": "manual"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + bridge = get_mock_bridge( + bridge_id="id-1234", host="2.2.2.2", username="username-abc" + ) + + with patch( + "aiohue.Bridge", return_value=bridge, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "2.2.2.2"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch("homeassistant.components.hue.config_flow.authenticate_bridge"), patch( + "homeassistant.components.hue.async_unload_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == "create_entry" + assert result["title"] == "Mock Bridge" + assert result["data"] == { + "host": "2.2.2.2", + "username": "username-abc", + } + entries = hass.config_entries.async_entries("hue") + assert len(entries) == 1 + entry = entries[-1] + assert entry.unique_id == "id-1234" + + +async def test_manual_flow_bridge_exist(hass, aioclient_mock): + """Test config flow discovers only already configured bridges.""" + MockConfigEntry( + domain="hue", unique_id="id-1234", data={"host": "2.2.2.2"} + ).add_to_hass(hass) + + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", return_value=[], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + bridge = get_mock_bridge( + bridge_id="id-1234", host="2.2.2.2", username="username-abc" + ) + + with patch( + "aiohue.Bridge", return_value=bridge, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "2.2.2.2"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_manual_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" aioclient_mock.get(URL_NUPNP, json=[]) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} ) - assert result["type"] == "abort" - assert result["reason"] == "no_bridges" + assert result["type"] == "form" + assert result["step_id"] == "manual" async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): @@ -103,22 +193,12 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} ) - assert result["type"] == "abort" - assert result["reason"] == "all_configured" - -async def test_flow_one_bridge_discovered(hass, aioclient_mock): - """Test config flow discovers one bridge.""" - aioclient_mock.get(URL_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} - ) assert result["type"] == "form" - assert result["step_id"] == "link" + assert result["step_id"] == "manual" -async def test_flow_two_bridges_discovered(hass, aioclient_mock): +async def test_flow_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" # Add ignored config entry. Should still show up as option. MockConfigEntry( @@ -144,6 +224,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): result["data_schema"]({"id": "bla"}) result["data_schema"]({"id": "beer"}) + result["data_schema"]({"id": "manual"}) async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): @@ -162,14 +243,13 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} ) + assert result["type"] == "form" - assert result["step_id"] == "link" - flow = next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert flow["context"]["unique_id"] == "beer" + assert result["step_id"] == "init" + assert result["data_schema"]({"id": "beer"}) + assert result["data_schema"]({"id": "manual"}) + with pytest.raises(vol.error.MultipleInvalid): + assert not result["data_schema"]({"id": "bla"}) async def test_flow_timeout_discovery(hass): @@ -199,13 +279,16 @@ async def test_flow_link_timeout(hass): const.DOMAIN, context={"source": "user"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": mock_bridge.id} + ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" - assert result["step_id"] == "link" - assert result["errors"] == {"base": "linking"} + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" async def test_flow_link_unknown_error(hass): @@ -219,6 +302,10 @@ async def test_flow_link_unknown_error(hass): const.DOMAIN, context={"source": "user"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": mock_bridge.id} + ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -241,6 +328,10 @@ async def test_flow_link_button_not_pressed(hass): const.DOMAIN, context={"source": "user"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": mock_bridge.id} + ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -263,13 +354,16 @@ async def test_flow_link_unknown_host(hass): const.DOMAIN, context={"source": "user"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"id": mock_bridge.id} + ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" - assert result["step_id"] == "link" - assert result["errors"] == {"base": "linking"} + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" async def test_bridge_ssdp(hass): @@ -436,7 +530,6 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): assert result["data"] == { "host": "2.2.2.2", "username": "username-abc", - "allow_hue_groups": False, } entries = hass.config_entries.async_entries("hue") assert len(entries) == 2 @@ -532,3 +625,30 @@ async def test_homekit_discovery_update_configuration(hass): assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" + + +async def test_options_flow(hass): + """Test options config flow.""" + entry = MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + const.CONF_ALLOW_HUE_GROUPS: True, + const.CONF_ALLOW_UNREACHABLE: True, + }, + ) + + assert result["type"] == "create_entry" + assert result["data"] == { + const.CONF_ALLOW_HUE_GROUPS: True, + const.CONF_ALLOW_UNREACHABLE: True, + } diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index a144902bbc8..033b5ae056a 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -54,11 +54,7 @@ async def test_setup_defined_hosts_known_auth(hass): hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, }, - "1.1.1.1": { - hue.CONF_HOST: "1.1.1.1", - hue.CONF_ALLOW_HUE_GROUPS: True, - hue.CONF_ALLOW_UNREACHABLE: False, - }, + "1.1.1.1": {hue.CONF_HOST: "1.1.1.1"}, } @@ -130,12 +126,10 @@ async def test_config_passed_to_config_entry(hass): ) assert len(mock_bridge.mock_calls) == 2 - p_hass, p_entry, p_allow_unreachable, p_allow_groups = mock_bridge.mock_calls[0][1] + p_hass, p_entry = mock_bridge.mock_calls[0][1] assert p_hass is hass assert p_entry is entry - assert p_allow_unreachable is True - assert p_allow_groups is False assert len(mock_registry.mock_calls) == 1 assert mock_registry.mock_calls[0][2] == {