Add aliases to area registry items (#84294)

* Add aliases to area registry items

* Update test

* Fix WS API
This commit is contained in:
Erik Montnemery 2022-12-21 08:44:44 +01:00 committed by GitHub
parent 556c355e9c
commit 1a42bd5c4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 181 additions and 44 deletions

View File

@ -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,
}

View File

@ -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()
]

View File

@ -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

View File

@ -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}]
},
}