diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2af35e8fb92eae..8b93faf7882b29 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -127,7 +127,6 @@ async def async_setup_entry(hass, config_entry): async def async_zha_shutdown(event): """Handle shutdown tasks.""" await zha_data[DATA_ZHA_GATEWAY].shutdown() - await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) asyncio.create_task(async_load_entities(hass)) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0a7278cb5d58d9..ad3d1ff18adb66 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -398,7 +398,8 @@ def async_cleanup_handles(self) -> None: @callback def async_update_last_seen(self, last_seen): """Set last seen on the zigpy device.""" - self._zigpy_device.last_seen = last_seen + if self._zigpy_device.last_seen is None and last_seen is not None: + self._zigpy_device.last_seen = last_seen @callback def async_get_info(self): diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index bc7ff42d25f30d..7360a4908b2bd2 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -2,6 +2,7 @@ import asyncio import collections +from datetime import timedelta import itertools import logging import os @@ -20,6 +21,7 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.event import async_track_time_interval from . import discovery, typing as zha_typing from .const import ( @@ -80,6 +82,7 @@ from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType _LOGGER = logging.getLogger(__name__) +_STORAGE_UPDATE_INTERVAL = timedelta(minutes=15) EntityReference = collections.namedtuple( "EntityReference", @@ -110,6 +113,7 @@ def __init__(self, hass, config, config_entry): self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) self._config_entry = config_entry + self._unsubs = [] async def async_initialize(self): """Initialize controller and connect radio.""" @@ -163,6 +167,11 @@ async def async_initialize(self): ) self.async_load_devices() self.async_load_groups() + self._unsubs.append( + async_track_time_interval( + self._hass, self._async_update_device_storage, _STORAGE_UPDATE_INTERVAL + ) + ) @callback def async_load_devices(self) -> None: @@ -463,11 +472,10 @@ def async_update_device(self, sender: zigpy_dev.Device, available: bool = True): if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self): + def _async_update_device_storage(self, *_): """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) - await self.zha_storage.async_save() async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): """Handle device joined and basic information discovered (async).""" @@ -576,6 +584,10 @@ async def shutdown(self): """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") await self.application_controller.shutdown() + for device in self.devices.values(): + device.async_cleanup_handles() + for unsubscribe in self._unsubs: + unsubscribe() @callback diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 00a4942c7b768f..3838e9b6a5081d 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -78,6 +78,9 @@ def async_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: ieee_str: str = str(device.ieee) old = self.devices[ieee_str] + if old is not None and device.last_seen is None: + return + changes = {} changes["last_seen"] = device.last_seen diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 80d96fa55bdcbe..8dabbe1397f1b1 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,5 +1,7 @@ """Test ZHA Gateway.""" +from datetime import timedelta import logging +import time import pytest import zigpy.profiles.zha as zha @@ -7,9 +9,12 @@ import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.util import dt from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway +from tests.common import async_fire_time_changed + IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" _LOGGER = logging.getLogger(__name__) @@ -167,3 +172,23 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord # the group entity should not have been cleaned up assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + + +async def test_saving_devices_with_delay(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 entry.last_seen == zha_dev_basic.last_seen + + zigpy_dev_basic.last_seen = None + last_seen = time.time() + zha_dev_basic.async_update_last_seen(last_seen) + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == last_seen diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 34323f5b6d4e34..5845f80b9441af 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -157,35 +157,6 @@ ("tests.components.yr.test_sensor", "test_default_setup"), ("tests.components.yr.test_sensor", "test_custom_setup"), ("tests.components.yr.test_sensor", "test_forecast_setup"), - ("tests.components.zha.test_api", "test_device_clusters"), - ("tests.components.zha.test_api", "test_device_cluster_attributes"), - ("tests.components.zha.test_api", "test_device_cluster_commands"), - ("tests.components.zha.test_api", "test_list_devices"), - ("tests.components.zha.test_api", "test_device_not_found"), - ("tests.components.zha.test_api", "test_list_groups"), - ("tests.components.zha.test_api", "test_get_group"), - ("tests.components.zha.test_api", "test_get_group_not_found"), - ("tests.components.zha.test_api", "test_list_groupable_devices"), - ("tests.components.zha.test_api", "test_add_group"), - ("tests.components.zha.test_api", "test_remove_group"), - ("tests.components.zha.test_binary_sensor", "test_binary_sensor"), - ("tests.components.zha.test_cover", "test_cover"), - ("tests.components.zha.test_device_action", "test_get_actions"), - ("tests.components.zha.test_device_action", "test_action"), - ("tests.components.zha.test_device_tracker", "test_device_tracker"), - ("tests.components.zha.test_device_trigger", "test_triggers"), - ("tests.components.zha.test_device_trigger", "test_no_triggers"), - ("tests.components.zha.test_device_trigger", "test_if_fires_on_event"), - ("tests.components.zha.test_device_trigger", "test_exception_no_triggers"), - ("tests.components.zha.test_device_trigger", "test_exception_bad_trigger"), - ("tests.components.zha.test_discover", "test_devices"), - ("tests.components.zha.test_discover", "test_device_override"), - ("tests.components.zha.test_fan", "test_fan"), - ("tests.components.zha.test_gateway", "test_gateway_group_methods"), - ("tests.components.zha.test_light", "test_light"), - ("tests.components.zha.test_lock", "test_lock"), - ("tests.components.zha.test_sensor", "test_sensor"), - ("tests.components.zha.test_switch", "test_switch"), ("tests.components.zwave.test_init", "test_power_schemes"), ( "tests.helpers.test_entity_platform",