Skip to content
Merged
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
2 changes: 1 addition & 1 deletion homeassistant/components/zha/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def device_class(self) -> str:
def async_set_state(self, attr_id, attr_name, value):
"""Set the state."""
self._state = bool(value)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_update(self):
"""Attempt to retrieve on off state from the binary sensor."""
Expand Down
16 changes: 11 additions & 5 deletions homeassistant/components/zha/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
from .helpers import LogMixin

_LOGGER = logging.getLogger(__name__)
_KEEP_ALIVE_INTERVAL = 7200
_CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours
_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours
_UPDATE_ALIVE_INTERVAL = (60, 90)
_CHECKIN_GRACE_PERIODS = 2

Expand Down Expand Up @@ -99,8 +100,12 @@ def __init__(
self._zigpy_device.__class__.__module__,
self._zigpy_device.__class__.__name__,
)
if self.is_mains_powered:
self._consider_unavailable_time = _CONSIDER_UNAVAILABLE_MAINS
else:
self._consider_unavailable_time = _CONSIDER_UNAVAILABLE_BATTERY
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
self._available_check = async_track_time_interval(
self._cancel_available_check = async_track_time_interval(
Comment thread
dmulcahey marked this conversation as resolved.
self.hass, self._check_available, timedelta(seconds=keep_alive_interval)
)
self._ha_device_id = None
Expand Down Expand Up @@ -279,7 +284,7 @@ async def _check_available(self, *_):
return

difference = time.time() - self.last_seen
if difference < _KEEP_ALIVE_INTERVAL:
if difference < self._consider_unavailable_time:
self.update_available(True)
self._checkins_missed_count = 0
return
Expand Down Expand Up @@ -363,9 +368,10 @@ async def async_initialize(self, from_cache=False):
self.debug("completed initialization")

@callback
def async_unsub_dispatcher(self):
"""Unsubscribe the dispatcher."""
def async_cleanup_handles(self) -> None:
"""Unsubscribe the dispatchers and timers."""
self._unsub()
self._cancel_available_check()

@callback
def async_update_last_seen(self, last_seen):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zha/core/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def device_removed(self, device):
entity_refs = self._device_registry.pop(device.ieee, None)
if zha_device is not None:
device_info = zha_device.async_get_info()
zha_device.async_unsub_dispatcher()
zha_device.async_cleanup_handles()
async_dispatcher_send(
self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee))
)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/zha/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ def async_set_position(self, attr_id, attr_name, value):
self._state = STATE_CLOSED
elif self._current_position == 100:
self._state = STATE_OPEN
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@callback
def async_update_state(self, state):
"""Handle state update from channel."""
_LOGGER.debug("state=%s", state)
self._state = state
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_open_cover(self, **kwargs):
"""Open the window cover."""
Expand Down Expand Up @@ -134,7 +134,7 @@ async def async_stop_cover(self, **kwargs):
res = await self._cover_channel.stop()
if isinstance(res, list) and res[1] is Status.SUCCESS:
self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_update(self):
"""Attempt to retrieve the open/close state of the cover."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zha/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value):
self.debug("battery_percentage_remaining updated: %s", value)
self._connected = True
self._battery_level = Battery.formatter(value)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@property
def battery_level(self):
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/zha/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ def available(self):
def async_set_available(self, available):
"""Set entity availability."""
self._available = available
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@callback
def async_update_state_attribute(self, key, value):
"""Update a single device state attribute."""
self._device_state_attributes.update({key: value})
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@callback
def async_set_state(self, attr_id, attr_name, value):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zha/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def device_state_attributes(self):
def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel."""
self._state = VALUE_TO_SPEED.get(value, self._state)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on."""
Expand Down
27 changes: 19 additions & 8 deletions homeassistant/components/zha/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
import functools
import logging
import random

from zigpy.zcl.foundation import Status

Expand Down Expand Up @@ -44,9 +45,9 @@
FLASH_EFFECTS = {light.FLASH_SHORT: EFFECT_BLINK, light.FLASH_LONG: EFFECT_BREATHE}

UNSUPPORTED_ATTRIBUTE = 0x86
SCAN_INTERVAL = timedelta(minutes=60)
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN)
PARALLEL_UPDATES = 5
PARALLEL_UPDATES = 0
_REFRESH_INTERVAL = (45, 75)


async def async_setup_entry(hass, config_entry, async_add_entities):
Expand Down Expand Up @@ -81,6 +82,7 @@ def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL)
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
self._identify_channel = self.zha_device.channels.identify_ch
self._cancel_refresh_handle = None

if self._level_channel:
self._supported_features |= light.SUPPORT_BRIGHTNESS
Expand Down Expand Up @@ -130,7 +132,7 @@ def set_level(self, value):
"""
value = max(0, min(254, value))
self._brightness = value
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@property
def hs_color(self):
Expand Down Expand Up @@ -163,7 +165,7 @@ def async_set_state(self, attr_id, attr_name, value):
self._state = bool(value)
if value:
self._off_brightness = None
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Run when about to be added to hass."""
Expand All @@ -175,7 +177,15 @@ async def async_added_to_hass(self):
await self.async_accept_signal(
self._level_channel, SIGNAL_SET_LEVEL, self.set_level
)
async_track_time_interval(self.hass, self.refresh, SCAN_INTERVAL)
refresh_interval = random.randint(*_REFRESH_INTERVAL)
self._cancel_refresh_handle = async_track_time_interval(
self.hass, self._refresh, timedelta(minutes=refresh_interval)
)

async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
self._cancel_refresh_handle()
await super().async_will_remove_from_hass()

@callback
def async_restore_last_state(self, last_state):
Expand Down Expand Up @@ -296,7 +306,7 @@ async def async_turn_on(self, **kwargs):

self._off_brightness = None
self.debug("turned on: %s", t_log)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
Expand All @@ -318,7 +328,7 @@ async def async_turn_off(self, **kwargs):
# store current brightness so that the next turn_on uses it.
self._off_brightness = self._brightness

self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_update(self):
"""Attempt to retrieve on off state from the light."""
Expand Down Expand Up @@ -384,6 +394,7 @@ async def async_get_state(self, from_cache=True):
if color_loop_active == 1:
self._effect = light.EFFECT_COLORLOOP

async def refresh(self, time):
async def _refresh(self, time):
"""Call async_get_state at an interval."""
await self.async_get_state(from_cache=False)
Comment thread
dmulcahey marked this conversation as resolved.
self.async_write_ha_state()
6 changes: 3 additions & 3 deletions homeassistant/components/zha/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ async def async_lock(self, **kwargs):
if not isinstance(result, list) or result[0] is not Status.SUCCESS:
self.error("Error with lock_door: %s", result)
return
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_unlock(self, **kwargs):
"""Unlock the lock."""
result = await self._doorlock_channel.unlock_door()
if not isinstance(result, list) or result[0] is not Status.SUCCESS:
self.error("Error with unlock_door: %s", result)
return
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_update(self):
"""Attempt to retrieve state from the lock."""
Expand All @@ -106,7 +106,7 @@ async def async_update(self):
def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel."""
self._state = VALUE_TO_STATE.get(value, self._state)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_get_state(self, from_cache=True):
"""Attempt to retrieve state from the lock."""
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/zha/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def async_set_state(self, attr_id, attr_name, value):
if value is not None:
value = self.formatter(value)
self._state = value
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@callback
def async_restore_last_state(self, last_state):
Expand Down Expand Up @@ -191,7 +191,7 @@ def async_update_state_attribute(self, key, value):
"""Update a single device state attribute."""
if key == "battery_voltage":
self._device_state_attributes[key] = round(value / 10, 1)
self.async_schedule_update_ha_state()
self.async_write_ha_state()


