mirror of
https://github.com/home-assistant/core.git
synced 2025-05-07 23:49:17 +00:00
Update modified_at datetime on storage collection changes (#125218)
This commit is contained in:
parent
de99dfef4e
commit
122f11c790
@ -7,6 +7,7 @@ import asyncio
|
|||||||
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from hashlib import md5
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
import logging
|
import logging
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
@ -25,6 +26,7 @@ from homeassistant.util import slugify
|
|||||||
from . import entity_registry
|
from . import entity_registry
|
||||||
from .entity import Entity
|
from .entity import Entity
|
||||||
from .entity_component import EntityComponent
|
from .entity_component import EntityComponent
|
||||||
|
from .json import json_bytes
|
||||||
from .storage import Store
|
from .storage import Store
|
||||||
from .typing import ConfigType, VolDictType
|
from .typing import ConfigType, VolDictType
|
||||||
|
|
||||||
@ -50,6 +52,7 @@ class CollectionChange:
|
|||||||
change_type: str
|
change_type: str
|
||||||
item_id: str
|
item_id: str
|
||||||
item: Any
|
item: Any
|
||||||
|
item_hash: str | None = None
|
||||||
|
|
||||||
|
|
||||||
type ChangeListener = Callable[
|
type ChangeListener = Callable[
|
||||||
@ -273,7 +276,9 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection](
|
|||||||
|
|
||||||
await self.notify_changes(
|
await self.notify_changes(
|
||||||
[
|
[
|
||||||
CollectionChange(CHANGE_ADDED, item[CONF_ID], item)
|
CollectionChange(
|
||||||
|
CHANGE_ADDED, item[CONF_ID], item, self._hash_item(item)
|
||||||
|
)
|
||||||
for item in raw_storage["items"]
|
for item in raw_storage["items"]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -313,7 +318,16 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection](
|
|||||||
item = self._create_item(item_id, validated_data)
|
item = self._create_item(item_id, validated_data)
|
||||||
self.data[item_id] = item
|
self.data[item_id] = item
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
await self.notify_changes([CollectionChange(CHANGE_ADDED, item_id, item)])
|
await self.notify_changes(
|
||||||
|
[
|
||||||
|
CollectionChange(
|
||||||
|
CHANGE_ADDED,
|
||||||
|
item_id,
|
||||||
|
item,
|
||||||
|
self._hash_item(self._serialize_item(item_id, item)),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
async def async_update_item(self, item_id: str, updates: dict) -> _ItemT:
|
async def async_update_item(self, item_id: str, updates: dict) -> _ItemT:
|
||||||
@ -331,7 +345,16 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection](
|
|||||||
self.data[item_id] = updated
|
self.data[item_id] = updated
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
await self.notify_changes([CollectionChange(CHANGE_UPDATED, item_id, updated)])
|
await self.notify_changes(
|
||||||
|
[
|
||||||
|
CollectionChange(
|
||||||
|
CHANGE_UPDATED,
|
||||||
|
item_id,
|
||||||
|
updated,
|
||||||
|
self._hash_item(self._serialize_item(item_id, updated)),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return self.data[item_id]
|
return self.data[item_id]
|
||||||
|
|
||||||
@ -365,6 +388,10 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection](
|
|||||||
def _data_to_save(self) -> _StoreT:
|
def _data_to_save(self) -> _StoreT:
|
||||||
"""Return JSON-compatible date for storing to file."""
|
"""Return JSON-compatible date for storing to file."""
|
||||||
|
|
||||||
|
def _hash_item(self, item: dict) -> str:
|
||||||
|
"""Return a hash of the item."""
|
||||||
|
return md5(json_bytes(item)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
class DictStorageCollection(StorageCollection[dict, SerializedStorageCollection]):
|
class DictStorageCollection(StorageCollection[dict, SerializedStorageCollection]):
|
||||||
"""A specialized StorageCollection where the items are untyped dicts."""
|
"""A specialized StorageCollection where the items are untyped dicts."""
|
||||||
@ -464,6 +491,10 @@ class _CollectionLifeCycle(Generic[_EntityT]):
|
|||||||
|
|
||||||
async def _update_entity(self, change_set: CollectionChange) -> None:
|
async def _update_entity(self, change_set: CollectionChange) -> None:
|
||||||
if entity := self.entities.get(change_set.item_id):
|
if entity := self.entities.get(change_set.item_id):
|
||||||
|
if change_set.item_hash:
|
||||||
|
self.ent_reg.async_update_entity_options(
|
||||||
|
entity.entity_id, "collection", {"hash": change_set.item_hash}
|
||||||
|
)
|
||||||
await entity.async_update_config(change_set.item)
|
await entity.async_update_config(change_set.item)
|
||||||
|
|
||||||
async def _collection_changed(self, change_set: Iterable[CollectionChange]) -> None:
|
async def _collection_changed(self, change_set: Iterable[CollectionChange]) -> None:
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ from homeassistant.helpers import (
|
|||||||
storage,
|
storage,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from tests.common import flush_store
|
from tests.common import flush_store
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
@ -254,6 +257,84 @@ async def test_storage_collection(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_storage_collection_update_modifiet_at(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that updating a storage collection will update the modified_at datetime in the entity registry."""
|
||||||
|
|
||||||
|
entities: dict[str, TestEntity] = {}
|
||||||
|
|
||||||
|
class TestEntity(MockEntity):
|
||||||
|
"""Entity that is config based."""
|
||||||
|
|
||||||
|
def __init__(self, config: ConfigType) -> None:
|
||||||
|
"""Initialize entity."""
|
||||||
|
super().__init__(config)
|
||||||
|
self._state = "initial"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_storage(cls, config: ConfigType) -> TestEntity:
|
||||||
|
"""Create instance from storage."""
|
||||||
|
obj = super().from_storage(config)
|
||||||
|
entities[obj.unique_id] = obj
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return state of entity."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def set_state(self, value: str) -> None:
|
||||||
|
"""Set value."""
|
||||||
|
self._state = value
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
store = storage.Store(hass, 1, "test-data")
|
||||||
|
data = {"id": "mock-1", "name": "Mock 1", "data": 1}
|
||||||
|
await store.async_save(
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
data,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
id_manager = collection.IDManager()
|
||||||
|
ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass)
|
||||||
|
await ent_comp.async_setup({})
|
||||||
|
coll = MockStorageCollection(store, id_manager)
|
||||||
|
collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, TestEntity)
|
||||||
|
changes = track_changes(coll)
|
||||||
|
|
||||||
|
await coll.async_load()
|
||||||
|
assert id_manager.has_id("mock-1")
|
||||||
|
assert len(changes) == 1
|
||||||
|
assert changes[0] == (collection.CHANGE_ADDED, "mock-1", data)
|
||||||
|
|
||||||
|
modified_1 = entity_registry.async_get("test.mock_1").modified_at
|
||||||
|
assert modified_1 == utcnow()
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=1))
|
||||||
|
|
||||||
|
updated_item = await coll.async_update_item("mock-1", {"data": 2})
|
||||||
|
assert id_manager.has_id("mock-1")
|
||||||
|
assert updated_item == {"id": "mock-1", "name": "Mock 1", "data": 2}
|
||||||
|
assert len(changes) == 2
|
||||||
|
assert changes[1] == (collection.CHANGE_UPDATED, "mock-1", updated_item)
|
||||||
|
|
||||||
|
modified_2 = entity_registry.async_get("test.mock_1").modified_at
|
||||||
|
assert modified_2 > modified_1
|
||||||
|
assert modified_2 == utcnow()
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=1))
|
||||||
|
|
||||||
|
entities["mock-1"].set_state("second")
|
||||||
|
|
||||||
|
modified_3 = entity_registry.async_get("test.mock_1").modified_at
|
||||||
|
assert modified_3 == modified_2
|
||||||
|
|
||||||
|
|
||||||
async def test_attach_entity_component_collection(hass: HomeAssistant) -> None:
|
async def test_attach_entity_component_collection(hass: HomeAssistant) -> None:
|
||||||
"""Test attaching collection to entity component."""
|
"""Test attaching collection to entity component."""
|
||||||
ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass)
|
ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user