mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
Remove ZHA device storage (#74837)
* Remove ZHA device storage * remove storage file if it exists
This commit is contained in:
parent
240a83239a
commit
edaafadde0
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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)
|
@ -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]
|
||||
|
@ -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"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user