From edaafadde0d31c400524276fabe8bf78ec314911 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 10 Jul 2022 13:46:22 -0400 Subject: [PATCH] Remove ZHA device storage (#74837) * Remove ZHA device storage * remove storage file if it exists --- homeassistant/components/zha/__init__.py | 12 +- homeassistant/components/zha/core/device.py | 8 - homeassistant/components/zha/core/gateway.py | 27 +--- homeassistant/components/zha/core/store.py | 146 ------------------- tests/components/zha/conftest.py | 17 +-- tests/components/zha/test_gateway.py | 64 +------- 6 files changed, 15 insertions(+), 259 deletions(-) delete mode 100644 homeassistant/components/zha/core/store.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0e11d992a25..80956117ff9 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,6 +1,7 @@ """Support for Zigbee Home Automation devices.""" import asyncio import logging +import os import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from . import api @@ -98,6 +100,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config.get(CONF_ENABLE_QUIRKS, True): setup_quirks(config) + # temporary code to remove the zha storage file from disk. this will be removed in 2022.10.0 + storage_path = hass.config.path(STORAGE_DIR, "zha.storage") + if os.path.isfile(storage_path): + _LOGGER.debug("removing ZHA storage file") + await hass.async_add_executor_job(os.remove, storage_path) + else: + _LOGGER.debug("ZHA storage file does not exist or was already removed") + zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() @@ -124,7 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Handle shutdown tasks.""" zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown @@ -137,7 +146,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e83c0afbceb..afd4f647ecb 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -470,8 +470,6 @@ class ZHADevice(LogMixin): self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") - entry = self.gateway.zha_storage.async_create_or_update_device(self) - self.debug("stored in registry: %s", entry) if ( should_identify @@ -496,12 +494,6 @@ class ZHADevice(LogMixin): for unsubscribe in self.unsubs: unsubscribe() - @callback - def async_update_last_seen(self, last_seen: float | None) -> None: - """Set last seen on the zigpy device.""" - if self._zigpy_device.last_seen is None and last_seen is not None: - self._zigpy_device.last_seen = last_seen - @property def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 099abfe5e88..7782d8ef7fd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -27,7 +27,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from . import discovery @@ -81,14 +80,12 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS -from .store import async_get_registry if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] @@ -118,7 +115,6 @@ class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" # -- Set in async_initialize -- - zha_storage: ZhaStorage ha_device_registry: dr.DeviceRegistry ha_entity_registry: er.EntityRegistry application_controller: ControllerApplication @@ -150,7 +146,6 @@ class ZHAGateway: discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) - self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = dr.async_get(self._hass) self.ha_entity_registry = er.async_get(self._hass) @@ -196,10 +191,9 @@ class ZHAGateway: zha_device = self._async_get_or_create_device(zigpy_device, restored=True) if zha_device.ieee == self.application_controller.ieee: self.coordinator_zha_device = zha_device - zha_dev_entry = self.zha_storage.devices.get(str(zigpy_device.ieee)) delta_msg = "not known" - if zha_dev_entry and zha_dev_entry.last_seen is not None: - delta = round(time.time() - zha_dev_entry.last_seen) + if zha_device.last_seen is not None: + delta = round(time.time() - zha_device.last_seen) zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( @@ -210,13 +204,6 @@ class ZHAGateway: delta_msg, zha_device.consider_unavailable_time, ) - # update the last seen time for devices every 10 minutes to avoid thrashing - # writes and shutdown issues where storage isn't updated - self._unsubs.append( - async_track_time_interval( - self._hass, self.async_update_device_storage, timedelta(minutes=10) - ) - ) @callback def async_load_groups(self) -> None: @@ -526,8 +513,6 @@ class ZHAGateway: model=zha_device.model, ) zha_device.set_device_id(device_registry_device.id) - entry = self.zha_storage.async_get_or_create_device(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -550,17 +535,9 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_: Any) -> None: - """Update the devices in the store.""" - for device in self.devices.values(): - self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - # This is an active device so set a last seen if it is none - if zha_device.last_seen is None: - zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py deleted file mode 100644 index 0b7564fe815..00000000000 --- a/homeassistant/components/zha/core/store.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Data storage helper for ZHA.""" -from __future__ import annotations - -from collections import OrderedDict -from collections.abc import MutableMapping -import datetime -import time -from typing import TYPE_CHECKING, Any, cast - -import attr - -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store -from homeassistant.loader import bind_hass - -if TYPE_CHECKING: - from .device import ZHADevice - -DATA_REGISTRY = "zha_storage" - -STORAGE_KEY = "zha.storage" -STORAGE_VERSION = 1 -SAVE_DELAY = 10 -TOMBSTONE_LIFETIME = datetime.timedelta(days=60).total_seconds() - - -@attr.s(slots=True, frozen=True) -class ZhaDeviceEntry: - """Zha Device storage Entry.""" - - name: str | None = attr.ib(default=None) - ieee: str | None = attr.ib(default=None) - last_seen: float | None = attr.ib(default=None) - - -class ZhaStorage: - """Class to hold a registry of zha devices.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the zha device storage.""" - self.hass: HomeAssistant = hass - self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - - @callback - def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - device_entry: ZhaDeviceEntry = ZhaDeviceEntry( - name=device.name, ieee=ieee_str, last_seen=device.last_seen - ) - self.devices[ieee_str] = device_entry - self.async_schedule_save() - return device_entry - - @callback - def async_get_or_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - return self.devices[ieee_str] - return self.async_create_device(device) - - @callback - def async_create_or_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create or update a ZhaDeviceEntry.""" - if str(device.ieee) in self.devices: - return self.async_update_device(device) - return self.async_create_device(device) - - @callback - def async_delete_device(self, device: ZHADevice) -> None: - """Delete ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - del self.devices[ieee_str] - self.async_schedule_save() - - @callback - def async_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Update name of ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - old = self.devices[ieee_str] - - if device.last_seen is None: - return old - - changes = {} - changes["last_seen"] = device.last_seen - - new = self.devices[ieee_str] = attr.evolve(old, **changes) - self.async_schedule_save() - return new - - async def async_load(self) -> None: - """Load the registry of zha device entries.""" - data = await self._store.async_load() - - devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() - - if data is not None: - for device in data["devices"]: - devices[device["ieee"]] = ZhaDeviceEntry( - name=device["name"], - ieee=device["ieee"], - last_seen=device.get("last_seen"), - ) - - self.devices = devices - - @callback - def async_schedule_save(self) -> None: - """Schedule saving the registry of zha devices.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - async def async_save(self) -> None: - """Save the registry of zha devices.""" - await self._store.async_save(self._data_to_save()) - - @callback - def _data_to_save(self) -> dict: - """Return data for the registry of zha devices to store in a file.""" - data = {} - - data["devices"] = [ - {"name": entry.name, "ieee": entry.ieee, "last_seen": entry.last_seen} - for entry in self.devices.values() - if entry.last_seen and (time.time() - entry.last_seen) < TOMBSTONE_LIFETIME - ] - - return data - - -@bind_hass -async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: - """Return zha device storage instance.""" - if (task := hass.data.get(DATA_REGISTRY)) is None: - - async def _load_reg() -> ZhaStorage: - registry = ZhaStorage(hass) - await registry.async_load() - return registry - - task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - - return cast(ZhaStorage, await task) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 482a11b95de..d9223027668 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -15,7 +15,6 @@ from zigpy.state import State import zigpy.types import zigpy.zdo.types as zdo_t -from homeassistant.components.zha import DOMAIN import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.setup import async_setup_component @@ -188,26 +187,14 @@ def zha_device_joined(hass, setup_zha): @pytest.fixture -def zha_device_restored(hass, zigpy_app_controller, setup_zha, hass_storage): +def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" async def _zha_device(zigpy_dev, last_seen=None): zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev if last_seen is not None: - hass_storage[f"{DOMAIN}.storage"] = { - "key": f"{DOMAIN}.storage", - "version": 1, - "data": { - "devices": [ - { - "ieee": str(zigpy_dev.ieee), - "last_seen": last_seen, - "name": f"{zigpy_dev.manufacturer} {zigpy_dev.model}", - } - ], - }, - } + zigpy_dev.last_seen = last_seen await setup_zha() zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b19c98548ce..3c8c3e78c0e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,7 +1,5 @@ """Test ZHA Gateway.""" import asyncio -import math -import time from unittest.mock import patch import pytest @@ -10,10 +8,9 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME from homeassistant.const import Platform -from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway +from .common import async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -214,62 +211,3 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 - - -async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): - """Test saving data after a delay.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - assert zha_dev_basic.last_seen is not None - last_seen = zha_dev_basic.last_seen - - # test that we can't set None as last seen any more - zha_dev_basic.async_update_last_seen(None) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we won't put None in storage - zigpy_dev_basic.last_seen = None - assert zha_dev_basic.last_seen is None - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - # test that we can still set a good last_seen - last_seen = time.time() - zha_dev_basic.async_update_last_seen(last_seen) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we still put good values in storage - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - -async def test_cleaning_up_storage(hass, zigpy_dev_basic, zha_dev_basic, hass_storage): - """Test cleaning up zha storage and remove stale devices.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - - assert hass_storage["zha.storage"]["data"]["devices"] - device = hass_storage["zha.storage"]["data"]["devices"][0] - assert device["ieee"] == str(zha_dev_basic.ieee) - - zha_dev_basic.device.last_seen = time.time() - TOMBSTONE_LIFETIME - 1 - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - assert not hass_storage["zha.storage"]["data"]["devices"]