mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add entity category support to HomeKit (#64492)
This commit is contained in:
parent
e248ef1dd7
commit
d53124910f
@ -35,6 +35,7 @@ from homeassistant.const import (
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
ENTITY_CATEGORIES,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_RELOAD,
|
||||
@ -43,7 +44,11 @@ from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, Unauthorized
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
BASE_FILTER_SCHEMA,
|
||||
FILTER_SCHEMA,
|
||||
EntityFilter,
|
||||
)
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.service import async_extract_referenced_entity_ids
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@ -469,7 +474,7 @@ class HomeKit:
|
||||
self._name = name
|
||||
self._port = port
|
||||
self._ip_address = ip_address
|
||||
self._filter = entity_filter
|
||||
self._filter: EntityFilter = entity_filter
|
||||
self._config = entity_config
|
||||
self._exclude_accessory_mode = exclude_accessory_mode
|
||||
self._advertise_ip = advertise_ip
|
||||
@ -661,6 +666,12 @@ class HomeKit:
|
||||
continue
|
||||
|
||||
if ent_reg_ent := ent_reg.async_get(entity_id):
|
||||
if (
|
||||
ent_reg_ent.entity_category in ENTITY_CATEGORIES
|
||||
and not self._filter.explicitly_included(entity_id)
|
||||
):
|
||||
continue
|
||||
|
||||
await self._async_set_device_info_attributes(
|
||||
ent_reg_ent, dev_reg, entity_id
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ from copy import deepcopy
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from typing import Final
|
||||
from typing import Any, Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -25,9 +25,10 @@ from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
ENTITY_CATEGORIES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import device_registry
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
CONF_EXCLUDE_DOMAINS,
|
||||
@ -127,6 +128,38 @@ _EMPTY_ENTITY_FILTER: Final = {
|
||||
}
|
||||
|
||||
|
||||
async def _async_domain_names(hass: HomeAssistant, domains: list[str]) -> str:
|
||||
"""Build a list of integration names from domains."""
|
||||
name_to_type_map = await _async_name_to_type_map(hass)
|
||||
return ", ".join(
|
||||
[name for domain, name in name_to_type_map.items() if domain in domains]
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_build_entites_filter(
|
||||
domains: list[str], entities: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Build an entities filter from domains and entities."""
|
||||
entity_filter = deepcopy(_EMPTY_ENTITY_FILTER)
|
||||
entity_filter[CONF_INCLUDE_ENTITIES] = entities
|
||||
# Include all of the domain if there are no entities
|
||||
# explicitly included as the user selected the domain
|
||||
domains_with_entities_selected = _domains_set_from_entities(entities)
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = [
|
||||
domain for domain in domains if domain not in domains_with_entities_selected
|
||||
]
|
||||
return entity_filter
|
||||
|
||||
|
||||
def _async_cameras_from_entities(entities: list[str]) -> set[str]:
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if entity_id.startswith(CAMERA_ENTITY_PREFIX)
|
||||
}
|
||||
|
||||
|
||||
async def _async_name_to_type_map(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Create a mapping of types of devices/entities HomeKit can support."""
|
||||
integrations = await asyncio.gather(
|
||||
@ -331,6 +364,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
):
|
||||
self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
|
||||
|
||||
if CONF_INCLUDE_EXCLUDE_MODE in self.hk_options:
|
||||
del self.hk_options[CONF_INCLUDE_EXCLUDE_MODE]
|
||||
|
||||
return self.async_create_entry(title="", data=self.hk_options)
|
||||
|
||||
all_supported_devices = await _async_get_supported_devices(self.hass)
|
||||
@ -398,99 +434,139 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
return self.async_show_form(step_id="cameras", data_schema=data_schema)
|
||||
|
||||
async def async_step_include_exclude(self, user_input=None):
|
||||
"""Choose entities to include or exclude from the domain."""
|
||||
async def async_step_accessory(self, user_input=None):
|
||||
"""Choose entity for the accessory."""
|
||||
domains = self.hk_options[CONF_DOMAINS]
|
||||
|
||||
if user_input is not None:
|
||||
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.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY
|
||||
or user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE
|
||||
):
|
||||
entity_filter[CONF_INCLUDE_ENTITIES] = entities
|
||||
# Include all of the domain if there are no entities
|
||||
# explicitly included as the user selected the domain
|
||||
domains_with_entities_selected = _domains_set_from_entities(entities)
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = [
|
||||
domain
|
||||
for domain in self.hk_options[CONF_DOMAINS]
|
||||
if domain not in domains_with_entities_selected
|
||||
]
|
||||
|
||||
self.included_cameras = {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if entity_id.startswith(CAMERA_ENTITY_PREFIX)
|
||||
}
|
||||
else:
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = self.hk_options[CONF_DOMAINS]
|
||||
entity_filter[CONF_EXCLUDE_ENTITIES] = entities
|
||||
if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]:
|
||||
camera_entities = _async_get_matching_entities(
|
||||
self.hass,
|
||||
domains=[CAMERA_DOMAIN],
|
||||
)
|
||||
self.included_cameras = {
|
||||
entity_id
|
||||
for entity_id in camera_entities
|
||||
if entity_id not in entities
|
||||
}
|
||||
else:
|
||||
self.included_cameras = set()
|
||||
|
||||
entities = cv.ensure_list(user_input[CONF_ENTITIES])
|
||||
entity_filter = _async_build_entites_filter(domains, entities)
|
||||
self.included_cameras = _async_cameras_from_entities(entities)
|
||||
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.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
# In accessory mode we can only have one
|
||||
default_value = next(
|
||||
iter(
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if entity_id in all_supported_entities
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="accessory",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITIES, default=default_value): vol.In(
|
||||
all_supported_entities
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_include(self, user_input=None):
|
||||
"""Choose entities to include from the domain on the bridge."""
|
||||
domains = self.hk_options[CONF_DOMAINS]
|
||||
if user_input is not None:
|
||||
entities = cv.ensure_list(user_input[CONF_ENTITIES])
|
||||
entity_filter = _async_build_entites_filter(domains, entities)
|
||||
self.included_cameras = _async_cameras_from_entities(entities)
|
||||
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.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass,
|
||||
domains=self.hk_options[CONF_DOMAINS],
|
||||
)
|
||||
data_schema = {}
|
||||
if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
|
||||
# In accessory mode we can only have one
|
||||
default_value = next(
|
||||
iter(
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if entity_id in all_supported_entities
|
||||
),
|
||||
None,
|
||||
)
|
||||
entity_schema = vol.In
|
||||
entities_schema_required = vol.Required
|
||||
else:
|
||||
# Bridge mode
|
||||
entities_schema_required = vol.Optional
|
||||
include_exclude_mode = MODE_INCLUDE
|
||||
if not entities:
|
||||
include_exclude_mode = MODE_EXCLUDE
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
data_schema[
|
||||
vol.Required(CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode)
|
||||
] = vol.In(INCLUDE_EXCLUDE_MODES)
|
||||
entity_schema = cv.multi_select
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
default_value = [
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if entity_id in all_supported_entities
|
||||
]
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
default_value = [
|
||||
entity_id for entity_id in entities if entity_id in all_supported_entities
|
||||
]
|
||||
|
||||
data_schema[
|
||||
entities_schema_required(CONF_ENTITIES, default=default_value)
|
||||
] = entity_schema(all_supported_entities)
|
||||
return self.async_show_form(
|
||||
step_id="include_exclude", data_schema=vol.Schema(data_schema)
|
||||
step_id="include",
|
||||
description_placeholders={
|
||||
"domains": await _async_domain_names(self.hass, domains)
|
||||
},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITIES, default=default_value): cv.multi_select(
|
||||
all_supported_entities
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_exclude(self, user_input=None):
|
||||
"""Choose entities to exclude from the domain on the bridge."""
|
||||
domains = self.hk_options[CONF_DOMAINS]
|
||||
|
||||
if user_input is not None:
|
||||
entity_filter = deepcopy(_EMPTY_ENTITY_FILTER)
|
||||
entities = cv.ensure_list(user_input[CONF_ENTITIES])
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = domains
|
||||
entity_filter[CONF_EXCLUDE_ENTITIES] = entities
|
||||
self.included_cameras = set()
|
||||
if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]:
|
||||
camera_entities = _async_get_matching_entities(
|
||||
self.hass, [CAMERA_DOMAIN]
|
||||
)
|
||||
self.included_cameras = {
|
||||
entity_id
|
||||
for entity_id in camera_entities
|
||||
if entity_id not in entities
|
||||
}
|
||||
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.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
ent_reg = entity_registry.async_get(self.hass)
|
||||
entity_cat_entities = set()
|
||||
for entity_id in all_supported_entities:
|
||||
if ent_reg_ent := ent_reg.async_get(entity_id):
|
||||
if ent_reg_ent.entity_category in ENTITY_CATEGORIES:
|
||||
entity_cat_entities.add(entity_id)
|
||||
# Remove entity category entities since we will exclude them anyways
|
||||
all_supported_entities = {
|
||||
k: v
|
||||
for k, v in all_supported_entities.items()
|
||||
if k not in entity_cat_entities
|
||||
}
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
default_value = [
|
||||
entity_id for entity_id in entities if entity_id in all_supported_entities
|
||||
]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="exclude",
|
||||
description_placeholders={
|
||||
"domains": await _async_domain_names(self.hass, domains)
|
||||
},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITIES, default=default_value): cv.multi_select(
|
||||
all_supported_entities
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
@ -500,17 +576,24 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
if user_input is not None:
|
||||
self.hk_options.update(user_input)
|
||||
return await self.async_step_include_exclude()
|
||||
if self.hk_options.get(CONF_HOMEKIT_MODE) == HOMEKIT_MODE_ACCESSORY:
|
||||
return await self.async_step_accessory()
|
||||
if user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE:
|
||||
return await self.async_step_include()
|
||||
return await self.async_step_exclude()
|
||||
|
||||
self.hk_options = deepcopy(dict(self.config_entry.options))
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
include_exclude_mode = MODE_INCLUDE
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
if homekit_mode != HOMEKIT_MODE_ACCESSORY:
|
||||
include_exclude_mode = MODE_INCLUDE if entities else MODE_EXCLUDE
|
||||
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))
|
||||
name_to_type_map = await _async_name_to_type_map(self.hass)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
@ -518,6 +601,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
|
||||
HOMEKIT_MODES
|
||||
),
|
||||
vol.Required(
|
||||
CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode
|
||||
): vol.In(INCLUDE_EXCLUDE_MODES),
|
||||
vol.Required(
|
||||
CONF_DOMAINS,
|
||||
default=domains,
|
||||
@ -540,7 +626,9 @@ async def _async_get_supported_devices(hass):
|
||||
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
|
||||
|
||||
|
||||
def _async_get_matching_entities(hass, domains=None):
|
||||
def _async_get_matching_entities(
|
||||
hass: HomeAssistant, domains: list[str] | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Fetch all entities or entities in the given domains."""
|
||||
return {
|
||||
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
|
||||
|
@ -7,20 +7,33 @@
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
|
||||
"mode": "HomeKit Mode",
|
||||
"include_exclude_mode": "Inclusion Mode",
|
||||
"domains": "[%key:component::homekit::config::step::user::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 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."
|
||||
"title": "Select mode and domains."
|
||||
},
|
||||
"include_exclude": {
|
||||
"accessory": {
|
||||
"data": {
|
||||
"entities": "Entity"
|
||||
},
|
||||
"title": "Select the entity for the accessory"
|
||||
},
|
||||
"include": {
|
||||
"data": {
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"entities": "Entities"
|
||||
},
|
||||
"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, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"title": "Select entities to be included"
|
||||
"description": "All “{domains}” entities will be included unless specific entities are selected.",
|
||||
"title": "Select the entities to be included"
|
||||
},
|
||||
"exclude": {
|
||||
"data": {
|
||||
"entities": "[%key:component::homekit::options::step::include::data::entities%]"
|
||||
},
|
||||
"description": "All “{domains}” entities will be included except for the excluded entities and categorized entities.",
|
||||
"title": "Select the entities to be excluded"
|
||||
},
|
||||
"cameras": {
|
||||
"data": {
|
||||
"camera_copy": "Cameras that support native H.264 streams",
|
||||
@ -45,7 +58,7 @@
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
},
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"title": "Select domains to be included"
|
||||
},
|
||||
"pairing": {
|
||||
|
@ -12,13 +12,19 @@
|
||||
"data": {
|
||||
"include_domains": "Domains to include"
|
||||
},
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"title": "Select domains to be included"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"accessory": {
|
||||
"data": {
|
||||
"entities": "Entity"
|
||||
},
|
||||
"title": "Select the entity for the accessory"
|
||||
},
|
||||
"advanced": {
|
||||
"data": {
|
||||
"auto_start": "Autostart (disable if you are calling the homekit.start service manually)",
|
||||
@ -35,21 +41,28 @@
|
||||
"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": "Camera Configuration"
|
||||
},
|
||||
"include_exclude": {
|
||||
"exclude": {
|
||||
"data": {
|
||||
"entities": "Entities",
|
||||
"mode": "Mode"
|
||||
"entities": "Entities"
|
||||
},
|
||||
"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, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.",
|
||||
"title": "Select entities to be included"
|
||||
"description": "All \u201c{domains}\u201d entities will be included except for the excluded entities and categorized entities.",
|
||||
"title": "Select the entities to be excluded"
|
||||
},
|
||||
"include": {
|
||||
"data": {
|
||||
"entities": "Entities"
|
||||
},
|
||||
"description": "All \u201c{domains}\u201d entities will be included unless specific entities are selected.",
|
||||
"title": "Select the entities to be included"
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"include_domains": "Domains to include",
|
||||
"mode": "Mode"
|
||||
"domains": "Domains to include",
|
||||
"include_exclude_mode": "Inclusion Mode",
|
||||
"mode": "HomeKit 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."
|
||||
"title": "Select mode and domains."
|
||||
},
|
||||
"yaml": {
|
||||
"description": "This entry is controlled via YAML",
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Test the HomeKit config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import voluptuous
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.homekit.const import (
|
||||
CONF_FILTER,
|
||||
@ -9,6 +12,8 @@ from homeassistant.components.homekit.const import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_NAME, CONF_PORT
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@ -296,15 +301,18 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "humidifier"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "humidifier"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"},
|
||||
user_input={"entities": ["climate.old"]},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "advanced"
|
||||
@ -348,11 +356,14 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
entities = result["data_schema"]({})["entities"]
|
||||
assert entities == ["climate.front_gate"]
|
||||
|
||||
@ -362,7 +373,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip):
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"},
|
||||
user_input={"entities": ["climate.old"]},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
@ -424,11 +435,14 @@ async def test_options_flow_devices(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
entry = entity_reg.async_get("light.ceiling_lights")
|
||||
assert entry is not None
|
||||
@ -438,7 +452,6 @@ async def test_options_flow_devices(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
@ -502,17 +515,19 @@ async def test_options_flow_devices_preserved_when_advanced_off(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
@ -557,11 +572,14 @@ async def test_options_flow_include_mode_with_non_existant_entity(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "include"
|
||||
|
||||
entities = result["data_schema"]({})["entities"]
|
||||
assert "climate.not_exist" not in entities
|
||||
@ -570,7 +588,6 @@ async def test_options_flow_include_mode_with_non_existant_entity(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.new", "climate.front_gate"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
@ -614,11 +631,14 @@ async def test_options_flow_exclude_mode_with_non_existant_entity(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["climate"]},
|
||||
user_input={
|
||||
"domains": ["climate"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
entities = result["data_schema"]({})["entities"]
|
||||
assert "climate.not_exist" not in entities
|
||||
@ -627,7 +647,6 @@ async def test_options_flow_exclude_mode_with_non_existant_entity(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.new", "climate.front_gate"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
@ -662,15 +681,18 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "include"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"entities": ["climate.new"], "include_exclude_mode": "include"},
|
||||
user_input={"entities": ["climate.new"]},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
@ -706,17 +728,19 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old", "camera.excluded"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -750,17 +774,19 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old", "camera.excluded"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -807,17 +833,19 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "include"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["camera.native_h264", "camera.transcode_h264"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -851,6 +879,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip):
|
||||
assert result["data_schema"]({}) == {
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"mode": "bridge",
|
||||
"include_exclude_mode": "include",
|
||||
}
|
||||
schema = result["data_schema"].schema
|
||||
assert _get_schema_default(schema, "domains") == [
|
||||
@ -860,30 +889,31 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip):
|
||||
"camera",
|
||||
]
|
||||
assert _get_schema_default(schema, "mode") == "bridge"
|
||||
assert _get_schema_default(schema, "include_exclude_mode") == "include"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
assert result["data_schema"]({}) == {
|
||||
"entities": ["camera.native_h264", "camera.transcode_h264"],
|
||||
"include_exclude_mode": "include",
|
||||
}
|
||||
schema = result["data_schema"].schema
|
||||
assert _get_schema_default(schema, "entities") == [
|
||||
"camera.native_h264",
|
||||
"camera.transcode_h264",
|
||||
]
|
||||
assert _get_schema_default(schema, "include_exclude_mode") == "include"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old", "camera.excluded"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -935,17 +965,19 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip):
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "include"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["camera.audio", "camera.no_audio"],
|
||||
"include_exclude_mode": "include",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -979,6 +1011,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip):
|
||||
assert result["data_schema"]({}) == {
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
"mode": "bridge",
|
||||
"include_exclude_mode": "include",
|
||||
}
|
||||
schema = result["data_schema"].schema
|
||||
assert _get_schema_default(schema, "domains") == [
|
||||
@ -988,30 +1021,31 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip):
|
||||
"camera",
|
||||
]
|
||||
assert _get_schema_default(schema, "mode") == "bridge"
|
||||
assert _get_schema_default(schema, "include_exclude_mode") == "include"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"domains": ["fan", "vacuum", "climate", "camera"]},
|
||||
user_input={
|
||||
"include_exclude_mode": "exclude",
|
||||
"domains": ["fan", "vacuum", "climate", "camera"],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "exclude"
|
||||
assert result["data_schema"]({}) == {
|
||||
"entities": ["camera.audio", "camera.no_audio"],
|
||||
"include_exclude_mode": "include",
|
||||
}
|
||||
schema = result["data_schema"].schema
|
||||
assert _get_schema_default(schema, "entities") == [
|
||||
"camera.audio",
|
||||
"camera.no_audio",
|
||||
]
|
||||
assert _get_schema_default(schema, "include_exclude_mode") == "include"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entities": ["climate.old", "camera.excluded"],
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
@ -1109,6 +1143,7 @@ async def test_options_flow_include_mode_basic_accessory(
|
||||
"alarm_control_panel",
|
||||
],
|
||||
"mode": "bridge",
|
||||
"include_exclude_mode": "exclude",
|
||||
}
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
@ -1117,7 +1152,7 @@ async def test_options_flow_include_mode_basic_accessory(
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "include_exclude"
|
||||
assert result2["step_id"] == "accessory"
|
||||
assert _get_schema_default(result2["data_schema"].schema, "entities") is None
|
||||
|
||||
result3 = await hass.config_entries.options.async_configure(
|
||||
@ -1147,6 +1182,7 @@ async def test_options_flow_include_mode_basic_accessory(
|
||||
assert result["data_schema"]({}) == {
|
||||
"domains": ["media_player"],
|
||||
"mode": "accessory",
|
||||
"include_exclude_mode": "include",
|
||||
}
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
@ -1155,7 +1191,7 @@ async def test_options_flow_include_mode_basic_accessory(
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "include_exclude"
|
||||
assert result2["step_id"] == "accessory"
|
||||
assert (
|
||||
_get_schema_default(result2["data_schema"].schema, "entities")
|
||||
== "media_player.tv"
|
||||
@ -1248,7 +1284,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
assert result["step_id"] == "accessory"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
@ -1289,3 +1325,81 @@ def _get_schema_default(schema, key_name):
|
||||
if schema_key == key_name:
|
||||
return schema_key.default()
|
||||
raise KeyError(f"{key_name} not found in schema")
|
||||
|
||||
|
||||
@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
|
||||
async def test_options_flow_exclude_mode_skips_category_entities(
|
||||
port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg
|
||||
):
|
||||
"""Ensure exclude mode does not offer category entities."""
|
||||
config_entry = _mock_config_entry_with_options_populated()
|
||||
await async_init_entry(hass, config_entry)
|
||||
|
||||
hass.states.async_set("media_player.tv", "off")
|
||||
hass.states.async_set("media_player.sonos", "off")
|
||||
hass.states.async_set("switch.other", "off")
|
||||
|
||||
sonos_config_switch: RegistryEntry = entity_reg.async_get_or_create(
|
||||
"switch",
|
||||
"sonos",
|
||||
"config",
|
||||
device_id="1234",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
hass.states.async_set(sonos_config_switch.entity_id, "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"
|
||||
assert result["data_schema"]({}) == {
|
||||
"domains": [
|
||||
"fan",
|
||||
"humidifier",
|
||||
"vacuum",
|
||||
"media_player",
|
||||
"climate",
|
||||
"alarm_control_panel",
|
||||
],
|
||||
"mode": "bridge",
|
||||
"include_exclude_mode": "exclude",
|
||||
}
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"domains": ["media_player", "switch"],
|
||||
"mode": "bridge",
|
||||
"include_exclude_mode": "exclude",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "exclude"
|
||||
assert _get_schema_default(result2["data_schema"].schema, "entities") == []
|
||||
|
||||
# sonos_config_switch.entity_id is a config category entity
|
||||
# so it should not be selectable since it will always be excluded
|
||||
with pytest.raises(voluptuous.error.MultipleInvalid):
|
||||
await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"entities": [sonos_config_switch.entity_id]},
|
||||
)
|
||||
|
||||
result4 = await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"entities": ["media_player.tv", "switch.other"]},
|
||||
)
|
||||
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
"mode": "bridge",
|
||||
"filter": {
|
||||
"exclude_domains": [],
|
||||
"exclude_entities": ["media_player.tv", "switch.other"],
|
||||
"include_domains": ["media_player", "switch"],
|
||||
"include_entities": [],
|
||||
},
|
||||
}
|
||||
|
@ -429,6 +429,62 @@ async def test_homekit_entity_glob_filter(hass, mock_async_zeroconf):
|
||||
assert hass.states.get("light.included_test") in filtered_states
|
||||
|
||||
|
||||
async def test_homekit_entity_glob_filter_with_config_entities(
|
||||
hass, mock_async_zeroconf, entity_reg
|
||||
):
|
||||
"""Test the entity filter with configuration entities."""
|
||||
entry = await async_init_integration(hass)
|
||||
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
select_config_entity: RegistryEntry = entity_reg.async_get_or_create(
|
||||
"select",
|
||||
"any",
|
||||
"any",
|
||||
device_id="1234",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
hass.states.async_set(select_config_entity.entity_id, "off")
|
||||
|
||||
switch_config_entity: RegistryEntry = entity_reg.async_get_or_create(
|
||||
"switch",
|
||||
"any",
|
||||
"any",
|
||||
device_id="1234",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
hass.states.async_set(switch_config_entity.entity_id, "off")
|
||||
hass.states.async_set("select.keep", "open")
|
||||
|
||||
hass.states.async_set("cover.excluded_test", "open")
|
||||
hass.states.async_set("light.included_test", "on")
|
||||
|
||||
entity_filter = generate_filter(
|
||||
["select"],
|
||||
["switch.test", switch_config_entity.entity_id],
|
||||
[],
|
||||
[],
|
||||
["*.included_*"],
|
||||
["*.excluded_*"],
|
||||
)
|
||||
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
|
||||
|
||||
homekit.bridge = Mock()
|
||||
homekit.bridge.accessories = {}
|
||||
|
||||
filtered_states = await homekit.async_configure_accessories()
|
||||
assert (
|
||||
hass.states.get(switch_config_entity.entity_id) in filtered_states
|
||||
) # explicitly included
|
||||
assert (
|
||||
hass.states.get(select_config_entity.entity_id) not in filtered_states
|
||||
) # not explicted included and its a config entity
|
||||
assert hass.states.get("cover.excluded_test") not in filtered_states
|
||||
assert hass.states.get("light.included_test") in filtered_states
|
||||
assert hass.states.get("select.keep") in filtered_states
|
||||
|
||||
|
||||
async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg):
|
||||
"""Test HomeKit start method."""
|
||||
entry = await async_init_integration(hass)
|
||||
|
Loading…
x
Reference in New Issue
Block a user