Add Hue manual bridge config flow + options flow (#37268)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Franck Nijhof 2020-07-02 14:12:24 +02:00 committed by GitHub
parent cf3f755edc
commit 235298a1b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 389 additions and 117 deletions

View file

@ -184,7 +184,7 @@ homeassistant/components/html5/* @robbiet480
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob homeassistant/components/hue/* @balloob @frenck
homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion homeassistant/components/hvv_departures/* @vigonotion

View file

@ -31,26 +31,25 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema(
{ {
# Validate as IP address and then convert back to a string. # Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
vol.Optional( vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean,
CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean,
): cv.boolean,
vol.Optional(
CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS
): cv.boolean,
vol.Optional("filename"): str, vol.Optional("filename"): str,
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ vol.All(
DOMAIN: vol.Schema( cv.deprecated(DOMAIN, invalidation_version="0.115.0"),
{ {
vol.Optional(CONF_BRIDGES): vol.All( DOMAIN: vol.Schema(
cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], {
) vol.Optional(CONF_BRIDGES): vol.All(
} cv.ensure_list, [BRIDGE_CONFIG_SCHEMA],
) )
}, }
)
},
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
@ -64,7 +63,7 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
hass.data[DATA_CONFIGS] = {} hass.data[DATA_CONFIGS] = {}
# User has configured bridges # User has not configured bridges
if CONF_BRIDGES not in conf: if CONF_BRIDGES not in conf:
return True return True
@ -105,16 +104,55 @@ async def async_setup_entry(
host = entry.data["host"] host = entry.data["host"]
config = hass.data[DATA_CONFIGS].get(host) config = hass.data[DATA_CONFIGS].get(host)
if config is None: # Migrate allow_unreachable from config entry data to config entry options
allow_unreachable = entry.data.get( if (
CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE CONF_ALLOW_UNREACHABLE not in entry.options
) and CONF_ALLOW_UNREACHABLE in entry.data
allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS) and entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE
else: ):
allow_unreachable = config[CONF_ALLOW_UNREACHABLE] options = {
allow_groups = config[CONF_ALLOW_HUE_GROUPS] **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(): if not await bridge.async_setup():
return False return False

View file

@ -14,7 +14,14 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv 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 .errors import AuthenticationRequired, CannotConnect
from .helpers import create_config_flow from .helpers import create_config_flow
from .sensor_base import SensorManager from .sensor_base import SensorManager
@ -33,12 +40,10 @@ _LOGGER = logging.getLogger(__name__)
class HueBridge: class HueBridge:
"""Manages a single Hue bridge.""" """Manages a single Hue bridge."""
def __init__(self, hass, config_entry, allow_unreachable, allow_groups): def __init__(self, hass, config_entry):
"""Initialize the system.""" """Initialize the system."""
self.config_entry = config_entry self.config_entry = config_entry
self.hass = hass self.hass = hass
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True self.available = True
self.authorized = False self.authorized = False
self.api = None self.api = None
@ -46,12 +51,27 @@ class HueBridge:
# Jobs to be executed when API is reset. # Jobs to be executed when API is reset.
self.reset_jobs = [] self.reset_jobs = []
self.sensor_manager = None self.sensor_manager = None
self.unsub_config_entry_listner = None
@property @property
def host(self): def host(self):
"""Return the host of this bridge.""" """Return the host of this bridge."""
return self.config_entry.data["host"] 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): async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter.""" """Set up a phue bridge based on host parameter."""
host = self.host host = self.host
@ -105,6 +125,10 @@ class HueBridge:
3 if self.api.config.modelid == "BSB001" else 10 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 self.authorized = True
return True return True
@ -160,6 +184,9 @@ class HueBridge:
while self.reset_jobs: while self.reset_jobs:
self.reset_jobs.pop()() 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 # If setup was successful, we set api variable, forwarded entry and
# register service # register service
results = await asyncio.gather( results = await asyncio.gather(
@ -244,8 +271,18 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge):
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
raise AuthenticationRequired raise AuthenticationRequired
except (asyncio.TimeoutError, client_exceptions.ClientOSError): except (
asyncio.TimeoutError,
client_exceptions.ClientOSError,
client_exceptions.ServerDisconnectedError,
client_exceptions.ContentTypeError,
):
raise CannotConnect raise CannotConnect
except aiohue.AiohueException: except aiohue.AiohueException:
LOGGER.exception("Unknown Hue linking error occurred") LOGGER.exception("Unknown Hue linking error occurred")
raise AuthenticationRequired raise AuthenticationRequired
async def _update_listener(hass, entry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View file

@ -1,6 +1,6 @@
"""Config flow to configure Philips Hue.""" """Config flow to configure Philips Hue."""
import asyncio import asyncio
from typing import Dict, Optional from typing import Any, Dict, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
import aiohue import aiohue
@ -10,12 +10,14 @@ import voluptuous as vol
from homeassistant import config_entries, core from homeassistant import config_entries, core
from homeassistant.components import ssdp 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 homeassistant.helpers import aiohttp_client
from .bridge import authenticate_bridge from .bridge import authenticate_bridge
from .const import ( # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import
CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
DOMAIN, DOMAIN,
LOGGER, LOGGER,
) )
@ -23,6 +25,7 @@ from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = "http://www.philips.com" HUE_MANUFACTURERURL = "http://www.philips.com"
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
HUE_MANUAL_BRIDGE_ID = "manual"
class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -31,7 +34,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 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): def __init__(self):
"""Initialize the Hue flow.""" """Initialize the Hue flow."""
@ -57,6 +64,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Handle a flow start.""" """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 ( if (
user_input is not None user_input is not None
and self.discovered_bridges 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"]] self.bridge = self.discovered_bridges[user_input["id"]]
await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) 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: try:
with async_timeout.timeout(5): with async_timeout.timeout(5):
bridges = await discover_nupnp( bridges = await discover_nupnp(
@ -75,34 +86,50 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except asyncio.TimeoutError: except asyncio.TimeoutError:
return self.async_abort(reason="discover_timeout") return self.async_abort(reason="discover_timeout")
if not bridges: if bridges:
return self.async_abort(reason="no_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 if not self.discovered_bridges:
already_configured = self._async_current_ids(False) return await self.async_step_manual()
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}
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema( data_schema=vol.Schema(
{ {
vol.Required("id"): vol.In( 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): async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge. """Attempt to link with the Hue bridge.
@ -118,35 +145,30 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
await authenticate_bridge(self.hass, bridge) 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: except AuthenticationRequired:
errors["base"] = "register_failed" errors["base"] = "register_failed"
except CannotConnect: except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) 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 except Exception: # pylint: disable=broad-except
LOGGER.exception( LOGGER.exception(
"Unknown error connecting with Hue bridge at %s", bridge.host "Unknown error connecting with Hue bridge at %s", bridge.host
) )
errors["base"] = "linking" 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): async def async_step_ssdp(self, discovery_info):
"""Handle a discovered Hue bridge. """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"]) self.bridge = self._async_get_bridge(import_info["host"])
return await self.async_step_link() 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,
}
),
)

View file

@ -21,6 +21,6 @@
"homekit": { "homekit": {
"models": ["BSB002"] "models": ["BSB002"]
}, },
"codeowners": ["@balloob"], "codeowners": ["@balloob", "@frenck"],
"quality_scale": "platinum" "quality_scale": "platinum"
} }

View file

@ -7,6 +7,12 @@
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
} }
}, },
"manual": {
"title": "Manual configure a Hue bridge",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"link": { "link": {
"title": "Link Hub", "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)" "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_long_press": "Both \"{subtype}\" released after long press",
"remote_double_button_short_press": "Both \"{subtype}\" released" "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"
}
}
}
} }
} }

View file

@ -2,6 +2,10 @@
import pytest import pytest
from homeassistant.components.hue import bridge, errors 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 homeassistant.exceptions import ConfigEntryNotReady
from tests.async_mock import AsyncMock, Mock, patch from tests.async_mock import AsyncMock, Mock, patch
@ -12,7 +16,8 @@ async def test_bridge_setup(hass):
entry = Mock() entry = Mock()
api = Mock(initialize=AsyncMock()) api = Mock(initialize=AsyncMock())
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch("aiohue.Bridge", return_value=api), patch.object(
hass.config_entries, "async_forward_entry_setup" 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.""" """Test we start config flow if username is no longer whitelisted."""
entry = Mock() entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch.object(
bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired 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.""" """Test we retry to connect if we cannot connect."""
entry = Mock() entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch.object(
bridge, "authenticate_bridge", side_effect=errors.CannotConnect 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.""" """Test calling reset when the entry contained wrong auth."""
entry = Mock() entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch.object(
bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired 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.""" """Test calling reset while the entry has been setup."""
entry = Mock() entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch(
"aiohue.Bridge", return_value=Mock() "aiohue.Bridge", return_value=Mock()
@ -95,7 +104,8 @@ async def test_handle_unauthorized(hass):
"""Test handling an unauthorized error on update.""" """Test handling an unauthorized error on update."""
entry = Mock(async_setup=AsyncMock()) entry = Mock(async_setup=AsyncMock())
entry.data = {"host": "1.2.3.4", "username": "mock-username"} 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( with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch(
"aiohue.Bridge", return_value=Mock() "aiohue.Bridge", return_value=Mock()

View file

@ -57,6 +57,13 @@ async def test_flow_works(hass):
const.DOMAIN, context={"source": "user"} 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["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
@ -76,21 +83,104 @@ async def test_flow_works(hass):
assert result["data"] == { assert result["data"] == {
"host": "1.2.3.4", "host": "1.2.3.4",
"username": "home-assistant#test-home", "username": "home-assistant#test-home",
"allow_hue_groups": False,
} }
assert len(mock_bridge.initialize.mock_calls) == 1 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.""" """Test config flow discovers no bridges."""
aioclient_mock.get(URL_NUPNP, json=[]) aioclient_mock.get(URL_NUPNP, json=[])
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": "user"} const.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "abort" assert result["type"] == "form"
assert result["reason"] == "no_bridges" assert result["step_id"] == "manual"
async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): 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( result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": "user"} 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["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.""" """Test config flow discovers two bridges."""
# Add ignored config entry. Should still show up as option. # Add ignored config entry. Should still show up as option.
MockConfigEntry( MockConfigEntry(
@ -144,6 +224,7 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
result["data_schema"]({"id": "bla"}) result["data_schema"]({"id": "bla"})
result["data_schema"]({"id": "beer"}) result["data_schema"]({"id": "beer"})
result["data_schema"]({"id": "manual"})
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): 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( result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": "user"} const.DOMAIN, context={"source": "user"}
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "init"
flow = next( assert result["data_schema"]({"id": "beer"})
flow assert result["data_schema"]({"id": "manual"})
for flow in hass.config_entries.flow.async_progress() with pytest.raises(vol.error.MultipleInvalid):
if flow["flow_id"] == result["flow_id"] assert not result["data_schema"]({"id": "bla"})
)
assert flow["context"]["unique_id"] == "beer"
async def test_flow_timeout_discovery(hass): async def test_flow_timeout_discovery(hass):
@ -199,13 +279,16 @@ async def test_flow_link_timeout(hass):
const.DOMAIN, context={"source": "user"} 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result["type"] == "form" assert result["type"] == "abort"
assert result["step_id"] == "link" assert result["reason"] == "cannot_connect"
assert result["errors"] == {"base": "linking"}
async def test_flow_link_unknown_error(hass): 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"} 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
@ -241,6 +328,10 @@ async def test_flow_link_button_not_pressed(hass):
const.DOMAIN, context={"source": "user"} 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
@ -263,13 +354,16 @@ async def test_flow_link_unknown_host(hass):
const.DOMAIN, context={"source": "user"} 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result["type"] == "form" assert result["type"] == "abort"
assert result["step_id"] == "link" assert result["reason"] == "cannot_connect"
assert result["errors"] == {"base": "linking"}
async def test_bridge_ssdp(hass): 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"] == { assert result["data"] == {
"host": "2.2.2.2", "host": "2.2.2.2",
"username": "username-abc", "username": "username-abc",
"allow_hue_groups": False,
} }
entries = hass.config_entries.async_entries("hue") entries = hass.config_entries.async_entries("hue")
assert len(entries) == 2 assert len(entries) == 2
@ -532,3 +625,30 @@ async def test_homekit_discovery_update_configuration(hass):
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
assert entry.data["host"] == "1.1.1.1" 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,
}

View file

@ -54,11 +54,7 @@ async def test_setup_defined_hosts_known_auth(hass):
hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True, hue.CONF_ALLOW_UNREACHABLE: True,
}, },
"1.1.1.1": { "1.1.1.1": {hue.CONF_HOST: "1.1.1.1"},
hue.CONF_HOST: "1.1.1.1",
hue.CONF_ALLOW_HUE_GROUPS: True,
hue.CONF_ALLOW_UNREACHABLE: False,
},
} }
@ -130,12 +126,10 @@ async def test_config_passed_to_config_entry(hass):
) )
assert len(mock_bridge.mock_calls) == 2 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_hass is hass
assert p_entry is entry 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 len(mock_registry.mock_calls) == 1
assert mock_registry.mock_calls[0][2] == { assert mock_registry.mock_calls[0][2] == {