core/tests/components/zha/test_device.py

308 lines
9.8 KiB
Python

"""Test zha device switch."""
from datetime import timedelta
import time
from unittest import mock
from unittest.mock import patch
import pytest
import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
from homeassistant.components.zha.core.const import (
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
)
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
import homeassistant.helpers.device_registry as dr
import homeassistant.util.dt as dt_util
from .common import async_enable_traffic, make_zcl_header
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE
from tests.common import async_fire_time_changed
@pytest.fixture
def zigpy_device(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: {
SIG_EP_INPUT: in_clusters,
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
return zigpy_device_mock(endpoints)
return _dev
@pytest.fixture
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: {
SIG_EP_INPUT: in_clusters,
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
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_mains(with_basic_channel=True)
@pytest.fixture
def device_without_basic_channel(zigpy_device):
"""Return a zha device with a basic channel present."""
return zigpy_device(with_basic_channel=False)
@pytest.fixture
async def ota_zha_device(zha_device_restored, zigpy_device_mock):
"""ZHA device with OTA cluster fixture."""
zigpy_dev = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [general.Basic.cluster_id],
SIG_EP_OUTPUT: [general.Ota.cluster_id],
SIG_EP_TYPE: 0x1234,
}
},
"00:11:22:33:44:55:66:77",
"test manufacturer",
"test model",
)
zha_device = await zha_device_restored(zigpy_dev)
return zha_device
def _send_time_changed(hass, seconds):
"""Send a time changed event."""
now = dt_util.utcnow() + timedelta(seconds=seconds)
async_fire_time_changed(hass, now)
@patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
new=mock.MagicMock(),
)
async def test_check_available_success(
hass, device_with_basic_channel, zha_device_restored
):
"""Check device availability success on 1st try."""
# pylint: disable=protected-access
zha_device = await zha_device_restored(device_with_basic_channel)
await async_enable_traffic(hass, [zha_device])
basic_ch = device_with_basic_channel.endpoints[3].basic
basic_ch.read_attributes.reset_mock()
device_with_basic_channel.last_seen = None
assert zha_device.available is True
_send_time_changed(hass, zha_device.consider_unavailable_time + 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_device.consider_unavailable_time - 2
)
_seens = [time.time(), device_with_basic_channel.last_seen]
def _update_last_seen(*args, **kwargs):
device_with_basic_channel.last_seen = _seens.pop()
basic_ch.read_attributes.side_effect = _update_last_seen
# successfully ping zigpy device, but zha_device is not yet available
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 1
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is False
# There was traffic from the device: pings, but not yet available
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 2
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is False
# There was traffic from the device: don't try to ping, marked as available
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 2
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is True
@patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
new=mock.MagicMock(),
)
async def test_check_available_unsuccessful(
hass, device_with_basic_channel, zha_device_restored
):
"""Check device availability all tries fail."""
# pylint: disable=protected-access
zha_device = await zha_device_restored(device_with_basic_channel)
await async_enable_traffic(hass, [zha_device])
basic_ch = device_with_basic_channel.endpoints[3].basic
assert zha_device.available is True
assert basic_ch.read_attributes.await_count == 0
device_with_basic_channel.last_seen = (
time.time() - zha_device.consider_unavailable_time - 2
)
# unsuccessfuly ping zigpy device, but zha_device is still available
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 1
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is True
# still no traffic, but zha_device is still available
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 2
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is True
# not even trying to update, device is unavailable
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert basic_ch.read_attributes.await_count == 2
assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"]
assert zha_device.available is False
@patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
new=mock.MagicMock(),
)
async def test_check_available_no_basic_channel(
hass, device_without_basic_channel, zha_device_restored, caplog
):
"""Check device availability for a device without basic cluster."""
# pylint: disable=protected-access
zha_device = await zha_device_restored(device_without_basic_channel)
await async_enable_traffic(hass, [zha_device])
assert zha_device.available is True
device_without_basic_channel.last_seen = (
time.time() - zha_device.consider_unavailable_time - 2
)
assert "does not have a mandatory basic cluster" not in caplog.text
_send_time_changed(hass, 91)
await hass.async_block_till_done()
assert zha_device.available is False
assert "does not have a mandatory basic cluster" in caplog.text
async def test_ota_sw_version(hass, ota_zha_device):
"""Test device entry gets sw_version updated via OTA channel."""
ota_ch = ota_zha_device.channels.pools[0].client_channels["1:0x0019"]
dev_registry = dr.async_get(hass)
entry = dev_registry.async_get(ota_zha_device.device_id)
assert entry.sw_version is None
cluster = ota_ch.cluster
hdr = make_zcl_header(1, global_command=False)
sw_version = 0x2345
cluster.handle_message(hdr, [1, 2, 3, sw_version, None])
await hass.async_block_till_done()
entry = dev_registry.async_get(ota_zha_device.device_id)
assert int(entry.sw_version, base=16) == sw_version
@pytest.mark.parametrize(
"device, last_seen_delta, is_available",
(
("zigpy_device", 0, True),
(
"zigpy_device",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2,
True,
),
(
"zigpy_device",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2,
True,
),
(
"zigpy_device",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2,
False,
),
("zigpy_device_mains", 0, True),
(
"zigpy_device_mains",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS - 2,
True,
),
(
"zigpy_device_mains",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2,
False,
),
(
"zigpy_device_mains",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2,
False,
),
(
"zigpy_device_mains",
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2,
False,
),
),
)
async def test_device_restore_availability(
hass, request, device, last_seen_delta, is_available, zha_device_restored
):
"""Test initial availability for restored devices."""
zigpy_device = request.getfixturevalue(device)()
zha_device = await zha_device_restored(
zigpy_device, last_seen=time.time() - last_seen_delta
)
entity_id = "switch.fakemanufacturer_fakemodel_e769900a_on_off"
await hass.async_block_till_done()
# ensure the switch entity was created
assert hass.states.get(entity_id).state is not None
assert zha_device.available is is_available
if is_available:
assert hass.states.get(entity_id).state == STATE_OFF
else:
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE