Remove ZHA device storage (#74837)

* Remove ZHA device storage

* remove storage file if it exists
This commit is contained in:
David F. Mulcahey 2022-07-10 13:46:22 -04:00 committed by GitHub
parent 240a83239a
commit edaafadde0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 15 additions and 259 deletions

View File

@ -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)

View File

@ -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."""

View File

@ -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,

View File

@ -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)

View File

@ -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]

View File

@ -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"]