Improve HomeKit Accessory Mode UX (#45402)
This commit is contained in:
parent
bc3610c8e1
commit
5f9a1d105c
6 changed files with 421 additions and 160 deletions
|
@ -575,13 +575,15 @@ class HomeKit:
|
|||
|
||||
bridged_states = []
|
||||
for state in self.hass.states.async_all():
|
||||
if not self._filter(state.entity_id):
|
||||
entity_id = state.entity_id
|
||||
|
||||
if not self._filter(entity_id):
|
||||
continue
|
||||
|
||||
ent_reg_ent = ent_reg.async_get(state.entity_id)
|
||||
ent_reg_ent = ent_reg.async_get(entity_id)
|
||||
if ent_reg_ent:
|
||||
await self._async_set_device_info_attributes(
|
||||
ent_reg_ent, dev_reg, state.entity_id
|
||||
ent_reg_ent, dev_reg, entity_id
|
||||
)
|
||||
self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state)
|
||||
|
||||
|
@ -612,13 +614,15 @@ class HomeKit:
|
|||
connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac)
|
||||
identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER)
|
||||
self._async_purge_old_bridges(dev_reg, identifier, connection)
|
||||
is_accessory_mode = self._homekit_mode == HOMEKIT_MODE_ACCESSORY
|
||||
hk_mode_name = "Accessory" if is_accessory_mode else "Bridge"
|
||||
dev_reg.async_get_or_create(
|
||||
config_entry_id=self._entry_id,
|
||||
identifiers={identifier},
|
||||
connections={connection},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=self._name,
|
||||
model="Home Assistant HomeKit Bridge",
|
||||
model=f"Home Assistant HomeKit {hk_mode_name}",
|
||||
)
|
||||
|
||||
@callback
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Config flow for HomeKit integration."""
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
|
||||
|
@ -6,7 +7,14 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_NAME, CONF_PORT
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_DOMAINS,
|
||||
CONF_ENTITIES,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
)
|
||||
from homeassistant.core import callback, split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
|
@ -27,6 +35,7 @@ from .const import (
|
|||
DEFAULT_HOMEKIT_MODE,
|
||||
HOMEKIT_MODE_ACCESSORY,
|
||||
HOMEKIT_MODES,
|
||||
SHORT_ACCESSORY_NAME,
|
||||
SHORT_BRIDGE_NAME,
|
||||
VIDEO_CODEC_COPY,
|
||||
)
|
||||
|
@ -80,6 +89,19 @@ DEFAULT_DOMAINS = [
|
|||
"water_heater",
|
||||
]
|
||||
|
||||
DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"]
|
||||
|
||||
CAMERA_ENTITY_PREFIX = "camera."
|
||||
|
||||
_EMPTY_ENTITY_FILTER = {
|
||||
CONF_INCLUDE_DOMAINS: [],
|
||||
CONF_EXCLUDE_DOMAINS: [],
|
||||
CONF_INCLUDE_ENTITIES: [],
|
||||
CONF_EXCLUDE_ENTITIES: [],
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HomeKit."""
|
||||
|
@ -89,51 +111,90 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
def __init__(self):
|
||||
"""Initialize config flow."""
|
||||
self.homekit_data = {}
|
||||
self.hk_data = {}
|
||||
self.entry_title = None
|
||||
|
||||
async def async_step_pairing(self, user_input=None):
|
||||
"""Pairing instructions."""
|
||||
async def async_step_accessory_mode(self, user_input=None):
|
||||
"""Choose specific entity in accessory mode."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.entry_title, data=self.homekit_data
|
||||
entity_id = user_input[CONF_ENTITY_ID]
|
||||
entity_filter = _EMPTY_ENTITY_FILTER.copy()
|
||||
entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
|
||||
self.hk_data[CONF_FILTER] = entity_filter
|
||||
if entity_id.startswith(CAMERA_ENTITY_PREFIX):
|
||||
self.hk_data[CONF_ENTITY_CONFIG] = {
|
||||
entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
|
||||
}
|
||||
return await self.async_step_pairing()
|
||||
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass, domains=DOMAINS_PREFER_ACCESSORY_MODE
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="pairing",
|
||||
description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]},
|
||||
step_id="accessory_mode",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
async def async_step_bridge_mode(self, user_input=None):
|
||||
"""Choose specific domains in bridge mode."""
|
||||
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
|
||||
entity_filter = _EMPTY_ENTITY_FILTER.copy()
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS]
|
||||
self.hk_data[CONF_FILTER] = entity_filter
|
||||
return await self.async_step_pairing()
|
||||
|
||||
default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS
|
||||
setup_schema = vol.Schema(
|
||||
return self.async_show_form(
|
||||
step_id="bridge_mode",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_INCLUDE_DOMAINS, default=default_domains
|
||||
): cv.multi_select(SUPPORTED_DOMAINS),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
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.hk_data)
|
||||
|
||||
self.hk_data[CONF_PORT] = await self._async_available_port()
|
||||
self.hk_data[CONF_NAME] = self._async_available_name(
|
||||
self.hk_data[CONF_HOMEKIT_MODE]
|
||||
)
|
||||
self.entry_title = f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}"
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=setup_schema, errors=errors
|
||||
step_id="pairing",
|
||||
description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]},
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.hk_data = {
|
||||
CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE],
|
||||
}
|
||||
if user_input[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
|
||||
return await self.async_step_accessory_mode()
|
||||
return await self.async_step_bridge_mode()
|
||||
|
||||
homekit_mode = self.hk_data.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
|
||||
HOMEKIT_MODES
|
||||
)
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input=None):
|
||||
|
@ -153,28 +214,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
@callback
|
||||
def _async_current_names(self):
|
||||
"""Return a set of bridge names."""
|
||||
current_entries = self._async_current_entries()
|
||||
|
||||
return {
|
||||
entry.data[CONF_NAME]
|
||||
for entry in current_entries
|
||||
for entry in self._async_current_entries()
|
||||
if CONF_NAME in entry.data
|
||||
}
|
||||
|
||||
@callback
|
||||
def _async_available_name(self):
|
||||
def _async_available_name(self, homekit_mode):
|
||||
"""Return an available for the bridge."""
|
||||
|
||||
base_name = SHORT_BRIDGE_NAME
|
||||
if homekit_mode == HOMEKIT_MODE_ACCESSORY:
|
||||
base_name = SHORT_ACCESSORY_NAME
|
||||
|
||||
# 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
|
||||
suggested_name = None
|
||||
while not suggested_name or suggested_name in self._async_current_names():
|
||||
trailer = "".join(random.choices(acceptable_chars, k=4))
|
||||
all_names = self._async_current_names()
|
||||
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}"
|
||||
suggested_name = f"{base_name} {trailer}"
|
||||
|
||||
return suggested_name
|
||||
|
||||
|
@ -196,12 +257,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a option flow for tado."""
|
||||
"""Handle a option flow for homekit."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.homekit_options = {}
|
||||
self.hk_options = {}
|
||||
self.included_cameras = set()
|
||||
|
||||
async def async_step_yaml(self, user_input=None):
|
||||
|
@ -217,17 +278,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
"""Choose advanced options."""
|
||||
if not self.show_advanced_options or user_input is not None:
|
||||
if user_input:
|
||||
self.homekit_options.update(user_input)
|
||||
self.hk_options.update(user_input)
|
||||
|
||||
self.homekit_options[CONF_AUTO_START] = self.homekit_options.get(
|
||||
self.hk_options[CONF_AUTO_START] = self.hk_options.get(
|
||||
CONF_AUTO_START, DEFAULT_AUTO_START
|
||||
)
|
||||
|
||||
for key in (CONF_DOMAINS, CONF_ENTITIES):
|
||||
if key in self.homekit_options:
|
||||
del self.homekit_options[key]
|
||||
if key in self.hk_options:
|
||||
del self.hk_options[key]
|
||||
|
||||
return self.async_create_entry(title="", data=self.homekit_options)
|
||||
return self.async_create_entry(title="", data=self.hk_options)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="advanced",
|
||||
|
@ -235,7 +296,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
{
|
||||
vol.Optional(
|
||||
CONF_AUTO_START,
|
||||
default=self.homekit_options.get(
|
||||
default=self.hk_options.get(
|
||||
CONF_AUTO_START, DEFAULT_AUTO_START
|
||||
),
|
||||
): bool
|
||||
|
@ -246,7 +307,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
async def async_step_cameras(self, user_input=None):
|
||||
"""Choose camera config."""
|
||||
if user_input is not None:
|
||||
entity_config = self.homekit_options[CONF_ENTITY_CONFIG]
|
||||
entity_config = self.hk_options[CONF_ENTITY_CONFIG]
|
||||
for entity_id in self.included_cameras:
|
||||
if entity_id in user_input[CONF_CAMERA_COPY]:
|
||||
entity_config.setdefault(entity_id, {})[
|
||||
|
@ -260,7 +321,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
return await self.async_step_advanced()
|
||||
|
||||
cameras_with_copy = []
|
||||
entity_config = self.homekit_options.setdefault(CONF_ENTITY_CONFIG, {})
|
||||
entity_config = self.hk_options.setdefault(CONF_ENTITY_CONFIG, {})
|
||||
for entity in self.included_cameras:
|
||||
hk_entity_config = entity_config.get(entity, {})
|
||||
if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY:
|
||||
|
@ -279,19 +340,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
async def async_step_include_exclude(self, user_input=None):
|
||||
"""Choose entities to include or exclude from the domain."""
|
||||
if user_input is not None:
|
||||
entity_filter = {
|
||||
CONF_INCLUDE_DOMAINS: [],
|
||||
CONF_EXCLUDE_DOMAINS: [],
|
||||
CONF_INCLUDE_ENTITIES: [],
|
||||
CONF_EXCLUDE_ENTITIES: [],
|
||||
}
|
||||
entity_filter = _EMPTY_ENTITY_FILTER.copy()
|
||||
if isinstance(user_input[CONF_ENTITIES], list):
|
||||
entities = user_input[CONF_ENTITIES]
|
||||
else:
|
||||
entities = [user_input[CONF_ENTITIES]]
|
||||
|
||||
if (
|
||||
self.homekit_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY
|
||||
self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY
|
||||
or user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE
|
||||
):
|
||||
entity_filter[CONF_INCLUDE_ENTITIES] = entities
|
||||
|
@ -300,7 +356,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
domains_with_entities_selected = _domains_set_from_entities(entities)
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = [
|
||||
domain
|
||||
for domain in self.homekit_options[CONF_DOMAINS]
|
||||
for domain in self.hk_options[CONF_DOMAINS]
|
||||
if domain not in domains_with_entities_selected
|
||||
]
|
||||
|
||||
|
@ -308,34 +364,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
if entity_id not in entities:
|
||||
self.included_cameras.remove(entity_id)
|
||||
else:
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = self.homekit_options[CONF_DOMAINS]
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = self.hk_options[CONF_DOMAINS]
|
||||
entity_filter[CONF_EXCLUDE_ENTITIES] = entities
|
||||
for entity_id in entities:
|
||||
if entity_id in self.included_cameras:
|
||||
self.included_cameras.remove(entity_id)
|
||||
|
||||
self.homekit_options[CONF_FILTER] = entity_filter
|
||||
self.hk_options[CONF_FILTER] = entity_filter
|
||||
|
||||
if self.included_cameras:
|
||||
return await self.async_step_cameras()
|
||||
|
||||
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,
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass,
|
||||
self.homekit_options[CONF_DOMAINS],
|
||||
domains=self.hk_options[CONF_DOMAINS],
|
||||
)
|
||||
self.included_cameras = {
|
||||
entity_id
|
||||
for entity_id in all_supported_entities
|
||||
if entity_id.startswith("camera.")
|
||||
if entity_id.startswith(CAMERA_ENTITY_PREFIX)
|
||||
}
|
||||
|
||||
data_schema = {}
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
if self.homekit_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
|
||||
if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
|
||||
entity_schema = vol.In
|
||||
else:
|
||||
if entities:
|
||||
|
@ -362,42 +417,43 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
return await self.async_step_yaml(user_input)
|
||||
|
||||
if user_input is not None:
|
||||
self.homekit_options.update(user_input)
|
||||
self.hk_options.update(user_input)
|
||||
return await self.async_step_include_exclude()
|
||||
|
||||
self.homekit_options = dict(self.config_entry.options)
|
||||
entity_filter = self.homekit_options.get(CONF_FILTER, {})
|
||||
hk_options = dict(self.config_entry.options)
|
||||
entity_filter = hk_options.get(CONF_FILTER, {})
|
||||
|
||||
homekit_mode = self.homekit_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
|
||||
homekit_mode = hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
|
||||
domains = entity_filter.get(CONF_INCLUDE_DOMAINS, [])
|
||||
include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES)
|
||||
if include_entities:
|
||||
domains.extend(_domains_set_from_entities(include_entities))
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
|
||||
vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
|
||||
HOMEKIT_MODES
|
||||
),
|
||||
vol.Optional(
|
||||
vol.Required(
|
||||
CONF_DOMAINS,
|
||||
default=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
|
||||
def _async_get_matching_entities(hass, domains=None):
|
||||
"""Fetch all entities or entities in the given domains."""
|
||||
return {
|
||||
state.entity_id: f"{state.entity_id} ({state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)})"
|
||||
for state in sorted(
|
||||
hass.states.async_all(domains and set(domains)),
|
||||
key=lambda item: item.entity_id,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _domains_set_from_entities(entity_ids):
|
||||
|
|
|
@ -104,6 +104,7 @@ SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory"
|
|||
BRIDGE_MODEL = "Bridge"
|
||||
BRIDGE_NAME = "Home Assistant Bridge"
|
||||
SHORT_BRIDGE_NAME = "HASS Bridge"
|
||||
SHORT_ACCESSORY_NAME = "HASS Accessory"
|
||||
BRIDGE_SERIAL_NUMBER = "homekit.bridge"
|
||||
MANUFACTURER = "Home Assistant"
|
||||
|
||||
|
|
|
@ -8,18 +8,18 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
|
||||
"include_domains": "[%key:component::homekit::config::step::bridge_mode::data::include_domains%]"
|
||||
},
|
||||
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
|
||||
"title": "Select domains to expose."
|
||||
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
|
||||
"title": "Select domains to be included."
|
||||
},
|
||||
"include_exclude": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"entities": "Entities"
|
||||
},
|
||||
"description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Select entities to be exposed"
|
||||
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Select entities to be included"
|
||||
},
|
||||
"cameras": {
|
||||
"data": {
|
||||
|
@ -41,11 +41,25 @@
|
|||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
"mode": "[%key:common::config_flow::data::mode%]"
|
||||
},
|
||||
"description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Activate HomeKit"
|
||||
},
|
||||
"accessory_mode": {
|
||||
"data": {
|
||||
"entity_id": "Entity"
|
||||
},
|
||||
"description": "Choose the entity to be included. In accessory mode, only a single entity is included.",
|
||||
"title": "Select entity to be included"
|
||||
},
|
||||
"bridge_mode": {
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
},
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included.",
|
||||
"title": "Select domains to be included"
|
||||
},
|
||||
"pairing": {
|
||||
"title": "Pair HomeKit",
|
||||
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
|
||||
|
|
|
@ -1,25 +1,44 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"port_name_in_use": "An accessory or bridge with the same name or port is already configured."
|
||||
},
|
||||
"step": {
|
||||
"accessory_mode": {
|
||||
"data": {
|
||||
"entity_id": "Entity"
|
||||
},
|
||||
"description": "Choose the entity to be included. In accessory mode, only a single entity is included.",
|
||||
"title": "Select entity to be included"
|
||||
},
|
||||
"bridge_mode": {
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
},
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included.",
|
||||
"title": "Select domains to be included"
|
||||
},
|
||||
"pairing": {
|
||||
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.",
|
||||
"title": "Pair HomeKit"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"mode": "Mode"
|
||||
},
|
||||
"description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each TV, media player and camera.",
|
||||
"title": "Activate HomeKit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"yaml": {
|
||||
"title": "Adjust HomeKit Options",
|
||||
"description": "This entry is controlled via YAML"
|
||||
},
|
||||
"init": {
|
||||
"advanced": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
|
||||
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
|
||||
},
|
||||
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
|
||||
"title": "Select domains to expose."
|
||||
},
|
||||
"include_exclude": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"entities": "Entities"
|
||||
},
|
||||
"description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Select entities to be exposed"
|
||||
"description": "These settings only need to be adjusted if HomeKit is not functional.",
|
||||
"title": "Advanced Configuration"
|
||||
},
|
||||
"cameras": {
|
||||
"data": {
|
||||
|
@ -28,31 +47,26 @@
|
|||
"description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.",
|
||||
"title": "Select camera video codec."
|
||||
},
|
||||
"advanced": {
|
||||
"include_exclude": {
|
||||
"data": {
|
||||
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
|
||||
"entities": "Entities",
|
||||
"mode": "Mode"
|
||||
},
|
||||
"description": "These settings only need to be adjusted if HomeKit is not functional.",
|
||||
"title": "Advanced Configuration"
|
||||
}
|
||||
}
|
||||
"description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Select entities to be included"
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"init": {
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
"include_domains": "Domains to include",
|
||||
"mode": "Mode"
|
||||
},
|
||||
"description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
|
||||
"title": "Activate HomeKit"
|
||||
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
|
||||
"title": "Select domains to be included."
|
||||
},
|
||||
"pairing": {
|
||||
"title": "Pair HomeKit",
|
||||
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"port_name_in_use": "An accessory or bridge with the same name or port is already configured."
|
||||
"yaml": {
|
||||
"description": "This entry is controlled via YAML",
|
||||
"title": "Adjust HomeKit Options"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ def _mock_config_entry_with_options_populated():
|
|||
)
|
||||
|
||||
|
||||
async def test_user_form(hass):
|
||||
"""Test we can setup a new instance."""
|
||||
async def test_setup_in_bridge_mode(hass):
|
||||
"""Test we can setup a new instance in bridge mode."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
@ -41,17 +41,23 @@ async def test_user_form(hass):
|
|||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"mode": "bridge"},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "bridge_mode"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.config_flow.find_next_available_port",
|
||||
return_value=12345,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{"include_domains": ["light"]},
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "pairing"
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result3["step_id"] == "pairing"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.async_setup", return_value=True
|
||||
|
@ -59,22 +65,23 @@ async def test_user_form(hass):
|
|||
"homeassistant.components.homekit.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result3["title"][:11] == "HASS Bridge"
|
||||
bridge_name = (result3["title"].split(":"))[0]
|
||||
assert result3["data"] == {
|
||||
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result4["title"][:11] == "HASS Bridge"
|
||||
bridge_name = (result4["title"].split(":"))[0]
|
||||
assert result4["data"] == {
|
||||
"filter": {
|
||||
"exclude_domains": [],
|
||||
"exclude_entities": [],
|
||||
"include_domains": ["light"],
|
||||
"include_entities": [],
|
||||
},
|
||||
"mode": "bridge",
|
||||
"name": bridge_name,
|
||||
"port": 12345,
|
||||
}
|
||||
|
@ -82,6 +89,66 @@ async def test_user_form(hass):
|
|||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_setup_in_accessory_mode(hass):
|
||||
"""Test we can setup a new instance in accessory."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.states.async_set("camera.mine", "off")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"mode": "accessory"},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "accessory_mode"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.config_flow.find_next_available_port",
|
||||
return_value=12345,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{"entity_id": "camera.mine"},
|
||||
)
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result3["step_id"] == "pairing"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.homekit.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result4["title"][:14] == "HASS Accessory"
|
||||
bridge_name = (result4["title"].split(":"))[0]
|
||||
assert result4["data"] == {
|
||||
"filter": {
|
||||
"exclude_domains": [],
|
||||
"exclude_entities": [],
|
||||
"include_domains": [],
|
||||
"include_entities": ["camera.mine"],
|
||||
},
|
||||
"mode": "accessory",
|
||||
"name": bridge_name,
|
||||
"entity_config": {"camera.mine": {"video_codec": "copy"}},
|
||||
"port": 12345,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(hass):
|
||||
"""Test we can import instance."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
@ -343,10 +410,11 @@ async def test_options_flow_exclude_mode_with_cameras(hass):
|
|||
|
||||
result3 = await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"camera_copy": []},
|
||||
user_input={"camera_copy": ["camera.native_h264"]},
|
||||
)
|
||||
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
assert config_entry.options == {
|
||||
"auto_start": True,
|
||||
"mode": "bridge",
|
||||
|
@ -356,7 +424,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass):
|
|||
"include_domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_entities": [],
|
||||
},
|
||||
"entity_config": {"camera.native_h264": {}},
|
||||
"entity_config": {"camera.native_h264": {"video_codec": "copy"}},
|
||||
}
|
||||
|
||||
|
||||
|
@ -458,7 +526,7 @@ async def test_options_flow_include_mode_with_cameras(hass):
|
|||
"include_domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_entities": [],
|
||||
},
|
||||
"entity_config": {"camera.native_h264": {}},
|
||||
"entity_config": {},
|
||||
}
|
||||
|
||||
|
||||
|
@ -519,19 +587,19 @@ async def test_options_flow_include_mode_basic_accessory(hass):
|
|||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["media_player"], "mode": "accessory"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "include_exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
result3 = await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"entities": "media_player.tv"},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
"auto_start": True,
|
||||
"mode": "accessory",
|
||||
|
@ -542,3 +610,107 @@ async def test_options_flow_include_mode_basic_accessory(hass):
|
|||
"include_entities": ["media_player.tv"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_converting_bridge_to_accessory_mode(hass):
|
||||
"""Test we can convert a bridge to accessory mode."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"mode": "bridge"},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "bridge_mode"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.config_flow.find_next_available_port",
|
||||
return_value=12345,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{"include_domains": ["light"]},
|
||||
)
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result3["step_id"] == "pairing"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.homekit.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result4["title"][:11] == "HASS Bridge"
|
||||
bridge_name = (result4["title"].split(":"))[0]
|
||||
assert result4["data"] == {
|
||||
"filter": {
|
||||
"exclude_domains": [],
|
||||
"exclude_entities": [],
|
||||
"include_domains": ["light"],
|
||||
"include_entities": [],
|
||||
},
|
||||
"mode": "bridge",
|
||||
"name": bridge_name,
|
||||
"port": 12345,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = result4["result"]
|
||||
|
||||
hass.states.async_set("camera.tv", "off")
|
||||
hass.states.async_set("camera.sonos", "off")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
config_entry.entry_id, context={"show_advanced_options": False}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["camera"], "mode": "accessory"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"entities": "camera.tv"},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "cameras"
|
||||
|
||||
result3 = await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"camera_copy": ["camera.tv"]},
|
||||
)
|
||||
|
||||
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
"auto_start": True,
|
||||
"entity_config": {"camera.tv": {"video_codec": "copy"}},
|
||||
"mode": "accessory",
|
||||
"filter": {
|
||||
"exclude_domains": [],
|
||||
"exclude_entities": [],
|
||||
"include_domains": [],
|
||||
"include_entities": ["camera.tv"],
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue