Add created_at/modified_at to device registry (#122369)

This commit is contained in:
Robert Resch 2024-07-22 19:15:23 +02:00 committed by GitHub
parent 19d9a91392
commit 4c853803f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 147 additions and 1 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Mapping
from datetime import datetime
from enum import StrEnum
from functools import cached_property, lru_cache, partial
import logging
@ -23,6 +24,7 @@ from homeassistant.core import (
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.dt import utc_from_timestamp, utcnow
from homeassistant.util.event_type import EventType
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.json import format_unserializable_data
@ -94,6 +96,7 @@ class DeviceInfo(TypedDict, total=False):
configuration_url: str | URL | None
connections: set[tuple[str, str]]
created_at: str
default_manufacturer: str
default_model: str
default_name: str
@ -102,6 +105,7 @@ class DeviceInfo(TypedDict, total=False):
manufacturer: str | None
model: str | None
model_id: str | None
modified_at: str
name: str | None
serial_number: str | None
suggested_area: str | None
@ -281,6 +285,7 @@ class DeviceEntry:
config_entries: set[str] = attr.ib(converter=set, factory=set)
configuration_url: str | None = attr.ib(default=None)
connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
created_at: datetime = attr.ib(factory=utcnow)
disabled_by: DeviceEntryDisabler | None = attr.ib(default=None)
entry_type: DeviceEntryType | None = attr.ib(default=None)
hw_version: str | None = attr.ib(default=None)
@ -290,6 +295,7 @@ class DeviceEntry:
manufacturer: str | None = attr.ib(default=None)
model: str | None = attr.ib(default=None)
model_id: str | None = attr.ib(default=None)
modified_at: datetime = attr.ib(factory=utcnow)
name_by_user: str | None = attr.ib(default=None)
name: str | None = attr.ib(default=None)
primary_config_entry: str | None = attr.ib(default=None)
@ -316,6 +322,7 @@ class DeviceEntry:
"configuration_url": self.configuration_url,
"config_entries": list(self.config_entries),
"connections": list(self.connections),
"created_at": self.created_at.timestamp(),
"disabled_by": self.disabled_by,
"entry_type": self.entry_type,
"hw_version": self.hw_version,
@ -325,6 +332,7 @@ class DeviceEntry:
"manufacturer": self.manufacturer,
"model": self.model,
"model_id": self.model_id,
"modified_at": self.modified_at.timestamp(),
"name_by_user": self.name_by_user,
"name": self.name,
"primary_config_entry": self.primary_config_entry,
@ -359,6 +367,7 @@ class DeviceEntry:
"config_entries": list(self.config_entries),
"configuration_url": self.configuration_url,
"connections": list(self.connections),
"created_at": self.created_at.isoformat(),
"disabled_by": self.disabled_by,
"entry_type": self.entry_type,
"hw_version": self.hw_version,
@ -368,6 +377,7 @@ class DeviceEntry:
"manufacturer": self.manufacturer,
"model": self.model,
"model_id": self.model_id,
"modified_at": self.modified_at.isoformat(),
"name_by_user": self.name_by_user,
"name": self.name,
"primary_config_entry": self.primary_config_entry,
@ -388,6 +398,8 @@ class DeletedDeviceEntry:
identifiers: set[tuple[str, str]] = attr.ib()
id: str = attr.ib()
orphaned_timestamp: float | None = attr.ib()
created_at: datetime = attr.ib(factory=utcnow)
modified_at: datetime = attr.ib(factory=utcnow)
def to_device_entry(
self,
@ -400,6 +412,7 @@ class DeletedDeviceEntry:
# type ignores: likely https://github.com/python/mypy/issues/8625
config_entries={config_entry_id}, # type: ignore[arg-type]
connections=self.connections & connections, # type: ignore[arg-type]
created_at=self.created_at,
identifiers=self.identifiers & identifiers, # type: ignore[arg-type]
id=self.id,
is_new=True,
@ -413,9 +426,11 @@ class DeletedDeviceEntry:
{
"config_entries": list(self.config_entries),
"connections": list(self.connections),
"created_at": self.created_at.isoformat(),
"identifiers": list(self.identifiers),
"id": self.id,
"orphaned_timestamp": self.orphaned_timestamp,
"modified_at": self.modified_at.isoformat(),
}
)
)
@ -490,8 +505,12 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
device.setdefault("primary_config_entry", None)
if old_minor_version < 7:
# Introduced in 2024.8
created_at = utc_from_timestamp(0).isoformat()
for device in old_data["devices"]:
device.setdefault("model_id", None)
device["created_at"] = device["modified_at"] = created_at
for device in old_data["deleted_devices"]:
device["created_at"] = device["modified_at"] = created_at
if old_major_version > 1:
raise NotImplementedError
@ -688,6 +707,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
config_entry_id: str,
configuration_url: str | URL | None | UndefinedType = UNDEFINED,
connections: set[tuple[str, str]] | None | UndefinedType = UNDEFINED,
created_at: str | datetime | UndefinedType = UNDEFINED, # will be ignored
default_manufacturer: str | None | UndefinedType = UNDEFINED,
default_model: str | None | UndefinedType = UNDEFINED,
default_name: str | None | UndefinedType = UNDEFINED,
@ -699,6 +719,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
manufacturer: str | None | UndefinedType = UNDEFINED,
model: str | None | UndefinedType = UNDEFINED,
model_id: str | None | UndefinedType = UNDEFINED,
modified_at: str | datetime | UndefinedType = UNDEFINED, # will be ignored
name: str | None | UndefinedType = UNDEFINED,
serial_number: str | None | UndefinedType = UNDEFINED,
suggested_area: str | None | UndefinedType = UNDEFINED,
@ -1035,6 +1056,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if not new_values:
return old
if not RUNTIME_ONLY_ATTRS.issuperset(new_values):
# Change modified_at if we are changing something that we store
new_values["modified_at"] = utcnow()
self.hass.verify_event_loop_thread("device_registry.async_update_device")
new = attr.evolve(old, **new_values)
self.devices[device_id] = new
@ -1114,6 +1139,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self.deleted_devices[device_id] = DeletedDeviceEntry(
config_entries=device.config_entries,
connections=device.connections,
created_at=device.created_at,
identifiers=device.identifiers,
id=device.id,
orphaned_timestamp=None,
@ -1149,6 +1175,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
tuple(conn) # type: ignore[misc]
for conn in device["connections"]
},
created_at=datetime.fromisoformat(device["created_at"]),
disabled_by=(
DeviceEntryDisabler(device["disabled_by"])
if device["disabled_by"]
@ -1169,6 +1196,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
manufacturer=device["manufacturer"],
model=device["model"],
model_id=device["model_id"],
modified_at=datetime.fromisoformat(device["modified_at"]),
name_by_user=device["name_by_user"],
name=device["name"],
primary_config_entry=device["primary_config_entry"],
@ -1181,8 +1209,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
deleted_devices[device["id"]] = DeletedDeviceEntry(
config_entries=set(device["config_entries"]),
connections={tuple(conn) for conn in device["connections"]},
created_at=datetime.fromisoformat(device["created_at"]),
identifiers={tuple(iden) for iden in device["identifiers"]},
id=device["id"],
modified_at=datetime.fromisoformat(device["modified_at"]),
orphaned_timestamp=device["orphaned_timestamp"],
)

View file

@ -1,5 +1,8 @@
"""Test device_registry API."""
from datetime import datetime
from freezegun.api import FrozenDateTimeFactory
import pytest
from pytest_unordered import unordered
@ -7,6 +10,7 @@ from homeassistant.components.config import device_registry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, MockModule, mock_integration
from tests.typing import MockHAClientWebSocket, WebSocketGenerator
@ -26,6 +30,7 @@ async def client_fixture(
return await hass_ws_client(hass)
@pytest.mark.usefixtures("freezer")
async def test_list_devices(
hass: HomeAssistant,
client: MockHAClientWebSocket,
@ -61,6 +66,7 @@ async def test_list_devices(
"config_entries": [entry.entry_id],
"configuration_url": None,
"connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]],
"created_at": utcnow().timestamp(),
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -69,6 +75,7 @@ async def test_list_devices(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().timestamp(),
"name_by_user": None,
"name": None,
"primary_config_entry": entry.entry_id,
@ -81,6 +88,7 @@ async def test_list_devices(
"config_entries": [entry.entry_id],
"configuration_url": None,
"connections": [],
"created_at": utcnow().timestamp(),
"disabled_by": None,
"entry_type": dr.DeviceEntryType.SERVICE,
"hw_version": None,
@ -89,6 +97,7 @@ async def test_list_devices(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().timestamp(),
"name_by_user": None,
"name": None,
"primary_config_entry": entry.entry_id,
@ -113,6 +122,7 @@ async def test_list_devices(
"config_entries": [entry.entry_id],
"configuration_url": None,
"connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]],
"created_at": utcnow().timestamp(),
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -122,6 +132,7 @@ async def test_list_devices(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().timestamp(),
"name_by_user": None,
"name": None,
"primary_config_entry": entry.entry_id,
@ -151,12 +162,15 @@ async def test_update_device(
hass: HomeAssistant,
client: MockHAClientWebSocket,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
payload_key: str,
payload_value: str | dr.DeviceEntryDisabler | None,
) -> None:
"""Test update entry."""
entry = MockConfigEntry(title=None)
entry.add_to_hass(hass)
created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00")
freezer.move_to(created_at)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={("ethernet", "12:34:56:78:90:AB:CD:EF")},
@ -167,6 +181,9 @@ async def test_update_device(
assert not getattr(device, payload_key)
modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00")
freezer.move_to(modified_at)
await client.send_json_auto_id(
{
"type": "config/device_registry/update",
@ -186,6 +203,12 @@ async def test_update_device(
assert msg["result"][payload_key] == payload_value
assert getattr(device, payload_key) == payload_value
for key, value in (
("created_at", created_at),
("modified_at", modified_at if payload_value is not None else created_at),
):
assert msg["result"][key] == value.timestamp()
assert getattr(device, key) == value
assert isinstance(device.disabled_by, (dr.DeviceEntryDisabler, type(None)))
@ -194,10 +217,13 @@ async def test_update_device_labels(
hass: HomeAssistant,
client: MockHAClientWebSocket,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test update entry labels."""
entry = MockConfigEntry(title=None)
entry.add_to_hass(hass)
created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00")
freezer.move_to(created_at)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={("ethernet", "12:34:56:78:90:AB:CD:EF")},
@ -207,6 +233,8 @@ async def test_update_device_labels(
)
assert not device.labels
modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00")
freezer.move_to(modified_at)
await client.send_json_auto_id(
{
@ -227,6 +255,12 @@ async def test_update_device_labels(
assert msg["result"]["labels"] == unordered(["label1", "label2"])
assert device.labels == {"label1", "label2"}
for key, value in (
("created_at", created_at),
("modified_at", modified_at),
):
assert msg["result"][key] == value.timestamp()
assert getattr(device, key) == value
async def test_remove_config_entry_from_device(

View file

@ -26,6 +26,8 @@ TO_EXCLUDE = {
"last_updated",
"last_changed",
"last_reported",
"created_at",
"modified_at",
}

View file

@ -293,6 +293,8 @@ async def test_snapshots(
device_dict = asdict(device)
device_dict.pop("id", None)
device_dict.pop("via_device_id", None)
device_dict.pop("created_at", None)
device_dict.pop("modified_at", None)
devices.append({"device": device_dict, "entities": entities})
assert snapshot == devices

View file

@ -2,11 +2,13 @@
from collections.abc import Iterable
from contextlib import AbstractContextManager, nullcontext
from datetime import datetime
from functools import partial
import time
from typing import Any
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from yarl import URL
@ -19,6 +21,7 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
)
from homeassistant.util.dt import utcnow
from tests.common import (
MockConfigEntry,
@ -177,12 +180,15 @@ async def test_multiple_config_entries(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_loading_from_storage(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test loading stored devices on start."""
created_at = "2024-01-01T00:00:00+00:00"
modified_at = "2024-02-01T00:00:00+00:00"
hass_storage[dr.STORAGE_KEY] = {
"version": dr.STORAGE_VERSION_MAJOR,
"minor_version": dr.STORAGE_VERSION_MINOR,
@ -193,6 +199,7 @@ async def test_loading_from_storage(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": "https://example.com/config",
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": created_at,
"disabled_by": dr.DeviceEntryDisabler.USER,
"entry_type": dr.DeviceEntryType.SERVICE,
"hw_version": "hw_version",
@ -202,6 +209,7 @@ async def test_loading_from_storage(
"manufacturer": "manufacturer",
"model": "model",
"model_id": "model_id",
"modified_at": modified_at,
"name_by_user": "Test Friendly Name",
"name": "name",
"primary_config_entry": mock_config_entry.entry_id,
@ -214,8 +222,10 @@ async def test_loading_from_storage(
{
"config_entries": [mock_config_entry.entry_id],
"connections": [["Zigbee", "23.45.67.89.01"]],
"created_at": created_at,
"id": "bcdefghijklmn",
"identifiers": [["serial", "3456ABCDEF12"]],
"modified_at": modified_at,
"orphaned_timestamp": None,
}
],
@ -227,6 +237,16 @@ async def test_loading_from_storage(
assert len(registry.devices) == 1
assert len(registry.deleted_devices) == 1
assert registry.deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry(
config_entries={mock_config_entry.entry_id},
connections={("Zigbee", "23.45.67.89.01")},
created_at=datetime.fromisoformat(created_at),
id="bcdefghijklmn",
identifiers={("serial", "3456ABCDEF12")},
modified_at=datetime.fromisoformat(modified_at),
orphaned_timestamp=None,
)
entry = registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
connections={("Zigbee", "01.23.45.67.89")},
@ -239,6 +259,7 @@ async def test_loading_from_storage(
config_entries={mock_config_entry.entry_id},
configuration_url="https://example.com/config",
connections={("Zigbee", "01.23.45.67.89")},
created_at=datetime.fromisoformat(created_at),
disabled_by=dr.DeviceEntryDisabler.USER,
entry_type=dr.DeviceEntryType.SERVICE,
hw_version="hw_version",
@ -248,6 +269,7 @@ async def test_loading_from_storage(
manufacturer="manufacturer",
model="model",
model_id="model_id",
modified_at=datetime.fromisoformat(modified_at),
name_by_user="Test Friendly Name",
name="name",
primary_config_entry=mock_config_entry.entry_id,
@ -270,10 +292,12 @@ async def test_loading_from_storage(
assert entry == dr.DeviceEntry(
config_entries={mock_config_entry.entry_id},
connections={("Zigbee", "23.45.67.89.01")},
created_at=datetime.fromisoformat(created_at),
id="bcdefghijklmn",
identifiers={("serial", "3456ABCDEF12")},
manufacturer="manufacturer",
model="model",
modified_at=utcnow(),
primary_config_entry=mock_config_entry.entry_id,
)
assert entry.id == "bcdefghijklmn"
@ -283,6 +307,7 @@ async def test_loading_from_storage(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_1_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -367,6 +392,7 @@ async def test_migration_1_1_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": None,
@ -376,6 +402,7 @@ async def test_migration_1_1_to_1_7(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().isoformat(),
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
@ -388,6 +415,7 @@ async def test_migration_1_1_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -397,6 +425,7 @@ async def test_migration_1_1_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": None,
@ -409,8 +438,10 @@ async def test_migration_1_1_to_1_7(
{
"config_entries": ["123456"],
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"id": "deletedid",
"identifiers": [["serial", "123456ABCDFF"]],
"modified_at": "1970-01-01T00:00:00+00:00",
"orphaned_timestamp": None,
}
],
@ -419,6 +450,7 @@ async def test_migration_1_1_to_1_7(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_2_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -442,6 +474,7 @@ async def test_migration_1_2_to_1_7(
"identifiers": [["serial", "123456ABCDEF"]],
"manufacturer": "manufacturer",
"model": "model",
"modified_at": utcnow().isoformat(),
"name": "name",
"name_by_user": None,
"sw_version": "version",
@ -458,6 +491,7 @@ async def test_migration_1_2_to_1_7(
"identifiers": [["serial", "mock-id-invalid-entry"]],
"manufacturer": None,
"model": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"sw_version": None,
@ -502,6 +536,7 @@ async def test_migration_1_2_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": None,
@ -511,6 +546,7 @@ async def test_migration_1_2_to_1_7(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().isoformat(),
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
@ -523,6 +559,7 @@ async def test_migration_1_2_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -532,6 +569,7 @@ async def test_migration_1_2_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": None,
@ -546,6 +584,7 @@ async def test_migration_1_2_to_1_7(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_3_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -631,6 +670,7 @@ async def test_migration_1_3_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
@ -640,6 +680,7 @@ async def test_migration_1_3_to_1_7(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().isoformat(),
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
@ -652,6 +693,7 @@ async def test_migration_1_3_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -661,6 +703,7 @@ async def test_migration_1_3_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name": None,
"name_by_user": None,
"primary_config_entry": None,
@ -675,6 +718,7 @@ async def test_migration_1_3_to_1_7(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_4_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -762,6 +806,7 @@ async def test_migration_1_4_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
@ -771,6 +816,7 @@ async def test_migration_1_4_to_1_7(
"manufacturer": "manufacturer",
"model": "model",
"model_id": None,
"modified_at": utcnow().isoformat(),
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
@ -783,6 +829,7 @@ async def test_migration_1_4_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -792,6 +839,7 @@ async def test_migration_1_4_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": None,
@ -806,6 +854,7 @@ async def test_migration_1_4_to_1_7(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_5_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -895,6 +944,7 @@ async def test_migration_1_5_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
@ -905,6 +955,7 @@ async def test_migration_1_5_to_1_7(
"model": "model",
"name": "name",
"model_id": None,
"modified_at": utcnow().isoformat(),
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
@ -916,6 +967,7 @@ async def test_migration_1_5_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -925,6 +977,7 @@ async def test_migration_1_5_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": None,
@ -939,6 +992,7 @@ async def test_migration_1_5_to_1_7(
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_migration_1_6_to_1_7(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@ -1030,6 +1084,7 @@ async def test_migration_1_6_to_1_7(
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
@ -1040,6 +1095,7 @@ async def test_migration_1_6_to_1_7(
"model": "model",
"name": "name",
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
@ -1051,6 +1107,7 @@ async def test_migration_1_6_to_1_7(
"config_entries": [None],
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": None,
"hw_version": None,
@ -1060,6 +1117,7 @@ async def test_migration_1_6_to_1_7(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "1970-01-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": None,
@ -1546,8 +1604,11 @@ async def test_update(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Verify that we can update some attributes of a device."""
created_at = datetime.fromisoformat("2024-01-01T01:00:00+00:00")
freezer.move_to(created_at)
update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED)
entry = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
@ -1559,7 +1620,11 @@ async def test_update(
assert not entry.area_id
assert not entry.labels
assert not entry.name_by_user
assert entry.created_at == created_at
assert entry.modified_at == created_at
modified_at = datetime.fromisoformat("2024-02-01T01:00:00+00:00")
freezer.move_to(modified_at)
with patch.object(device_registry, "async_schedule_save") as mock_save:
updated_entry = device_registry.async_update_device(
entry.id,
@ -1589,6 +1654,7 @@ async def test_update(
config_entries={mock_config_entry.entry_id},
configuration_url="https://example.com/config",
connections={("mac", "65:43:21:fe:dc:ba")},
created_at=created_at,
disabled_by=dr.DeviceEntryDisabler.USER,
entry_type=dr.DeviceEntryType.SERVICE,
hw_version="hw_version",
@ -1598,6 +1664,7 @@ async def test_update(
manufacturer="Test Producer",
model="Test Model",
model_id="Test Model Name",
modified_at=modified_at,
name_by_user="Test Friendly Name",
name="name",
serial_number="serial_no",
@ -2616,6 +2683,7 @@ async def test_loading_invalid_configuration_url_from_storage(
"config_entries": ["1234"],
"configuration_url": "invalid",
"connections": [],
"created_at": "2024-01-01T00:00:00+00:00",
"disabled_by": None,
"entry_type": dr.DeviceEntryType.SERVICE,
"hw_version": None,
@ -2625,6 +2693,7 @@ async def test_loading_invalid_configuration_url_from_storage(
"manufacturer": None,
"model": None,
"model_id": None,
"modified_at": "2024-02-01T00:00:00+00:00",
"name_by_user": None,
"name": None,
"primary_config_entry": "1234",

View file

@ -155,7 +155,16 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
serialized["via_device_id"] = ANY
if serialized["primary_config_entry"] is not None:
serialized["primary_config_entry"] = ANY
return serialized
return cls._remove_created_and_modified_at(serialized)
@classmethod
def _remove_created_and_modified_at(
cls, data: SerializableData
) -> SerializableData:
"""Remove created_at and modified_at from the data."""
data.pop("created_at", None)
data.pop("modified_at", None)
return data
@classmethod
def _serializable_entity_registry_entry(