Add Hue manual bridge config flow + options flow (#37268)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
cf3f755edc
commit
235298a1b2
9 changed files with 389 additions and 117 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -21,6 +21,6 @@
|
|||
"homekit": {
|
||||
"models": ["BSB002"]
|
||||
},
|
||||
"codeowners": ["@balloob"],
|
||||
"codeowners": ["@balloob", "@frenck"],
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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] == {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue