core/homeassistant/components/hue/config_flow.py

214 lines
7.3 KiB
Python
Raw Normal View History

"""Config flow to configure Philips Hue."""
import asyncio
from typing import Dict, Optional
from urllib.parse import urlparse
import aiohue
from aiohue.discovery import discover_nupnp, normalize_bridge_id
import async_timeout
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.components import ssdp
from homeassistant.const import CONF_HOST
from homeassistant.helpers import aiohttp_client
from .bridge import authenticate_bridge
from .const import ( # pylint: disable=unused-import
CONF_ALLOW_HUE_GROUPS,
DOMAIN,
LOGGER,
)
from .errors import AuthenticationRequired, CannotConnect
2019-07-31 19:25:30 +00:00
HUE_MANUFACTURERURL = "http://www.philips.com"
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Hue config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
def __init__(self):
"""Initialize the Hue flow."""
self.bridge: Optional[aiohue.Bridge] = None
self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
# This is for backwards compatibility.
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):
"""Handle a flow start."""
if (
user_input is not None
and self.discovered_bridges is not None
and user_input["id"] in self.discovered_bridges
):
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:
with async_timeout.timeout(5):
bridges = await discover_nupnp(
websession=aiohttp_client.async_get_clientsession(self.hass)
)
except asyncio.TimeoutError:
2019-07-31 19:25:30 +00:00
return self.async_abort(reason="discover_timeout")
if not bridges:
2019-07-31 19:25:30 +00:00
return self.async_abort(reason="no_bridges")
# Find already configured hosts
2019-12-18 06:41:01 +00:00
already_configured = self._async_current_ids(False)
bridges = [bridge for bridge in bridges if bridge.id not in already_configured]
if not bridges:
2019-07-31 19:25:30 +00:00
return self.async_abort(reason="all_configured")
if len(bridges) == 1:
self.bridge = bridges[0]
await self.async_set_unique_id(self.bridge.id, raise_on_progress=False)
return await self.async_step_link()
self.discovered_bridges = {bridge.id: bridge for bridge in bridges}
return self.async_show_form(
2019-07-31 19:25:30 +00:00
step_id="init",
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):
"""Attempt to link with the Hue bridge.
Given a configured host, will ask the user to press the link button
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 = {}
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:
2019-07-31 19:25:30 +00:00
errors["base"] = "register_failed"
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host)
2019-07-31 19:25:30 +00:00
errors["base"] = "linking"
except Exception: # pylint: disable=broad-except
LOGGER.exception(
"Unknown error connecting with Hue bridge at %s", bridge.host
2019-07-31 19:25:30 +00:00
)
errors["base"] = "linking"
2019-07-31 19:25:30 +00:00
return self.async_show_form(step_id="link", errors=errors)
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered Hue bridge.
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.
"""
# Filter out non-Hue bridges #1
if discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != HUE_MANUFACTURERURL:
2019-07-31 19:25:30 +00:00
return self.async_abort(reason="not_hue_bridge")
# Filter out non-Hue bridges #2
if any(
name in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "")
for name in HUE_IGNORED_BRIDGE_NAMES
):
return self.async_abort(reason="not_hue_bridge")
if (
ssdp.ATTR_SSDP_LOCATION not in discovery_info
or ssdp.ATTR_UPNP_SERIAL not in discovery_info
):
return self.async_abort(reason="not_hue_bridge")
host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname
bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL])
await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host})
self.bridge = bridge
return await self.async_step_link()
async def async_step_homekit(self, homekit_info):
"""Handle HomeKit discovery."""
bridge = self._async_get_bridge(
homekit_info["host"], homekit_info["properties"]["id"]
)
await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host})
self.bridge = bridge
return await self.async_step_link()
async def async_step_import(self, import_info):
"""Import a new bridge as a config entry.
This flow is triggered by `async_setup` for both configured and
discovered bridges. Triggered for any bridge that does not have a
config entry yet (based on host).
This flow is also triggered by `async_step_discovery`.
"""
# Check if host exists, abort if so.
if any(
import_info["host"] == entry.data["host"]
for entry in self._async_current_entries()
):
return self.async_abort(reason="already_configured")
self.bridge = self._async_get_bridge(import_info["host"])
return await self.async_step_link()