Skip to content

Commit

Permalink
Add initial label registry foundation
Browse files Browse the repository at this point in the history
Add label support to device registry

Add label support to entity registry

Add support for calling services by labels

Rebase fixes, test fixes, linter fixes, catch up

Ran black

Update test snapshosts

Update snapshots for renault

Register API

Fix entity registry tests

Set up registry in bootstrap

Add labels to partial dict

Add support to Syrupy for test snapshotting

Adjust more tests
  • Loading branch information
frenck committed Nov 3, 2023
1 parent c63e3c3 commit e830c41
Show file tree
Hide file tree
Showing 57 changed files with 2,846 additions and 21 deletions.
2 changes: 2 additions & 0 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
entity,
entity_registry,
issue_registry,
label_registry,
recorder,
template,
)
Expand Down Expand Up @@ -245,6 +246,7 @@ def _cache_uname_processor() -> None:
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
label_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
template.async_load_custom_templates(hass),
)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"core",
"device_registry",
"entity_registry",
"label_registry",
"script",
"scene",
)
Expand Down
128 changes: 128 additions & 0 deletions homeassistant/components/config/label_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Websocket API to interact with the label 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.label_registry import LabelEntry, async_get


async def async_setup(hass: HomeAssistant) -> bool:
"""Register the Label Registry WS commands."""
websocket_api.async_register_command(hass, websocket_list_labels)
websocket_api.async_register_command(hass, websocket_create_label)
websocket_api.async_register_command(hass, websocket_delete_label)
websocket_api.async_register_command(hass, websocket_update_label)
return True


@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/list",
}
)
@callback
def websocket_list_labels(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle list labels command."""
registry = async_get(hass)
connection.send_result(
msg["id"],
[_entry_dict(entry) for entry in registry.async_list_labels()],
)


@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/create",
vol.Required("name"): str,
vol.Optional("color"): vol.Any(str, None),
vol.Optional("description"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
}
)
@websocket_api.require_admin
@callback
def websocket_create_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Create label command."""
registry = async_get(hass)

data = dict(msg)
data.pop("type")
data.pop("id")

try:
entry = 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/label_registry/delete",
vol.Required("label_id"): str,
}
)
@websocket_api.require_admin
@callback
def websocket_delete_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Delete label command."""
registry = async_get(hass)

try:
registry.async_delete(msg["label_id"])
except KeyError:
connection.send_error(msg["id"], "invalid_info", "Label ID doesn't exist")
else:
connection.send_message(websocket_api.result_message(msg["id"], "success"))


@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/update",
vol.Required("label_id"): str,
vol.Optional("color"): vol.Any(str, None),
vol.Optional("description"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
vol.Optional("name"): str,
}
)
@websocket_api.require_admin
@callback
def websocket_update_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle update label websocket command."""
registry = async_get(hass)

data = dict(msg)
data.pop("type")
data.pop("id")

try:
entry = registry.async_update(**data)
except ValueError as err:
connection.send_error(msg["id"], "invalid_info", str(err))
else:
connection.send_result(msg["id"], _entry_dict(entry))


@callback
def _entry_dict(entry: LabelEntry) -> dict[str, Any]:
"""Convert entry to API format."""
return {
"color": entry.color,
"description": entry.description,
"icon": entry.icon,
"label_id": entry.label_id,
"name": entry.name,
}
4 changes: 4 additions & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ class Platform(StrEnum):
# Contains one string, the device ID
ATTR_DEVICE_ID: Final = "device_id"

# Label identifier. Also used as service calls target parameter in which case
# it contains one string or a list of strings, each being an label id.
ATTR_LABEL_ID: Final = "label_id"

# String with a friendly name for the entity
ATTR_FRIENDLY_NAME: Final = "friendly_name"

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/helpers/config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ATTR_AREA_ID,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_LABEL_ID,
CONF_ABOVE,
CONF_ALIAS,
CONF_ATTRIBUTE,
Expand Down Expand Up @@ -1068,6 +1069,9 @@ def expand_condition_shorthand(value: Any | None) -> Any:
vol.Optional(ATTR_AREA_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
vol.Optional(ATTR_LABEL_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
}

TARGET_SERVICE_FIELDS = {
Expand All @@ -1085,6 +1089,9 @@ def expand_condition_shorthand(value: Any | None) -> Any:
vol.Optional(ATTR_AREA_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
vol.Optional(ATTR_LABEL_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
}


Expand Down
30 changes: 28 additions & 2 deletions homeassistant/helpers/device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
STORAGE_KEY = "core.device_registry"
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 3
STORAGE_VERSION_MINOR = 4
SAVE_DELAY = 10
CLEANUP_DELAY = 10

Expand Down Expand Up @@ -80,6 +80,7 @@ class DeviceEntry:
hw_version: str | None = attr.ib(default=None)
id: str = attr.ib(factory=uuid_util.random_uuid_hex)
identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
labels: set[str] = attr.ib(converter=set, factory=set)
manufacturer: str | None = attr.ib(default=None)
model: str | None = attr.ib(default=None)
name_by_user: str | None = attr.ib(default=None)
Expand Down Expand Up @@ -219,6 +220,10 @@ async def _async_migrate_func(
# Version 1.3 adds hw_version
for device in old_data["devices"]:
device["hw_version"] = None
if old_minor_version < 4:
# Introduced in 2022.10
for device in old_data["devices"]:
device["labels"] = device.get("labels", [])

if old_major_version > 1:
raise NotImplementedError
Expand Down Expand Up @@ -432,6 +437,7 @@ def async_update_device(
disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED,
entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED,
hw_version: str | None | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
manufacturer: str | None | UndefinedType = UNDEFINED,
merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED,
merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
Expand Down Expand Up @@ -522,10 +528,11 @@ def async_update_device(
("disabled_by", disabled_by),
("entry_type", entry_type),
("hw_version", hw_version),
("labels", labels),
("manufacturer", manufacturer),
("model", model),
("name", name),
("name_by_user", name_by_user),
("name", name),
("suggested_area", suggested_area),
("sw_version", sw_version),
("via_device_id", via_device_id),
Expand Down Expand Up @@ -615,6 +622,7 @@ async def async_load(self) -> None:
tuple(iden) # type: ignore[misc]
for iden in device["identifiers"]
},
labels=set(device["labels"]),
manufacturer=device["manufacturer"],
model=device["model"],
name_by_user=device["name_by_user"],
Expand Down Expand Up @@ -663,6 +671,7 @@ def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
"hw_version": entry.hw_version,
"id": entry.id,
"identifiers": list(entry.identifiers),
"labels": list(entry.labels),
"manufacturer": entry.manufacturer,
"model": entry.model,
"name_by_user": entry.name_by_user,
Expand Down Expand Up @@ -734,6 +743,15 @@ def async_clear_area_id(self, area_id: str) -> None:
if area_id == device.area_id:
self.async_update_device(dev_id, area_id=None)

@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for device_id, entry in self.devices.items():
if label_id in entry.labels:
labels = entry.labels.copy()
labels.remove(label_id)
self.async_update_device(device_id, labels=labels)


@callback
def async_get(hass: HomeAssistant) -> DeviceRegistry:
Expand All @@ -754,6 +772,14 @@ def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[Devic
return [device for device in registry.devices.values() if device.area_id == area_id]


@callback
def async_entries_for_label(
registry: DeviceRegistry, label_id: str
) -> list[DeviceEntry]:
"""Return entries that match an label."""
return [device for device in registry.devices.values() if label_id in device.labels]


@callback
def async_entries_for_config_entry(
registry: DeviceRegistry, config_entry_id: str
Expand Down
Loading

0 comments on commit e830c41

Please sign in to comment.