mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add aliases to area registry items (#84294)
* Add aliases to area registry items * Update test * Fix WS API
This commit is contained in:
parent
556c355e9c
commit
1a42bd5c4c
@ -35,6 +35,7 @@ def websocket_list_areas(
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/area_registry/create",
|
||||
vol.Optional("aliases"): list,
|
||||
vol.Required("name"): str,
|
||||
vol.Optional("picture"): vol.Any(str, None),
|
||||
}
|
||||
@ -53,6 +54,10 @@ def websocket_create_area(
|
||||
data.pop("type")
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
try:
|
||||
entry = registry.async_create(**data)
|
||||
except ValueError as err:
|
||||
@ -88,6 +93,7 @@ def websocket_delete_area(
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/area_registry/update",
|
||||
vol.Optional("aliases"): list,
|
||||
vol.Required("area_id"): str,
|
||||
vol.Optional("name"): str,
|
||||
vol.Optional("picture"): vol.Any(str, None),
|
||||
@ -107,6 +113,10 @@ def websocket_update_area(
|
||||
data.pop("type")
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
try:
|
||||
entry = registry.async_update(**data)
|
||||
except ValueError as err:
|
||||
@ -118,4 +128,9 @@ def websocket_update_area(
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Convert entry to API format."""
|
||||
return {"area_id": entry.id, "name": entry.name, "picture": entry.picture}
|
||||
return {
|
||||
"aliases": entry.aliases,
|
||||
"area_id": entry.id,
|
||||
"name": entry.name,
|
||||
"picture": entry.picture,
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Container, Iterable, MutableMapping
|
||||
from typing import Any, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import attr
|
||||
|
||||
@ -20,7 +20,7 @@ DATA_REGISTRY = "area_registry"
|
||||
EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated"
|
||||
STORAGE_KEY = "core.area_registry"
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 2
|
||||
STORAGE_VERSION_MINOR = 3
|
||||
SAVE_DELAY = 10
|
||||
|
||||
|
||||
@ -30,8 +30,11 @@ class AreaEntry:
|
||||
|
||||
name: str = attr.ib()
|
||||
normalized_name: str = attr.ib()
|
||||
picture: str | None = attr.ib(default=None)
|
||||
aliases: set[str] = attr.ib(
|
||||
converter=attr.converters.default_if_none(factory=set) # type: ignore[misc]
|
||||
)
|
||||
id: str | None = attr.ib(default=None)
|
||||
picture: str | None = attr.ib(default=None)
|
||||
|
||||
def generate_id(self, existing_ids: Container[str]) -> None:
|
||||
"""Initialize ID."""
|
||||
@ -43,14 +46,14 @@ class AreaEntry:
|
||||
object.__setattr__(self, "id", suggestion)
|
||||
|
||||
|
||||
class AreaRegistryStore(Store[dict[str, list[dict[str, Optional[str]]]]]):
|
||||
class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]):
|
||||
"""Store area registry data."""
|
||||
|
||||
async def _async_migrate_func(
|
||||
self,
|
||||
old_major_version: int,
|
||||
old_minor_version: int,
|
||||
old_data: dict[str, list[dict[str, str | None]]],
|
||||
old_data: dict[str, list[dict[str, Any]]],
|
||||
) -> dict[str, Any]:
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version < 2:
|
||||
@ -60,6 +63,11 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Optional[str]]]]]):
|
||||
# Populate keys which were introduced before version 1.2
|
||||
area.setdefault("picture", None)
|
||||
|
||||
if old_minor_version < 3:
|
||||
# Version 1.3 adds aliases
|
||||
for area in old_data["areas"]:
|
||||
area["aliases"] = []
|
||||
|
||||
if old_major_version > 1:
|
||||
raise NotImplementedError
|
||||
return old_data
|
||||
@ -107,14 +115,22 @@ class AreaRegistry:
|
||||
return self.async_create(name)
|
||||
|
||||
@callback
|
||||
def async_create(self, name: str, picture: str | None = None) -> AreaEntry:
|
||||
def async_create(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
aliases: set[str] | None = None,
|
||||
picture: str | None = None,
|
||||
) -> AreaEntry:
|
||||
"""Create a new area."""
|
||||
normalized_name = normalize_area_name(name)
|
||||
|
||||
if self.async_get_area_by_name(name):
|
||||
raise ValueError(f"The name {name} ({normalized_name}) is already in use")
|
||||
|
||||
area = AreaEntry(name=name, normalized_name=normalized_name, picture=picture)
|
||||
area = AreaEntry(
|
||||
aliases=aliases, name=name, normalized_name=normalized_name, picture=picture
|
||||
)
|
||||
area.generate_id(self.areas)
|
||||
assert area.id is not None
|
||||
self.areas[area.id] = area
|
||||
@ -147,11 +163,15 @@ class AreaRegistry:
|
||||
def async_update(
|
||||
self,
|
||||
area_id: str,
|
||||
*,
|
||||
aliases: set[str] | UndefinedType = UNDEFINED,
|
||||
name: str | UndefinedType = UNDEFINED,
|
||||
picture: str | None | UndefinedType = UNDEFINED,
|
||||
) -> AreaEntry:
|
||||
"""Update name of area."""
|
||||
updated = self._async_update(area_id, name=name, picture=picture)
|
||||
updated = self._async_update(
|
||||
area_id, aliases=aliases, name=name, picture=picture
|
||||
)
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_AREA_REGISTRY_UPDATED, {"action": "update", "area_id": area_id}
|
||||
)
|
||||
@ -161,16 +181,22 @@ class AreaRegistry:
|
||||
def _async_update(
|
||||
self,
|
||||
area_id: str,
|
||||
*,
|
||||
aliases: set[str] | UndefinedType = UNDEFINED,
|
||||
name: str | UndefinedType = UNDEFINED,
|
||||
picture: str | None | UndefinedType = UNDEFINED,
|
||||
) -> AreaEntry:
|
||||
"""Update name of area."""
|
||||
old = self.areas[area_id]
|
||||
|
||||
changes = {}
|
||||
new_values = {}
|
||||
|
||||
if picture is not UNDEFINED:
|
||||
changes["picture"] = picture
|
||||
for attr_name, value in (
|
||||
("aliases", aliases),
|
||||
("picture", picture),
|
||||
):
|
||||
if value is not UNDEFINED and value != getattr(old, attr_name):
|
||||
new_values[attr_name] = value
|
||||
|
||||
normalized_name = None
|
||||
|
||||
@ -184,13 +210,13 @@ class AreaRegistry:
|
||||
f"The name {name} ({normalized_name}) is already in use"
|
||||
)
|
||||
|
||||
changes["name"] = name
|
||||
changes["normalized_name"] = normalized_name
|
||||
new_values["name"] = name
|
||||
new_values["normalized_name"] = normalized_name
|
||||
|
||||
if not changes:
|
||||
if not new_values:
|
||||
return old
|
||||
|
||||
new = self.areas[area_id] = attr.evolve(old, **changes)
|
||||
new = self.areas[area_id] = attr.evolve(old, **new_values)
|
||||
if normalized_name is not None:
|
||||
self._normalized_name_area_idx[
|
||||
normalized_name
|
||||
@ -210,6 +236,7 @@ class AreaRegistry:
|
||||
assert area["name"] is not None and area["id"] is not None
|
||||
normalized_name = normalize_area_name(area["name"])
|
||||
areas[area["id"]] = AreaEntry(
|
||||
aliases=set(area["aliases"]),
|
||||
id=area["id"],
|
||||
name=area["name"],
|
||||
normalized_name=normalized_name,
|
||||
@ -225,12 +252,17 @@ class AreaRegistry:
|
||||
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]:
|
||||
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return data of area registry to store in a file."""
|
||||
data = {}
|
||||
|
||||
data["areas"] = [
|
||||
{"name": entry.name, "id": entry.id, "picture": entry.picture}
|
||||
{
|
||||
"aliases": list(entry.aliases),
|
||||
"name": entry.name,
|
||||
"id": entry.id,
|
||||
"picture": entry.picture,
|
||||
}
|
||||
for entry in self.areas.values()
|
||||
]
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""Test area_registry API."""
|
||||
import pytest
|
||||
from pytest_unordered import unordered
|
||||
|
||||
from homeassistant.components.config import area_registry
|
||||
|
||||
from tests.common import mock_area_registry
|
||||
from tests.common import ANY, mock_area_registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -21,31 +22,68 @@ def registry(hass):
|
||||
|
||||
async def test_list_areas(hass, client, registry):
|
||||
"""Test list entries."""
|
||||
registry.async_create("mock 1")
|
||||
registry.async_create("mock 2", "/image/example.png")
|
||||
area1 = registry.async_create("mock 1")
|
||||
area2 = registry.async_create(
|
||||
"mock 2", aliases={"alias_1", "alias_2"}, picture="/image/example.png"
|
||||
)
|
||||
|
||||
await client.send_json({"id": 1, "type": "config/area_registry/list"})
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert len(msg["result"]) == len(registry.areas)
|
||||
assert msg["result"][0]["name"] == "mock 1"
|
||||
assert msg["result"][0]["picture"] is None
|
||||
assert msg["result"][1]["name"] == "mock 2"
|
||||
assert msg["result"][1]["picture"] == "/image/example.png"
|
||||
assert msg["result"] == [
|
||||
{
|
||||
"aliases": [],
|
||||
"area_id": area1.id,
|
||||
"name": "mock 1",
|
||||
"picture": None,
|
||||
},
|
||||
{
|
||||
"aliases": unordered(["alias_1", "alias_2"]),
|
||||
"area_id": area2.id,
|
||||
"name": "mock 2",
|
||||
"picture": "/image/example.png",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_create_area(hass, client, registry):
|
||||
"""Test create entry."""
|
||||
# Create area with only mandatory parameters
|
||||
await client.send_json(
|
||||
{"id": 1, "name": "mock", "type": "config/area_registry/create"}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert "mock" in msg["result"]["name"]
|
||||
assert msg["result"] == {
|
||||
"aliases": [],
|
||||
"area_id": ANY,
|
||||
"name": "mock",
|
||||
"picture": None,
|
||||
}
|
||||
assert len(registry.areas) == 1
|
||||
|
||||
# Create area with all parameters
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"aliases": ["alias_1", "alias_2"],
|
||||
"name": "mock 2",
|
||||
"picture": "/image/example.png",
|
||||
"type": "config/area_registry/create",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["result"] == {
|
||||
"aliases": unordered(["alias_1", "alias_2"]),
|
||||
"area_id": ANY,
|
||||
"name": "mock 2",
|
||||
"picture": "/image/example.png",
|
||||
}
|
||||
assert len(registry.areas) == 2
|
||||
|
||||
|
||||
async def test_create_area_with_name_already_in_use(hass, client, registry):
|
||||
"""Test create entry that should fail."""
|
||||
@ -100,6 +138,7 @@ async def test_update_area(hass, client, registry):
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"aliases": ["alias_1", "alias_2"],
|
||||
"area_id": area.id,
|
||||
"name": "mock 2",
|
||||
"picture": "/image/example.png",
|
||||
@ -109,14 +148,18 @@ async def test_update_area(hass, client, registry):
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["result"]["area_id"] == area.id
|
||||
assert msg["result"]["name"] == "mock 2"
|
||||
assert msg["result"]["picture"] == "/image/example.png"
|
||||
assert msg["result"] == {
|
||||
"aliases": unordered(["alias_1", "alias_2"]),
|
||||
"area_id": area.id,
|
||||
"name": "mock 2",
|
||||
"picture": "/image/example.png",
|
||||
}
|
||||
assert len(registry.areas) == 1
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"aliases": ["alias_1", "alias_1"],
|
||||
"area_id": area.id,
|
||||
"picture": None,
|
||||
"type": "config/area_registry/update",
|
||||
@ -125,9 +168,12 @@ async def test_update_area(hass, client, registry):
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["result"]["area_id"] == area.id
|
||||
assert msg["result"]["name"] == "mock 2"
|
||||
assert msg["result"]["picture"] is None
|
||||
assert msg["result"] == {
|
||||
"aliases": ["alias_1"],
|
||||
"area_id": area.id,
|
||||
"name": "mock 2",
|
||||
"picture": None,
|
||||
}
|
||||
assert len(registry.areas) == 1
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@ import pytest
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import area_registry
|
||||
|
||||
from tests.common import flush_store, mock_area_registry
|
||||
from tests.common import ANY, flush_store, mock_area_registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -38,17 +38,39 @@ async def test_list_areas(registry):
|
||||
|
||||
async def test_create_area(hass, registry, update_events):
|
||||
"""Make sure that we can create an area."""
|
||||
# Create area with only mandatory parameters
|
||||
area = registry.async_create("mock")
|
||||
|
||||
assert area.id == "mock"
|
||||
assert area.name == "mock"
|
||||
assert area == area_registry.AreaEntry(
|
||||
name="mock", normalized_name=ANY, aliases=set(), id=ANY, picture=None
|
||||
)
|
||||
assert len(registry.areas) == 1
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(update_events) == 1
|
||||
assert update_events[0]["action"] == "create"
|
||||
assert update_events[0]["area_id"] == area.id
|
||||
assert update_events[-1]["action"] == "create"
|
||||
assert update_events[-1]["area_id"] == area.id
|
||||
|
||||
# Create area with all parameters
|
||||
area = registry.async_create(
|
||||
"mock 2", aliases={"alias_1", "alias_2"}, picture="/image/example.png"
|
||||
)
|
||||
|
||||
assert area == area_registry.AreaEntry(
|
||||
name="mock 2",
|
||||
normalized_name=ANY,
|
||||
aliases={"alias_1", "alias_2"},
|
||||
id=ANY,
|
||||
picture="/image/example.png",
|
||||
)
|
||||
assert len(registry.areas) == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(update_events) == 2
|
||||
assert update_events[-1]["action"] == "create"
|
||||
assert update_events[-1]["area_id"] == area.id
|
||||
|
||||
|
||||
async def test_create_area_with_name_already_in_use(hass, registry, update_events):
|
||||
@ -70,7 +92,7 @@ async def test_create_area_with_id_already_in_use(registry):
|
||||
"""Make sure that we can't create an area with a name already in use."""
|
||||
area1 = registry.async_create("mock")
|
||||
|
||||
updated_area1 = registry.async_update(area1.id, "New Name")
|
||||
updated_area1 = registry.async_update(area1.id, name="New Name")
|
||||
assert updated_area1.id == area1.id
|
||||
|
||||
area2 = registry.async_create("mock")
|
||||
@ -108,10 +130,21 @@ async def test_update_area(hass, registry, update_events):
|
||||
"""Make sure that we can read areas."""
|
||||
area = registry.async_create("mock")
|
||||
|
||||
updated_area = registry.async_update(area.id, name="mock1")
|
||||
updated_area = registry.async_update(
|
||||
area.id,
|
||||
aliases={"alias_1", "alias_2"},
|
||||
name="mock1",
|
||||
picture="/image/example.png",
|
||||
)
|
||||
|
||||
assert updated_area != area
|
||||
assert updated_area.name == "mock1"
|
||||
assert updated_area == area_registry.AreaEntry(
|
||||
name="mock1",
|
||||
normalized_name=ANY,
|
||||
aliases={"alias_1", "alias_2"},
|
||||
id=ANY,
|
||||
picture="/image/example.png",
|
||||
)
|
||||
assert len(registry.areas) == 1
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@ -198,7 +231,16 @@ async def test_loading_area_from_storage(hass, hass_storage):
|
||||
hass_storage[area_registry.STORAGE_KEY] = {
|
||||
"version": area_registry.STORAGE_VERSION_MAJOR,
|
||||
"minor_version": area_registry.STORAGE_VERSION_MINOR,
|
||||
"data": {"areas": [{"id": "12345A", "name": "mock", "picture": "blah"}]},
|
||||
"data": {
|
||||
"areas": [
|
||||
{
|
||||
"aliases": ["alias_1", "alias_2"],
|
||||
"id": "12345A",
|
||||
"name": "mock",
|
||||
"picture": "blah",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
await area_registry.async_load(hass)
|
||||
@ -228,7 +270,9 @@ async def test_migration_from_1_1(hass, hass_storage):
|
||||
"version": area_registry.STORAGE_VERSION_MAJOR,
|
||||
"minor_version": area_registry.STORAGE_VERSION_MINOR,
|
||||
"key": area_registry.STORAGE_KEY,
|
||||
"data": {"areas": [{"id": "12345A", "name": "mock", "picture": None}]},
|
||||
"data": {
|
||||
"areas": [{"aliases": [], "id": "12345A", "name": "mock", "picture": None}]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user