Add entity category support to HomeKit (#64492)
This commit is contained in:
parent
e248ef1dd7
commit
d53124910f
6 changed files with 447 additions and 152 deletions
|
@ -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": [],
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue