diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 294a555149640..1d6fdcf36d456 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -62,6 +62,7 @@ from .exceptions import HomeAssistantError from .helpers import ( area_registry, + category_registry, config_validation as cv, device_registry, entity, @@ -342,6 +343,7 @@ def _cache_uname_processor() -> None: template.async_setup(hass) await asyncio.gather( create_eager_task(area_registry.async_load(hass)), + create_eager_task(category_registry.async_load(hass)), create_eager_task(device_registry.async_load(hass)), create_eager_task(entity_registry.async_load(hass)), create_eager_task(floor_registry.async_load(hass)), diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index b536433cf1b09..d71a00ce3bdb4 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -14,6 +14,7 @@ auth, auth_provider_homeassistant, automation, + category_registry, config_entries, core, device_registry, @@ -30,6 +31,7 @@ auth, auth_provider_homeassistant, automation, + category_registry, config_entries, core, device_registry, diff --git a/homeassistant/components/config/category_registry.py b/homeassistant/components/config/category_registry.py new file mode 100644 index 0000000000000..045243a355b86 --- /dev/null +++ b/homeassistant/components/config/category_registry.py @@ -0,0 +1,134 @@ +"""Websocket API to interact with the category registry.""" +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import category_registry as cr, config_validation as cv + + +@callback +def async_setup(hass: HomeAssistant) -> bool: + """Register the category registry WS commands.""" + websocket_api.async_register_command(hass, websocket_list_categories) + websocket_api.async_register_command(hass, websocket_create_category) + websocket_api.async_register_command(hass, websocket_delete_category) + websocket_api.async_register_command(hass, websocket_update_category) + return True + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/list", + vol.Required("scope"): str, + } +) +@callback +def websocket_list_categories( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle list categories command.""" + category_registry = cr.async_get(hass) + connection.send_result( + msg["id"], + [ + _entry_dict(entry) + for entry in category_registry.async_list_categories(scope=msg["scope"]) + ], + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/create", + vol.Required("scope"): str, + vol.Required("name"): str, + vol.Optional("icon"): vol.Any(cv.icon, None), + } +) +@websocket_api.require_admin +@callback +def websocket_create_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Create category command.""" + category_registry = cr.async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") + + try: + entry = category_registry.async_create(**data) + except ValueError as err: + connection.send_error(msg["id"], "invalid_info", str(err)) + else: + connection.send_result(msg["id"], _entry_dict(entry)) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/delete", + vol.Required("scope"): str, + vol.Required("category_id"): str, + } +) +@websocket_api.require_admin +@callback +def websocket_delete_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Delete category command.""" + category_registry = cr.async_get(hass) + + try: + category_registry.async_delete( + scope=msg["scope"], category_id=msg["category_id"] + ) + except KeyError: + connection.send_error(msg["id"], "invalid_info", "Category ID doesn't exist") + else: + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/update", + vol.Required("scope"): str, + vol.Required("category_id"): str, + vol.Optional("name"): str, + vol.Optional("icon"): vol.Any(cv.icon, None), + } +) +@websocket_api.require_admin +@callback +def websocket_update_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle update category websocket command.""" + category_registry = cr.async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") + + try: + entry = category_registry.async_update(**data) + except ValueError as err: + connection.send_error(msg["id"], "invalid_info", str(err)) + except KeyError: + connection.send_error(msg["id"], "invalid_info", "Category ID doesn't exist") + else: + connection.send_result(msg["id"], _entry_dict(entry)) + + +@callback +def _entry_dict(entry: cr.CategoryEntry) -> dict[str, Any]: + """Convert entry to API format.""" + return { + "category_id": entry.category_id, + "icon": entry.icon, + "name": entry.name, + } diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 8a172b17921de..2d8fe1a3645c1 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -153,6 +153,16 @@ def websocket_get_entities( # If passed in, we update value. Passing None will remove old value. vol.Optional("aliases"): list, vol.Optional("area_id"): vol.Any(str, None), + # Categories is a mapping of key/value (scope/category_id) pairs. + # If passed in, we update/adjust only the provided scope(s). + # Other category scopes in the entity, are left as is. + # + # Categorized items such as entities + # can only be in 1 category ID per scope at a time. + # Therefore, passing in a category ID will either add or move + # the entity to that specific category. Passing in None will + # remove the entity from the category. + vol.Optional("categories"): cv.schema_with_slug_keys(vol.Any(str, None)), vol.Optional("device_class"): vol.Any(str, None), vol.Optional("icon"): vol.Any(str, None), vol.Optional("name"): vol.Any(str, None), @@ -227,6 +237,18 @@ def websocket_update_entity( ) return + # Update the categories if provided + if "categories" in msg: + categories = entity_entry.categories.copy() + for scope, category_id in msg["categories"].items(): + if scope in categories and category_id is None: + # Remove the category from the scope as it was unset + del categories[scope] + elif category_id is not None: + # Add or update the category for the given scope + categories[scope] = category_id + changes["categories"] = categories + try: if changes: entity_entry = registry.async_update_entity(entity_id, **changes) diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py new file mode 100644 index 0000000000000..686ddaa69c84e --- /dev/null +++ b/homeassistant/helpers/category_registry.py @@ -0,0 +1,208 @@ +"""Provide a way to categorize things within a defined scope.""" +from __future__ import annotations + +from collections.abc import Iterable +import dataclasses +from dataclasses import dataclass, field +from typing import Literal, TypedDict, cast + +from homeassistant.core import HomeAssistant, callback +from homeassistant.util.ulid import ulid_now + +from .registry import BaseRegistry +from .typing import UNDEFINED, EventType, UndefinedType + +DATA_REGISTRY = "category_registry" +EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" +STORAGE_KEY = "core.category_registry" +STORAGE_VERSION_MAJOR = 1 + + +class EventCategoryRegistryUpdatedData(TypedDict): + """Event data for when the category registry is updated.""" + + action: Literal["create", "remove", "update"] + scope: str + category_id: str + + +EventCategoryRegistryUpdated = EventType[EventCategoryRegistryUpdatedData] + + +@dataclass(slots=True, kw_only=True, frozen=True) +class CategoryEntry: + """Category registry entry.""" + + category_id: str = field(default_factory=ulid_now) + icon: str | None = None + name: str + + +class CategoryRegistry(BaseRegistry): + """Class to hold a registry of categories by scope.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the category registry.""" + self.hass = hass + self.categories: dict[str, dict[str, CategoryEntry]] = {} + self._store = hass.helpers.storage.Store( + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + ) + + @callback + def async_get_category( + self, *, scope: str, category_id: str + ) -> CategoryEntry | None: + """Get category by ID.""" + if scope not in self.categories: + return None + return self.categories[scope].get(category_id) + + @callback + def async_list_categories(self, *, scope: str) -> Iterable[CategoryEntry]: + """Get all categories.""" + if scope not in self.categories: + return [] + return self.categories[scope].values() + + @callback + def async_create( + self, + *, + name: str, + scope: str, + icon: str | None = None, + ) -> CategoryEntry: + """Create a new category.""" + self._async_ensure_name_is_available(scope, name) + category = CategoryEntry( + icon=icon, + name=name, + ) + + if scope not in self.categories: + self.categories[scope] = {} + + self.categories[scope][category.category_id] = category + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="create", scope=scope, category_id=category.category_id + ), + ) + return category + + @callback + def async_delete(self, *, scope: str, category_id: str) -> None: + """Delete category.""" + del self.categories[scope][category_id] + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="remove", + scope=scope, + category_id=category_id, + ), + ) + self.async_schedule_save() + + @callback + def async_update( + self, + *, + scope: str, + category_id: str, + icon: str | None | UndefinedType = UNDEFINED, + name: str | UndefinedType = UNDEFINED, + ) -> CategoryEntry: + """Update name or icon of the category.""" + old = self.categories[scope][category_id] + changes = {} + + if icon is not UNDEFINED and icon != old.icon: + changes["icon"] = icon + + if name is not UNDEFINED and name != old.name: + changes["name"] = name + self._async_ensure_name_is_available(scope, name, category_id) + + if not changes: + return old + + new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="update", scope=scope, category_id=category_id + ), + ) + + return new + + async def async_load(self) -> None: + """Load the category registry.""" + data = await self._store.async_load() + category_entries: dict[str, dict[str, CategoryEntry]] = {} + + if data is not None: + for scope, categories in data["categories"].items(): + category_entries[scope] = { + category["category_id"]: CategoryEntry( + category_id=category["category_id"], + icon=category["icon"], + name=category["name"], + ) + for category in categories + } + + self.categories = category_entries + + @callback + def _data_to_save(self) -> dict[str, dict[str, list[dict[str, str | None]]]]: + """Return data of category registry to store in a file.""" + return { + "categories": { + scope: [ + { + "category_id": entry.category_id, + "icon": entry.icon, + "name": entry.name, + } + for entry in entries.values() + ] + for scope, entries in self.categories.items() + } + } + + @callback + def _async_ensure_name_is_available( + self, scope: str, name: str, category_id: str | None = None + ) -> None: + """Ensure name is available within the scope.""" + if scope not in self.categories: + return + for category in self.categories[scope].values(): + if ( + category.name.casefold() == name.casefold() + and category.category_id != category_id + ): + raise ValueError(f"The name '{name}' is already in use") + + +@callback +def async_get(hass: HomeAssistant) -> CategoryRegistry: + """Get category registry.""" + return cast(CategoryRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load category registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = CategoryRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3c437faf18ced..d0ac52fe9fc5d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -67,7 +67,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 13 +STORAGE_VERSION_MINOR = 14 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -164,6 +164,7 @@ class RegistryEntry: previous_unique_id: str | None = attr.ib(default=None) aliases: set[str] = attr.ib(factory=set) area_id: str | None = attr.ib(default=None) + categories: dict[str, str] = attr.ib(factory=dict) capabilities: Mapping[str, Any] | None = attr.ib(default=None) config_entry_id: str | None = attr.ib(default=None) device_class: str | None = attr.ib(default=None) @@ -262,6 +263,7 @@ def as_partial_dict(self) -> dict[str, Any]: # it every time return { "area_id": self.area_id, + "categories": self.categories, "config_entry_id": self.config_entry_id, "device_id": self.device_id, "disabled_by": self.disabled_by, @@ -319,6 +321,7 @@ def as_storage_fragment(self) -> json_fragment: { "aliases": list(self.aliases), "area_id": self.area_id, + "categories": self.categories, "capabilities": self.capabilities, "config_entry_id": self.config_entry_id, "device_class": self.device_class, @@ -498,6 +501,11 @@ async def _async_migrate_func( # noqa: C901 for entity in data["entities"]: entity["labels"] = [] + if old_major_version == 1 and old_minor_version < 14: + # Version 1.14 adds categories + for entity in data["entities"]: + entity["categories"] = {} + if old_major_version > 1: raise NotImplementedError return data @@ -952,6 +960,7 @@ def _async_update_entity( *, aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + categories: dict[str, str] | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, @@ -1003,6 +1012,7 @@ def _async_update_entity( for attr_name, value in ( ("aliases", aliases), ("area_id", area_id), + ("categories", categories), ("capabilities", capabilities), ("config_entry_id", config_entry_id), ("device_class", device_class), @@ -1081,6 +1091,7 @@ def async_update_entity( *, aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + categories: dict[str, str] | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, @@ -1106,6 +1117,7 @@ def async_update_entity( entity_id, aliases=aliases, area_id=area_id, + categories=categories, capabilities=capabilities, config_entry_id=config_entry_id, device_class=device_class, @@ -1196,6 +1208,7 @@ async def async_load(self) -> None: entities[entity["entity_id"]] = RegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], + categories=entity["categories"], capabilities=entity["capabilities"], config_entry_id=entity["config_entry_id"], device_class=entity["device_class"], @@ -1255,6 +1268,17 @@ def _data_to_save(self) -> dict[str, Any]: ], } + @callback + def async_clear_category_id(self, scope: str, category_id: str) -> None: + """Clear category id from registry entries.""" + for entity_id, entry in self.entities.items(): + if ( + existing_category_id := entry.categories.get(scope) + ) and category_id == existing_category_id: + categories = entry.categories.copy() + del categories[scope] + self.async_update_entity(entity_id, categories=categories) + @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" @@ -1344,6 +1368,21 @@ def async_entries_for_label( return [entry for entry in registry.entities.values() if label_id in entry.labels] +@callback +def async_entries_for_category( + registry: EntityRegistry, scope: str, category_id: str +) -> list[RegistryEntry]: + """Return entries that match a category in a scope.""" + return [ + entry + for entry in registry.entities.values() + if ( + (existing_category_id := entry.categories.get(scope)) + and category_id == existing_category_id + ) + ] + + @callback def async_entries_for_config_entry( registry: EntityRegistry, config_entry_id: str @@ -1386,13 +1425,13 @@ def async_config_entry_disabled_by_changed( def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Clean up device registry when entities removed.""" # pylint: disable-next=import-outside-toplevel - from . import event, label_registry as lr + from . import category_registry as cr, event, label_registry as lr @callback - def _label_removed_from_registry_filter( - event: lr.EventLabelRegistryUpdated, + def _removed_from_registry_filter( + event: lr.EventLabelRegistryUpdated | cr.EventCategoryRegistryUpdated, ) -> bool: - """Filter all except for the remove action from label registry events.""" + """Filter all except for the remove action from registry events.""" return event.data["action"] == "remove" @callback @@ -1402,10 +1441,23 @@ def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None: hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, - event_filter=_label_removed_from_registry_filter, + event_filter=_removed_from_registry_filter, listener=_handle_label_registry_update, ) + @callback + def _handle_category_registry_update( + event: cr.EventCategoryRegistryUpdated, + ) -> None: + """Update entity that have a category that has been removed.""" + registry.async_clear_category_id(event.data["scope"], event.data["category_id"]) + + hass.bus.async_listen( + event_type=cr.EVENT_CATEGORY_REGISTRY_UPDATED, + event_filter=_removed_from_registry_filter, + listener=_handle_category_registry_update, + ) + @callback def cleanup(_: datetime) -> None: """Clean up entity registry.""" diff --git a/pyproject.toml b/pyproject.toml index f258a0aa2f761..bc5c3c53b85c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -688,6 +688,7 @@ ignore = [ [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" "homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" "homeassistant.helpers.device_registry" = "dr" "homeassistant.helpers.entity_registry" = "er" diff --git a/tests/common.py b/tests/common.py index 2e6b30d680b1b..2b1db405de706 100644 --- a/tests/common.py +++ b/tests/common.py @@ -59,6 +59,7 @@ ) from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, device_registry as dr, entity, entity_platform, @@ -334,6 +335,7 @@ def async_create_task(coroutine, name=None, eager_start=False): "homeassistant.helpers.restore_state.start.async_at_start", ): await ar.async_load(hass) + await cr.async_load(hass) await dr.async_load(hass) await er.async_load(hass) await fr.async_load(hass) diff --git a/tests/components/config/test_category_registry.py b/tests/components/config/test_category_registry.py new file mode 100644 index 0000000000000..fd762d80c3b81 --- /dev/null +++ b/tests/components/config/test_category_registry.py @@ -0,0 +1,380 @@ +"""Test category registry API.""" +import pytest + +from homeassistant.components.config import category_registry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import category_registry as cr + +from tests.common import ANY +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + + +@pytest.fixture(name="client") +async def client_fixture( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> MockHAClientWebSocket: + """Fixture that can interact with the config manager API.""" + category_registry.async_setup(hass) + return await hass_ws_client(hass) + + +async def test_list_categories( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test list entries.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:home", + ) + category3 = category_registry.async_create( + scope="zone", + name="Grocery stores", + icon="mdi:store", + ) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + await client.send_json_auto_id( + {"type": "config/category_registry/list", "scope": "automation"} + ) + + msg = await client.receive_json() + + assert len(msg["result"]) == 2 + assert msg["result"][0] == { + "category_id": category1.category_id, + "name": "Energy saving", + "icon": "mdi:leaf", + } + assert msg["result"][1] == { + "category_id": category2.category_id, + "name": "Something else", + "icon": "mdi:home", + } + + await client.send_json_auto_id( + {"type": "config/category_registry/list", "scope": "zone"} + ) + + msg = await client.receive_json() + + assert len(msg["result"]) == 1 + assert msg["result"][0] == { + "category_id": category3.category_id, + "name": "Grocery stores", + "icon": "mdi:store", + } + + +async def test_create_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test create entry.""" + await client.send_json_auto_id( + { + "type": "config/category_registry/create", + "scope": "automation", + "name": "Energy saving", + "icon": "mdi:leaf", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + assert msg["result"] == { + "icon": "mdi:leaf", + "category_id": ANY, + "name": "Energy saving", + } + + await client.send_json_auto_id( + { + "scope": "automation", + "name": "Something else", + "type": "config/category_registry/create", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + assert msg["result"] == { + "icon": None, + "category_id": ANY, + "name": "Something else", + } + + # Test adding the same one again in a different scope + await client.send_json_auto_id( + { + "type": "config/category_registry/create", + "scope": "script", + "name": "Energy saving", + "icon": "mdi:leaf", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["script"]) == 1 + + assert msg["result"] == { + "icon": "mdi:leaf", + "category_id": ANY, + "name": "Energy saving", + } + + +async def test_create_category_with_name_already_in_use( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test create entry that should fail.""" + category_registry.async_create( + scope="automation", + name="Energy saving", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "name": "ENERGY SAVING", + "type": "config/category_registry/create", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "The name 'ENERGY SAVING' is already in use" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + +async def test_delete_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test delete entry.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert len(category_registry.categories) == 1 + assert not category_registry.categories["automation"] + + +async def test_delete_non_existing_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test delete entry that should fail.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": "idkfa", + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "bullshizzle", + "category_id": category.category_id, + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "ENERGY SAVING", + "icon": "mdi:left", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert msg["result"] == { + "icon": "mdi:left", + "category_id": category.category_id, + "name": "ENERGY SAVING", + } + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "Energy saving", + "icon": None, + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert msg["result"] == { + "icon": None, + "category_id": category.category_id, + "name": "Energy saving", + } + + +async def test_update_with_name_already_in_use( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry.""" + category_registry.async_create( + scope="automation", + name="Energy saving", + ) + category = category_registry.async_create( + scope="automation", + name="Something else", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "ENERGY SAVING", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "The name 'ENERGY SAVING' is already in use" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + +async def test_update_non_existing_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry that should fail.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": "idkfa", + "name": "New category name", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "bullshizzle", + "category_id": category.category_id, + "name": "New category name", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 36e3b8b46ff95..30a7106c0dd77 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -61,6 +61,7 @@ async def test_list_entities( assert msg["result"] == [ { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -80,6 +81,7 @@ async def test_list_entities( }, { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -126,6 +128,7 @@ class Unserializable: assert msg["result"] == [ { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -349,6 +352,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -382,6 +386,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -440,6 +445,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -464,6 +470,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -514,13 +521,14 @@ async def test_update_entity( assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" - # UPDATE AREA, DEVICE_CLASS, HIDDEN_BY, ICON AND NAME + # Update area, categories, device_class, hidden_by, icon, labels & name await client.send_json_auto_id( { "type": "config/entity_registry/update", "entity_id": "test_domain.world", "aliases": ["alias_1", "alias_2"], "area_id": "mock-area-id", + "categories": {"scope1": "id", "scope2": "id"}, "device_class": "custom_device_class", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", @@ -535,6 +543,7 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -561,7 +570,7 @@ async def test_update_entity( assert state.name == "after update" assert state.attributes[ATTR_ICON] == "icon:after update" - # UPDATE HIDDEN_BY TO ILLEGAL VALUE + # Update hidden_by to illegal value await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -575,7 +584,7 @@ async def test_update_entity( assert registry.entities["test_domain.world"].hidden_by is RegistryEntryHider.USER - # UPDATE DISABLED_BY TO USER + # Update disabled_by to user await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -592,7 +601,7 @@ async def test_update_entity( registry.entities["test_domain.world"].disabled_by is RegistryEntryDisabler.USER ) - # UPDATE DISABLED_BY TO NONE + # Update disabled_by to None await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -608,6 +617,7 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -631,7 +641,7 @@ async def test_update_entity( "require_restart": True, } - # UPDATE ENTITY OPTION + # Update entity option await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -648,6 +658,127 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": [], + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Add a category to the entity + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope3": "id"}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope2": "id", "scope3": "id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": [], + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Move the entity to a different category + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope3": "other_id"}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope2": "id", "scope3": "other_id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": [], + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Move the entity to a different category + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope2": None}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope3": "other_id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -702,6 +833,7 @@ async def test_update_entity_require_restart( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": config_entry.entry_id, "device_class": None, "device_id": None, @@ -815,6 +947,7 @@ async def test_update_entity_no_changes( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -904,6 +1037,7 @@ async def test_update_entity_id( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 18ad22921ed57..0b6e9aebecd09 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -61,6 +61,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -111,6 +113,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -159,6 +163,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -208,6 +214,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -258,6 +266,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -308,6 +318,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -356,6 +368,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -405,6 +419,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -455,6 +471,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -505,6 +523,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -553,6 +573,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -602,6 +624,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -652,6 +676,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -702,6 +728,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -750,6 +778,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -799,6 +829,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -849,6 +881,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -899,6 +933,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -947,6 +983,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -996,6 +1034,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1046,6 +1086,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1096,6 +1138,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1144,6 +1188,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1193,6 +1239,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1243,6 +1291,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1293,6 +1343,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1341,6 +1393,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1390,6 +1444,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1440,6 +1496,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1490,6 +1548,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1538,6 +1598,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1587,6 +1649,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1637,6 +1701,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1687,6 +1753,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1737,6 +1805,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -1787,6 +1857,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -1821,6 +1893,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -1862,6 +1936,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -1894,6 +1970,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -1928,6 +2006,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -1965,6 +2045,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2002,6 +2084,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2039,6 +2123,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2073,6 +2159,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2114,6 +2202,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2146,6 +2236,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2180,6 +2272,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2217,6 +2311,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2254,6 +2350,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2291,6 +2389,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2325,6 +2425,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2366,6 +2468,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2398,6 +2502,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2432,6 +2538,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2469,6 +2577,8 @@ 'capabilities': dict({ 'state_class': 'total_increasing', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2506,6 +2616,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2543,6 +2655,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2577,6 +2691,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2618,6 +2734,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2650,6 +2768,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2688,6 +2808,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2720,6 +2842,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2758,6 +2882,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2790,6 +2916,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2828,6 +2956,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2860,6 +2990,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2898,6 +3030,8 @@ 'check-wiring', ]), }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2930,6 +3064,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', @@ -2996,6 +3132,8 @@ 'capabilities': dict({ 'state_class': 'measurement', }), + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': None, @@ -3038,6 +3176,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, 'disabled_by': 'integration', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 94e7dd7bc17e7..10f62920d8e2b 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -37,6 +37,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -78,6 +80,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -126,6 +130,8 @@ 'manual', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -174,6 +180,8 @@ 'purifying', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -220,6 +228,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -262,6 +272,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -304,6 +316,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -354,6 +368,8 @@ 'sleepy', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -411,6 +427,8 @@ 'router', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -459,6 +477,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -497,6 +517,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -535,6 +557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -609,6 +633,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -680,6 +706,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -719,6 +747,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -758,6 +788,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -799,6 +831,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -841,6 +875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -911,6 +947,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -950,6 +988,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -989,6 +1029,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1030,6 +1072,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1072,6 +1116,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1142,6 +1188,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1181,6 +1229,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1220,6 +1270,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1261,6 +1313,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1303,6 +1357,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1377,6 +1433,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1419,6 +1477,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1463,6 +1523,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1505,6 +1567,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1575,6 +1639,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1614,6 +1680,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1655,6 +1723,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1733,6 +1803,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1775,6 +1847,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1823,6 +1897,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1882,6 +1958,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1924,6 +2002,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1998,6 +2078,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2039,6 +2121,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2117,6 +2201,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2156,6 +2242,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2195,6 +2283,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2243,6 +2333,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2299,6 +2391,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2341,6 +2435,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2385,6 +2481,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2428,6 +2526,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2469,6 +2569,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2507,6 +2609,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2581,6 +2685,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2622,6 +2728,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2665,6 +2773,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2708,6 +2818,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2751,6 +2863,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2794,6 +2908,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2837,6 +2953,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2878,6 +2996,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2917,6 +3037,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2992,6 +3114,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3031,6 +3155,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3072,6 +3198,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3145,6 +3273,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3184,6 +3314,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3223,6 +3355,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3261,6 +3395,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3311,6 +3447,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3373,6 +3511,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3421,6 +3561,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3465,6 +3607,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3508,6 +3652,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3581,6 +3727,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3620,6 +3768,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3661,6 +3811,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3734,6 +3886,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3773,6 +3927,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3814,6 +3970,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3891,6 +4049,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3930,6 +4090,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3969,6 +4131,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4007,6 +4171,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4057,6 +4223,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4119,6 +4287,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4167,6 +4337,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4211,6 +4383,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4254,6 +4428,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4331,6 +4507,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4370,6 +4548,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4441,6 +4621,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4491,6 +4673,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4552,6 +4736,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4596,6 +4782,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4639,6 +4827,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4712,6 +4902,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4751,6 +4943,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4792,6 +4986,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4865,6 +5061,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4904,6 +5102,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4945,6 +5145,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5022,6 +5224,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5061,6 +5265,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5100,6 +5306,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5138,6 +5346,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5192,6 +5402,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5259,6 +5471,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5307,6 +5521,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5351,6 +5567,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5394,6 +5612,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5471,6 +5691,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5510,6 +5732,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5549,6 +5773,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5590,6 +5816,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5633,6 +5861,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5674,6 +5904,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5748,6 +5980,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5792,6 +6026,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5839,6 +6075,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5883,6 +6121,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5926,6 +6166,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5970,6 +6212,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6013,6 +6257,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6090,6 +6336,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6131,6 +6379,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6174,6 +6424,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6217,6 +6469,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6260,6 +6514,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6301,6 +6557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6340,6 +6598,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6414,6 +6674,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6453,6 +6715,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6491,6 +6755,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6532,6 +6798,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6607,6 +6875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6646,6 +6916,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6720,6 +6992,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6759,6 +7033,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6801,6 +7077,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6875,6 +7153,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6946,6 +7226,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6985,6 +7267,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7027,6 +7311,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7105,6 +7391,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7146,6 +7434,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7221,6 +7511,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7292,6 +7584,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7333,6 +7627,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7413,6 +7709,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7462,6 +7760,8 @@ 'min_temp': 7, 'target_temp_step': 1.0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7514,6 +7814,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7563,6 +7865,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7607,6 +7911,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7650,6 +7956,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7723,6 +8031,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7798,6 +8108,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7869,6 +8181,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7912,6 +8226,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7958,6 +8274,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8036,6 +8354,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8075,6 +8395,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8117,6 +8439,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8191,6 +8515,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8262,6 +8588,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8301,6 +8629,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8343,6 +8673,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8421,6 +8753,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8462,6 +8796,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8537,6 +8873,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8608,6 +8946,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8649,6 +8989,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8730,6 +9072,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8801,6 +9145,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8842,6 +9188,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8923,6 +9271,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8976,6 +9326,8 @@ ]), 'target_temp_step': 1.0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9033,6 +9385,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9082,6 +9436,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9126,6 +9482,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9169,6 +9527,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9242,6 +9602,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9317,6 +9679,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9388,6 +9752,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9434,6 +9800,8 @@ 'max_humidity': 100, 'min_humidity': 0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9485,6 +9853,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9562,6 +9932,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9633,6 +10005,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9679,6 +10053,8 @@ 'max_humidity': 80, 'min_humidity': 20, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9730,6 +10106,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9807,6 +10185,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9878,6 +10258,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9926,6 +10308,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9982,6 +10366,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10060,6 +10446,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10115,6 +10503,8 @@ 'min_temp': 18, 'target_temp_step': 0.5, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10175,6 +10565,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10252,6 +10644,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10299,6 +10693,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10384,6 +10780,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10431,6 +10829,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10516,6 +10916,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10563,6 +10965,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10648,6 +11052,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10695,6 +11101,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10780,6 +11188,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10827,6 +11237,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10922,6 +11334,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10969,6 +11383,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11064,6 +11480,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11107,6 +11525,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11154,6 +11574,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11201,6 +11623,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11248,6 +11672,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11293,6 +11719,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11367,6 +11795,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11410,6 +11840,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11486,6 +11918,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11529,6 +11963,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11605,6 +12041,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11648,6 +12086,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11724,6 +12164,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11767,6 +12209,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11843,6 +12287,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11886,6 +12332,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11962,6 +12410,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12005,6 +12455,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12081,6 +12533,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12124,6 +12578,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12200,6 +12656,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12275,6 +12733,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12323,6 +12783,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12413,6 +12875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12454,6 +12918,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12495,6 +12961,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12570,6 +13038,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12611,6 +13081,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12652,6 +13124,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12690,6 +13164,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12764,6 +13240,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12812,6 +13290,8 @@ 'max_temp': 37, 'min_temp': 4.5, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12870,6 +13350,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12914,6 +13396,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12957,6 +13441,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13034,6 +13520,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13083,6 +13571,8 @@ 'HDMI 4', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13133,6 +13623,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13207,6 +13699,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13248,6 +13742,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13323,6 +13819,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13398,6 +13896,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13437,6 +13937,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13475,6 +13977,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13513,6 +14017,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13551,6 +14057,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13589,6 +14097,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13663,6 +14173,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13706,6 +14218,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13786,6 +14300,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13834,6 +14350,8 @@ 'max_temp': 35, 'min_temp': 7, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13889,6 +14407,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13938,6 +14458,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13982,6 +14504,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14025,6 +14549,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14102,6 +14628,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14150,6 +14678,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14223,6 +14753,8 @@ 'sleepy', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14280,6 +14812,8 @@ 'router', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14364,6 +14898,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14403,6 +14939,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14442,6 +14980,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14487,6 +15027,8 @@ 'long_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14532,6 +15074,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14570,6 +15114,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14644,6 +15190,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14683,6 +15231,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14722,6 +15272,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14797,6 +15349,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14838,6 +15392,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14880,6 +15436,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14923,6 +15481,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14966,6 +15526,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15009,6 +15571,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15086,6 +15650,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15125,6 +15691,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15166,6 +15734,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15207,6 +15777,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15248,6 +15820,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15289,6 +15863,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15330,6 +15906,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15371,6 +15949,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15412,6 +15992,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15489,6 +16071,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15528,6 +16112,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15570,6 +16156,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15644,6 +16232,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15715,6 +16305,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15754,6 +16346,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15796,6 +16390,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15874,6 +16470,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15913,6 +16511,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15955,6 +16555,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16029,6 +16631,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16068,6 +16672,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16110,6 +16716,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16184,6 +16792,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16223,6 +16833,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16265,6 +16877,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16339,6 +16953,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16410,6 +17026,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16449,6 +17067,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16491,6 +17111,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16569,6 +17191,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16608,6 +17232,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16683,6 +17309,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16724,6 +17352,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16772,6 +17402,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16852,6 +17484,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16923,6 +17557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16964,6 +17600,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17007,6 +17645,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17050,6 +17690,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17123,6 +17765,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17162,6 +17806,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17239,6 +17885,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17285,6 +17933,8 @@ 'max_humidity': 100, 'min_humidity': 0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17343,6 +17993,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17412,6 +18064,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17456,6 +18110,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17533,6 +18189,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17574,6 +18232,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17615,6 +18275,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, diff --git a/tests/conftest.py b/tests/conftest.py index 40bd4157957f0..1f8ecc407d7ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,7 @@ from homeassistant.core import CoreState, HassJob, HomeAssistant from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, config_entry_oauth2_flow, device_registry as dr, entity_registry as er, @@ -1618,6 +1619,12 @@ def mock_bluetooth( """Mock out bluetooth from starting.""" +@pytest.fixture +def category_registry(hass: HomeAssistant) -> cr.CategoryRegistry: + """Return the category registry from the current hass instance.""" + return cr.async_get(hass) + + @pytest.fixture def area_registry(hass: HomeAssistant) -> ar.AreaRegistry: """Return the area registry from the current hass instance.""" diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py new file mode 100644 index 0000000000000..d204ec21e39b0 --- /dev/null +++ b/tests/helpers/test_category_registry.py @@ -0,0 +1,395 @@ +"""Tests for the category registry.""" +import re +from typing import Any + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import category_registry as cr + +from tests.common import async_capture_events, flush_store + + +async def test_list_categories_for_scope( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can read categories for scope.""" + categories = category_registry.async_list_categories(scope="automation") + assert len(list(categories)) == len( + category_registry.categories.get("automation", {}) + ) + + +async def test_create_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can create new categories.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + assert category.category_id + assert category.name == "Energy saving" + assert category.icon == "mdi:leaf" + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await hass.async_block_till_done() + + assert len(update_events) == 1 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_create_category_with_name_already_in_use( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can't create a category with the same name within a scope.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + with pytest.raises( + ValueError, + match=re.escape("The name 'ENERGY SAVING' is already in use"), + ): + category_registry.async_create( + scope="automation", + name="ENERGY SAVING", + icon="mdi:leaf", + ) + + await hass.async_block_till_done() + + assert len(category_registry.categories["automation"]) == 1 + assert len(update_events) == 1 + + +async def test_create_category_with_duplicate_name_in_other_scopes( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make we can create the same category in multiple scopes.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category_registry.async_create( + scope="script", + name="Energy saving", + icon="mdi:leaf", + ) + + await hass.async_block_till_done() + + assert len(category_registry.categories["script"]) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert len(update_events) == 2 + + +async def test_delete_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can delete a category.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + assert len(category_registry.categories["automation"]) == 1 + + category_registry.async_delete(scope="automation", category_id=category.category_id) + + assert not category_registry.categories["automation"] + + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + assert update_events[1].data == { + "action": "remove", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_delete_non_existing_category( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can't delete a category that doesn't exist.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + with pytest.raises(KeyError): + category_registry.async_delete(scope="automation", category_id="") + + with pytest.raises(KeyError): + category_registry.async_delete(scope="", category_id=category.category_id) + + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can update categories.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + ) + + assert len(category_registry.categories["automation"]) == 1 + assert category.category_id + assert category.name == "Energy saving" + assert category.icon is None + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="ENERGY SAVING", + icon="mdi:leaf", + ) + + assert updated_category != category + assert updated_category.category_id == category.category_id + assert updated_category.name == "ENERGY SAVING" + assert updated_category.icon == "mdi:leaf" + + assert len(category_registry.categories["automation"]) == 1 + + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + assert update_events[1].data == { + "action": "update", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_update_category_with_same_data( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can reapply the same data to a category and it won't update.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="Energy saving", + icon="mdi:leaf", + ) + assert category == updated_category + + await hass.async_block_till_done() + + # No update event + assert len(update_events) == 1 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_update_category_with_same_name_change_case( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can reapply the same name with a different case to a category.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="ENERGY SAVING", + ) + + assert updated_category.category_id == category.category_id + assert updated_category.name == "ENERGY SAVING" + assert updated_category.icon == "mdi:leaf" + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category_with_name_already_in_use( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can't update a category with a name already in use.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:leaf", + ) + + with pytest.raises( + ValueError, + match=re.escape("The name 'ENERGY SAVING' is already in use"), + ): + category_registry.async_update( + scope="automation", + category_id=category2.category_id, + name="ENERGY SAVING", + ) + + assert category1.name == "Energy saving" + assert category2.name == "Something else" + assert len(category_registry.categories["automation"]) == 2 + + +async def test_load_categories( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can load/save data correctly.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:leaf", + ) + category3 = category_registry.async_create( + scope="zone", + name="Grocery stores", + icon="mdi:store", + ) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + registry2 = cr.CategoryRegistry(hass) + await flush_store(category_registry._store) + await registry2.async_load() + + assert len(registry2.categories) == 2 + assert len(registry2.categories["automation"]) == 2 + assert len(registry2.categories["zone"]) == 1 + assert list(category_registry.categories) == list(registry2.categories) + assert list(category_registry.categories["automation"]) == list( + registry2.categories["automation"] + ) + assert list(category_registry.categories["zone"]) == list( + registry2.categories["zone"] + ) + + category1_registry2 = registry2.async_get_category( + scope="automation", category_id=category1.category_id + ) + assert category1_registry2.category_id == category1.category_id + assert category1_registry2.name == category1.name + assert category1_registry2.icon == category1.icon + + category2_registry2 = registry2.async_get_category( + scope="automation", category_id=category2.category_id + ) + assert category2_registry2.category_id == category2.category_id + assert category2_registry2.name == category2.name + assert category2_registry2.icon == category2.icon + + category3_registry2 = registry2.async_get_category( + scope="zone", category_id=category3.category_id + ) + assert category3_registry2.category_id == category3.category_id + assert category3_registry2.name == category3.name + assert category3_registry2.icon == category3.icon + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_loading_categories_from_storage( + hass: HomeAssistant, hass_storage: Any +) -> None: + """Test loading stored categories on start.""" + hass_storage[cr.STORAGE_KEY] = { + "version": cr.STORAGE_VERSION_MAJOR, + "data": { + "categories": { + "automation": [ + { + "category_id": "uuid1", + "name": "Energy saving", + "icon": "mdi:leaf", + }, + { + "category_id": "uuid2", + "name": "Something else", + "icon": None, + }, + ], + "zone": [ + { + "category_id": "uuid3", + "name": "Grocery stores", + "icon": "mdi:store", + }, + ], + } + }, + } + + await cr.async_load(hass) + category_registry = cr.async_get(hass) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + category1 = category_registry.async_get_category( + scope="automation", category_id="uuid1" + ) + assert category1.category_id == "uuid1" + assert category1.name == "Energy saving" + assert category1.icon == "mdi:leaf" + + category2 = category_registry.async_get_category( + scope="automation", category_id="uuid2" + ) + assert category2.category_id == "uuid2" + assert category2.name == "Something else" + assert category2.icon is None + + category3 = category_registry.async_get_category(scope="zone", category_id="uuid3") + assert category3.category_id == "uuid3" + assert category3.name == "Grocery stores" + assert category3.icon == "mdi:store" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 15703af810330..b029933ebbe7e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -280,7 +280,9 @@ async def test_loading_saving_data( orig_entry2.entity_id, "light", {"minimum_brightness": 20} ) entity_registry.async_update_entity( - orig_entry2.entity_id, labels={"label1", "label2"} + 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") @@ -310,6 +312,7 @@ async def test_loading_saving_data( assert orig_entry4 == 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" @@ -1847,3 +1850,76 @@ async def test_entries_for_label(entity_registry: er.EntityRegistry) -> None: 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", "") diff --git a/tests/syrupy.py b/tests/syrupy.py index 348a618e90e2f..d489724a940dd 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -166,7 +166,7 @@ def _serializable_entity_registry_entry( cls, data: er.RegistryEntry ) -> SerializableData: """Prepare a Home Assistant entity registry entry for serialization.""" - return EntityRegistryEntrySnapshot( + serialized = EntityRegistryEntrySnapshot( attrs.asdict(data) | { "config_entry_id": ANY, @@ -175,6 +175,8 @@ def _serializable_entity_registry_entry( "options": {k: dict(v) for k, v in data.options.items()}, } ) + serialized.pop("categories") + return serialized @classmethod def _serializable_flow_result(cls, data: FlowResult) -> SerializableData: