"""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