mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add category registry (#110897)
* Add category registry * Add entity registry support * Update homeassistant/components/config/entity_registry.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Use ulid instead * Add tests for adding same name in different scopes * Handle keyerror on update * Lookup tweak * Omit categories from entity registry snapshots * Use base registry * Update snapshots * Update snapshots --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
436c83e8a7
commit
0e2775667d
@ -62,6 +62,7 @@ from .const import (
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
@ -342,6 +343,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> 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)),
|
||||
|
@ -14,6 +14,7 @@ from . import (
|
||||
auth,
|
||||
auth_provider_homeassistant,
|
||||
automation,
|
||||
category_registry,
|
||||
config_entries,
|
||||
core,
|
||||
device_registry,
|
||||
@ -30,6 +31,7 @@ SECTIONS = (
|
||||
auth,
|
||||
auth_provider_homeassistant,
|
||||
automation,
|
||||
category_registry,
|
||||
config_entries,
|
||||
core,
|
||||
device_registry,
|
||||
|
134
homeassistant/components/config/category_registry.py
Normal file
134
homeassistant/components/config/category_registry.py
Normal file
@ -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,
|
||||
}
|
@ -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)
|
||||
|
208
homeassistant/helpers/category_registry.py
Normal file
208
homeassistant/helpers/category_registry.py
Normal file
@ -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()
|
@ -67,7 +67,7 @@ EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated"
|
||||
_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 @@ class RegistryEntry:
|
||||
# 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 @@ class RegistryEntry:
|
||||
{
|
||||
"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 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
*,
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
*,
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
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 @@ class EntityRegistry(BaseRegistry):
|
||||
],
|
||||
}
|
||||
|
||||
@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 _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> 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."""
|
||||
|
@ -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"
|
||||
|
@ -59,6 +59,7 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
category_registry as cr,
|
||||
device_registry as dr,
|
||||
entity,
|
||||
entity_platform,
|
||||
@ -334,6 +335,7 @@ async def async_test_home_assistant(
|
||||
"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)
|
||||
|
380
tests/components/config/test_category_registry.py
Normal file
380
tests/components/config/test_category_registry.py
Normal file
@ -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
|
@ -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 @@ async def test_list_entities(
|
||||
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,
|
||||
|
@ -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',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -53,6 +53,7 @@ from homeassistant.const import HASSIO_USER_NAME
|
||||
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."""
|
||||
|
395
tests/helpers/test_category_registry.py
Normal file
395
tests/helpers/test_category_registry.py
Normal file
@ -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"
|
@ -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", "")
|
||||
|
@ -166,7 +166,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
||||
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 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
||||
"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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user