@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/zha/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,21 @@ async def async_turn_on(self, **kwargs):
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = True
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
result = await self._on_off_channel.off()
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = False
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@callback
def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel."""
self._state = bool(value)
self.async_schedule_update_ha_state()
self.async_write_ha_state()

@property
def device_state_attributes(self):
Expand Down
38 changes: 29 additions & 9 deletions tests/components/zha/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import zigpy.zcl.clusters.general as general

import homeassistant.components.zha.core.device as zha_core_device
import homeassistant.core as ha
import homeassistant.util.dt as dt_util

from .common import async_enable_traffic

from tests.common import async_fire_time_changed


@pytest.fixture
def zigpy_device(zigpy_device_mock):
Expand All @@ -32,9 +33,28 @@ def _dev(with_basic_channel: bool = True):


@pytest.fixture
def device_with_basic_channel(zigpy_device):
def zigpy_device_mains(zigpy_device_mock):
"""Device tracker zigpy device."""

def _dev(with_basic_channel: bool = True):
in_clusters = [general.OnOff.cluster_id]
if with_basic_channel:
in_clusters.append(general.Basic.cluster_id)

endpoints = {
3: {"in_clusters": in_clusters, "out_clusters": [], "device_type": 0}
}
return zigpy_device_mock(
endpoints, node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00"
)

return _dev


@pytest.fixture
def device_with_basic_channel(zigpy_device_mains):
"""Return a zha device with a basic channel present."""
return zigpy_device(with_basic_channel=True)
return zigpy_device_mains(with_basic_channel=True)


@pytest.fixture
Expand All @@ -45,8 +65,8 @@ def device_without_basic_channel(zigpy_device):

def _send_time_changed(hass, seconds):
"""Send a time changed event."""
now = dt_util.utcnow() + timedelta(seconds)
hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
now = dt_util.utcnow() + timedelta(seconds=seconds)
async_fire_time_changed(hass, now)


@asynctest.patch(
Expand All @@ -66,13 +86,13 @@ async def test_check_available_success(
basic_ch.read_attributes.reset_mock()
device_with_basic_channel.last_seen = None
assert zha_device.available is True
_send_time_changed(hass, 61)
_send_time_changed(hass, zha_core_device._CONSIDER_UNAVAILABLE_MAINS + 2)
await hass.async_block_till_done()
assert zha_device.available is False
assert basic_ch.read_attributes.await_count == 0

device_with_basic_channel.last_seen = (
time.time() - zha_core_device._KEEP_ALIVE_INTERVAL - 2
time.time() - zha_core_device._CONSIDER_UNAVAILABLE_MAINS - 2
)
_seens = [time.time(), device_with_basic_channel.last_seen]

Expand Down Expand Up @@ -121,7 +141,7 @@ async def test_check_available_unsuccessful(
assert basic_ch.read_attributes.await_count == 0

device_with_basic_channel.last_seen = (
time.time() - zha_core_device._KEEP_ALIVE_INTERVAL - 2
time.time() - zha_core_device._CONSIDER_UNAVAILABLE_MAINS - 2
)

# unsuccessfuly ping zigpy device, but zha_device is still available
Expand Down Expand Up @@ -162,7 +182,7 @@ async def test_check_available_no_basic_channel(
assert zha_device.available is True

device_without_basic_channel.last_seen = (
time.time() - zha_core_device._KEEP_ALIVE_INTERVAL - 2
time.time() - zha_core_device._CONSIDER_UNAVAILABLE_BATTERY - 2
)

assert "does not have a mandatory basic cluster" not in caplog.text
Expand Down
Loading