Skip to content

Commit

Permalink
Add created_at/modified_at to config entries (home-assistant#122456)
Browse files Browse the repository at this point in the history
  • Loading branch information
edenhaus authored Jul 29, 2024
1 parent 20c4f84 commit ad50136
Show file tree
Hide file tree
Showing 67 changed files with 441 additions and 393 deletions.
67 changes: 49 additions & 18 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from contextvars import ContextVar
from copy import deepcopy
from datetime import datetime
from enum import Enum, StrEnum
import functools
from functools import cached_property
Expand Down Expand Up @@ -69,6 +70,7 @@
from .util import ulid as ulid_util
from .util.async_ import create_eager_task
from .util.decorator import Registry
from .util.dt import utc_from_timestamp, utcnow
from .util.enum import try_parse_enum

if TYPE_CHECKING:
Expand Down Expand Up @@ -118,7 +120,7 @@

STORAGE_KEY = "core.config_entries"
STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 2
STORAGE_VERSION_MINOR = 3

SAVE_DELAY = 1

Expand Down Expand Up @@ -303,15 +305,19 @@ class ConfigEntry(Generic[_DataT]):
_background_tasks: set[asyncio.Future[Any]]
_integration_for_domain: loader.Integration | None
_tries: int
created_at: datetime
modified_at: datetime

def __init__(
self,
*,
created_at: datetime | None = None,
data: Mapping[str, Any],
disabled_by: ConfigEntryDisabler | None = None,
domain: str,
entry_id: str | None = None,
minor_version: int,
modified_at: datetime | None = None,
options: Mapping[str, Any] | None,
pref_disable_new_entities: bool | None = None,
pref_disable_polling: bool | None = None,
Expand Down Expand Up @@ -415,6 +421,8 @@ def __init__(

_setter(self, "_integration_for_domain", None)
_setter(self, "_tries", 0)
_setter(self, "created_at", created_at or utcnow())
_setter(self, "modified_at", modified_at or utcnow())

def __repr__(self) -> str:
"""Representation of ConfigEntry."""
Expand Down Expand Up @@ -483,8 +491,10 @@ def clear_cache(self) -> None:
def as_json_fragment(self) -> json_fragment:
"""Return JSON fragment of a config entry."""
json_repr = {
"created_at": self.created_at.timestamp(),
"entry_id": self.entry_id,
"domain": self.domain,
"modified_at": self.modified_at.timestamp(),
"title": self.title,
"source": self.source,
"state": self.state.value,
Expand Down Expand Up @@ -831,6 +841,10 @@ async def async_unload(

async def async_remove(self, hass: HomeAssistant) -> None:
"""Invoke remove callback on component."""
old_modified_at = self.modified_at
object.__setattr__(self, "modified_at", utcnow())
self.clear_cache()

if self.source == SOURCE_IGNORE:
return

Expand Down Expand Up @@ -862,6 +876,8 @@ async def async_remove(self, hass: HomeAssistant) -> None:
self.title,
integration.domain,
)
# Restore modified_at
object.__setattr__(self, "modified_at", old_modified_at)

@callback
def _async_set_state(
Expand Down Expand Up @@ -950,11 +966,13 @@ def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE:
def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this entry."""
return {
"created_at": self.created_at.isoformat(),
"data": dict(self.data),
"disabled_by": self.disabled_by,
"domain": self.domain,
"entry_id": self.entry_id,
"minor_version": self.minor_version,
"modified_at": self.modified_at.isoformat(),
"options": dict(self.options),
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
Expand Down Expand Up @@ -1599,25 +1617,34 @@ async def _async_migrate_func(
) -> dict[str, Any]:
"""Migrate to the new version."""
data = old_data
if old_major_version == 1 and old_minor_version < 2:
# Version 1.2 implements migration and freezes the available keys
for entry in data["entries"]:
# Populate keys which were introduced before version 1.2

pref_disable_new_entities = entry.get("pref_disable_new_entities")
if pref_disable_new_entities is None and "system_options" in entry:
pref_disable_new_entities = entry.get("system_options", {}).get(
"disable_new_entities"
if old_major_version == 1:
if old_minor_version < 2:
# Version 1.2 implements migration and freezes the available keys
for entry in data["entries"]:
# Populate keys which were introduced before version 1.2

pref_disable_new_entities = entry.get("pref_disable_new_entities")
if pref_disable_new_entities is None and "system_options" in entry:
pref_disable_new_entities = entry.get("system_options", {}).get(
"disable_new_entities"
)

entry.setdefault("disabled_by", entry.get("disabled_by"))
entry.setdefault("minor_version", entry.get("minor_version", 1))
entry.setdefault("options", entry.get("options", {}))
entry.setdefault(
"pref_disable_new_entities", pref_disable_new_entities
)
entry.setdefault(
"pref_disable_polling", entry.get("pref_disable_polling")
)
entry.setdefault("unique_id", entry.get("unique_id"))

entry.setdefault("disabled_by", entry.get("disabled_by"))
entry.setdefault("minor_version", entry.get("minor_version", 1))
entry.setdefault("options", entry.get("options", {}))
entry.setdefault("pref_disable_new_entities", pref_disable_new_entities)
entry.setdefault(
"pref_disable_polling", entry.get("pref_disable_polling")
)
entry.setdefault("unique_id", entry.get("unique_id"))
if old_minor_version < 3:
# Version 1.3 adds the created_at and modified_at fields
created_at = utc_from_timestamp(0).isoformat()
for entry in data["entries"]:
entry["created_at"] = entry["modified_at"] = created_at

if old_major_version > 1:
raise NotImplementedError
Expand Down Expand Up @@ -1793,11 +1820,13 @@ async def async_initialize(self) -> None:
entry_id = entry["entry_id"]

config_entry = ConfigEntry(
created_at=datetime.fromisoformat(entry["created_at"]),
data=entry["data"],
disabled_by=try_parse_enum(ConfigEntryDisabler, entry["disabled_by"]),
domain=entry["domain"],
entry_id=entry_id,
minor_version=entry["minor_version"],
modified_at=datetime.fromisoformat(entry["modified_at"]),
options=entry["options"],
pref_disable_new_entities=entry["pref_disable_new_entities"],
pref_disable_polling=entry["pref_disable_polling"],
Expand Down Expand Up @@ -2014,6 +2043,8 @@ def async_update_entry(
if not changed:
return False

_setter(entry, "modified_at", utcnow())

for listener in entry.update_listeners:
self.hass.async_create_task(
listener(self.hass, entry),
Expand Down
3 changes: 2 additions & 1 deletion tests/components/aemet/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.components.aemet.const import DOMAIN
from homeassistant.core import HomeAssistant
Expand All @@ -30,4 +31,4 @@ async def test_config_entry_diagnostics(
return_value={},
):
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/airly/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Airly diagnostics."""

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -22,4 +23,4 @@ async def test_entry_diagnostics(

result = await get_diagnostics_for_config_entry(hass, hass_client, entry)

assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
8 changes: 4 additions & 4 deletions tests/components/airnow/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -27,7 +28,6 @@ async def test_entry_diagnostics(
return_value="PST",
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== snapshot
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
) == snapshot(exclude=props("created_at", "modified_at"))
8 changes: 4 additions & 4 deletions tests/components/airvisual/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test AirVisual diagnostics."""

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -16,7 +17,6 @@ async def test_entry_diagnostics(
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== snapshot
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
) == snapshot(exclude=props("created_at", "modified_at"))
8 changes: 4 additions & 4 deletions tests/components/airvisual_pro/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test AirVisual Pro diagnostics."""

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -16,7 +17,6 @@ async def test_entry_diagnostics(
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== snapshot
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
) == snapshot(exclude=props("created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/airzone/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from aioairzone.const import RAW_HVAC, RAW_VERSION, RAW_WEBSERVER
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.components.airzone.const import DOMAIN
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -37,4 +38,4 @@ async def test_config_entry_diagnostics(
},
):
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/airzone_cloud/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
RAW_WEBSERVERS,
)
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.components.airzone_cloud.const import DOMAIN
from homeassistant.const import CONF_ID
Expand Down Expand Up @@ -111,4 +112,4 @@ async def test_config_entry_diagnostics(
return_value=RAW_DATA_MOCK,
):
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
8 changes: 4 additions & 4 deletions tests/components/ambient_station/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Ambient PWS diagnostics."""

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.components.ambient_station import AmbientStationConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -20,7 +21,6 @@ async def test_entry_diagnostics(
"""Test config entry diagnostics."""
ambient = config_entry.runtime_data
ambient.stations = data_station
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
== snapshot
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
) == snapshot(exclude=props("created_at", "modified_at"))
8 changes: 4 additions & 4 deletions tests/components/axis/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -20,7 +21,6 @@ async def test_entry_diagnostics(
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
assert (
await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup)
== snapshot
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, config_entry_setup
) == snapshot(exclude=props("created_at", "modified_at"))
2 changes: 1 addition & 1 deletion tests/components/blink/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ async def test_entry_diagnostics(
hass, hass_client, mock_config_entry
)

assert result == snapshot(exclude=props("entry_id"))
assert result == snapshot(exclude=props("entry_id", "created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/braviatv/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import patch

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.components.braviatv.const import CONF_USE_PSK, DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN
Expand Down Expand Up @@ -71,4 +72,4 @@ async def test_entry_diagnostics(
assert await async_setup_component(hass, DOMAIN, {})
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)

assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/co2signal/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand All @@ -20,4 +21,4 @@ async def test_entry_diagnostics(
"""Test config entry diagnostics."""
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)

assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
3 changes: 2 additions & 1 deletion tests/components/coinbase/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import patch

from syrupy import SnapshotAssertion
from syrupy.filters import props

from homeassistant.core import HomeAssistant

Expand Down Expand Up @@ -40,4 +41,4 @@ async def test_entry_diagnostics(

result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)

assert result == snapshot
assert result == snapshot(exclude=props("created_at", "modified_at"))
Loading

0 comments on commit ad50136

Please sign in to comment.