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/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

View file

@ -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

View file

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

View file

@ -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,
}
),
)

View file

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

View file

@ -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"
}
}
}
}
}
}

View file

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

View file

@ -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,
}

View file

@ -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] == {