Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion homeassistant/components/zha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/zha/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
16 changes: 14 additions & 2 deletions homeassistant/components/zha/core/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import collections
from datetime import timedelta
import itertools
import logging
import os
Expand All @@ -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 (
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)."""
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/zha/core/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions tests/components/zha/test_gateway.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
"""Test ZHA Gateway."""
from datetime import timedelta
import logging
import time

import pytest
import zigpy.profiles.zha as zha
import zigpy.zcl.clusters.general as general
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__)
Expand Down Expand Up @@ -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
29 changes: 0 additions & 29 deletions tests/ignore_uncaught_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down