core/homeassistant/components/hue/config_flow.py

251 lines
8.3 KiB
Python

"""Config flow to configure Philips Hue."""
import asyncio
import json
import os
from aiohue.discovery import discover_nupnp
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .bridge import get_bridge
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = "http://www.philips.com"
@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):
"""Handle a Hue config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
if user_input is not None:
self.host = self.context["host"] = user_input["host"]
return await self.async_step_link()
websession = aiohttp_client.async_get_clientsession(self.hass)
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=websession)
except asyncio.TimeoutError:
return self.async_abort(reason="discover_timeout")
if not bridges:
return self.async_abort(reason="no_bridges")
# Find already configured hosts
configured = configured_hosts(self.hass)
hosts = [bridge.host for bridge in bridges if bridge.host not in configured]
if not hosts:
return self.async_abort(reason="all_configured")
if len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({vol.Required("host"): vol.In(hosts)}),
)
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.
"""
errors = {}
# We will always try linking in case the user has already pressed
# the link button.
try:
bridge = await get_bridge(self.hass, self.host, username=None)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
errors["base"] = "register_failed"
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
errors["base"] = "linking"
except Exception: # pylint: disable=broad-except
LOGGER.exception(
"Unknown error connecting with Hue bridge at %s", self.host
)
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)
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.
"""
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason="not_hue_bridge")
# Filter out emulated Hue
if "HASS Bridge" in discovery_info.get("name", ""):
return self.async_abort(reason="already_configured")
host = self.context["host"] = discovery_info.get("host")
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")
# This value is based off host/description.xml and is, weirdly, missing
# 4 characters in the middle of the serial compared to results returned
# from the NUPNP API or when querying the bridge API for bridgeid.
# (on first gen Hue hub)
serial = discovery_info.get("serial")
return await self.async_step_import(
{
"host": host,
# This format is the legacy format that Hue used for discovery
"path": "phue-{}.conf".format(serial),
}
)
async def async_step_homekit(self, homekit_info):
"""Handle HomeKit discovery."""
host = self.context["host"] = homekit_info.get("host")
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")
return await self.async_step_import({"host": host})
async def async_step_import(self, import_info):
"""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
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`.
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"]
path = import_info.get("path")
if path is not None:
username = await self.hass.async_add_job(
_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
same_hub_entries = [
entry.entry_id
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.data["bridge_id"] == bridge_id or entry.data["host"] == host
]
if same_hub_entries:
await asyncio.wait(
[
self.hass.config_entries.async_remove(entry_id)
for entry_id in same_hub_entries
]
)
return self.async_create_entry(
title=bridge.config.name,
data={"host": host, "bridge_id": bridge_id, "username": bridge.username},
)