diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index f1731f43473..7565c87b6b2 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -14,6 +14,7 @@ from .normalized_name_base_registry import ( NormalizedNameBaseRegistryItems, normalize_name, ) +from .registry import BaseRegistry from .storage import Store from .typing import UNDEFINED, UndefinedType @@ -22,7 +23,6 @@ EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated" STORAGE_KEY = "core.area_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 6 -SAVE_DELAY = 10 class EventAreaRegistryUpdatedData(TypedDict): @@ -86,7 +86,7 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): return old_data -class AreaRegistry: +class AreaRegistry(BaseRegistry): """Class to hold a registry of areas.""" areas: NormalizedNameBaseRegistryItems[AreaEntry] @@ -273,11 +273,6 @@ class AreaRegistry: self.areas = areas self._area_data = areas.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the area registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data of area registry to store in a file.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5cbc40e209f..253b7eaa65c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -31,6 +31,7 @@ from .deprecation import ( ) from .frame import report from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes +from .registry import BaseRegistry from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -45,7 +46,7 @@ EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 5 -SAVE_DELAY = 10 + CLEANUP_DELAY = 10 CONNECTION_BLUETOOTH = "bluetooth" @@ -456,7 +457,7 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): return None -class DeviceRegistry: +class DeviceRegistry(BaseRegistry): """Class to hold a registry of devices.""" devices: DeviceRegistryItems[DeviceEntry] @@ -898,11 +899,6 @@ class DeviceRegistry: self.deleted_devices = deleted_devices self._device_data = devices.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the device registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data of device registry to store in a file.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index eb0aace4265..df923c15c37 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -52,6 +52,7 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes +from .registry import BaseRegistry from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -61,7 +62,7 @@ T = TypeVar("T") DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" -SAVE_DELAY = 10 + _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 @@ -549,7 +550,7 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): return [data[key] for key in self._area_id_index.get(area_id, ())] -class EntityRegistry: +class EntityRegistry(BaseRegistry): """Class to hold a registry of entities.""" deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry] @@ -1182,11 +1183,6 @@ class EntityRegistry: self.entities = entities self._entities_data = entities.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the entity registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, Any]: """Return data of entity registry to store in a file.""" diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 978471d7cd2..d966f6045b6 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -14,6 +14,7 @@ from .normalized_name_base_registry import ( NormalizedNameBaseRegistryItems, normalize_name, ) +from .registry import BaseRegistry from .storage import Store from .typing import UNDEFINED, EventType, UndefinedType @@ -21,7 +22,6 @@ DATA_REGISTRY = "floor_registry" EVENT_FLOOR_REGISTRY_UPDATED = "floor_registry_updated" STORAGE_KEY = "core.floor_registry" STORAGE_VERSION_MAJOR = 1 -SAVE_DELAY = 10 class EventFloorRegistryUpdatedData(TypedDict): @@ -44,7 +44,7 @@ class FloorEntry(NormalizedNameBaseRegistryEntry): level: int = 0 -class FloorRegistry: +class FloorRegistry(BaseRegistry): """Class to hold a registry of floors.""" floors: NormalizedNameBaseRegistryItems[FloorEntry] @@ -209,11 +209,6 @@ class FloorRegistry: self.floors = floors self._floor_data = floors.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the floor registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, str | int | list[str] | None]]]: """Return data of floor registry to store in a file.""" diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 27d568a13de..fc8c547404d 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -14,6 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util +from .registry import BaseRegistry from .storage import Store DATA_REGISTRY = "issue_registry" @@ -21,7 +22,6 @@ EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 2 -SAVE_DELAY = 10 class IssueSeverity(StrEnum): @@ -92,7 +92,7 @@ class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]): return old_data -class IssueRegistry: +class IssueRegistry(BaseRegistry): """Class to hold a registry of issues.""" def __init__(self, hass: HomeAssistant, *, read_only: bool = False) -> None: @@ -259,11 +259,6 @@ class IssueRegistry: self.issues = issues - @callback - def async_schedule_save(self) -> None: - """Schedule saving the issue registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: """Return data of issue registry to store in a file.""" diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index ef3abc19d8c..34c23c98f2a 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -14,6 +14,7 @@ from .normalized_name_base_registry import ( NormalizedNameBaseRegistryItems, normalize_name, ) +from .registry import BaseRegistry from .storage import Store from .typing import UNDEFINED, EventType, UndefinedType @@ -21,7 +22,6 @@ DATA_REGISTRY = "label_registry" EVENT_LABEL_REGISTRY_UPDATED = "label_registry_updated" STORAGE_KEY = "core.label_registry" STORAGE_VERSION_MAJOR = 1 -SAVE_DELAY = 10 class EventLabelRegistryUpdatedData(TypedDict): @@ -44,7 +44,7 @@ class LabelEntry(NormalizedNameBaseRegistryEntry): icon: str | None = None -class LabelRegistry: +class LabelRegistry(BaseRegistry): """Class to hold a registry of labels.""" labels: NormalizedNameBaseRegistryItems[LabelEntry] @@ -205,11 +205,6 @@ class LabelRegistry: self.labels = labels self._label_data = labels.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the label registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: """Return data of label registry to store in a file.""" diff --git a/homeassistant/helpers/registry.py b/homeassistant/helpers/registry.py new file mode 100644 index 00000000000..7b13941e5d2 --- /dev/null +++ b/homeassistant/helpers/registry.py @@ -0,0 +1,35 @@ +"""Provide a base implementation for registries.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +from homeassistant.core import CoreState, HomeAssistant, callback + +if TYPE_CHECKING: + from .storage import Store + +SAVE_DELAY = 10 +SAVE_DELAY_STARTING = 300 + + +class BaseRegistry(ABC): + """Class to implement a registry.""" + + hass: HomeAssistant + _store: Store + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the registry.""" + # Schedule the save past startup to avoid writing + # the file while the system is starting. + delay = ( + SAVE_DELAY_STARTING if self.hass.state is CoreState.starting else SAVE_DELAY + ) + self._store.async_delay_save(self._data_to_save, delay) + + @callback + @abstractmethod + def _data_to_save(self) -> dict[str, Any]: + """Return data of registry to store in a file.""" diff --git a/tests/helpers/test_registry.py b/tests/helpers/test_registry.py new file mode 100644 index 00000000000..2782d3140f7 --- /dev/null +++ b/tests/helpers/test_registry.py @@ -0,0 +1,51 @@ +"""Tests for the registry.""" +from typing import Any + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import storage +from homeassistant.helpers.registry import SAVE_DELAY, SAVE_DELAY_STARTING, BaseRegistry + +from tests.common import async_fire_time_changed + + +class SampleRegistry(BaseRegistry): + """Class to hold a registry of X.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the registry.""" + self.hass = hass + self._store = storage.Store(hass, 1, "test") + self.save_calls = 0 + + def _data_to_save(self) -> None: + """Return data of registry to save.""" + self.save_calls += 1 + return None + + +async def test_async_schedule_save( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_storage: dict[str, Any] +) -> None: + """Test saving the registry.""" + registry = SampleRegistry(hass) + hass.set_state(CoreState.starting) + + registry.async_schedule_save() + freezer.tick(SAVE_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 0 + + freezer.tick(SAVE_DELAY_STARTING) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 1 + + hass.set_state(CoreState.running) + registry.async_schedule_save() + freezer.tick(SAVE_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 2