Convert Hue to use unique ID (#30000)

* Convert Hue to use unique ID

* Fix normalization

* Store/restore unique ID

* Fix tests
This commit is contained in:
Paulus Schoutsen 2019-12-16 19:45:09 +01:00 committed by GitHub
parent 575eb48feb
commit 58b5833d64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 385 additions and 440 deletions

View file

@ -2,16 +2,14 @@
import ipaddress import ipaddress
import logging import logging
from aiohue.util import normalize_bridge_id
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core from homeassistant import config_entries, core
from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from .bridge import HueBridge, normalize_bridge_id from .bridge import HueBridge
from .config_flow import ( # Loading the config flow file will register the flow
configured_hosts,
)
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,8 +30,6 @@ 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),
# This is for legacy reasons and is only used for importing auth.
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional( vol.Optional(
CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE
): cv.boolean, ): cv.boolean,
@ -65,7 +61,6 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
hass.data[DATA_CONFIGS] = {} hass.data[DATA_CONFIGS] = {}
configured = configured_hosts(hass)
# User has configured bridges # User has configured bridges
if CONF_BRIDGES not in conf: if CONF_BRIDGES not in conf:
@ -73,29 +68,28 @@ async def async_setup(hass, config):
bridges = conf[CONF_BRIDGES] bridges = conf[CONF_BRIDGES]
configured_hosts = set(
entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN)
)
for bridge_conf in bridges: for bridge_conf in bridges:
host = bridge_conf[CONF_HOST] host = bridge_conf[CONF_HOST]
# Store config in hass.data so the config entry can find it # Store config in hass.data so the config entry can find it
hass.data[DATA_CONFIGS][host] = bridge_conf hass.data[DATA_CONFIGS][host] = bridge_conf
# If configured, the bridge will be set up during config entry phase if host in configured_hosts:
if host in configured:
continue continue
# No existing config entry found, try importing it or trigger link # No existing config entry found, trigger link config flow. Because we're
# config flow if no existing auth. Because we're inside the setup of # inside the setup of this component we'll have to use hass.async_add_job
# this component we'll have to use hass.async_add_job to avoid a # to avoid a deadlock: creating a config entry will set up the component
# deadlock: creating a config entry will set up the component but the # but the setup would block till the entry is created!
# setup would block till the entry is created!
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_IMPORT}, context={"source": config_entries.SOURCE_IMPORT},
data={ data={"host": bridge_conf[CONF_HOST]},
"host": bridge_conf[CONF_HOST],
"path": bridge_conf[CONF_FILENAME],
},
) )
) )

View file

@ -6,6 +6,7 @@ import async_timeout
import slugify as unicode_slug import slugify as unicode_slug
import voluptuous as vol import voluptuous as vol
from homeassistant import core
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
@ -45,8 +46,15 @@ class HueBridge:
host = self.host host = self.host
hass = self.hass hass = self.hass
bridge = aiohue.Bridge(
host,
username=self.config_entry.data["username"],
websession=aiohttp_client.async_get_clientsession(hass),
)
try: try:
self.api = await get_bridge(hass, host, self.config_entry.data["username"]) await authenticate_bridge(hass, bridge)
except AuthenticationRequired: except AuthenticationRequired:
# Usernames can become invalid if hub is reset or user removed. # Usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new # We are going to fail the config entry setup and initiate a new
@ -63,6 +71,8 @@ class HueBridge:
LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) LOGGER.exception("Unknown error connecting with Hue bridge at %s", host)
return False return False
self.api = bridge
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(self.config_entry, "light") hass.config_entries.async_forward_entry_setup(self.config_entry, "light")
) )
@ -175,16 +185,12 @@ class HueBridge:
create_config_flow(self.hass, self.host) create_config_flow(self.hass, self.host)
async def get_bridge(hass, host, username=None): async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge):
"""Create a bridge object and verify authentication.""" """Create a bridge object and verify authentication."""
bridge = aiohue.Bridge(
host, username=username, websession=aiohttp_client.async_get_clientsession(hass)
)
try: try:
with async_timeout.timeout(10): with async_timeout.timeout(10):
# Create username if we don't have one # Create username if we don't have one
if not username: if not bridge.username:
device_name = unicode_slug.slugify( device_name = unicode_slug.slugify(
hass.config.location_name, max_length=19 hass.config.location_name, max_length=19
) )
@ -193,7 +199,6 @@ async def get_bridge(hass, host, username=None):
# Initialize bridge (and validate our username) # Initialize bridge (and validate our username)
await bridge.initialize() await bridge.initialize()
return bridge
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
raise AuthenticationRequired raise AuthenticationRequired
except (asyncio.TimeoutError, aiohue.RequestError): except (asyncio.TimeoutError, aiohue.RequestError):
@ -201,25 +206,3 @@ async def get_bridge(hass, host, username=None):
except aiohue.AiohueException: except aiohue.AiohueException:
LOGGER.exception("Unknown Hue linking error occurred") LOGGER.exception("Unknown Hue linking error occurred")
raise AuthenticationRequired raise AuthenticationRequired
def normalize_bridge_id(bridge_id: str):
"""Normalize a bridge identifier.
There are three sources where we receive bridge ID from:
- ssdp/upnp: <host>/description.xml, field root/device/serialNumber
- nupnp: "id" field
- Hue Bridge API: config.bridgeid
The SSDP/UPNP source does not contain the middle 4 characters compared
to the other sources. In all our tests the middle 4 characters are "fffe".
"""
if len(bridge_id) == 16:
return bridge_id[0:6] + bridge_id[-6:]
if len(bridge_id) == 12:
return bridge_id
LOGGER.warning("Unexpected bridge id number found: %s", bridge_id)
return bridge_id

