mirror of
https://github.com/home-assistant/core.git
synced 2025-06-03 12:47:06 +00:00

* Config flow for homekit Allows multiple homekit bridges to run HAP-python state is now stored at .storage/homekit.{entry_id}.state aids is now stored at .storage/homekit.{entry_id}.aids Overcomes 150 device limit by supporting multiple bridges. Name and port are now automatically allocated to avoid conflicts which was one of the main reasons pairing failed. YAML configuration remains available in order to offer entity specific configuration. Entries created by config flow can add and remove included domains and entities without having to restart * Fix services as there are multiple now * migrate in executor * drop title from strings * Update homeassistant/components/homekit/strings.json Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Make auto_start advanced mode only, add coverage * put back title * more references * delete port since manual config is no longer needed Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
302 lines
9.8 KiB
Python
302 lines
9.8 KiB
Python
"""Config flow for HomeKit integration."""
|
|
import logging
|
|
import random
|
|
import string
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.config_entries import SOURCE_IMPORT
|
|
from homeassistant.const import CONF_NAME, CONF_PORT
|
|
from homeassistant.core import callback, split_entity_id
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entityfilter import (
|
|
CONF_EXCLUDE_DOMAINS,
|
|
CONF_EXCLUDE_ENTITIES,
|
|
CONF_INCLUDE_DOMAINS,
|
|
CONF_INCLUDE_ENTITIES,
|
|
)
|
|
|
|
from .const import (
|
|
CONF_AUTO_START,
|
|
CONF_FILTER,
|
|
CONF_SAFE_MODE,
|
|
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
|
DEFAULT_AUTO_START,
|
|
DEFAULT_CONFIG_FLOW_PORT,
|
|
DEFAULT_SAFE_MODE,
|
|
DEFAULT_ZEROCONF_DEFAULT_INTERFACE,
|
|
SHORT_BRIDGE_NAME,
|
|
)
|
|
from .const import DOMAIN # pylint:disable=unused-import
|
|
from .util import find_next_available_port
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_DOMAINS = "domains"
|
|
SUPPORTED_DOMAINS = [
|
|
"alarm_control_panel",
|
|
"automation",
|
|
"binary_sensor",
|
|
"climate",
|
|
"cover",
|
|
"demo",
|
|
"device_tracker",
|
|
"fan",
|
|
"input_boolean",
|
|
"light",
|
|
"lock",
|
|
"media_player",
|
|
"person",
|
|
"remote",
|
|
"scene",
|
|
"script",
|
|
"sensor",
|
|
"switch",
|
|
"vacuum",
|
|
"water_heater",
|
|
]
|
|
|
|
DEFAULT_DOMAINS = [
|
|
"alarm_control_panel",
|
|
"climate",
|
|
"cover",
|
|
"light",
|
|
"lock",
|
|
"media_player",
|
|
"switch",
|
|
"vacuum",
|
|
"water_heater",
|
|
]
|
|
|
|
|
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
"""Handle a config flow for HomeKit."""
|
|
|
|
VERSION = 1
|
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
|
|
|
def __init__(self):
|
|
"""Initialize config flow."""
|
|
self.homekit_data = {}
|
|
self.entry_title = None
|
|
|
|
async def async_step_pairing(self, user_input=None):
|
|
"""Pairing instructions."""
|
|
if user_input is not None:
|
|
return self.async_create_entry(
|
|
title=self.entry_title, data=self.homekit_data
|
|
)
|
|
return self.async_show_form(
|
|
step_id="pairing",
|
|
description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]},
|
|
)
|
|
|
|
async def async_step_user(self, user_input=None):
|
|
"""Handle the initial step."""
|
|
errors = {}
|
|
if user_input is not None:
|
|
port = await self._async_available_port()
|
|
name = self._async_available_name()
|
|
title = f"{name}:{port}"
|
|
self.homekit_data = user_input.copy()
|
|
self.homekit_data[CONF_NAME] = name
|
|
self.homekit_data[CONF_PORT] = port
|
|
self.homekit_data[CONF_FILTER] = {
|
|
CONF_INCLUDE_DOMAINS: user_input[CONF_INCLUDE_DOMAINS],
|
|
CONF_INCLUDE_ENTITIES: [],
|
|
CONF_EXCLUDE_DOMAINS: [],
|
|
CONF_EXCLUDE_ENTITIES: [],
|
|
}
|
|
del self.homekit_data[CONF_INCLUDE_DOMAINS]
|
|
self.entry_title = title
|
|
return await self.async_step_pairing()
|
|
|
|
default_domains = [] if self._async_current_entries() else DEFAULT_DOMAINS
|
|
setup_schema = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool,
|
|
vol.Required(
|
|
CONF_INCLUDE_DOMAINS, default=default_domains
|
|
): cv.multi_select(SUPPORTED_DOMAINS),
|
|
}
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="user", data_schema=setup_schema, errors=errors
|
|
)
|
|
|
|
async def async_step_import(self, user_input=None):
|
|
"""Handle import from yaml."""
|
|
if not self._async_is_unique_name_port(user_input):
|
|
return self.async_abort(reason="port_name_in_use")
|
|
return self.async_create_entry(
|
|
title=f"{user_input[CONF_NAME]}:{user_input[CONF_PORT]}", data=user_input
|
|
)
|
|
|
|
async def _async_available_port(self):
|
|
"""Return an available port the bridge."""
|
|
return await self.hass.async_add_executor_job(
|
|
find_next_available_port, DEFAULT_CONFIG_FLOW_PORT
|
|
)
|
|
|
|
@callback
|
|
def _async_available_name(self):
|
|
"""Return an available for the bridge."""
|
|
current_entries = self._async_current_entries()
|
|
|
|
# We always pick a RANDOM name to avoid Zeroconf
|
|
# name collisions. If the name has been seen before
|
|
# pairing will probably fail.
|
|
acceptable_chars = string.ascii_uppercase + string.digits
|
|
trailer = "".join(random.choices(acceptable_chars, k=4))
|
|
all_names = {entry.data[CONF_NAME] for entry in current_entries}
|
|
suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}"
|
|
while suggested_name in all_names:
|
|
trailer = "".join(random.choices(acceptable_chars, k=4))
|
|
suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}"
|
|
|
|
return suggested_name
|
|
|
|
@callback
|
|
def _async_is_unique_name_port(self, user_input):
|
|
"""Determine is a name or port is already used."""
|
|
name = user_input[CONF_NAME]
|
|
port = user_input[CONF_PORT]
|
|
for entry in self._async_current_entries():
|
|
if entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port:
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
@callback
|
|
def async_get_options_flow(config_entry):
|
|
"""Get the options flow for this handler."""
|
|
return OptionsFlowHandler(config_entry)
|
|
|
|
|
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
|
"""Handle a option flow for tado."""
|
|
|
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
|
"""Initialize options flow."""
|
|
self.config_entry = config_entry
|
|
self.homekit_options = {}
|
|
|
|
async def async_step_yaml(self, user_input=None):
|
|
"""No options for yaml managed entries."""
|
|
if user_input is not None:
|
|
# Apparently not possible to abort an options flow
|
|
# at the moment
|
|
return self.async_create_entry(title="", data=self.config_entry.options)
|
|
|
|
return self.async_show_form(step_id="yaml")
|
|
|
|
async def async_step_advanced(self, user_input=None):
|
|
"""Choose advanced options."""
|
|
if user_input is not None:
|
|
self.homekit_options.update(user_input)
|
|
del self.homekit_options[CONF_INCLUDE_DOMAINS]
|
|
return self.async_create_entry(title="", data=self.homekit_options)
|
|
|
|
schema_base = {}
|
|
|
|
if self.show_advanced_options:
|
|
schema_base[
|
|
vol.Optional(
|
|
CONF_AUTO_START,
|
|
default=self.homekit_options.get(
|
|
CONF_AUTO_START, DEFAULT_AUTO_START
|
|
),
|
|
)
|
|
] = bool
|
|
else:
|
|
self.homekit_options[CONF_AUTO_START] = self.homekit_options.get(
|
|
CONF_AUTO_START, DEFAULT_AUTO_START
|
|
)
|
|
|
|
schema_base.update(
|
|
{
|
|
vol.Optional(
|
|
CONF_SAFE_MODE,
|
|
default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE),
|
|
): bool,
|
|
vol.Optional(
|
|
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
|
default=self.homekit_options.get(
|
|
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
|
DEFAULT_ZEROCONF_DEFAULT_INTERFACE,
|
|
),
|
|
): bool,
|
|
}
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="advanced", data_schema=vol.Schema(schema_base)
|
|
)
|
|
|
|
async def async_step_exclude(self, user_input=None):
|
|
"""Choose entities to exclude from the domain."""
|
|
if user_input is not None:
|
|
self.homekit_options[CONF_FILTER] = {
|
|
CONF_INCLUDE_DOMAINS: self.homekit_options[CONF_INCLUDE_DOMAINS],
|
|
CONF_EXCLUDE_DOMAINS: self.homekit_options.get(
|
|
CONF_EXCLUDE_DOMAINS, []
|
|
),
|
|
CONF_INCLUDE_ENTITIES: self.homekit_options.get(
|
|
CONF_INCLUDE_ENTITIES, []
|
|
),
|
|
CONF_EXCLUDE_ENTITIES: user_input[CONF_EXCLUDE_ENTITIES],
|
|
}
|
|
return await self.async_step_advanced()
|
|
|
|
entity_filter = self.homekit_options.get(CONF_FILTER, {})
|
|
all_supported_entities = await self.hass.async_add_executor_job(
|
|
_get_entities_matching_domains,
|
|
self.hass,
|
|
self.homekit_options[CONF_INCLUDE_DOMAINS],
|
|
)
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_EXCLUDE_ENTITIES,
|
|
default=entity_filter.get(CONF_EXCLUDE_ENTITIES, []),
|
|
): cv.multi_select(all_supported_entities),
|
|
}
|
|
)
|
|
return self.async_show_form(step_id="exclude", data_schema=data_schema)
|
|
|
|
async def async_step_init(self, user_input=None):
|
|
"""Handle options flow."""
|
|
if self.config_entry.source == SOURCE_IMPORT:
|
|
return await self.async_step_yaml(user_input)
|
|
|
|
if user_input is not None:
|
|
self.homekit_options.update(user_input)
|
|
return await self.async_step_exclude()
|
|
|
|
self.homekit_options = dict(self.config_entry.options)
|
|
entity_filter = self.homekit_options.get(CONF_FILTER, {})
|
|
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_INCLUDE_DOMAINS,
|
|
default=entity_filter.get(CONF_INCLUDE_DOMAINS, []),
|
|
): cv.multi_select(SUPPORTED_DOMAINS)
|
|
}
|
|
)
|
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
|
|
|
|
|
def _get_entities_matching_domains(hass, domains):
|
|
"""List entities in the given domains."""
|
|
included_domains = set(domains)
|
|
entity_ids = [
|
|
state.entity_id
|
|
for state in hass.states.all()
|
|
if (split_entity_id(state.entity_id))[0] in included_domains
|
|
]
|
|
entity_ids.sort()
|
|
return entity_ids
|