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.""" """Support for Zigbee Home Automation devices."""
import asyncio import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from zhaquirks import setup as setup_quirks from zhaquirks import setup as setup_quirks
@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import api 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): if config.get(CONF_ENABLE_QUIRKS, True):
setup_quirks(config) 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) zha_gateway = ZHAGateway(hass, config, config_entry)
await zha_gateway.async_initialize() await zha_gateway.async_initialize()
@ -124,7 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Handle shutdown tasks.""" """Handle shutdown tasks."""
zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY]
await zha_gateway.shutdown() await zha_gateway.shutdown()
await zha_gateway.async_update_device_storage()
zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once(
ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown 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.""" """Unload ZHA config entry."""
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
await zha_gateway.shutdown() await zha_gateway.shutdown()
await zha_gateway.async_update_device_storage()
GROUP_PROBE.cleanup() GROUP_PROBE.cleanup()
api.async_unload_api(hass) api.async_unload_api(hass)

View File

@ -470,8 +470,6 @@ class ZHADevice(LogMixin):
self.debug("started configuration") self.debug("started configuration")
await self._channels.async_configure() await self._channels.async_configure()
self.debug("completed configuration") self.debug("completed configuration")
entry = self.gateway.zha_storage.async_create_or_update_device(self)
self.debug("stored in registry: %s", entry)
if ( if (
should_identify should_identify
@ -496,12 +494,6 @@ class ZHADevice(LogMixin):
for unsubscribe in self.unsubs: for unsubscribe in self.unsubs:
unsubscribe() 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 @property
def zha_device_info(self) -> dict[str, Any]: def zha_device_info(self) -> dict[str, Any]:
"""Get ZHA device information.""" """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 import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import discovery from . import discovery
@ -81,14 +80,12 @@ from .const import (
from .device import DeviceStatus, ZHADevice from .device import DeviceStatus, ZHADevice
from .group import GroupMember, ZHAGroup from .group import GroupMember, ZHAGroup
from .registries import GROUP_ENTITY_DOMAINS from .registries import GROUP_ENTITY_DOMAINS
from .store import async_get_registry
if TYPE_CHECKING: if TYPE_CHECKING:
from logging import Filter, LogRecord from logging import Filter, LogRecord
from ..entity import ZhaEntity from ..entity import ZhaEntity
from .channels.base import ZigbeeChannel from .channels.base import ZigbeeChannel
from .store import ZhaStorage
_LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LogFilterType = Union[Filter, Callable[[LogRecord], int]]
@ -118,7 +115,6 @@ class ZHAGateway:
"""Gateway that handles events that happen on the ZHA Zigbee network.""" """Gateway that handles events that happen on the ZHA Zigbee network."""
# -- Set in async_initialize -- # -- Set in async_initialize --
zha_storage: ZhaStorage
ha_device_registry: dr.DeviceRegistry ha_device_registry: dr.DeviceRegistry
ha_entity_registry: er.EntityRegistry ha_entity_registry: er.EntityRegistry
application_controller: ControllerApplication application_controller: ControllerApplication
@ -150,7 +146,6 @@ class ZHAGateway:
discovery.PROBE.initialize(self._hass) discovery.PROBE.initialize(self._hass)
discovery.GROUP_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_device_registry = dr.async_get(self._hass)
self.ha_entity_registry = er.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) zha_device = self._async_get_or_create_device(zigpy_device, restored=True)
if zha_device.ieee == self.application_controller.ieee: if zha_device.ieee == self.application_controller.ieee:
self.coordinator_zha_device = zha_device self.coordinator_zha_device = zha_device
zha_dev_entry = self.zha_storage.devices.get(str(zigpy_device.ieee))
delta_msg = "not known" delta_msg = "not known"
if zha_dev_entry and zha_dev_entry.last_seen is not None: if zha_device.last_seen is not None:
delta = round(time.time() - zha_dev_entry.last_seen) delta = round(time.time() - zha_device.last_seen)
zha_device.available = delta < zha_device.consider_unavailable_time zha_device.available = delta < zha_device.consider_unavailable_time
delta_msg = f"{str(timedelta(seconds=delta))} ago" delta_msg = f"{str(timedelta(seconds=delta))} ago"
_LOGGER.debug( _LOGGER.debug(
@ -210,13 +204,6 @@ class ZHAGateway:
delta_msg, delta_msg,
zha_device.consider_unavailable_time, 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 @callback
def async_load_groups(self) -> None: def async_load_groups(self) -> None:
@ -526,8 +513,6 @@ class ZHAGateway:
model=zha_device.model, model=zha_device.model,
) )
zha_device.set_device_id(device_registry_device.id) 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 return zha_device
@callback @callback
@ -550,17 +535,9 @@ class ZHAGateway:
if device.status is DeviceStatus.INITIALIZED: if device.status is DeviceStatus.INITIALIZED:
device.update_available(available) 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: async def async_device_initialized(self, device: zigpy.device.Device) -> None:
"""Handle device joined and basic information discovered (async).""" """Handle device joined and basic information discovered (async)."""
zha_device = self._async_get_or_create_device(device) 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( _LOGGER.debug(
"device - %s:%s entering async_device_initialized - is_new_join: %s", "device - %s:%s entering async_device_initialized - is_new_join: %s",
device.nwk, 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.types
import zigpy.zdo.types as zdo_t 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.const as zha_const
import homeassistant.components.zha.core.device as zha_core_device import homeassistant.components.zha.core.device as zha_core_device
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -188,26 +187,14 @@ def zha_device_joined(hass, setup_zha):
@pytest.fixture @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.""" """Return a restored ZHA device."""
async def _zha_device(zigpy_dev, last_seen=None): async def _zha_device(zigpy_dev, last_seen=None):
zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev
if last_seen is not None: if last_seen is not None:
hass_storage[f"{DOMAIN}.storage"] = { zigpy_dev.last_seen = last_seen
"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}",
}
],
},
}
await setup_zha() await setup_zha()
zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]

View File

@ -1,7 +1,5 @@
"""Test ZHA Gateway.""" """Test ZHA Gateway."""
import asyncio import asyncio
import math
import time
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -10,10 +8,9 @@ import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.clusters.lighting as lighting
from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.core.group import GroupMember
from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME
from homeassistant.const import Platform 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 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" 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 len(zha_group.members) == 1
assert zha_group.members[0].device is device_light_1 assert zha_group.members[0].device is device_light_1
assert zha_group.group_id == 0x1234 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"]