Improve HomeKit Accessory Mode UX (#45402)

This commit is contained in:
J. Nick Koston 2021-01-29 09:57:13 -06:00 committed by GitHub
parent bc3610c8e1
commit 5f9a1d105c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 421 additions and 160 deletions

View file

@ -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

View file

@ -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_accessory_mode(self, user_input=None):
"""Choose specific entity in accessory mode."""
if user_input is not None:
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="accessory_mode",
data_schema=vol.Schema(
{vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)}
),
)
async def async_step_bridge_mode(self, user_input=None):
"""Choose specific domains in bridge mode."""
if user_input is not None:
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
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.homekit_data
)
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="pairing",
description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]},
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:
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: [],
self.hk_data = {
CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE],
}
del self.homekit_data[CONF_INCLUDE_DOMAINS]
self.entry_title = title
return await self.async_step_pairing()
default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS
setup_schema = vol.Schema(
{
vol.Required(
CONF_INCLUDE_DOMAINS, default=default_domains
): cv.multi_select(SUPPORTED_DOMAINS),
}
)
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=setup_schema, errors=errors
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
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:
suggested_name = None
while not suggested_name or suggested_name in self._async_current_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))
data_schema = vol.Schema(
{
vol.Optional(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
HOMEKIT_MODES
),
vol.Optional(
CONF_DOMAINS,
default=domains,
): cv.multi_select(SUPPORTED_DOMAINS),
}
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
HOMEKIT_MODES
),
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):

View file

@ -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"

View file

@ -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."

View file

@ -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"
}
}
},
"config": {
"step": {
"user": {
"data": {
"include_domains": "Domains to include"
},
"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": "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"
},
"pairing": {
"title": "Pair HomeKit",
"description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
"init": {
"data": {
"include_domains": "Domains to include",
"mode": "Mode"
},
"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."
},
"yaml": {
"description": "This entry is controlled via YAML",
"title": "Adjust HomeKit Options"
}
},
"abort": {
"port_name_in_use": "An accessory or bridge with the same name or port is already configured."
}
}
}

View file

@ -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"],
},
}