Add entity category support to HomeKit (#64492)

This commit is contained in:
J. Nick Koston 2022-01-19 21:48:50 -10:00 committed by GitHub
parent e248ef1dd7
commit d53124910f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 447 additions and 152 deletions

View file

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