core/tests/helpers/test_entity_registry.py

2233 lines
80 KiB
Python

"""Tests for the Entity Registry."""
from datetime import datetime, timedelta
from functools import partial
from typing import Any
from unittest.mock import patch
import attr
from freezegun.api import FrozenDateTimeFactory
import pytest
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
STATE_UNAVAILABLE,
EntityCategory,
)
from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.exceptions import MaxLengthExceeded
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utc_from_timestamp
from tests.common import (
ANY,
MockConfigEntry,
async_capture_events,
async_fire_time_changed,
flush_store,
)
YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open"
async def test_get(entity_registry: er.EntityRegistry) -> None:
"""Test we can get an item."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
assert entity_registry.async_get(entry.entity_id) is entry
assert entity_registry.async_get(entry.id) is entry
assert entity_registry.async_get("blah") is None
assert entity_registry.async_get("blah.blah") is None
async def test_get_or_create_returns_same_entry(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Make sure we do not duplicate entries."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
entry = entity_registry.async_get_or_create("light", "hue", "1234")
entry2 = entity_registry.async_get_or_create("light", "hue", "1234")
await hass.async_block_till_done()
assert len(entity_registry.entities) == 1
assert entry is entry2
assert entry.entity_id == "light.hue_1234"
assert len(update_events) == 1
assert update_events[0].data == {
"action": "create",
"entity_id": entry.entity_id,
}
def test_get_or_create_suggested_object_id(entity_registry: er.EntityRegistry) -> None:
"""Test that suggested_object_id works."""
entry = entity_registry.async_get_or_create(
"light", "hue", "1234", suggested_object_id="beer"
)
assert entry.entity_id == "light.beer"
def test_get_or_create_updates_data(
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that we update data in get_or_create."""
orig_config_entry = MockConfigEntry(domain="light")
created = datetime.fromisoformat("2024-02-14T12:00:00.0+00:00")
freezer.move_to(created)
orig_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
capabilities={"max": 100},
config_entry=orig_config_entry,
device_id="mock-dev-id",
disabled_by=er.RegistryEntryDisabler.HASS,
entity_category=EntityCategory.CONFIG,
has_entity_name=True,
hidden_by=er.RegistryEntryHider.INTEGRATION,
original_device_class="mock-device-class",
original_icon="initial-original_icon",
original_name="initial-original_name",
supported_features=5,
translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
assert set(entity_registry.async_device_ids()) == {"mock-dev-id"}
assert orig_entry == er.RegistryEntry(
"light.hue_5678",
"5678",
"hue",
capabilities={"max": 100},
config_entry_id=orig_config_entry.entry_id,
created_at=created,
device_class=None,
device_id="mock-dev-id",
disabled_by=er.RegistryEntryDisabler.HASS,
entity_category=EntityCategory.CONFIG,
has_entity_name=True,
hidden_by=er.RegistryEntryHider.INTEGRATION,
icon=None,
id=orig_entry.id,
modified_at=created,
name=None,
original_device_class="mock-device-class",
original_icon="initial-original_icon",
original_name="initial-original_name",
supported_features=5,
translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
new_config_entry = MockConfigEntry(domain="light")
modified = created + timedelta(minutes=5)
freezer.move_to(modified)
new_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
capabilities={"new-max": 150},
config_entry=new_config_entry,
device_id="new-mock-dev-id",
disabled_by=er.RegistryEntryDisabler.USER,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=False,
hidden_by=er.RegistryEntryHider.USER,
original_device_class="new-mock-device-class",
original_icon="updated-original_icon",
original_name="updated-original_name",
supported_features=10,
translation_key="updated-translation_key",
unit_of_measurement="updated-unit_of_measurement",
)
assert new_entry == er.RegistryEntry(
"light.hue_5678",
"5678",
"hue",
aliases=set(),
area_id=None,
capabilities={"new-max": 150},
config_entry_id=new_config_entry.entry_id,
created_at=created,
device_class=None,
device_id="new-mock-dev-id",
disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=False,
hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated
icon=None,
id=orig_entry.id,
modified_at=modified,
name=None,
original_device_class="new-mock-device-class",
original_icon="updated-original_icon",
original_name="updated-original_name",
supported_features=10,
translation_key="updated-translation_key",
unit_of_measurement="updated-unit_of_measurement",
)
assert set(entity_registry.async_device_ids()) == {"new-mock-dev-id"}
modified = created + timedelta(minutes=5)
freezer.move_to(modified)
new_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
capabilities=None,
config_entry=None,
device_id=None,
disabled_by=None,
entity_category=None,
has_entity_name=None,
hidden_by=None,
original_device_class=None,
original_icon=None,
original_name=None,
supported_features=None,
translation_key=None,
unit_of_measurement=None,
)
assert new_entry == er.RegistryEntry(
"light.hue_5678",
"5678",
"hue",
aliases=set(),
area_id=None,
capabilities=None,
config_entry_id=None,
created_at=created,
device_class=None,
device_id=None,
disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated
entity_category=None,
has_entity_name=None,
hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated
icon=None,
id=orig_entry.id,
modified_at=modified,
name=None,
original_device_class=None,
original_icon=None,
original_name=None,
supported_features=0, # supported_features is stored as an int
translation_key=None,
unit_of_measurement=None,
)
assert set(entity_registry.async_device_ids()) == set()
def test_get_or_create_suggested_object_id_conflict_register(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we don't generate an entity id that is already registered."""
entry = entity_registry.async_get_or_create(
"light", "hue", "1234", suggested_object_id="beer"
)
entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", suggested_object_id="beer"
)
assert entry.entity_id == "light.beer"
assert entry2.entity_id == "light.beer_2"
def test_get_or_create_suggested_object_id_conflict_existing(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that we don't generate an entity id that currently exists."""
hass.states.async_set("light.hue_1234", "on")
entry = entity_registry.async_get_or_create("light", "hue", "1234")
assert entry.entity_id == "light.hue_1234_2"
def test_create_triggers_save(entity_registry: er.EntityRegistry) -> None:
"""Test that registering entry triggers a save."""
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
entity_registry.async_get_or_create("light", "hue", "1234")
assert len(mock_schedule_save.mock_calls) == 1
async def test_loading_saving_data(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that we load/save data correctly."""
mock_config = MockConfigEntry(domain="light")
orig_entry1 = entity_registry.async_get_or_create("light", "hue", "1234")
orig_entry2 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
capabilities={"max": 100},
config_entry=mock_config,
device_id="mock-dev-id",
disabled_by=er.RegistryEntryDisabler.HASS,
entity_category=EntityCategory.CONFIG,
hidden_by=er.RegistryEntryHider.INTEGRATION,
has_entity_name=True,
original_device_class="mock-device-class",
original_icon="hass:original-icon",
original_name="Original Name",
supported_features=5,
translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
entity_registry.async_update_entity(
orig_entry2.entity_id,
aliases={"initial_alias_1", "initial_alias_2"},
area_id="mock-area-id",
device_class="user-class",
name="User Name",
icon="hass:user-icon",
)
entity_registry.async_update_entity_options(
orig_entry2.entity_id, "light", {"minimum_brightness": 20}
)
entity_registry.async_update_entity(
orig_entry2.entity_id,
categories={"scope", "id"},
labels={"label1", "label2"},
)
orig_entry2 = entity_registry.async_get(orig_entry2.entity_id)
orig_entry3 = entity_registry.async_get_or_create("light", "hue", "ABCD")
orig_entry4 = entity_registry.async_get_or_create("light", "hue", "EFGH")
entity_registry.async_remove(orig_entry3.entity_id)
entity_registry.async_remove(orig_entry4.entity_id)
assert len(entity_registry.entities) == 2
assert len(entity_registry.deleted_entities) == 2
# Now load written data in new registry
registry2 = er.EntityRegistry(hass)
await flush_store(entity_registry._store)
await registry2.async_load()
# Ensure same order
assert list(entity_registry.entities) == list(registry2.entities)
assert list(entity_registry.deleted_entities) == list(registry2.deleted_entities)
new_entry1 = entity_registry.async_get_or_create("light", "hue", "1234")
new_entry2 = entity_registry.async_get_or_create("light", "hue", "5678")
new_entry3 = entity_registry.async_get_or_create("light", "hue", "ABCD")
new_entry4 = entity_registry.async_get_or_create("light", "hue", "EFGH")
assert orig_entry1 == new_entry1
assert orig_entry2 == new_entry2
# By converting a deleted device to a active device, the modified_at will be updated
assert orig_entry3.modified_at < new_entry3.modified_at
assert attr.evolve(orig_entry3, modified_at=new_entry3.modified_at) == new_entry3
assert orig_entry4.modified_at < new_entry4.modified_at
assert attr.evolve(orig_entry4, modified_at=new_entry4.modified_at) == new_entry4
assert new_entry2.area_id == "mock-area-id"
assert new_entry2.categories == {"scope", "id"}
assert new_entry2.capabilities == {"max": 100}
assert new_entry2.config_entry_id == mock_config.entry_id
assert new_entry2.device_class == "user-class"
assert new_entry2.device_id == "mock-dev-id"
assert new_entry2.disabled_by is er.RegistryEntryDisabler.HASS
assert new_entry2.entity_category == "config"
assert new_entry2.icon == "hass:user-icon"
assert new_entry2.hidden_by == er.RegistryEntryHider.INTEGRATION
assert new_entry2.has_entity_name is True
assert new_entry2.labels == {"label1", "label2"}
assert new_entry2.name == "User Name"
assert new_entry2.options == {"light": {"minimum_brightness": 20}}
assert new_entry2.original_device_class == "mock-device-class"
assert new_entry2.original_icon == "hass:original-icon"
assert new_entry2.original_name == "Original Name"
assert new_entry2.supported_features == 5
assert new_entry2.translation_key == "initial-translation_key"
assert new_entry2.unit_of_measurement == "initial-unit_of_measurement"
def test_generate_entity_considers_registered_entities(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we don't create entity id that are already registered."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
assert entry.entity_id == "light.hue_1234"
assert (
entity_registry.async_generate_entity_id("light", "hue_1234")
== "light.hue_1234_2"
)
def test_generate_entity_considers_existing_entities(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that we don't create entity id that currently exists."""
hass.states.async_set("light.kitchen", "on")
assert (
entity_registry.async_generate_entity_id("light", "kitchen")
== "light.kitchen_2"
)
def test_is_registered(entity_registry: er.EntityRegistry) -> None:
"""Test that is_registered works."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
assert entity_registry.async_is_registered(entry.entity_id)
assert not entity_registry.async_is_registered("light.non_existing")
@pytest.mark.parametrize("load_registries", [False])
async def test_filter_on_load(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test we transform some data when loading from storage."""
hass_storage[er.STORAGE_KEY] = {
"version": er.STORAGE_VERSION_MAJOR,
"minor_version": 1,
"data": {
"entities": [
{
"entity_id": "test.named",
"platform": "super_platform",
"unique_id": "with-name",
"name": "registry override",
},
# This entity's name should be None
{
"entity_id": "test.no_name",
"platform": "super_platform",
"unique_id": "without-name",
},
{
"entity_id": "test.disabled_user",
"platform": "super_platform",
"unique_id": "disabled-user",
"disabled_by": "user", # We store the string representation
},
{
"entity_id": "test.disabled_hass",
"platform": "super_platform",
"unique_id": "disabled-hass",
"disabled_by": "hass", # We store the string representation
},
]
},
}
await er.async_load(hass)
registry = er.async_get(hass)
assert len(registry.entities) == 4
assert set(registry.entities.keys()) == {
"test.disabled_hass",
"test.disabled_user",
"test.named",
"test.no_name",
}
entry_with_name = registry.async_get_or_create(
"test", "super_platform", "with-name"
)
entry_without_name = registry.async_get_or_create(
"test", "super_platform", "without-name"
)
assert entry_with_name.name == "registry override"
assert entry_without_name.name is None
assert not entry_with_name.disabled
assert entry_with_name.created_at == utc_from_timestamp(0)
assert entry_with_name.modified_at == utc_from_timestamp(0)
entry_disabled_hass = registry.async_get_or_create(
"test", "super_platform", "disabled-hass"
)
entry_disabled_user = registry.async_get_or_create(
"test", "super_platform", "disabled-user"
)
assert entry_disabled_hass.disabled
assert entry_disabled_hass.disabled_by is er.RegistryEntryDisabler.HASS
assert entry_disabled_user.disabled
assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER
@pytest.mark.parametrize("load_registries", [False])
async def test_load_bad_data(
hass: HomeAssistant,
hass_storage: dict[str, Any],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test loading invalid data."""
hass_storage[er.STORAGE_KEY] = {
"version": er.STORAGE_VERSION_MAJOR,
"minor_version": er.STORAGE_VERSION_MINOR,
"data": {
"entities": [
{
"aliases": [],
"area_id": None,
"capabilities": None,
"categories": {},
"config_entry_id": None,
"created_at": "2024-02-14T12:00:00.900075+00:00",
"device_class": None,
"device_id": None,
"disabled_by": None,
"entity_category": None,
"entity_id": "test.test1",
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": "00001",
"labels": [],
"modified_at": "2024-02-14T12:00:00.900075+00:00",
"name": None,
"options": None,
"original_device_class": None,
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"previous_unique_id": None,
"supported_features": 0,
"translation_key": None,
"unique_id": 123, # Should trigger warning
"unit_of_measurement": None,
},
{
"aliases": [],
"area_id": None,
"capabilities": None,
"categories": {},
"config_entry_id": None,
"created_at": "2024-02-14T12:00:00.900075+00:00",
"device_class": None,
"device_id": None,
"disabled_by": None,
"entity_category": None,
"entity_id": "test.test2",
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": "00002",
"labels": [],
"modified_at": "2024-02-14T12:00:00.900075+00:00",
"name": None,
"options": None,
"original_device_class": None,
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"previous_unique_id": None,
"supported_features": 0,
"translation_key": None,
"unique_id": ["not", "valid"], # Should not load
"unit_of_measurement": None,
},
],
"deleted_entities": [
{
"config_entry_id": None,
"created_at": "2024-02-14T12:00:00.900075+00:00",
"entity_id": "test.test3",
"id": "00003",
"modified_at": "2024-02-14T12:00:00.900075+00:00",
"orphaned_timestamp": None,
"platform": "super_platform",
"unique_id": 234, # Should not load
},
{
"config_entry_id": None,
"created_at": "2024-02-14T12:00:00.900075+00:00",
"entity_id": "test.test4",
"id": "00004",
"modified_at": "2024-02-14T12:00:00.900075+00:00",
"orphaned_timestamp": None,
"platform": "super_platform",
"unique_id": ["also", "not", "valid"], # Should not load
},
],
},
}
await er.async_load(hass)
registry = er.async_get(hass)
assert len(registry.entities) == 1
assert set(registry.entities.keys()) == {"test.test1"}
assert len(registry.deleted_entities) == 1
assert set(registry.deleted_entities.keys()) == {("test", "super_platform", 234)}
assert (
"'test' from integration super_platform has a non string unique_id '123', "
"please create a bug report" not in caplog.text
)
assert (
"'test' from integration super_platform has a non string unique_id '234', "
"please create a bug report" not in caplog.text
)
assert (
"Entity registry entry 'test.test2' from integration super_platform could not "
"be loaded: 'unique_id must be a string, got ['not', 'valid']', please create "
"a bug report" in caplog.text
)
def test_async_get_entity_id(entity_registry: er.EntityRegistry) -> None:
"""Test that entity_id is returned."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
assert entry.entity_id == "light.hue_1234"
assert (
entity_registry.async_get_entity_id("light", "hue", "1234") == "light.hue_1234"
)
assert entity_registry.async_get_entity_id("light", "hue", "123") is None
async def test_updating_config_entry_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that we update config entry id in registry."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config_1
)
mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2")
entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config_2
)
assert entry.entity_id == entry2.entity_id
assert entry2.config_entry_id == "mock-id-2"
await hass.async_block_till_done()
assert len(update_events) == 2
assert update_events[0].data == {
"action": "create",
"entity_id": entry.entity_id,
}
assert update_events[1].data == {
"action": "update",
"entity_id": entry.entity_id,
"changes": {"config_entry_id": "mock-id-1"},
}
async def test_removing_config_entry_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that we update config entry id in registry."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config
)
assert entry.config_entry_id == "mock-id-1"
entity_registry.async_clear_config_entry("mock-id-1")
assert not entity_registry.entities
await hass.async_block_till_done()
assert len(update_events) == 2
assert update_events[0].data == {
"action": "create",
"entity_id": entry.entity_id,
}
assert update_events[1].data == {
"action": "remove",
"entity_id": entry.entity_id,
}
async def test_deleted_entity_removing_config_entry_id(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we update config entry id in registry on deleted entity."""
mock_config1 = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2")
entry1 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config1
)
assert entry1.config_entry_id == "mock-id-1"
entry2 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=mock_config2
)
assert entry2.config_entry_id == "mock-id-2"
entity_registry.async_remove(entry1.entity_id)
entity_registry.async_remove(entry2.entity_id)
assert len(entity_registry.entities) == 0
assert len(entity_registry.deleted_entities) == 2
deleted_entry1 = entity_registry.deleted_entities[("light", "hue", "5678")]
assert deleted_entry1.config_entry_id == "mock-id-1"
assert deleted_entry1.orphaned_timestamp is None
deleted_entry2 = entity_registry.deleted_entities[("light", "hue", "1234")]
assert deleted_entry2.config_entry_id == "mock-id-2"
assert deleted_entry2.orphaned_timestamp is None
entity_registry.async_clear_config_entry("mock-id-1")
assert len(entity_registry.entities) == 0
assert len(entity_registry.deleted_entities) == 2
deleted_entry1 = entity_registry.deleted_entities[("light", "hue", "5678")]
assert deleted_entry1.config_entry_id is None
assert deleted_entry1.orphaned_timestamp is not None
assert entity_registry.deleted_entities[("light", "hue", "1234")] == deleted_entry2
async def test_removing_area_id(entity_registry: er.EntityRegistry) -> None:
"""Make sure we can clear area id."""
entry = entity_registry.async_get_or_create("light", "hue", "5678")
entry_w_area = entity_registry.async_update_entity(
entry.entity_id, area_id="12345A"
)
entity_registry.async_clear_area_id("12345A")
entry_wo_area = entity_registry.async_get(entry.entity_id)
assert not entry_wo_area.area_id
assert entry_w_area != entry_wo_area
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None:
"""Test migration from version 1.1."""
hass_storage[er.STORAGE_KEY] = {
"version": 1,
"minor_version": 1,
"data": {
"entities": [
{
"device_class": "best_class",
"entity_id": "test.entity",
"platform": "super_platform",
"unique_id": "very_unique",
},
]
},
}
await er.async_load(hass)
registry = er.async_get(hass)
entry = registry.async_get_or_create("test", "super_platform", "very_unique")
assert entry.device_class is None
assert entry.original_device_class == "best_class"
# Check we store migrated data
await flush_store(registry._store)
assert hass_storage[er.STORAGE_KEY] == {
"version": er.STORAGE_VERSION_MAJOR,
"minor_version": er.STORAGE_VERSION_MINOR,
"key": er.STORAGE_KEY,
"data": {
"entities": [
{
"aliases": [],
"area_id": None,
"capabilities": {},
"categories": {},
"config_entry_id": None,
"created_at": "1970-01-01T00:00:00+00:00",
"device_id": None,
"disabled_by": None,
"entity_category": None,
"entity_id": "test.entity",
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"modified_at": "1970-01-01T00:00:00+00:00",
"name": None,
"options": {},
"original_device_class": "best_class",
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"previous_unique_id": None,
"supported_features": 0,
"translation_key": None,
"unique_id": "very_unique",
"unit_of_measurement": None,
"device_class": None,
}
],
"deleted_entities": [],
},
}
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None:
"""Test migration from version 1.7.
This tests cleanup after frontend bug which incorrectly updated device_class
"""
entity_dict = {
"area_id": None,
"capabilities": {},
"config_entry_id": None,
"device_id": None,
"disabled_by": None,
"entity_category": None,
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": "12345",
"name": None,
"options": None,
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"supported_features": 0,
"unique_id": "very_unique",
"unit_of_measurement": None,
}
hass_storage[er.STORAGE_KEY] = {
"version": 1,
"minor_version": 7,
"data": {
"entities": [
{
**entity_dict,
"device_class": "original_class_by_integration",
"entity_id": "test.entity",
"original_device_class": "new_class_by_integration",
},
{
**entity_dict,
"device_class": "class_by_user",
"entity_id": "binary_sensor.entity",
"original_device_class": "class_by_integration",
},
{
**entity_dict,
"device_class": "class_by_user",
"entity_id": "cover.entity",
"original_device_class": "class_by_integration",
},
]
},
}
await er.async_load(hass)
registry = er.async_get(hass)
entry = registry.async_get_or_create("test", "super_platform", "very_unique")
assert entry.device_class is None
assert entry.original_device_class == "new_class_by_integration"
entry = registry.async_get_or_create(
"binary_sensor", "super_platform", "very_unique"
)
assert entry.device_class == "class_by_user"
assert entry.original_device_class == "class_by_integration"
entry = registry.async_get_or_create("cover", "super_platform", "very_unique")
assert entry.device_class == "class_by_user"
assert entry.original_device_class == "class_by_integration"
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_11(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test migration from version 1.11.
This is the first version which has deleted entities, make sure deleted entities
are updated.
"""
hass_storage[er.STORAGE_KEY] = {
"version": 1,
"minor_version": 11,
"data": {
"entities": [
{
"aliases": [],
"area_id": None,
"capabilities": {},
"config_entry_id": None,
"device_id": None,
"disabled_by": None,
"entity_category": None,
"entity_id": "test.entity",
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": "12345",
"modified_at": "1970-01-01T00:00:00+00:00",
"name": None,
"options": {},
"original_device_class": "best_class",
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"supported_features": 0,
"translation_key": None,
"unique_id": "very_unique",
"unit_of_measurement": None,
"device_class": None,
}
],
"deleted_entities": [
{
"config_entry_id": None,
"entity_id": "test.deleted_entity",
"id": "23456",
"orphaned_timestamp": None,
"platform": "super_duper_platform",
"unique_id": "very_very_unique",
}
],
},
}
await er.async_load(hass)
registry = er.async_get(hass)
entry = registry.async_get_or_create("test", "super_platform", "very_unique")
assert entry.device_class is None
assert entry.original_device_class == "best_class"
# Check migrated data
await flush_store(registry._store)
assert hass_storage[er.STORAGE_KEY] == {
"version": er.STORAGE_VERSION_MAJOR,
"minor_version": er.STORAGE_VERSION_MINOR,
"key": er.STORAGE_KEY,
"data": {
"entities": [
{
"aliases": [],
"area_id": None,
"capabilities": {},
"categories": {},
"config_entry_id": None,
"created_at": "1970-01-01T00:00:00+00:00",
"device_id": None,
"disabled_by": None,
"entity_category": None,
"entity_id": "test.entity",
"has_entity_name": False,
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"modified_at": "1970-01-01T00:00:00+00:00",
"name": None,
"options": {},
"original_device_class": "best_class",
"original_icon": None,
"original_name": None,
"platform": "super_platform",
"previous_unique_id": None,
"supported_features": 0,
"translation_key": None,
"unique_id": "very_unique",
"unit_of_measurement": None,
"device_class": None,
}
],
"deleted_entities": [
{
"config_entry_id": None,
"created_at": "1970-01-01T00:00:00+00:00",
"entity_id": "test.deleted_entity",
"id": "23456",
"modified_at": "1970-01-01T00:00:00+00:00",
"orphaned_timestamp": None,
"platform": "super_duper_platform",
"unique_id": "very_very_unique",
}
],
},
}
async def test_update_entity_unique_id(entity_registry: er.EntityRegistry) -> None:
"""Test entity's unique_id is updated."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config
)
assert (
entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id
)
new_unique_id = "1234"
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
updated_entry = entity_registry.async_update_entity(
entry.entity_id, new_unique_id=new_unique_id
)
assert updated_entry != entry
assert updated_entry.unique_id == new_unique_id
assert updated_entry.previous_unique_id == "5678"
assert mock_schedule_save.call_count == 1
assert entity_registry.async_get_entity_id("light", "hue", "5678") is None
assert (
entity_registry.async_get_entity_id("light", "hue", "1234") == entry.entity_id
)
async def test_update_entity_unique_id_conflict(
entity_registry: er.EntityRegistry,
) -> None:
"""Test migration raises when unique_id already in use."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config
)
entry2 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=mock_config
)
with (
patch.object(entity_registry, "async_schedule_save") as mock_schedule_save,
pytest.raises(ValueError),
):
entity_registry.async_update_entity(
entry.entity_id, new_unique_id=entry2.unique_id
)
assert mock_schedule_save.call_count == 0
assert (
entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id
)
assert (
entity_registry.async_get_entity_id("light", "hue", "1234") == entry2.entity_id
)
async def test_update_entity_entity_id(entity_registry: er.EntityRegistry) -> None:
"""Test entity's entity_id is updated."""
entry = entity_registry.async_get_or_create("light", "hue", "5678")
assert (
entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id
)
new_entity_id = "light.blah"
assert new_entity_id != entry.entity_id
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
updated_entry = entity_registry.async_update_entity(
entry.entity_id, new_entity_id=new_entity_id
)
assert updated_entry != entry
assert updated_entry.entity_id == new_entity_id
assert mock_schedule_save.call_count == 1
assert entity_registry.async_get(entry.entity_id) is None
assert entity_registry.async_get(new_entity_id) is not None
async def test_update_entity_entity_id_entity_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test update raises when entity_id already in use."""
entry = entity_registry.async_get_or_create("light", "hue", "5678")
entry2 = entity_registry.async_get_or_create("light", "hue", "1234")
state_entity_id = "light.blah"
hass.states.async_set(state_entity_id, "on")
assert entry.entity_id != state_entity_id
assert entry2.entity_id != state_entity_id
# Try updating to a registered entity_id
with (
patch.object(entity_registry, "async_schedule_save") as mock_schedule_save,
pytest.raises(ValueError),
):
entity_registry.async_update_entity(
entry.entity_id, new_entity_id=entry2.entity_id
)
assert mock_schedule_save.call_count == 0
assert (
entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id
)
assert entity_registry.async_get(entry.entity_id) is entry
assert (
entity_registry.async_get_entity_id("light", "hue", "1234") == entry2.entity_id
)
assert entity_registry.async_get(entry2.entity_id) is entry2
# Try updating to an entity_id which is in the state machine
with (
patch.object(entity_registry, "async_schedule_save") as mock_schedule_save,
pytest.raises(ValueError),
):
entity_registry.async_update_entity(
entry.entity_id, new_entity_id=state_entity_id
)
assert mock_schedule_save.call_count == 0
assert (
entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id
)
assert entity_registry.async_get(entry.entity_id) is entry
assert entity_registry.async_get(state_entity_id) is None
async def test_update_entity(entity_registry: er.EntityRegistry) -> None:
"""Test updating entity."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config
)
for attr_name, new_value in (
("aliases", {"alias_1", "alias_2"}),
("disabled_by", er.RegistryEntryDisabler.USER),
("icon", "new icon"),
("name", "new name"),
):
changes = {attr_name: new_value}
updated_entry = entity_registry.async_update_entity(entry.entity_id, **changes)
assert updated_entry != entry
assert getattr(updated_entry, attr_name) == new_value
assert getattr(updated_entry, attr_name) != getattr(entry, attr_name)
assert (
entity_registry.async_get_entity_id("light", "hue", "5678")
== updated_entry.entity_id
)
entry = updated_entry
async def test_update_entity_options(entity_registry: er.EntityRegistry) -> None:
"""Test updating entity."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config
)
entity_registry.async_update_entity_options(
entry.entity_id, "light", {"minimum_brightness": 20}
)
new_entry_1 = entity_registry.async_get(entry.entity_id)
assert entry.options == {}
assert new_entry_1.options == {"light": {"minimum_brightness": 20}}
# Test it's not possible to modify the options
with pytest.raises(RuntimeError):
new_entry_1.options["blah"] = {}
with pytest.raises(RuntimeError):
new_entry_1.options["light"] = {}
with pytest.raises(RuntimeError):
new_entry_1.options["light"]["blah"] = 123
with pytest.raises(RuntimeError):
new_entry_1.options["light"]["minimum_brightness"] = 123
entity_registry.async_update_entity_options(
entry.entity_id, "light", {"minimum_brightness": 30}
)
new_entry_2 = entity_registry.async_get(entry.entity_id)
assert entry.options == {}
assert new_entry_1.options == {"light": {"minimum_brightness": 20}}
assert new_entry_2.options == {"light": {"minimum_brightness": 30}}
async def test_disabled_by(entity_registry: er.EntityRegistry) -> None:
"""Test that we can disable an entry when we create it."""
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", disabled_by=er.RegistryEntryDisabler.HASS
)
assert entry.disabled_by is er.RegistryEntryDisabler.HASS
assert entry.disabled is True
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", disabled_by=er.RegistryEntryDisabler.INTEGRATION
)
assert entry.disabled_by is er.RegistryEntryDisabler.HASS
assert entry.disabled is True
entry2 = entity_registry.async_get_or_create("light", "hue", "1234")
assert entry2.disabled_by is None
assert entry2.disabled is False
async def test_disabled_by_config_entry_pref(
entity_registry: er.EntityRegistry,
) -> None:
"""Test config entry preference setting disabled_by."""
mock_config = MockConfigEntry(
domain="light",
entry_id="mock-id-1",
pref_disable_new_entities=True,
)
entry = entity_registry.async_get_or_create(
"light", "hue", "AAAA", config_entry=mock_config
)
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
entry2 = entity_registry.async_get_or_create(
"light",
"hue",
"BBBB",
config_entry=mock_config,
disabled_by=er.RegistryEntryDisabler.USER,
)
assert entry2.disabled_by is er.RegistryEntryDisabler.USER
async def test_hidden_by(entity_registry: er.EntityRegistry) -> None:
"""Test that we can hide an entry when we create it."""
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", hidden_by=er.RegistryEntryHider.USER
)
assert entry.hidden_by is er.RegistryEntryHider.USER
assert entry.hidden is True
entry = entity_registry.async_get_or_create(
"light", "hue", "5678", disabled_by=er.RegistryEntryHider.INTEGRATION
)
assert entry.hidden_by is er.RegistryEntryHider.USER
assert entry.hidden is True
entry2 = entity_registry.async_get_or_create("light", "hue", "1234")
assert entry2.hidden_by is None
assert entry2.hidden is False
async def test_restore_states(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test restoring states."""
hass.set_state(CoreState.not_running)
entity_registry.async_get_or_create(
"light",
"hue",
"1234",
suggested_object_id="simple",
)
# Should not be created
entity_registry.async_get_or_create(
"light",
"hue",
"5678",
suggested_object_id="disabled",
disabled_by=er.RegistryEntryDisabler.HASS,
)
entity_registry.async_get_or_create(
"light",
"hue",
"9012",
suggested_object_id="all_info_set",
capabilities={"max": 100},
supported_features=5,
original_device_class="mock-device-class",
original_name="Mock Original Name",
original_icon="hass:original-icon",
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
simple = hass.states.get("light.simple")
assert simple is not None
assert simple.state == STATE_UNAVAILABLE
assert simple.attributes == {"restored": True, "supported_features": 0}
disabled = hass.states.get("light.disabled")
assert disabled is None
all_info_set = hass.states.get("light.all_info_set")
assert all_info_set is not None
assert all_info_set.state == STATE_UNAVAILABLE
assert all_info_set.attributes == {
"max": 100,
"supported_features": 5,
"device_class": "mock-device-class",
"restored": True,
"friendly_name": "Mock Original Name",
"icon": "hass:original-icon",
}
entity_registry.async_remove("light.disabled")
entity_registry.async_remove("light.simple")
entity_registry.async_remove("light.all_info_set")
await hass.async_block_till_done()
assert hass.states.get("light.simple") is None
assert hass.states.get("light.disabled") is None
assert hass.states.get("light.all_info_set") is None
async def test_remove_device_removes_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that we remove entities tied to a device."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
assert entity_registry.async_is_registered(entry.entity_id)
device_registry.async_remove_device(device_entry.id)
await hass.async_block_till_done()
assert not entity_registry.async_is_registered(entry.entity_id)
async def test_remove_config_entry_from_device_removes_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we remove entities tied to a device when config entry is removed."""
config_entry_1 = MockConfigEntry(domain="hue")
config_entry_1.add_to_hass(hass)
config_entry_2 = MockConfigEntry(domain="device_tracker")
config_entry_2.add_to_hass(hass)
# Create device with two config entries
device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry_2.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
assert device_entry.config_entries == {
config_entry_1.entry_id,
config_entry_2.entry_id,
}
# Create one entity for each config entry
entry_1 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry_1,
device_id=device_entry.id,
)
entry_2 = entity_registry.async_get_or_create(
"sensor",
"device_tracker",
"6789",
config_entry=config_entry_2,
device_id=device_entry.id,
)
assert entity_registry.async_is_registered(entry_1.entity_id)
assert entity_registry.async_is_registered(entry_2.entity_id)
# Remove the first config entry from the device, the entity associated with it
# should be removed
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry_1.entry_id
)
await hass.async_block_till_done()
assert device_registry.async_get(device_entry.id)
assert not entity_registry.async_is_registered(entry_1.entity_id)
assert entity_registry.async_is_registered(entry_2.entity_id)
# Remove the second config entry from the device, the entity associated with it
# (and the device itself) should be removed
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry_2.entry_id
)
await hass.async_block_till_done()
assert not device_registry.async_get(device_entry.id)
assert not entity_registry.async_is_registered(entry_1.entity_id)
assert not entity_registry.async_is_registered(entry_2.entity_id)
async def test_remove_config_entry_from_device_removes_entities_2(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we don't remove entities with no config entry when device is modified."""
config_entry_1 = MockConfigEntry(domain="hue")
config_entry_1.add_to_hass(hass)
config_entry_2 = MockConfigEntry(domain="device_tracker")
config_entry_2.add_to_hass(hass)
# Create device with two config entries
device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry_2.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
assert device_entry.config_entries == {
config_entry_1.entry_id,
config_entry_2.entry_id,
}
# Create one entity for each config entry
entry_1 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
device_id=device_entry.id,
)
assert entity_registry.async_is_registered(entry_1.entity_id)
# Remove the first config entry from the device
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry_1.entry_id
)
await hass.async_block_till_done()
assert device_registry.async_get(device_entry.id)
assert entity_registry.async_is_registered(entry_1.entity_id)
async def test_update_device_race(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test race when a device is created, updated and removed."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Create device
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
# Update it
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={("bridgeid", "0123")},
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
# Add entity to the device
entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
assert entity_registry.async_is_registered(entry.entity_id)
device_registry.async_remove_device(device_entry.id)
await hass.async_block_till_done()
assert not entity_registry.async_is_registered(entry.entity_id)
async def test_disable_device_disables_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we disable entities tied to a device."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entry1 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
entry2 = entity_registry.async_get_or_create(
"light",
"hue",
"ABCD",
config_entry=config_entry,
device_id=device_entry.id,
disabled_by=er.RegistryEntryDisabler.USER,
)
entry3 = entity_registry.async_get_or_create(
"light",
"hue",
"EFGH",
config_entry=config_entry,
device_id=device_entry.id,
disabled_by=er.RegistryEntryDisabler.CONFIG_ENTRY,
)
assert not entry1.disabled
assert entry2.disabled
assert entry3.disabled
device_registry.async_update_device(
device_entry.id, disabled_by=dr.DeviceEntryDisabler.USER
)
await hass.async_block_till_done()
entry1 = entity_registry.async_get(entry1.entity_id)
assert entry1.disabled
assert entry1.disabled_by is er.RegistryEntryDisabler.DEVICE
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by is er.RegistryEntryDisabler.USER
entry3 = entity_registry.async_get(entry3.entity_id)
assert entry3.disabled
assert entry3.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
device_registry.async_update_device(device_entry.id, disabled_by=None)
await hass.async_block_till_done()
entry1 = entity_registry.async_get(entry1.entity_id)
assert not entry1.disabled
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by is er.RegistryEntryDisabler.USER
entry3 = entity_registry.async_get(entry3.entity_id)
assert entry3.disabled
assert entry3.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
async def test_disable_config_entry_disables_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that we disable entities tied to a config entry."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entry1 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
entry2 = entity_registry.async_get_or_create(
"light",
"hue",
"ABCD",
config_entry=config_entry,
device_id=device_entry.id,
disabled_by=er.RegistryEntryDisabler.USER,
)
entry3 = entity_registry.async_get_or_create(
"light",
"hue",
"EFGH",
config_entry=config_entry,
device_id=device_entry.id,
disabled_by=er.RegistryEntryDisabler.DEVICE,
)
assert not entry1.disabled
assert entry2.disabled
assert entry3.disabled
await hass.config_entries.async_set_disabled_by(
config_entry.entry_id, config_entries.ConfigEntryDisabler.USER
)
await hass.async_block_till_done()
entry1 = entity_registry.async_get(entry1.entity_id)
assert entry1.disabled
assert entry1.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by is er.RegistryEntryDisabler.USER
entry3 = entity_registry.async_get(entry3.entity_id)
assert entry3.disabled
assert entry3.disabled_by is er.RegistryEntryDisabler.DEVICE
await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None)
await hass.async_block_till_done()
entry1 = entity_registry.async_get(entry1.entity_id)
assert not entry1.disabled
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by is er.RegistryEntryDisabler.USER
# The device was re-enabled, so entity disabled by the device will be re-enabled too
entry3 = entity_registry.async_get(entry3.entity_id)
assert not entry3.disabled_by
async def test_disabled_entities_excluded_from_entity_list(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that disabled entities are excluded from async_entries_for_device."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entry1 = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
entry2 = entity_registry.async_get_or_create(
"light",
"hue",
"ABCD",
config_entry=config_entry,
device_id=device_entry.id,
disabled_by=er.RegistryEntryDisabler.USER,
)
entries = er.async_entries_for_device(entity_registry, device_entry.id)
assert entries == [entry1]
entries = er.async_entries_for_device(
entity_registry, device_entry.id, include_disabled_entities=True
)
assert entries == [entry1, entry2]
ent_reg = er.async_get(hass)
assert ent_reg.entities.get_entries_for_device_id(device_entry.id) == [entry1]
assert ent_reg.entities.get_entries_for_device_id(
device_entry.id, include_disabled_entities=True
) == [entry1, entry2]
async def test_entity_max_length_exceeded(entity_registry: er.EntityRegistry) -> None:
"""Test that an exception is raised when the max character length is exceeded."""
long_domain_name = (
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890123456789012345678901234567890"
)
with pytest.raises(MaxLengthExceeded) as exc_info:
entity_registry.async_generate_entity_id(long_domain_name, "sensor")
assert exc_info.value.property_name == "domain"
assert exc_info.value.max_length == 64
assert exc_info.value.value == long_domain_name
# Try again but force a number to get added to the entity ID
long_entity_id_name = (
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567890123456789012345678901234567890"
"1234567890123456789012345678901234567"
)
known = []
new_id = entity_registry.async_generate_entity_id(
"sensor", long_entity_id_name, known
)
assert new_id == "sensor." + long_entity_id_name[: 255 - 7]
known.append(new_id)
new_id = entity_registry.async_generate_entity_id(
"sensor", long_entity_id_name, known
)
assert new_id == "sensor." + long_entity_id_name[: 255 - 7 - 2] + "_2"
known.append(new_id)
new_id = entity_registry.async_generate_entity_id(
"sensor", long_entity_id_name, known
)
assert new_id == "sensor." + long_entity_id_name[: 255 - 7 - 2] + "_3"
async def test_resolve_entity_ids(entity_registry: er.EntityRegistry) -> None:
"""Test resolving entity IDs."""
entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", suggested_object_id="beer"
)
assert entry1.entity_id == "light.beer"
entry2 = entity_registry.async_get_or_create(
"light", "hue", "2345", suggested_object_id="milk"
)
assert entry2.entity_id == "light.milk"
expected = ["light.beer", "light.milk"]
assert (
er.async_validate_entity_ids(entity_registry, [entry1.id, entry2.id])
== expected
)
expected = ["light.beer", "light.milk"]
assert (
er.async_validate_entity_ids(entity_registry, ["light.beer", entry2.id])
== expected
)
with pytest.raises(vol.Invalid):
er.async_validate_entity_ids(entity_registry, ["light.beer", "bad_uuid"])
expected = ["light.unknown"]
assert er.async_validate_entity_ids(entity_registry, ["light.unknown"]) == expected
with pytest.raises(vol.Invalid):
er.async_validate_entity_ids(entity_registry, ["unknown_uuid"])
def test_entity_registry_items() -> None:
"""Test the EntityRegistryItems container."""
entities = er.EntityRegistryItems()
assert entities.get_entity_id(("a", "b", "c")) is None
assert entities.get_entry("abc") is None
entry1 = er.RegistryEntry("test.entity1", "1234", "hue")
entry2 = er.RegistryEntry("test.entity2", "2345", "hue")
entities["test.entity1"] = entry1
entities["test.entity2"] = entry2
assert entities["test.entity1"] is entry1
assert entities["test.entity2"] is entry2
assert entities.get_entity_id(("test", "hue", "1234")) is entry1.entity_id
assert entities.get_entry(entry1.id) is entry1
assert entities.get_entity_id(("test", "hue", "2345")) is entry2.entity_id
assert entities.get_entry(entry2.id) is entry2
entities.pop("test.entity1")
del entities["test.entity2"]
assert entities.get_entity_id(("test", "hue", "1234")) is None
assert entities.get_entry(entry1.id) is None
assert entities.get_entity_id(("test", "hue", "2345")) is None
assert entities.get_entry(entry2.id) is None
async def test_disabled_by_str_not_allowed(entity_registry: er.EntityRegistry) -> None:
"""Test we need to pass disabled by type."""
with pytest.raises(ValueError):
entity_registry.async_get_or_create(
"light", "hue", "1234", disabled_by=er.RegistryEntryDisabler.USER.value
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(ValueError):
entity_registry.async_update_entity(
entity_id, disabled_by=er.RegistryEntryDisabler.USER.value
)
async def test_entity_category_str_not_allowed(
entity_registry: er.EntityRegistry,
) -> None:
"""Test we need to pass entity category type."""
with pytest.raises(ValueError):
entity_registry.async_get_or_create(
"light", "hue", "1234", entity_category=EntityCategory.DIAGNOSTIC.value
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(ValueError):
entity_registry.async_update_entity(
entity_id, entity_category=EntityCategory.DIAGNOSTIC.value
)
async def test_hidden_by_str_not_allowed(entity_registry: er.EntityRegistry) -> None:
"""Test we need to pass hidden by type."""
with pytest.raises(ValueError):
entity_registry.async_get_or_create(
"light", "hue", "1234", hidden_by=er.RegistryEntryHider.USER.value
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(ValueError):
entity_registry.async_update_entity(
entity_id, hidden_by=er.RegistryEntryHider.USER.value
)
async def test_unique_id_non_hashable(entity_registry: er.EntityRegistry) -> None:
"""Test unique_id which is not hashable."""
with pytest.raises(TypeError):
entity_registry.async_get_or_create("light", "hue", ["not", "valid"])
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(TypeError):
entity_registry.async_update_entity(entity_id, new_unique_id=["not", "valid"])
async def test_unique_id_non_string(
entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture
) -> None:
"""Test unique_id which is not a string."""
entity_registry.async_get_or_create("light", "hue", 1234)
assert (
"'light' from integration hue has a non string unique_id '1234', "
"please create a bug report" in caplog.text
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
entity_registry.async_update_entity(entity_id, new_unique_id=2345)
assert (
"'light' from integration hue has a non string unique_id '2345', "
"please create a bug report" in caplog.text
)
def test_migrate_entity_to_new_platform(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test migrate_entity_to_new_platform."""
orig_config_entry = MockConfigEntry(domain="light")
orig_unique_id = "5678"
orig_entry = entity_registry.async_get_or_create(
"light",
"hue",
orig_unique_id,
suggested_object_id="light",
config_entry=orig_config_entry,
disabled_by=er.RegistryEntryDisabler.USER,
entity_category=EntityCategory.CONFIG,
original_device_class="mock-device-class",
original_icon="initial-original_icon",
original_name="initial-original_name",
)
assert entity_registry.async_get("light.light") is orig_entry
entity_registry.async_update_entity(
"light.light",
name="new_name",
icon="new_icon",
)
new_config_entry = MockConfigEntry(domain="light")
new_unique_id = "1234"
assert entity_registry.async_update_entity_platform(
"light.light",
"hue2",
new_unique_id=new_unique_id,
new_config_entry_id=new_config_entry.entry_id,
)
assert not entity_registry.async_get_entity_id("light", "hue", orig_unique_id)
assert (new_entry := entity_registry.async_get("light.light")) is not orig_entry
assert new_entry.config_entry_id == new_config_entry.entry_id
assert new_entry.unique_id == new_unique_id
assert new_entry.name == "new_name"
assert new_entry.icon == "new_icon"
assert new_entry.platform == "hue2"
# Test nonexisting entity
with pytest.raises(KeyError):
entity_registry.async_update_entity_platform(
"light.not_a_real_light",
"hue2",
new_unique_id=new_unique_id,
new_config_entry_id=new_config_entry.entry_id,
)
# Test migrate entity without new config entry ID
with pytest.raises(ValueError):
entity_registry.async_update_entity_platform(
"light.light",
"hue3",
)
# Test entity with a state
hass.states.async_set("light.light", "on")
with pytest.raises(ValueError):
entity_registry.async_update_entity_platform(
"light.light",
"hue2",
new_unique_id=new_unique_id,
new_config_entry_id=new_config_entry.entry_id,
)
async def test_restore_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Make sure entity registry id is stable and entity_id is reused if possible."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
config_entry = MockConfigEntry(domain="light")
entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry
)
entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=config_entry
)
entry1 = entity_registry.async_update_entity(
entry1.entity_id, new_entity_id="light.custom_1"
)
entity_registry.async_remove(entry1.entity_id)
entity_registry.async_remove(entry2.entity_id)
assert len(entity_registry.entities) == 0
assert len(entity_registry.deleted_entities) == 2
# Re-add entities
entry1_restored = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry
)
entry2_restored = entity_registry.async_get_or_create("light", "hue", "5678")
assert len(entity_registry.entities) == 2
assert len(entity_registry.deleted_entities) == 0
assert entry1 != entry1_restored
# entity_id is not restored
assert attr.evolve(entry1, entity_id="light.hue_1234") == entry1_restored
assert entry2 != entry2_restored
# Config entry is not restored
assert attr.evolve(entry2, config_entry_id=None) == entry2_restored
# Remove two of the entities again, then bump time
entity_registry.async_remove(entry1_restored.entity_id)
entity_registry.async_remove(entry2.entity_id)
assert len(entity_registry.entities) == 0
assert len(entity_registry.deleted_entities) == 2
freezer.tick(timedelta(seconds=er.ORPHANED_ENTITY_KEEP_SECONDS + 1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Re-add two entities, expect to get a new id after the purge for entity w/o config entry
entry1_restored = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry
)
entry2_restored = entity_registry.async_get_or_create("light", "hue", "5678")
assert len(entity_registry.entities) == 2
assert len(entity_registry.deleted_entities) == 0
assert entry1.id == entry1_restored.id
assert entry2.id != entry2_restored.id
# Remove the first entity, then its config entry, finally bump time
entity_registry.async_remove(entry1_restored.entity_id)
assert len(entity_registry.entities) == 1
assert len(entity_registry.deleted_entities) == 1
entity_registry.async_clear_config_entry(config_entry.entry_id)
freezer.tick(timedelta(seconds=er.ORPHANED_ENTITY_KEEP_SECONDS + 1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Re-add the entity, expect to get a new id after the purge
entry1_restored = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry
)
assert len(entity_registry.entities) == 2
assert len(entity_registry.deleted_entities) == 0
assert entry1.id != entry1_restored.id
# Check the events
await hass.async_block_till_done()
assert len(update_events) == 13
assert update_events[0].data == {"action": "create", "entity_id": "light.hue_1234"}
assert update_events[1].data == {"action": "create", "entity_id": "light.hue_5678"}
assert update_events[2].data["action"] == "update"
assert update_events[3].data == {"action": "remove", "entity_id": "light.custom_1"}
assert update_events[4].data == {"action": "remove", "entity_id": "light.hue_5678"}
# Restore entities the 1st time
assert update_events[5].data == {"action": "create", "entity_id": "light.hue_1234"}
assert update_events[6].data == {"action": "create", "entity_id": "light.hue_5678"}
assert update_events[7].data == {"action": "remove", "entity_id": "light.hue_1234"}
assert update_events[8].data == {"action": "remove", "entity_id": "light.hue_5678"}
# Restore entities the 2nd time
assert update_events[9].data == {"action": "create", "entity_id": "light.hue_1234"}
assert update_events[10].data == {"action": "create", "entity_id": "light.hue_5678"}
assert update_events[11].data == {"action": "remove", "entity_id": "light.hue_1234"}
# Restore entities the 3rd time
assert update_events[12].data == {"action": "create", "entity_id": "light.hue_1234"}
async def test_async_migrate_entry_delete_self(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test async_migrate_entry."""
config_entry1 = MockConfigEntry(domain="test1")
config_entry2 = MockConfigEntry(domain="test2")
entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1"
)
entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=config_entry1, original_name="Entry 2"
)
entry3 = entity_registry.async_get_or_create(
"light", "hue", "90AB", config_entry=config_entry2, original_name="Entry 3"
)
@callback
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
entries.add(entity_entry.entity_id)
if entity_entry == entry1:
entity_registry.async_remove(entry1.entity_id)
return None
if entity_entry == entry2:
return {"original_name": "Entry 2 renamed"}
return None
entries = set()
await er.async_migrate_entries(hass, config_entry1.entry_id, _async_migrator)
assert entries == {entry1.entity_id, entry2.entity_id}
assert not entity_registry.async_is_registered(entry1.entity_id)
entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.original_name == "Entry 2 renamed"
assert entity_registry.async_get(entry3.entity_id) is entry3
async def test_async_migrate_entry_delete_other(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test async_migrate_entry."""
config_entry1 = MockConfigEntry(domain="test1")
config_entry2 = MockConfigEntry(domain="test2")
entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1"
)
entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=config_entry1, original_name="Entry 2"
)
entity_registry.async_get_or_create(
"light", "hue", "90AB", config_entry=config_entry2, original_name="Entry 3"
)
@callback
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
entries.add(entity_entry.entity_id)
if entity_entry == entry1:
entity_registry.async_remove(entry2.entity_id)
return None
if entity_entry == entry2:
pytest.fail("We should not get here")
return None
entries = set()
await er.async_migrate_entries(hass, config_entry1.entry_id, _async_migrator)
assert entries == {entry1.entity_id}
assert not entity_registry.async_is_registered(entry2.entity_id)
async def test_removing_labels(entity_registry: er.EntityRegistry) -> None:
"""Make sure we can clear labels."""
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="5678",
)
entry = entity_registry.async_update_entity(
entry.entity_id, labels={"label1", "label2"}
)
entity_registry.async_clear_label_id("label1")
entry_cleared_label1 = entity_registry.async_get(entry.entity_id)
entity_registry.async_clear_label_id("label2")
entry_cleared_label2 = entity_registry.async_get(entry.entity_id)
assert entry_cleared_label1
assert entry_cleared_label2
assert entry != entry_cleared_label1
assert entry != entry_cleared_label2
assert entry_cleared_label1 != entry_cleared_label2
assert entry.labels == {"label1", "label2"}
assert entry_cleared_label1.labels == {"label2"}
assert not entry_cleared_label2.labels
async def test_entries_for_label(entity_registry: er.EntityRegistry) -> None:
"""Test getting entity entries by label."""
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="000",
)
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="123",
)
label_1 = entity_registry.async_update_entity(entry.entity_id, labels={"label1"})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="456",
)
label_2 = entity_registry.async_update_entity(entry.entity_id, labels={"label2"})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="789",
)
label_1_and_2 = entity_registry.async_update_entity(
entry.entity_id, labels={"label1", "label2"}
)
entries = er.async_entries_for_label(entity_registry, "label1")
assert len(entries) == 2
assert entries == [label_1, label_1_and_2]
entries = er.async_entries_for_label(entity_registry, "label2")
assert len(entries) == 2
assert entries == [label_2, label_1_and_2]
assert not er.async_entries_for_label(entity_registry, "unknown")
assert not er.async_entries_for_label(entity_registry, "")
async def test_removing_categories(entity_registry: er.EntityRegistry) -> None:
"""Make sure we can clear categories."""
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="5678",
)
entry = entity_registry.async_update_entity(
entry.entity_id, categories={"scope1": "id", "scope2": "id"}
)
entity_registry.async_clear_category_id("scope1", "id")
entry_cleared_scope1 = entity_registry.async_get(entry.entity_id)
entity_registry.async_clear_category_id("scope2", "id")
entry_cleared_scope2 = entity_registry.async_get(entry.entity_id)
assert entry_cleared_scope1
assert entry_cleared_scope2
assert entry != entry_cleared_scope1
assert entry != entry_cleared_scope2
assert entry_cleared_scope1 != entry_cleared_scope2
assert entry.categories == {"scope1": "id", "scope2": "id"}
assert entry_cleared_scope1.categories == {"scope2": "id"}
assert not entry_cleared_scope2.categories
async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None:
"""Test getting entity entries by category."""
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="000",
)
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="123",
)
category_1 = entity_registry.async_update_entity(
entry.entity_id, categories={"scope1": "id"}
)
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="456",
)
category_2 = entity_registry.async_update_entity(
entry.entity_id, categories={"scope2": "id"}
)
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="789",
)
category_1_and_2 = entity_registry.async_update_entity(
entry.entity_id, categories={"scope1": "id", "scope2": "id"}
)
entries = er.async_entries_for_category(entity_registry, "scope1", "id")
assert len(entries) == 2
assert entries == [category_1, category_1_and_2]
entries = er.async_entries_for_category(entity_registry, "scope2", "id")
assert len(entries) == 2
assert entries == [category_2, category_1_and_2]
assert not er.async_entries_for_category(entity_registry, "unknown", "id")
assert not er.async_entries_for_category(entity_registry, "", "id")
assert not er.async_entries_for_category(entity_registry, "scope1", "unknown")
assert not er.async_entries_for_category(entity_registry, "scope1", "")
async def test_get_or_create_thread_safety(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test call async_get_or_create_from a thread."""
with pytest.raises(
RuntimeError,
match="Detected code that calls entity_registry.async_get_or_create from a thread.",
):
await hass.async_add_executor_job(
entity_registry.async_get_or_create, "light", "hue", "1234"
)
async def test_async_update_entity_thread_safety(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test call async_get_or_create from a thread."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
with pytest.raises(
RuntimeError,
match="Detected code that calls entity_registry.async_update_entity from a thread.",
):
await hass.async_add_executor_job(
partial(
entity_registry.async_update_entity,
entry.entity_id,
new_unique_id="5678",
)
)
async def test_async_remove_thread_safety(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test call async_remove from a thread."""
entry = entity_registry.async_get_or_create("light", "hue", "1234")
with pytest.raises(
RuntimeError,
match="Detected code that calls entity_registry.async_remove from a thread.",
):
await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id)