View file

@ -1,51 +1,24 @@
"""Config flow to configure Philips Hue.""" """Config flow to configure Philips Hue."""
import asyncio import asyncio
import json from typing import Dict, Optional
import os
from aiohue.discovery import discover_nupnp import aiohue
from aiohue.discovery import discover_nupnp, normalize_bridge_id
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries, core
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .bridge import get_bridge, normalize_bridge_id from .bridge import authenticate_bridge
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER # pylint: disable=unused-import
from .errors import AuthenticationRequired, CannotConnect from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = "http://www.philips.com" HUE_MANUFACTURERURL = "http://www.philips.com"
HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"]
@callback
def configured_hosts(hass):
"""Return a set of the configured hosts."""
return set(
entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN)
)
def _find_username_from_config(hass, filename):
"""Load username from config.
This was a legacy way of configuring Hue until Home Assistant 0.67.
"""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
with open(path) as inp:
try:
return list(json.load(inp).values())[0]["username"]
except ValueError:
# If we get invalid JSON
return None
class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Hue config flow.""" """Handle a Hue config flow."""
@ -56,23 +29,45 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the Hue flow.""" """Initialize the Hue flow."""
self.host = None self.bridge: Optional[aiohue.Bridge] = None
self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
# This is for backwards compatibility.
return await self.async_step_init(user_input) return await self.async_step_init(user_input)
@core.callback
def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None):
"""Return a bridge object."""
if bridge_id is not None:
bridge_id = normalize_bridge_id(bridge_id)
return aiohue.Bridge(
host,
websession=aiohttp_client.async_get_clientsession(self.hass),
bridge_id=bridge_id,
)
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."""
if user_input is not None: if (
self.host = self.context["host"] = user_input["host"] user_input is not None
return await self.async_step_link() and self.discovered_bridges is not None
# pylint: disable=unsupported-membership-test
websession = aiohttp_client.async_get_clientsession(self.hass) and user_input["id"] in self.discovered_bridges
):
# pylint: disable=unsubscriptable-object
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({})
try: try:
with async_timeout.timeout(5): with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=websession) bridges = await discover_nupnp(
websession=aiohttp_client.async_get_clientsession(self.hass)
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return self.async_abort(reason="discover_timeout") return self.async_abort(reason="discover_timeout")
@ -80,20 +75,28 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_bridges") return self.async_abort(reason="no_bridges")
# Find already configured hosts # Find already configured hosts
configured = configured_hosts(self.hass) already_configured = self._async_current_ids()
bridges = [bridge for bridge in bridges if bridge.id not in already_configured]
hosts = [bridge.host for bridge in bridges if bridge.host not in configured] if not bridges:
if not hosts:
return self.async_abort(reason="all_configured") return self.async_abort(reason="all_configured")
if len(hosts) == 1: if len(bridges) == 1:
self.host = hosts[0] self.bridge = bridges[0]
await self.async_set_unique_id(self.bridge.id, raise_on_progress=False)
return await self.async_step_link() 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({vol.Required("host"): vol.In(hosts)}), data_schema=vol.Schema(
{
vol.Required("id"): vol.In(
{bridge.id: bridge.host for bridge in bridges}
)
}
),
) )
async def async_step_link(self, user_input=None): async def async_step_link(self, user_input=None):
@ -102,31 +105,39 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Given a configured host, will ask the user to press the link button Given a configured host, will ask the user to press the link button
to connect to the bridge. to connect to the bridge.
""" """
if user_input is None:
return self.async_show_form(step_id="link")
bridge = self.bridge
assert bridge is not None
errors = {} errors = {}
# We will always try linking in case the user has already pressed
# the link button.
try: try:
bridge = await get_bridge(self.hass, self.host, username=None) await authenticate_bridge(self.hass, bridge)
return await self._entry_from_bridge(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},
)
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", self.host) LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host)
errors["base"] = "linking" errors["base"] = "linking"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception( LOGGER.exception(
"Unknown error connecting with Hue bridge at %s", self.host "Unknown error connecting with Hue bridge at %s", bridge.host
) )
errors["base"] = "linking" errors["base"] = "linking"
# If there was no user input, do not show the errors.
if user_input is None:
errors = {}
return self.async_show_form(step_id="link", errors=errors) return self.async_show_form(step_id="link", errors=errors)
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info):
@ -135,113 +146,55 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
This flow is triggered by the SSDP component. It will check if the This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not. host is already configured and delegate to the import step if not.
""" """
# Filter out non-Hue bridges #1
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason="not_hue_bridge") return self.async_abort(reason="not_hue_bridge")
# Filter out non-Hue bridges #2
if any( if any(
name in discovery_info.get(ATTR_NAME, "") name in discovery_info.get(ATTR_NAME, "")
for name in HUE_IGNORED_BRIDGE_NAMES for name in HUE_IGNORED_BRIDGE_NAMES
): ):
return self.async_abort(reason="not_hue_bridge") return self.async_abort(reason="not_hue_bridge")
host = self.context["host"] = discovery_info.get("host") if "host" not in discovery_info or "serial" not in discovery_info:
return self.async_abort(reason="not_hue_bridge")
if any( bridge = self._async_get_bridge(
host == flow["context"].get("host") for flow in self._async_in_progress() discovery_info["host"], discovery_info["serial"]
):
return self.async_abort(reason="already_in_progress")
if host in configured_hosts(self.hass):
return self.async_abort(reason="already_configured")
bridge_id = discovery_info.get("serial")
await self.async_set_unique_id(normalize_bridge_id(bridge_id))
return await self.async_step_import(
{
"host": host,
# This format is the legacy format that Hue used for discovery
"path": f"phue-{bridge_id}.conf",
}
) )
await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured()
self.bridge = bridge
return await self.async_step_link()
async def async_step_homekit(self, homekit_info): async def async_step_homekit(self, homekit_info):
"""Handle HomeKit discovery.""" """Handle HomeKit discovery."""
host = self.context["host"] = homekit_info.get("host") bridge = self._async_get_bridge(
homekit_info["host"], homekit_info["properties"]["id"]
if any(
host == flow["context"].get("host") for flow in self._async_in_progress()
):
return self.async_abort(reason="already_in_progress")
if host in configured_hosts(self.hass):
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(
normalize_bridge_id(homekit_info["properties"]["id"].replace(":", ""))
) )
return await self.async_step_import({"host": host}) await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured()
self.bridge = bridge
return await self.async_step_link()
async def async_step_import(self, import_info): async def async_step_import(self, import_info):
"""Import a new bridge as a config entry. """Import a new bridge as a config entry.
Will read authentication from Phue config file if available.
This flow is triggered by `async_setup` for both configured and This flow is triggered by `async_setup` for both configured and
discovered bridges. Triggered for any bridge that does not have a discovered bridges. Triggered for any bridge that does not have a
config entry yet (based on host). config entry yet (based on host).
This flow is also triggered by `async_step_discovery`. This flow is also triggered by `async_step_discovery`.
If an existing config file is found, we will validate the credentials
and create an entry. Otherwise we will delegate to `link` step which
will ask user to link the bridge.
""" """
host = self.context["host"] = import_info["host"] # Check if host exists, abort if so.
path = import_info.get("path") if any(
import_info["host"] == entry.data["host"]
for entry in self._async_current_entries()
):
return self.async_abort(reason="already_configured")
if path is not None: self.bridge = self._async_get_bridge(import_info["host"])
username = await self.hass.async_add_job( return await self.async_step_link()
_find_username_from_config, self.hass, self.hass.config.path(path)
)
else:
username = None
try:
bridge = await get_bridge(self.hass, host, username)
LOGGER.info("Imported authentication for %s from %s", host, path)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
self.host = host
LOGGER.info("Invalid authentication for %s, requesting link.", host)
return await self.async_step_link()
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", host)
return self.async_abort(reason="cannot_connect")
except Exception: # pylint: disable=broad-except
LOGGER.exception("Unknown error connecting with Hue bridge at %s", host)
return self.async_abort(reason="unknown")
async def _entry_from_bridge(self, bridge):
"""Return a config entry from an initialized bridge."""
# Remove all other entries of hubs with same ID or host
host = bridge.host
bridge_id = bridge.config.bridgeid
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": host, "bridge_id": bridge_id, "username": bridge.username},
)

View file

@ -3,21 +3,15 @@
"name": "Philips Hue", "name": "Philips Hue",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue", "documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": [ "requirements": ["aiohue==1.10.1"],
"aiohue==1.9.2"
],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Royal Philips Electronics" "manufacturer": "Royal Philips Electronics"
} }
], ],
"homekit": { "homekit": {
"models": [ "models": ["BSB002"]
"BSB002"
]
}, },
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": ["@balloob"]
"@balloob"
]
} }

View file

@ -75,10 +75,6 @@ class OperationNotAllowed(ConfigError):
"""Raised when a config entry operation is not allowed.""" """Raised when a config entry operation is not allowed."""
class UniqueIdInProgress(data_entry_flow.AbortFlow):
"""Error to indicate that the unique Id is in progress."""
class ConfigEntry: class ConfigEntry:
"""Hold a configuration entry.""" """Hold a configuration entry."""
@ -379,6 +375,7 @@ class ConfigEntry:
"system_options": self.system_options.as_dict(), "system_options": self.system_options.as_dict(),
"source": self.source, "source": self.source,
"connection_class": self.connection_class, "connection_class": self.connection_class,
"unique_id": self.unique_id,
} }
@ -482,6 +479,8 @@ class ConfigEntries:
options=entry.get("options"), options=entry.get("options"),
# New in 0.98 # New in 0.98
system_options=entry.get("system_options", {}), system_options=entry.get("system_options", {}),
# New in 0.104
unique_id=entry.get("unique_id"),
) )
for entry in config["entries"] for entry in config["entries"]
] ]
@ -617,11 +616,20 @@ class ConfigEntries:
# Check if config entry exists with unique ID. Unload it. # Check if config entry exists with unique ID. Unload it.
existing_entry = None existing_entry = None
unique_id = flow.context.get("unique_id")
if unique_id is not None: if flow.unique_id is not None:
# Abort all flows in progress with same unique ID.
for progress_flow in self.flow.async_progress():
if (
progress_flow["handler"] == flow.handler
and progress_flow["flow_id"] != flow.flow_id
and progress_flow["context"].get("unique_id") == flow.unique_id
):
self.flow.async_abort(progress_flow["flow_id"])
# Find existing entry.
for check_entry in self.async_entries(result["handler"]): for check_entry in self.async_entries(result["handler"]):
if check_entry.unique_id == unique_id: if check_entry.unique_id == flow.unique_id:
existing_entry = check_entry existing_entry = check_entry
break break
@ -643,16 +651,17 @@ class ConfigEntries:
system_options={}, system_options={},
source=flow.context["source"], source=flow.context["source"],
connection_class=flow.CONNECTION_CLASS, connection_class=flow.CONNECTION_CLASS,
unique_id=unique_id, unique_id=flow.unique_id,
) )
self._entries.append(entry) self._entries.append(entry)
self._async_schedule_save()
await self.async_setup(entry.entry_id) await self.async_setup(entry.entry_id)
if existing_entry is not None: if existing_entry is not None:
await self.async_remove(existing_entry.entry_id) await self.async_remove(existing_entry.entry_id)
self._async_schedule_save()
result["result"] = entry result["result"] = entry
return result return result
@ -723,8 +732,6 @@ async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]:
class ConfigFlow(data_entry_flow.FlowHandler): class ConfigFlow(data_entry_flow.FlowHandler):
"""Base class for config flows with some helpers.""" """Base class for config flows with some helpers."""
unique_id = None
def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None:
"""Initialize a subclass, register if possible.""" """Initialize a subclass, register if possible."""
super().__init_subclass__(**kwargs) # type: ignore super().__init_subclass__(**kwargs) # type: ignore
@ -733,12 +740,30 @@ class ConfigFlow(data_entry_flow.FlowHandler):
CONNECTION_CLASS = CONN_CLASS_UNKNOWN CONNECTION_CLASS = CONN_CLASS_UNKNOWN
@property
def unique_id(self) -> Optional[str]:
"""Return unique ID if available."""
# pylint: disable=no-member
if not self.context:
return None
return cast(Optional[str], self.context.get("unique_id"))
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow":
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
raise data_entry_flow.UnknownHandler raise data_entry_flow.UnknownHandler
@callback
def _abort_if_unique_id_configured(self) -> None:
"""Abort if the unique ID is already configured."""
if self.unique_id is None:
return
if self.unique_id in self._async_current_ids():
raise data_entry_flow.AbortFlow("already_configured")
async def async_set_unique_id( async def async_set_unique_id(
self, unique_id: str, *, raise_on_progress: bool = True self, unique_id: str, *, raise_on_progress: bool = True
) -> Optional[ConfigEntry]: ) -> Optional[ConfigEntry]:
@ -749,7 +774,7 @@ class ConfigFlow(data_entry_flow.FlowHandler):
if raise_on_progress: if raise_on_progress:
for progress in self._async_in_progress(): for progress in self._async_in_progress():
if progress["context"].get("unique_id") == unique_id: if progress["context"].get("unique_id") == unique_id:
raise UniqueIdInProgress("already_in_progress") raise data_entry_flow.AbortFlow("already_in_progress")
# pylint: disable=no-member # pylint: disable=no-member
self.context["unique_id"] = unique_id self.context["unique_id"] = unique_id
@ -766,6 +791,15 @@ class ConfigFlow(data_entry_flow.FlowHandler):
assert self.hass is not None assert self.hass is not None
return self.hass.config_entries.async_entries(self.handler) return self.hass.config_entries.async_entries(self.handler)
@callback
def _async_current_ids(self) -> Set[Optional[str]]:
"""Return current unique IDs."""
assert self.hass is not None
return set(
entry.unique_id
for entry in self.hass.config_entries.async_entries(self.handler)
)
@callback @callback
def _async_in_progress(self) -> List[Dict]: def _async_in_progress(self) -> List[Dict]:
"""Return other in progress flows for current domain.""" """Return other in progress flows for current domain."""

View file

@ -1134,6 +1134,9 @@ class ServiceRegistry:
self._services[domain].pop(service) self._services[domain].pop(service)
if not self._services[domain]:
self._services.pop(domain)
self._hass.bus.async_fire( self._hass.bus.async_fire(
EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service}
) )

View file

@ -163,7 +163,7 @@ aioharmony==0.1.13
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==1.9.2 aiohue==1.10.1
# homeassistant.components.imap # homeassistant.components.imap
aioimaplib==0.7.15 aioimaplib==0.7.15

View file

@ -63,7 +63,7 @@ aioesphomeapi==2.6.1
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==1.9.2 aiohue==1.10.1
# homeassistant.components.notion # homeassistant.components.notion
aionotion==1.1.0 aionotion==1.1.0

View file

@ -9,107 +9,110 @@ from homeassistant.exceptions import ConfigEntryNotReady
from tests.common import mock_coro from tests.common import mock_coro
async def test_bridge_setup(): async def test_bridge_setup(hass):
"""Test a successful setup.""" """Test a successful setup."""
hass = Mock()
entry = Mock() entry = Mock()
api = Mock() api = Mock(initialize=mock_coro)
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) hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", return_value=mock_coro(api)): with patch("aiohue.Bridge", return_value=api), patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward:
assert await hue_bridge.async_setup() is True assert await hue_bridge.async_setup() is True
assert hue_bridge.api is api assert hue_bridge.api is api
forward_entries = set( assert len(mock_forward.mock_calls) == 3
c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls forward_entries = set(c[1][1] for c in mock_forward.mock_calls)
)
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3
assert forward_entries == set(["light", "binary_sensor", "sensor"]) assert forward_entries == set(["light", "binary_sensor", "sensor"])
async def test_bridge_setup_invalid_username(): 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."""
hass = Mock()
entry = Mock()
entry.data = {"host": "1.2.3.4", "username": "mock-username"}
hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired):
assert await hue_bridge.async_setup() is False
assert len(hass.async_create_task.mock_calls) == 1
assert len(hass.config_entries.flow.async_init.mock_calls) == 1
assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == {
"host": "1.2.3.4"
}
async def test_bridge_setup_timeout(hass):
"""Test we retry to connect if we cannot connect."""
hass = Mock()
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) hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object( with patch.object(
bridge, "get_bridge", side_effect=errors.CannotConnect bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
assert await hue_bridge.async_setup() is False
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][2]["data"] == {"host": "1.2.3.4"}
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)
with patch.object(
bridge, "authenticate_bridge", side_effect=errors.CannotConnect
), pytest.raises(ConfigEntryNotReady): ), pytest.raises(ConfigEntryNotReady):
await hue_bridge.async_setup() await hue_bridge.async_setup()
async def test_reset_if_entry_had_wrong_auth(): 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."""
hass = Mock()
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) hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired): with patch.object(
bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired
), patch.object(bridge, "create_config_flow") as mock_create:
assert await hue_bridge.async_setup() is False assert await hue_bridge.async_setup() is False
assert len(hass.async_create_task.mock_calls) == 1 assert len(mock_create.mock_calls) == 1
assert await hue_bridge.async_reset() assert await hue_bridge.async_reset()
async def test_reset_unloads_entry_if_setup(): 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."""
hass = Mock()
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) hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): with patch.object(
bridge, "authenticate_bridge", return_value=mock_coro(Mock())
), patch("aiohue.Bridge", return_value=Mock()), patch.object(
hass.config_entries, "async_forward_entry_setup"
) as mock_forward:
assert await hue_bridge.async_setup() is True assert await hue_bridge.async_setup() is True
assert len(hass.services.async_register.mock_calls) == 1 assert len(hass.services.async_services()) == 1
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 assert len(mock_forward.mock_calls) == 3
hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) with patch.object(
assert await hue_bridge.async_reset() hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True)
) as mock_forward:
assert await hue_bridge.async_reset()
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3 assert len(mock_forward.mock_calls) == 3
assert len(hass.services.async_remove.mock_calls) == 1 assert len(hass.services.async_services()) == 0
async def test_handle_unauthorized(): async def test_handle_unauthorized(hass):
"""Test handling an unauthorized error on update.""" """Test handling an unauthorized error on update."""
hass = Mock()
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) hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): with patch.object(
bridge, "authenticate_bridge", return_value=mock_coro(Mock())
), patch("aiohue.Bridge", return_value=Mock()):
assert await hue_bridge.async_setup() is True assert await hue_bridge.async_setup() is True
assert hue_bridge.authorized is True assert hue_bridge.authorized is True
await hue_bridge.handle_unauthorized_error() with patch.object(bridge, "create_config_flow") as mock_create:
await hue_bridge.handle_unauthorized_error()
assert hue_bridge.authorized is False assert hue_bridge.authorized is False
assert len(hass.async_create_task.mock_calls) == 4 assert len(mock_create.mock_calls) == 1
assert len(hass.config_entries.flow.async_init.mock_calls) == 1 assert mock_create.mock_calls[0][1][1] == "1.2.3.4"
assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == {
"host": "1.2.3.4"
}

