From fe45935f38274a3c34d6dbf5f9ab06e1d368031d Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 24 May 2020 11:10:16 -0400 Subject: [PATCH] Implement Keen vents as zha cover devices (#36080) * Implement Keen vents as cover devices * Update homeassistant/components/zha/cover.py --- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/cover.py | 30 ++++++++ tests/components/zha/test_cover.py | 69 +++++++++++++++++++ tests/components/zha/zha_devices_list.py | 24 +++---- 4 files changed, 112 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 3a8bcaa148a..c9b3435482b 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -98,7 +98,7 @@ DEVICE_CLASS = { zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: COVER, zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 35d488f2c35..235368080f0 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -1,4 +1,5 @@ """Support for ZHA covers.""" +import asyncio import functools import logging from typing import List, Optional @@ -8,6 +9,7 @@ from zigpy.zcl.foundation import Status from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, + DEVICE_CLASS_DAMPER, DEVICE_CLASS_SHADE, DOMAIN, CoverEntity, @@ -278,3 +280,31 @@ class Shade(ZhaEntity, CoverEntity): if not isinstance(res, list) or res[1] != Status.SUCCESS: self.debug("couldn't stop cover: %s", res) return + + +@STRICT_MATCH( + channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF}, manufacturers="Keen Home Inc" +) +class KeenVent(Shade): + """Keen vent cover.""" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_DAMPER + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + position = self._position or 100 + tasks = [ + self._level_channel.move_to_level_with_on_off(position * 255 / 100, 1), + self._on_off_channel.on(), + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + if any([isinstance(result, Exception) for result in results]): + self.debug("couldn't open cover") + return + + self._is_open = True + self._position = position + self.async_write_ha_state() diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 63623a5ce9e..c3404a2bb83 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -61,6 +61,22 @@ def zigpy_shade_device(zigpy_device_mock): return zigpy_device_mock(endpoints) +@pytest.fixture +def zigpy_keen_vent(zigpy_device_mock): + """Zigpy Keen Vent device.""" + + endpoints = { + 1: { + "device_type": 3, + "in_clusters": [general.LevelControl.cluster_id, general.OnOff.cluster_id], + "out_clusters": [], + } + } + return zigpy_device_mock( + endpoints, manufacturer="Keen Home Inc", model="SV02-612-MP-1.3" + ) + + @patch( "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" ) @@ -306,3 +322,56 @@ async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): # test that the cover was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_OPEN assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 50 + + +async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): + """Test keen vent.""" + + # load up cover domain + zha_device = await zha_device_joined_restored(zigpy_keen_vent) + + cluster_on_off = zigpy_keen_vent.endpoints.get(1).on_off + cluster_level = zigpy_keen_vent.endpoints.get(1).level + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + + # test that the state has changed from unavailable to off + await send_attributes_report(hass, cluster_on_off, {8: 0, 0: False, 1: 1}) + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI command fails + p1 = patch.object(cluster_on_off, "request", side_effect=asyncio.TimeoutError) + p2 = patch.object(cluster_level, "request", AsyncMock(return_value=[4, 0])) + + with p1, p2: + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert cluster_level.request.call_count == 1 + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI command success + p1 = patch.object(cluster_on_off, "request", AsyncMock(return_value=[1, 0])) + p2 = patch.object(cluster_level, "request", AsyncMock(return_value=[4, 0])) + + with p1, p2: + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + await asyncio.sleep(0) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert cluster_level.request.call_count == 1 + assert hass.states.get(entity_id).state == STATE_OPEN + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 100 diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 53e1e845d5d..0b1ec9ae2c6 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -1036,16 +1036,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"], @@ -1094,16 +1094,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"], @@ -1152,16 +1152,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"],