"""Test Home Assistant exposed entities helper.""" import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, ExposedEntities, ExposedEntity, async_expose_entity, async_get_assistant_settings, async_get_entity_settings, async_listen_entity_updates, async_should_expose, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import flush_store from tests.typing import WebSocketGenerator @pytest.fixture(name="entities") def entities_fixture( hass: HomeAssistant, entity_registry: er.EntityRegistry, request: pytest.FixtureRequest, ) -> dict[str, str]: """Set up the test environment.""" if request.param == "entities_unique_id": return entities_unique_id(entity_registry) if request.param == "entities_no_unique_id": return entities_no_unique_id(hass) raise RuntimeError("Invalid setup fixture") def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]: """Create some entities in the entity registry.""" entry_blocked = entity_registry.async_get_or_create( "group", "test", "unique", suggested_object_id="all_locks" ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] entry_lock = entity_registry.async_get_or_create("lock", "test", "unique1") entry_binary_sensor = entity_registry.async_get_or_create( "binary_sensor", "test", "unique1" ) entry_binary_sensor_door = entity_registry.async_get_or_create( "binary_sensor", "test", "unique2", original_device_class="door", ) entry_sensor = entity_registry.async_get_or_create("sensor", "test", "unique1") entry_sensor_temperature = entity_registry.async_get_or_create( "sensor", "test", "unique2", original_device_class="temperature", ) return { "blocked": entry_blocked.entity_id, "lock": entry_lock.entity_id, "binary_sensor": entry_binary_sensor.entity_id, "door_sensor": entry_binary_sensor_door.entity_id, "sensor": entry_sensor.entity_id, "temperature_sensor": entry_sensor_temperature.entity_id, } def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]: """Create some entities not in the entity registry.""" blocked = CLOUD_NEVER_EXPOSED_ENTITIES[0] lock = "lock.test" binary_sensor = "binary_sensor.test" door_sensor = "binary_sensor.door" sensor = "sensor.test" sensor_temperature = "sensor.temperature" hass.states.async_set(binary_sensor, "on", {}) hass.states.async_set(door_sensor, "on", {"device_class": "door"}) hass.states.async_set(sensor, "on", {}) hass.states.async_set(sensor_temperature, "on", {"device_class": "temperature"}) return { "blocked": blocked, "lock": lock, "binary_sensor": binary_sensor, "door_sensor": door_sensor, "sensor": sensor, "temperature_sensor": sensor_temperature, } async def test_load_preferences(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" assert await async_setup_component(hass, "homeassistant", {}) exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] assert exposed_entities._assistants == {} exposed_entities.async_set_expose_new_entities("test1", True) exposed_entities.async_set_expose_new_entities("test2", False) async_expose_entity(hass, "test1", "light.kitchen", True) async_expose_entity(hass, "test1", "light.living_room", True) async_expose_entity(hass, "test2", "light.kitchen", True) async_expose_entity(hass, "test2", "light.kitchen", True) assert list(exposed_entities._assistants) == ["test1", "test2"] assert list(exposed_entities.entities) == ["light.kitchen", "light.living_room"] await flush_store(exposed_entities._store) exposed_entities2 = ExposedEntities(hass) await exposed_entities2.async_initialize() assert exposed_entities._assistants == exposed_entities2._assistants assert exposed_entities.entities == exposed_entities2.entities async def test_expose_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, ) -> None: """Test expose entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() entry1 = entity_registry.async_get_or_create("test", "test", "unique1") entry2 = entity_registry.async_get_or_create("test", "test", "unique2") exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa"], "entity_ids": [entry1.entity_id], "should_expose": True, } ) response = await ws_client.receive_json() assert response["success"] entry1 = entity_registry.async_get(entry1.entity_id) assert entry1.options == {"cloud.alexa": {"should_expose": True}} entry2 = entity_registry.async_get(entry2.entity_id) assert entry2.options == {} assert len(exposed_entities.entities) == 0 # Update options await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa", "cloud.google_assistant"], "entity_ids": [entry1.entity_id, entry2.entity_id], "should_expose": False, } ) response = await ws_client.receive_json() assert response["success"] entry1 = entity_registry.async_get(entry1.entity_id) assert entry1.options == { "cloud.alexa": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False}, } entry2 = entity_registry.async_get(entry2.entity_id) assert entry2.options == { "cloud.alexa": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False}, } assert len(exposed_entities.entities) == 0 async def test_expose_entity_unknown( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, ) -> None: """Test behavior when exposing an unknown entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa"], "entity_ids": ["test.test"], "should_expose": True, } ) response = await ws_client.receive_json() assert response["success"] assert len(exposed_entities.entities) == 1 assert exposed_entities.entities == { "test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}}) } # Update options await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa", "cloud.google_assistant"], "entity_ids": ["test.test", "test.test2"], "should_expose": False, } ) response = await ws_client.receive_json() assert response["success"] assert len(exposed_entities.entities) == 2 assert exposed_entities.entities == { "test.test": ExposedEntity( { "cloud.alexa": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False}, } ), "test.test2": ExposedEntity( { "cloud.alexa": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False}, } ), } async def test_expose_entity_blocked( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, ) -> None: """Test behavior when exposing a blocked entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() # Set options await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa"], "entity_ids": ["group.all_locks"], "should_expose": True, } ) response = await ws_client.receive_json() assert not response["success"] assert response["error"] == { "code": "not_allowed", "message": "can't expose 'group.all_locks'", } @pytest.mark.parametrize("expose_new", [True, False]) async def test_expose_new_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, expose_new, ) -> None: """Test expose entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() entry1 = entity_registry.async_get_or_create("climate", "test", "unique1") entry2 = entity_registry.async_get_or_create("climate", "test", "unique2") await ws_client.send_json_auto_id( { "type": "homeassistant/expose_new_entities/get", "assistant": "cloud.alexa", } ) response = await ws_client.receive_json() assert response["success"] assert response["result"] == {"expose_new": False} # Check if exposed - should be False assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False # Expose new entities to Alexa await ws_client.send_json_auto_id( { "type": "homeassistant/expose_new_entities/set", "assistant": "cloud.alexa", "expose_new": expose_new, } ) response = await ws_client.receive_json() assert response["success"] await ws_client.send_json_auto_id( { "type": "homeassistant/expose_new_entities/get", "assistant": "cloud.alexa", } ) response = await ws_client.receive_json() assert response["success"] assert response["result"] == {"expose_new": expose_new} # Check again if exposed - should still be False assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False # Check if exposed - should be True assert async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new async def test_listen_updates( hass: HomeAssistant, entity_registry: er.EntityRegistry, ) -> None: """Test listen to updates.""" calls = [] def listener(): calls.append(None) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() async_listen_entity_updates(hass, "cloud.alexa", listener) entry = entity_registry.async_get_or_create("climate", "test", "unique1") # Call for another assistant - listener not called async_expose_entity(hass, "cloud.google_assistant", entry.entity_id, True) assert len(calls) == 0 # Call for our assistant - listener called async_expose_entity(hass, "cloud.alexa", entry.entity_id, True) assert len(calls) == 1 # Settings not changed - listener not called async_expose_entity(hass, "cloud.alexa", entry.entity_id, True) assert len(calls) == 1 # Settings changed - listener called async_expose_entity(hass, "cloud.alexa", entry.entity_id, False) assert len(calls) == 2 async def test_get_assistant_settings( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test get assistant settings.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() entry = entity_registry.async_get_or_create("climate", "test", "unique1") assert async_get_assistant_settings(hass, "cloud.alexa") == {} async_expose_entity(hass, "cloud.alexa", entry.entity_id, True) async_expose_entity(hass, "cloud.alexa", "light.not_in_registry", True) assert async_get_assistant_settings(hass, "cloud.alexa") == snapshot assert async_get_assistant_settings(hass, "cloud.google_assistant") == snapshot with pytest.raises(HomeAssistantError): async_get_entity_settings(hass, "light.unknown") @pytest.mark.parametrize( "entities", ["entities_unique_id", "entities_no_unique_id"], indirect=True ) async def test_should_expose( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, entities: dict[str, str], ) -> None: """Test expose entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() # Expose new entities to Alexa await ws_client.send_json_auto_id( { "type": "homeassistant/expose_new_entities/set", "assistant": "cloud.alexa", "expose_new": True, } ) response = await ws_client.receive_json() assert response["success"] # Unknown entity is not exposed assert async_should_expose(hass, "test.test", "test.test") is False # Blocked entity is not exposed assert async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False # Lock is exposed assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is True # Binary sensor without device class is not exposed assert async_should_expose(hass, "cloud.alexa", entities["binary_sensor"]) is False # Binary sensor with certain device class is exposed assert async_should_expose(hass, "cloud.alexa", entities["door_sensor"]) is True # Sensor without device class is not exposed assert async_should_expose(hass, "cloud.alexa", entities["sensor"]) is False # Sensor with certain device class is exposed assert ( async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True ) # The second time we check, it should load it from storage assert ( async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True ) # Check with a different assistant exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False) assert ( async_should_expose( hass, "cloud.no_default_expose", entities["temperature_sensor"] ) is False ) async def test_should_expose_hidden_categorized( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, ) -> None: """Test expose entity.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() # Expose new entities to Alexa await ws_client.send_json_auto_id( { "type": "homeassistant/expose_new_entities/set", "assistant": "cloud.alexa", "expose_new": True, } ) response = await ws_client.receive_json() assert response["success"] entity_registry.async_get_or_create( "lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER ) assert async_should_expose(hass, "cloud.alexa", "lock.test_unique2") is False # Entity with category is not exposed entity_registry.async_get_or_create( "lock", "test", "unique3", entity_category=EntityCategory.CONFIG ) assert async_should_expose(hass, "cloud.alexa", "lock.test_unique3") is False async def test_list_exposed_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, ) -> None: """Test list exposed entities.""" ws_client = await hass_ws_client(hass) assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() entry1 = entity_registry.async_get_or_create("test", "test", "unique1") entry2 = entity_registry.async_get_or_create("test", "test", "unique2") # Set options for registered entities await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa", "cloud.google_assistant"], "entity_ids": [entry1.entity_id, entry2.entity_id], "should_expose": True, } ) response = await ws_client.receive_json() assert response["success"] # Set options for entities not in the entity registry await ws_client.send_json_auto_id( { "type": "homeassistant/expose_entity", "assistants": ["cloud.alexa", "cloud.google_assistant"], "entity_ids": [ "test.test", "test.test2", ], "should_expose": False, } ) response = await ws_client.receive_json() assert response["success"] # List exposed entities await ws_client.send_json_auto_id({"type": "homeassistant/expose_entity/list"}) response = await ws_client.receive_json() assert response["success"] assert response["result"] == { "exposed_entities": { "test.test": {"cloud.alexa": False, "cloud.google_assistant": False}, "test.test2": {"cloud.alexa": False, "cloud.google_assistant": False}, "test.test_unique1": {"cloud.alexa": True, "cloud.google_assistant": True}, "test.test_unique2": {"cloud.alexa": True, "cloud.google_assistant": True}, }, } async def test_listeners( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Make sure we call entity listeners.""" assert await async_setup_component(hass, "homeassistant", {}) exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] callbacks = [] exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1)) async_expose_entity(hass, "test1", "light.kitchen", True) assert len(callbacks) == 1 entry1 = entity_registry.async_get_or_create("switch", "test", "unique1") async_expose_entity(hass, "test1", entry1.entity_id, True)