1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +00:00

Refactor handling of exposed entities for cloud Alexa and Google (#89877)

* Refactor handling of exposed entities for cloud Alexa

* Tweak WS API

* Validate assistant parameter

* Address some review comments

* Refactor handling of exposed entities for cloud Google

* Raise when attempting to expose an unknown entity

* Add tests

* Adjust cloud tests

* Allow getting expose new entities flag

* Test Alexa migration

* Test Google migration

* Add WS command cloud/google_assistant/entities/get

* Fix return value

* Update typing

* Address review comments

* Rename async_get_exposed_entities to async_get_assistant_settings
This commit is contained in:
Erik Montnemery
2023-04-06 19:09:45 +02:00
committed by GitHub
parent 0d84106947
commit 44c89a6b6c
17 changed files with 1607 additions and 305 deletions

View File

@@ -6,10 +6,22 @@ import pytest
from homeassistant.components.alexa import errors
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
from homeassistant.components.cloud.const import (
PREF_ALEXA_DEFAULT_EXPOSE,
PREF_ALEXA_ENTITY_CONFIGS,
PREF_SHOULD_EXPOSE,
)
from homeassistant.components.cloud.prefs import CloudPreferences
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
from tests.common import async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -21,10 +33,23 @@ def cloud_stub():
return Mock(is_logged_in=True, subscription_expired=False)
def expose_new(hass, expose_new):
"""Enable exposing new entities to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new)
def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose)
async def test_alexa_config_expose_entity_prefs(
hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry
) -> None:
"""Test Alexa config should expose using prefs."""
assert await async_setup_component(hass, "homeassistant", {})
entity_entry1 = entity_registry.async_get_or_create(
"light",
"test",
@@ -53,54 +78,62 @@ async def test_alexa_config_expose_entity_prefs(
suggested_object_id="hidden_user_light",
hidden_by=er.RegistryEntryHider.USER,
)
entity_entry5 = entity_registry.async_get_or_create(
"light",
"test",
"light_basement_id",
suggested_object_id="basement",
)
entity_entry6 = entity_registry.async_get_or_create(
"light",
"test",
"light_entrance_id",
suggested_object_id="entrance",
)
entity_conf = {"should_expose": False}
await cloud_prefs.async_update(
alexa_entity_configs={"light.kitchen": entity_conf},
alexa_default_expose=["light"],
alexa_enabled=True,
alexa_report_state=False,
)
expose_new(hass, True)
expose_entity(hass, entity_entry5.entity_id, False)
conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
# can't expose an entity which is not in the entity registry
with pytest.raises(HomeAssistantError):
expose_entity(hass, "light.kitchen", True)
assert not conf.should_expose("light.kitchen")
assert not conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id)
assert not conf.should_expose(entity_entry3.entity_id)
assert not conf.should_expose(entity_entry4.entity_id)
entity_conf["should_expose"] = True
assert conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed
assert not conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id)
assert not conf.should_expose(entity_entry3.entity_id)
assert not conf.should_expose(entity_entry4.entity_id)
# this has been hidden
assert not conf.should_expose(entity_entry5.entity_id)
# exposed by default
assert conf.should_expose(entity_entry6.entity_id)
entity_conf["should_expose"] = None
assert conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed
assert not conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id)
assert not conf.should_expose(entity_entry3.entity_id)
assert not conf.should_expose(entity_entry4.entity_id)
expose_entity(hass, entity_entry5.entity_id, True)
assert conf.should_expose(entity_entry5.entity_id)
expose_entity(hass, entity_entry5.entity_id, None)
assert not conf.should_expose(entity_entry5.entity_id)
assert "alexa" not in hass.config.components
await cloud_prefs.async_update(
alexa_default_expose=["sensor"],
)
await hass.async_block_till_done()
assert "alexa" in hass.config.components
assert not conf.should_expose("light.kitchen")
assert not conf.should_expose(entity_entry5.entity_id)
async def test_alexa_config_report_state(
hass: HomeAssistant, cloud_prefs, cloud_stub
) -> None:
"""Test Alexa config should expose using prefs."""
assert await async_setup_component(hass, "homeassistant", {})
await cloud_prefs.async_update(
alexa_report_state=False,
)
@@ -134,6 +167,8 @@ async def test_alexa_config_invalidate_token(
hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test Alexa config should expose using prefs."""
assert await async_setup_component(hass, "homeassistant", {})
aioclient_mock.post(
"https://example/access_token",
json={
@@ -181,10 +216,18 @@ async def test_alexa_config_fail_refresh_token(
hass: HomeAssistant,
cloud_prefs,
aioclient_mock: AiohttpClientMocker,
entity_registry: er.EntityRegistry,
reject_reason,
expected_exception,
) -> None:
"""Test Alexa config failing to refresh token."""
assert await async_setup_component(hass, "homeassistant", {})
# Enable exposing new entities to Alexa
expose_new(hass, True)
# Register a fan entity
entity_entry = entity_registry.async_get_or_create(
"fan", "test", "unique", suggested_object_id="test_fan"
)
aioclient_mock.post(
"https://example/access_token",
@@ -216,7 +259,7 @@ async def test_alexa_config_fail_refresh_token(
assert conf.should_report_state is False
assert conf.is_reporting_states is False
hass.states.async_set("fan.test_fan", "off")
hass.states.async_set(entity_entry.entity_id, "off")
# Enable state reporting
await cloud_prefs.async_update(alexa_report_state=True)
@@ -227,7 +270,7 @@ async def test_alexa_config_fail_refresh_token(
assert conf.is_reporting_states is True
# Change states to trigger event listener
hass.states.async_set("fan.test_fan", "on")
hass.states.async_set(entity_entry.entity_id, "on")
await hass.async_block_till_done()
# Invalidate the token and try to fetch another
@@ -240,7 +283,7 @@ async def test_alexa_config_fail_refresh_token(
)
# Change states to trigger event listener
hass.states.async_set("fan.test_fan", "off")
hass.states.async_set(entity_entry.entity_id, "off")
await hass.async_block_till_done()
# Check state reporting is still wanted in cloud prefs, but disabled for Alexa
@@ -292,16 +335,30 @@ def patch_sync_helper():
async def test_alexa_update_expose_trigger_sync(
hass: HomeAssistant, cloud_prefs, cloud_stub
hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs, cloud_stub
) -> None:
"""Test Alexa config responds to updating exposed entities."""
hass.states.async_set("binary_sensor.door", "on")
assert await async_setup_component(hass, "homeassistant", {})
# Enable exposing new entities to Alexa
expose_new(hass, True)
# Register entities
binary_sensor_entry = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique", suggested_object_id="door"
)
sensor_entry = entity_registry.async_get_or_create(
"sensor", "test", "unique", suggested_object_id="temp"
)
light_entry = entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
hass.states.async_set(binary_sensor_entry.entity_id, "on")
hass.states.async_set(
"sensor.temp",
sensor_entry.entity_id,
"23",
{"device_class": "temperature", "unit_of_measurement": "°C"},
)
hass.states.async_set("light.kitchen", "off")
hass.states.async_set(light_entry.entity_id, "off")
await cloud_prefs.async_update(
alexa_enabled=True,
@@ -313,34 +370,26 @@ async def test_alexa_update_expose_trigger_sync(
await conf.async_initialize()
with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update_alexa_entity_config(
entity_id="light.kitchen", should_expose=True
)
expose_entity(hass, light_entry.entity_id, True)
await hass.async_block_till_done()
async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert to_update == ["light.kitchen"]
assert to_update == [light_entry.entity_id]
assert to_remove == []
with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update_alexa_entity_config(
entity_id="light.kitchen", should_expose=False
)
await cloud_prefs.async_update_alexa_entity_config(
entity_id="binary_sensor.door", should_expose=True
)
await cloud_prefs.async_update_alexa_entity_config(
entity_id="sensor.temp", should_expose=True
)
expose_entity(hass, light_entry.entity_id, False)
expose_entity(hass, binary_sensor_entry.entity_id, True)
expose_entity(hass, sensor_entry.entity_id, True)
await hass.async_block_till_done()
async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"]
assert to_remove == ["light.kitchen"]
assert sorted(to_update) == [binary_sensor_entry.entity_id, sensor_entry.entity_id]
assert to_remove == [light_entry.entity_id]
with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update(
@@ -350,56 +399,65 @@ async def test_alexa_update_expose_trigger_sync(
assert conf._alexa_sync_unsub is None
assert to_update == []
assert to_remove == ["binary_sensor.door", "sensor.temp", "light.kitchen"]
assert to_remove == [
binary_sensor_entry.entity_id,
sensor_entry.entity_id,
light_entry.entity_id,
]
async def test_alexa_entity_registry_sync(
hass: HomeAssistant, mock_cloud_login, cloud_prefs
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_cloud_login,
cloud_prefs,
) -> None:
"""Test Alexa config responds to entity registry."""
# Enable exposing new entities to Alexa
expose_new(hass, True)
await alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
).async_initialize()
with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(
er.EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "create", "entity_id": "light.kitchen"},
entry = entity_registry.async_get_or_create(
"light", "test", "unique", suggested_object_id="kitchen"
)
await hass.async_block_till_done()
assert to_update == ["light.kitchen"]
assert to_update == [entry.entity_id]
assert to_remove == []
with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(
er.EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "remove", "entity_id": "light.kitchen"},
{"action": "remove", "entity_id": entry.entity_id},
)
await hass.async_block_till_done()
assert to_update == []
assert to_remove == ["light.kitchen"]
assert to_remove == [entry.entity_id]
with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(
er.EVENT_ENTITY_REGISTRY_UPDATED,
{
"action": "update",
"entity_id": "light.kitchen",
"entity_id": entry.entity_id,
"changes": ["entity_id"],
"old_entity_id": "light.living_room",
},
)
await hass.async_block_till_done()
assert to_update == ["light.kitchen"]
assert to_update == [entry.entity_id]
assert to_remove == ["light.living_room"]
with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(
er.EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]},
{"action": "update", "entity_id": entry.entity_id, "changes": ["icon"]},
)
await hass.async_block_till_done()
@@ -411,6 +469,7 @@ async def test_alexa_update_report_state(
hass: HomeAssistant, cloud_prefs, cloud_stub
) -> None:
"""Test Alexa config responds to reporting state."""
assert await async_setup_component(hass, "homeassistant", {})
await cloud_prefs.async_update(
alexa_report_state=False,
)
@@ -450,6 +509,7 @@ async def test_alexa_handle_logout(
hass: HomeAssistant, cloud_prefs, cloud_stub
) -> None:
"""Test Alexa config responds to logging out."""
assert await async_setup_component(hass, "homeassistant", {})
aconf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
@@ -475,3 +535,118 @@ async def test_alexa_handle_logout(
await hass.async_block_till_done()
assert len(mock_enable.return_value.mock_calls) == 1
async def test_alexa_config_migrate_expose_entity_prefs(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
cloud_stub,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
assert await async_setup_component(hass, "homeassistant", {})
entity_exposed = entity_registry.async_get_or_create(
"light",
"test",
"light_exposed",
suggested_object_id="exposed",
)
entity_migrated = entity_registry.async_get_or_create(
"light",
"test",
"light_migrated",
suggested_object_id="migrated",
)
entity_config = entity_registry.async_get_or_create(
"light",
"test",
"light_config",
suggested_object_id="config",
entity_category=EntityCategory.CONFIG,
)
entity_default = entity_registry.async_get_or_create(
"light",
"test",
"light_default",
suggested_object_id="default",
)
entity_blocked = entity_registry.async_get_or_create(
"group",
"test",
"group_all_locks",
suggested_object_id="all_locks",
)
assert entity_blocked.entity_id == "group.all_locks"
await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False,
alexa_settings_version=1,
)
expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_exposed.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
assert entity_exposed.options == {"cloud.alexa": {"should_expose": True}}
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
assert entity_migrated.options == {"cloud.alexa": {"should_expose": False}}
entity_config = entity_registry.async_get(entity_config.entity_id)
assert entity_config.options == {"cloud.alexa": {"should_expose": False}}
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
assert entity_blocked.options == {"cloud.alexa": {"should_expose": False}}
async def test_alexa_config_migrate_expose_entity_prefs_default_none(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
cloud_stub,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config."""
assert await async_setup_component(hass, "homeassistant", {})
entity_default = entity_registry.async_get_or_create(
"light",
"test",
"light_default",
suggested_object_id="default",
)
await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False,
alexa_settings_version=1,
)
cloud_prefs._prefs[PREF_ALEXA_DEFAULT_EXPOSE] = None
conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
)
await conf.async_initialize()
entity_default = entity_registry.async_get(entity_default.entity_id)
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}