View file

@ -6,50 +6,52 @@ import aiohue
import pytest import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant.components.hue import config_flow, const, errors from homeassistant import data_entry_flow
from homeassistant.components.hue import config_flow, const
from tests.common import MockConfigEntry, mock_coro from tests.common import MockConfigEntry, mock_coro
async def test_flow_works(hass, aioclient_mock): async def test_flow_works(hass):
"""Test config flow .""" """Test config flow ."""
aioclient_mock.get( mock_bridge = Mock()
const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] mock_bridge.host = "1.2.3.4"
) mock_bridge.username = None
mock_bridge.config.name = "Mock Bridge"
mock_bridge.id = "aabbccddeeff"
async def mock_create_user(username):
mock_bridge.username = username
mock_bridge.create_user = mock_create_user
mock_bridge.initialize.return_value = mock_coro()
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
await flow.async_step_init()
with patch("aiohue.Bridge") as mock_bridge: with patch(
"homeassistant.components.hue.config_flow.discover_nupnp",
return_value=mock_coro([mock_bridge]),
):
result = await flow.async_step_init()
def mock_constructor(host, websession, username=None): assert result["type"] == "form"
"""Fake the bridge constructor.""" assert result["step_id"] == "link"
mock_bridge.host = host
return mock_bridge
mock_bridge.side_effect = mock_constructor assert flow.context["unique_id"] == "aabbccddeeff"
mock_bridge.username = "username-abc"
mock_bridge.config.name = "Mock Bridge"
mock_bridge.config.bridgeid = "bridge-id-1234"
mock_bridge.create_user.return_value = mock_coro()
mock_bridge.initialize.return_value = mock_coro()
result = await flow.async_step_link(user_input={}) result = await flow.async_step_link(user_input={})
assert mock_bridge.host == "1.2.3.4"
assert len(mock_bridge.create_user.mock_calls) == 1
assert len(mock_bridge.initialize.mock_calls) == 1
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == "Mock Bridge" assert result["title"] == "Mock Bridge"
assert result["data"] == { assert result["data"] == {
"host": "1.2.3.4", "host": "1.2.3.4",
"bridge_id": "bridge-id-1234", "username": "home-assistant#test-home",
"username": "username-abc",
} }
assert len(mock_bridge.initialize.mock_calls) == 1
async def test_flow_no_discovered_bridges(hass, aioclient_mock): async def test_flow_no_discovered_bridges(hass, aioclient_mock):
"""Test config flow discovers no bridges.""" """Test config flow discovers no bridges."""
@ -66,9 +68,12 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
aioclient_mock.get( aioclient_mock.get(
const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}]
) )
MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) MockConfigEntry(
domain="hue", unique_id="bla", data={"host": "1.2.3.4"}
).add_to_hass(hass)
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {}
result = await flow.async_step_init() result = await flow.async_step_init()
assert result["type"] == "abort" assert result["type"] == "abort"
@ -81,6 +86,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock):
) )
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {}
result = await flow.async_step_init() result = await flow.async_step_init()
assert result["type"] == "form" assert result["type"] == "form"
@ -104,10 +110,10 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
assert result["step_id"] == "init" assert result["step_id"] == "init"
with pytest.raises(vol.Invalid): with pytest.raises(vol.Invalid):
assert result["data_schema"]({"host": "0.0.0.0"}) assert result["data_schema"]({"id": "not-discovered"})
result["data_schema"]({"host": "1.2.3.4"}) result["data_schema"]({"id": "bla"})
result["data_schema"]({"host": "5.6.7.8"}) result["data_schema"]({"id": "beer"})
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
@ -119,14 +125,17 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
{"internalipaddress": "5.6.7.8", "id": "beer"}, {"internalipaddress": "5.6.7.8", "id": "beer"},
], ],
) )
MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) MockConfigEntry(
domain="hue", unique_id="bla", data={"host": "1.2.3.4"}
).add_to_hass(hass)
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {}
result = await flow.async_step_init() result = await flow.async_step_init()
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
assert flow.host == "5.6.7.8" assert flow.bridge.host == "5.6.7.8"
async def test_flow_timeout_discovery(hass): async def test_flow_timeout_discovery(hass):
@ -147,6 +156,7 @@ async def test_flow_link_timeout(hass):
"""Test config flow .""" """Test config flow ."""
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.bridge = Mock()
with patch("aiohue.Bridge.create_user", side_effect=asyncio.TimeoutError): with patch("aiohue.Bridge.create_user", side_effect=asyncio.TimeoutError):
result = await flow.async_step_link({}) result = await flow.async_step_link({})
@ -160,9 +170,11 @@ async def test_flow_link_button_not_pressed(hass):
"""Test config flow .""" """Test config flow ."""
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.bridge = Mock(
username=None, create_user=Mock(side_effect=aiohue.LinkButtonNotPressed)
)
with patch("aiohue.Bridge.create_user", side_effect=aiohue.LinkButtonNotPressed): result = await flow.async_step_link({})
result = await flow.async_step_link({})
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
@ -173,6 +185,7 @@ async def test_flow_link_unknown_host(hass):
"""Test config flow .""" """Test config flow ."""
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.bridge = Mock()
with patch("aiohue.Bridge.create_user", side_effect=aiohue.RequestError): with patch("aiohue.Bridge.create_user", side_effect=aiohue.RequestError):
result = await flow.async_step_link({}) result = await flow.async_step_link({})
@ -188,16 +201,13 @@ async def test_bridge_ssdp(hass):
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
with patch.object( result = await flow.async_step_ssdp(
config_flow, "get_bridge", side_effect=errors.AuthenticationRequired {
): "host": "0.0.0.0",
result = await flow.async_step_ssdp( "serial": "1234",
{ "manufacturerURL": config_flow.HUE_MANUFACTURERURL,
"host": "0.0.0.0", }
"serial": "1234", )
"manufacturerURL": config_flow.HUE_MANUFACTURERURL,
}
)
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
@ -255,47 +265,22 @@ async def test_bridge_ssdp_espalexa(hass):
async def test_bridge_ssdp_already_configured(hass): async def test_bridge_ssdp_already_configured(hass):
"""Test if a discovered bridge has already been configured.""" """Test if a discovered bridge has already been configured."""
MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) MockConfigEntry(
domain="hue", unique_id="1234", data={"host": "0.0.0.0"}
).add_to_hass(hass)
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
result = await flow.async_step_ssdp( with pytest.raises(data_entry_flow.AbortFlow):
{ await flow.async_step_ssdp(
"host": "0.0.0.0", {
"serial": "1234", "host": "0.0.0.0",
"manufacturerURL": config_flow.HUE_MANUFACTURERURL, "serial": "1234",
} "manufacturerURL": config_flow.HUE_MANUFACTURERURL,
) }
)
assert result["type"] == "abort"
async def test_import_with_existing_config(hass):
"""Test importing a host with an existing config file."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
flow.context = {}
bridge = Mock()
bridge.username = "username-abc"
bridge.config.bridgeid = "bridge-id-1234"
bridge.config.name = "Mock Bridge"
bridge.host = "0.0.0.0"
with patch.object(
config_flow, "_find_username_from_config", return_value="mock-user"
), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)):
result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"})
assert result["type"] == "create_entry"
assert result["title"] == "Mock Bridge"
assert result["data"] == {
"host": "0.0.0.0",
"bridge_id": "bridge-id-1234",
"username": "username-abc",
}
async def test_import_with_no_config(hass): async def test_import_with_no_config(hass):
@ -304,45 +289,12 @@ async def test_import_with_no_config(hass):
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
with patch.object( result = await flow.async_step_import({"host": "0.0.0.0"})
config_flow, "get_bridge", side_effect=errors.AuthenticationRequired
):
result = await flow.async_step_import({"host": "0.0.0.0"})
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
async def test_import_with_existing_but_invalid_config(hass):
"""Test importing a host with a config file with invalid username."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
flow.context = {}
with patch.object(
config_flow, "_find_username_from_config", return_value="mock-user"
), patch.object(
config_flow, "get_bridge", side_effect=errors.AuthenticationRequired
):
result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"})
assert result["type"] == "form"
assert result["step_id"] == "link"
async def test_import_cannot_connect(hass):
"""Test importing a host that we cannot conncet to."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
flow.context = {}
with patch.object(config_flow, "get_bridge", side_effect=errors.CannotConnect):
result = await flow.async_step_import({"host": "0.0.0.0"})
assert result["type"] == "abort"
assert result["reason"] == "cannot_connect"
async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
"""Test that we clean up entries for same host and bridge. """Test that we clean up entries for same host and bridge.
@ -351,38 +303,45 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
all existing entries that either have same IP or same bridge_id. all existing entries that either have same IP or same bridge_id.
""" """
orig_entry = MockConfigEntry( orig_entry = MockConfigEntry(
domain="hue", domain="hue", data={"host": "0.0.0.0", "username": "aaaa"}, unique_id="id-1234",
data={"host": "0.0.0.0", "bridge_id": "id-1234"},
unique_id="id-1234",
) )
orig_entry.add_to_hass(hass) orig_entry.add_to_hass(hass)
MockConfigEntry( MockConfigEntry(
domain="hue", domain="hue", data={"host": "1.2.3.4", "username": "bbbb"}, unique_id="id-5678",
data={"host": "1.2.3.4", "bridge_id": "id-5678"},
unique_id="id-5678",
).add_to_hass(hass) ).add_to_hass(hass)
assert len(hass.config_entries.async_entries("hue")) == 2 assert len(hass.config_entries.async_entries("hue")) == 2
bridge = Mock() bridge = Mock()
bridge.username = "username-abc" bridge.username = "username-abc"
bridge.config.bridgeid = "id-1234"
bridge.config.name = "Mock Bridge" bridge.config.name = "Mock Bridge"
bridge.host = "0.0.0.0" bridge.host = "0.0.0.0"
bridge.id = "id-1234"
with patch.object( with patch(
config_flow, "_find_username_from_config", return_value="mock-user" "aiohue.Bridge", return_value=bridge,
), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"hue", data={"host": "2.2.2.2"}, context={"source": "import"} "hue", data={"host": "2.2.2.2"}, context={"source": "import"}
) )
assert result["type"] == "form"
assert result["step_id"] == "link"
with patch(
"homeassistant.components.hue.config_flow.authenticate_bridge",
return_value=mock_coro(),
), patch(
"homeassistant.components.hue.async_setup_entry",
side_effect=lambda _, _2: mock_coro(True),
):
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == "Mock Bridge" assert result["title"] == "Mock Bridge"
assert result["data"] == { assert result["data"] == {
"host": "0.0.0.0", "host": "0.0.0.0",
"bridge_id": "id-1234",
"username": "username-abc", "username": "username-abc",
} }
entries = hass.config_entries.async_entries("hue") entries = hass.config_entries.async_entries("hue")
@ -398,17 +357,14 @@ async def test_bridge_homekit(hass):
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
with patch.object( result = await flow.async_step_homekit(
config_flow, "get_bridge", side_effect=errors.AuthenticationRequired {
): "host": "0.0.0.0",
result = await flow.async_step_homekit( "serial": "1234",
{ "manufacturerURL": config_flow.HUE_MANUFACTURERURL,
"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"},
"serial": "1234", }
"manufacturerURL": config_flow.HUE_MANUFACTURERURL, )
"properties": {"id": "aa:bb:cc:dd:ee:ff"},
}
)
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
@ -416,12 +372,15 @@ async def test_bridge_homekit(hass):
async def test_bridge_homekit_already_configured(hass): async def test_bridge_homekit_already_configured(hass):
"""Test if a HomeKit discovered bridge has already been configured.""" """Test if a HomeKit discovered bridge has already been configured."""
MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) MockConfigEntry(
domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"}
).add_to_hass(hass)
flow = config_flow.HueFlowHandler() flow = config_flow.HueFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
result = await flow.async_step_homekit({"host": "0.0.0.0"}) with pytest.raises(data_entry_flow.AbortFlow):
await flow.async_step_homekit(
assert result["type"] == "abort" {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}
)

View file

@ -9,13 +9,10 @@ from tests.common import MockConfigEntry, mock_coro
async def test_setup_with_no_config(hass): async def test_setup_with_no_config(hass):
"""Test that we do not discover anything or try to set up a bridge.""" """Test that we do not discover anything or try to set up a bridge."""
with patch.object(hass, "config_entries") as mock_config_entries, patch.object( assert await async_setup_component(hass, hue.DOMAIN, {}) is True
hue, "configured_hosts", return_value=[]
):
assert await async_setup_component(hass, hue.DOMAIN, {}) is True
# No flows started # No flows started
assert len(mock_config_entries.flow.mock_calls) == 0 assert len(hass.config_entries.flow.async_progress()) == 0
# No configs stored # No configs stored
assert hass.data[hue.DOMAIN] == {} assert hass.data[hue.DOMAIN] == {}
@ -23,9 +20,9 @@ async def test_setup_with_no_config(hass):
async def test_setup_defined_hosts_known_auth(hass): async def test_setup_defined_hosts_known_auth(hass):
"""Test we don't initiate a config entry if config bridge is known.""" """Test we don't initiate a config entry if config bridge is known."""
with patch.object(hass, "config_entries") as mock_config_entries, patch.object( MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass)
hue, "configured_hosts", return_value=["0.0.0.0"]
): with patch.object(hue, "async_setup_entry", return_value=mock_coro(True)):
assert ( assert (
await async_setup_component( await async_setup_component(
hass, hass,
@ -34,7 +31,6 @@ async def test_setup_defined_hosts_known_auth(hass):
hue.DOMAIN: { hue.DOMAIN: {
hue.CONF_BRIDGES: { hue.CONF_BRIDGES: {
hue.CONF_HOST: "0.0.0.0", hue.CONF_HOST: "0.0.0.0",
hue.CONF_FILENAME: "bla.conf",
hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True, hue.CONF_ALLOW_UNREACHABLE: True,
} }
@ -45,13 +41,12 @@ async def test_setup_defined_hosts_known_auth(hass):
) )
# Flow started for discovered bridge # Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 0 assert len(hass.config_entries.flow.async_progress()) == 0
# Config stored for domain. # Config stored for domain.
assert hass.data[hue.DATA_CONFIGS] == { assert hass.data[hue.DATA_CONFIGS] == {
"0.0.0.0": { "0.0.0.0": {
hue.CONF_HOST: "0.0.0.0", hue.CONF_HOST: "0.0.0.0",
hue.CONF_FILENAME: "bla.conf",
hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True, hue.CONF_ALLOW_UNREACHABLE: True,
} }
@ -60,40 +55,30 @@ async def test_setup_defined_hosts_known_auth(hass):
async def test_setup_defined_hosts_no_known_auth(hass): async def test_setup_defined_hosts_no_known_auth(hass):
"""Test we initiate config entry if config bridge is not known.""" """Test we initiate config entry if config bridge is not known."""
with patch.object(hass, "config_entries") as mock_config_entries, patch.object( assert (
hue, "configured_hosts", return_value=[] await async_setup_component(
): hass,
mock_config_entries.flow.async_init.return_value = mock_coro() hue.DOMAIN,
assert ( {
await async_setup_component( hue.DOMAIN: {
hass, hue.CONF_BRIDGES: {
hue.DOMAIN, hue.CONF_HOST: "0.0.0.0",
{ hue.CONF_ALLOW_HUE_GROUPS: False,
hue.DOMAIN: { hue.CONF_ALLOW_UNREACHABLE: True,
hue.CONF_BRIDGES: {
hue.CONF_HOST: "0.0.0.0",
hue.CONF_FILENAME: "bla.conf",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
}
} }
}, }
) },
is True
) )
is True
)
# Flow started for discovered bridge # Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 1 assert len(hass.config_entries.flow.async_progress()) == 1
assert mock_config_entries.flow.mock_calls[0][2]["data"] == {
"host": "0.0.0.0",
"path": "bla.conf",
}
# Config stored for domain. # Config stored for domain.
assert hass.data[hue.DATA_CONFIGS] == { assert hass.data[hue.DATA_CONFIGS] == {
"0.0.0.0": { "0.0.0.0": {
hue.CONF_HOST: "0.0.0.0", hue.CONF_HOST: "0.0.0.0",
hue.CONF_FILENAME: "bla.conf",
hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True, hue.CONF_ALLOW_UNREACHABLE: True,
} }
@ -126,7 +111,6 @@ async def test_config_passed_to_config_entry(hass):
hue.DOMAIN: { hue.DOMAIN: {
hue.CONF_BRIDGES: { hue.CONF_BRIDGES: {
hue.CONF_HOST: "0.0.0.0", hue.CONF_HOST: "0.0.0.0",
hue.CONF_FILENAME: "bla.conf",
hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True, hue.CONF_ALLOW_UNREACHABLE: True,
} }
@ -166,7 +150,7 @@ async def test_unload_entry(hass):
return_value=mock_coro(Mock()), return_value=mock_coro(Mock()),
): ):
mock_bridge.return_value.async_setup.return_value = mock_coro(True) mock_bridge.return_value.async_setup.return_value = mock_coro(True)
mock_bridge.return_value.api.config = Mock() mock_bridge.return_value.api.config = Mock(bridgeid="aabbccddeeff")
assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert await async_setup_component(hass, hue.DOMAIN, {}) is True
assert len(mock_bridge.return_value.mock_calls) == 1 assert len(mock_bridge.return_value.mock_calls) == 1

View file

@ -434,8 +434,8 @@ async def test_saving_and_loading(hass):
VERSION = 5 VERSION = 5
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@asyncio.coroutine async def async_step_user(self, user_input=None):
def async_step_user(self, user_input=None): await self.async_set_unique_id("unique")
return self.async_create_entry(title="Test Title", data={"token": "abcd"}) return self.async_create_entry(title="Test Title", data={"token": "abcd"})
with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): with patch.dict(config_entries.HANDLERS, {"test": TestFlow}):
@ -477,6 +477,7 @@ async def test_saving_and_loading(hass):
assert orig.data == loaded.data assert orig.data == loaded.data
assert orig.source == loaded.source assert orig.source == loaded.source
assert orig.connection_class == loaded.connection_class assert orig.connection_class == loaded.connection_class
assert orig.unique_id == loaded.unique_id
async def test_forward_entry_sets_up_component(hass): async def test_forward_entry_sets_up_component(hass):
@ -1108,3 +1109,40 @@ async def test_unique_id_in_progress(hass, manager):
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result2["reason"] == "already_in_progress" assert result2["reason"] == "already_in_progress"
async def test_finish_flow_aborts_progress(hass, manager):
"""Test that when finishing a flow, we abort other flows in progress with unique ID."""
mock_integration(
hass,
MockModule("comp", async_setup_entry=MagicMock(return_value=mock_coro(True))),
)
mock_entity_platform(hass, "config_flow.comp", None)
class TestFlow(config_entries.ConfigFlow):
VERSION = 1
async def async_step_user(self, user_input=None):
await self.async_set_unique_id("mock-unique-id", raise_on_progress=False)
if user_input is None:
return self.async_show_form(step_id="discovery")
return self.async_create_entry(title="yo", data={})
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
# Create one to be in progress
result = await manager.flow.async_init(
"comp", context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# Will finish and cancel other one.
result2 = await manager.flow.async_init(
"comp", context={"source": config_entries.SOURCE_USER}, data={}
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert len(hass.config_entries.flow.async_progress()) == 0