Add created_at/modified_at to floor registry (#122071)

This commit is contained in:
Robert Resch 2024-07-17 13:18:26 +02:00 committed by GitHub
parent 385f5be7e8
commit a0f91d27a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 23 deletions

View File

@ -5,10 +5,12 @@ from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
import dataclasses import dataclasses
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal, TypedDict from datetime import datetime
from typing import Any, Literal, TypedDict
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.dt import utc_from_timestamp, utcnow
from homeassistant.util.event_type import EventType from homeassistant.util.event_type import EventType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
@ -28,6 +30,7 @@ EVENT_FLOOR_REGISTRY_UPDATED: EventType[EventFloorRegistryUpdatedData] = EventTy
) )
STORAGE_KEY = "core.floor_registry" STORAGE_KEY = "core.floor_registry"
STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 2
class _FloorStoreData(TypedDict): class _FloorStoreData(TypedDict):
@ -38,6 +41,8 @@ class _FloorStoreData(TypedDict):
icon: str | None icon: str | None
level: int | None level: int | None
name: str name: str
created_at: datetime
modified_at: datetime
class FloorRegistryStoreData(TypedDict): class FloorRegistryStoreData(TypedDict):
@ -66,6 +71,28 @@ class FloorEntry(NormalizedNameBaseRegistryEntry):
level: int | None = None level: int | None = None
class FloorRegistryStore(Store[FloorRegistryStoreData]):
"""Store floor registry data."""
async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict[str, list[dict[str, Any]]],
) -> FloorRegistryStoreData:
"""Migrate to the new version."""
if old_major_version > STORAGE_VERSION_MAJOR:
raise ValueError("Can't migrate to future version")
if old_major_version == 1:
if old_minor_version < 2:
# Version 1.2 implements migration and adds created_at and modified_at
for floor in old_data["floors"]:
floor["created_at"] = floor["modified_at"] = utc_from_timestamp(0)
return old_data # type: ignore[return-value]
class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
"""Class to hold a registry of floors.""" """Class to hold a registry of floors."""
@ -75,11 +102,12 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the floor registry.""" """Initialize the floor registry."""
self.hass = hass self.hass = hass
self._store = Store( self._store = FloorRegistryStore(
hass, hass,
STORAGE_VERSION_MAJOR, STORAGE_VERSION_MAJOR,
STORAGE_KEY, STORAGE_KEY,
atomic_writes=True, atomic_writes=True,
minor_version=STORAGE_VERSION_MINOR,
) )
@callback @callback
@ -175,7 +203,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
) -> FloorEntry: ) -> FloorEntry:
"""Update name of the floor.""" """Update name of the floor."""
old = self.floors[floor_id] old = self.floors[floor_id]
changes = { changes: dict[str, Any] = {
attr_name: value attr_name: value
for attr_name, value in ( for attr_name, value in (
("aliases", aliases), ("aliases", aliases),
@ -191,8 +219,10 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
if not changes: if not changes:
return old return old
changes["modified_at"] = utcnow()
self.hass.verify_event_loop_thread("floor_registry.async_update") self.hass.verify_event_loop_thread("floor_registry.async_update")
new = self.floors[floor_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] new = self.floors[floor_id] = dataclasses.replace(old, **changes)
self.async_schedule_save() self.async_schedule_save()
self.hass.bus.async_fire_internal( self.hass.bus.async_fire_internal(
@ -220,6 +250,8 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
name=floor["name"], name=floor["name"],
level=floor["level"], level=floor["level"],
normalized_name=normalized_name, normalized_name=normalized_name,
created_at=floor["created_at"],
modified_at=floor["modified_at"],
) )
self.floors = floors self.floors = floors
@ -236,6 +268,8 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
"icon": entry.icon, "icon": entry.icon,
"level": entry.level, "level": entry.level,
"name": entry.name, "name": entry.name,
"created_at": entry.created_at,
"modified_at": entry.modified_at,
} }
for entry in self.floors.values() for entry in self.floors.values()
] ]

View File

@ -1,15 +1,18 @@
"""Tests for the floor registry.""" """Tests for the floor registry."""
from datetime import datetime
from functools import partial from functools import partial
import re import re
from typing import Any from typing import Any
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import area_registry as ar, floor_registry as fr from homeassistant.helpers import area_registry as ar, floor_registry as fr
from homeassistant.util.dt import utcnow
from tests.common import async_capture_events, flush_store from tests.common import ANY, async_capture_events, flush_store
async def test_list_floors(floor_registry: fr.FloorRegistry) -> None: async def test_list_floors(floor_registry: fr.FloorRegistry) -> None:
@ -18,8 +21,10 @@ async def test_list_floors(floor_registry: fr.FloorRegistry) -> None:
assert len(list(floors)) == len(floor_registry.floors) assert len(list(floors)) == len(floor_registry.floors)
@pytest.mark.usefixtures("freezer")
async def test_create_floor( async def test_create_floor(
hass: HomeAssistant, floor_registry: fr.FloorRegistry hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
) -> None: ) -> None:
"""Make sure that we can create floors.""" """Make sure that we can create floors."""
update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED)
@ -30,11 +35,16 @@ async def test_create_floor(
level=1, level=1,
) )
assert floor.floor_id == "first_floor" assert floor == fr.FloorEntry(
assert floor.name == "First floor" floor_id="first_floor",
assert floor.icon == "mdi:home-floor-1" name="First floor",
assert floor.aliases == {"first", "ground", "ground floor"} icon="mdi:home-floor-1",
assert floor.level == 1 aliases={"first", "ground", "ground floor"},
level=1,
created_at=utcnow(),
modified_at=utcnow(),
normalized_name=ANY,
)
assert len(floor_registry.floors) == 1 assert len(floor_registry.floors) == 1
@ -116,18 +126,31 @@ async def test_delete_non_existing_floor(floor_registry: fr.FloorRegistry) -> No
async def test_update_floor( async def test_update_floor(
hass: HomeAssistant, floor_registry: fr.FloorRegistry hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Make sure that we can update floors.""" """Make sure that we can update floors."""
created_at = datetime.fromisoformat("2024-01-01T01:00:00+00:00")
freezer.move_to(created_at)
update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED)
floor = floor_registry.async_create("First floor") floor = floor_registry.async_create("First floor")
assert floor == fr.FloorEntry(
floor_id="first_floor",
name="First floor",
icon=None,
aliases=set(),
level=None,
created_at=created_at,
modified_at=created_at,
normalized_name=ANY,
)
assert len(floor_registry.floors) == 1 assert len(floor_registry.floors) == 1
assert floor.floor_id == "first_floor"
assert floor.name == "First floor" modified_at = datetime.fromisoformat("2024-02-01T01:00:00+00:00")
assert floor.icon is None freezer.move_to(modified_at)
assert floor.aliases == set()
assert floor.level is None
updated_floor = floor_registry.async_update( updated_floor = floor_registry.async_update(
floor.floor_id, floor.floor_id,
@ -138,11 +161,16 @@ async def test_update_floor(
) )
assert updated_floor != floor assert updated_floor != floor
assert updated_floor.floor_id == "first_floor" assert updated_floor == fr.FloorEntry(
assert updated_floor.name == "Second floor" floor_id="first_floor",
assert updated_floor.icon == "mdi:home-floor-2" name="Second floor",
assert updated_floor.aliases == {"ground", "downstairs"} icon="mdi:home-floor-2",
assert updated_floor.level == 2 aliases={"ground", "downstairs"},
level=2,
created_at=created_at,
modified_at=modified_at,
normalized_name=ANY,
)
assert len(floor_registry.floors) == 1 assert len(floor_registry.floors) == 1
@ -280,7 +308,8 @@ async def test_load_floors(
@pytest.mark.parametrize("load_registries", [False]) @pytest.mark.parametrize("load_registries", [False])
async def test_loading_floors_from_storage( async def test_loading_floors_from_storage(
hass: HomeAssistant, hass_storage: dict[str, Any] hass: HomeAssistant,
hass_storage: dict[str, Any],
) -> None: ) -> None:
"""Test loading stored floors on start.""" """Test loading stored floors on start."""
hass_storage[fr.STORAGE_KEY] = { hass_storage[fr.STORAGE_KEY] = {
@ -392,3 +421,52 @@ async def test_async_update_thread_safety(
await hass.async_add_executor_job( await hass.async_add_executor_job(
partial(floor_registry.async_update, any_floor.floor_id, name="new name") partial(floor_registry.async_update, any_floor.floor_id, name="new name")
) )
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_from_1_1(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test migration from version 1.1."""
hass_storage[fr.STORAGE_KEY] = {
"version": 1,
"data": {
"floors": [
{
"floor_id": "12345A",
"name": "mock",
"aliases": [],
"icon": None,
"level": None,
}
]
},
}
await fr.async_load(hass)
registry = fr.async_get(hass)
# Test data was loaded
entry = registry.async_get_floor_by_name("mock")
assert entry.floor_id == "12345A"
# Check we store migrated data
await flush_store(registry._store)
assert hass_storage[fr.STORAGE_KEY] == {
"version": fr.STORAGE_VERSION_MAJOR,
"minor_version": fr.STORAGE_VERSION_MINOR,
"key": fr.STORAGE_KEY,
"data": {
"floors": [
{
"aliases": [],
"icon": None,
"floor_id": "12345A",
"level": None,
"name": "mock",
"created_at": "1970-01-01T00:00:00+00:00",
"modified_at": "1970-01-01T00:00:00+00:00",
}
]
},
}