From 0993e1c9f2ee59849bc2c375a8d0579080d52bde Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 16:53:31 -0400 Subject: [PATCH 01/11] add HomeKit humidifier/dehumidifier --- .../components/homekit_controller/const.py | 1 + .../homekit_controller/humidifier.py | 224 ++++++++++++++++++ .../homekit_controller/test_humidifier.py | 148 ++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 homeassistant/components/homekit_controller/humidifier.py create mode 100644 tests/components/homekit_controller/test_humidifier.py diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index b1e32417137c38..c7b2c1a76e24de 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -25,6 +25,7 @@ "motion": "binary_sensor", "carbon-dioxide": "sensor", "humidity": "sensor", + "humidifier-dehumidifier": "humidifier", "light": "sensor", "temperature": "sensor", "battery": "sensor", diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py new file mode 100644 index 00000000000000..b4cf2d9ca79f81 --- /dev/null +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -0,0 +1,224 @@ +"""Support for HomeKit Controller humidifier.""" +from typing import Any, Dict, List, Optional + +from aiohomekit.model.characteristics import CharacteristicsTypes + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import ( + ATTR_AVAILABLE_MODES, + ATTR_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, + ATTR_MODE, + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + SUPPORT_MODES, +) +from homeassistant.core import callback + +from . import KNOWN_DEVICES, HomeKitEntity + +SUPPORT_FLAGS = 0 + +ATTR_DEHUMIDIFIER_THRESHOLD = "dehumidifier_threshold" +ATTR_HUMIDIFIER_THRESHOLD = "humidifier_threshold" +ATTR_CURRENT_HUMIDITY = "current_humidity" + +HK_MODE_TO_HA = { + 0: "off", + 1: "auto", + 2: "humidifying", + 3: "dehumidifying", +} + +HA_MODE_TO_HK = { + "auto": 0, + "humidifying": 1, + "dehumidifying": 2, +} + + +class HomeKitHumidifierDehumidifier(HomeKitEntity, HumidifierEntity): + """Representation of a HomeKit Controller Humidifier.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD, + ] + + def _char_threshold_for_current_mode(self): + if self.device_class == DEVICE_CLASS_DEHUMIDIFIER: + return CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + + if self.device_class == DEVICE_CLASS_HUMIDIFIER: + return CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + + return ( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + if self.mode == "dehumidifying" + else CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + + @property + def device_class(self) -> str: + """Return the device class of the device.""" + dehumidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + humidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + + if dehumidifier_threshold is not None and humidifier_threshold is not None: + return None + + if dehumidifier_threshold is not None: + return DEVICE_CLASS_DEHUMIDIFIER + + if humidifier_threshold is not None: + return DEVICE_CLASS_HUMIDIFIER + + return None + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS | SUPPORT_MODES + + @property + def is_on(self): + """Return true if device is on.""" + return self.service.value(CharacteristicsTypes.ACTIVE) + + async def async_turn_on(self, **kwargs): + """Turn the specified valve on.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) + + async def async_turn_off(self, **kwargs): + """Turn the specified valve off.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def capability_attributes(self) -> Dict[str, Any]: + """Return capability attributes.""" + data = { + ATTR_MIN_HUMIDITY: self.min_humidity, + ATTR_MAX_HUMIDITY: self.max_humidity, + ATTR_AVAILABLE_MODES: self.available_modes, + } + + return data + + @property + def state_attributes(self) -> Dict[str, Any]: + """Return the optional state attributes.""" + data = { + ATTR_MODE: self.mode, + ATTR_HUMIDITY: self.target_humidity, + ATTR_CURRENT_HUMIDITY: self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT + ), + } + + dehumidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + if dehumidifier_threshold is not None: + data[ATTR_DEHUMIDIFIER_THRESHOLD] = dehumidifier_threshold + + humidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + if humidifier_threshold is not None: + data[ATTR_HUMIDIFIER_THRESHOLD] = humidifier_threshold + + return data + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + return self.service.value(self._char_threshold_for_current_mode()) + + @property + def mode(self) -> Optional[str]: + """Return the current mode, e.g., home, auto, baby. + + Requires SUPPORT_MODES. + """ + mode = self.service.value( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + return HK_MODE_TO_HA.get(mode, "unknown") + + @property + def available_modes(self) -> Optional[List[str]]: + """Return a list of available modes. + + Requires SUPPORT_MODES. + """ + available_modes = [ + "off", + "auto", + ] + humidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + if humidifier_threshold is not None: + available_modes.append("humidifying") + + dehumidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + if dehumidifier_threshold is not None: + available_modes.append("dehumidifying") + + return available_modes + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.async_put_characteristics( + {self._char_threshold_for_current_mode(): humidity} + ) + + async def async_set_mode(self, mode: str) -> None: + """Set new mode.""" + + if mode == "off": + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + else: + new_mode = HA_MODE_TO_HK.get(mode, "unknown") + await self.async_put_characteristics( + {CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: new_mode} + ) + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return self.service[self._char_threshold_for_current_mode()].minValue + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return self.service[self._char_threshold_for_current_mode()].maxValue + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit humidifer.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_service(aid, service): + if service["stype"] != "humidifier-dehumidifier": + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([HomeKitHumidifierDehumidifier(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py new file mode 100644 index 00000000000000..28844d98538ea1 --- /dev/null +++ b/tests/components/homekit_controller/test_humidifier.py @@ -0,0 +1,148 @@ +"""Basic checks for HomeKit Humidifier/Dehumidifier.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from homeassistant.components.humidifier import DOMAIN + +from tests.components.homekit_controller.common import setup_test_component + +ACTIVE = ("humidifier-dehumidifier", "active") +CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE = ( + "humidifier-dehumidifier", + "humidifier-dehumidifier.state.current", +) +TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE = ( + "humidifier-dehumidifier", + "humidifier-dehumidifier.state.target", +) +RELATIVE_HUMIDITY_CURRENT = ("humidifier-dehumidifier", "relative-humidity.current") +RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD = ( + "humidifier-dehumidifier", + "relative-humidity.humidifier-threshold", +) +RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD = ( + "humidifier-dehumidifier", + "relative-humidity.dehumidifier-threshold", +) + + +def create_humidifier_dehumidifier_service(accessory): + """Define a humidifier characteristics as per page 219 of HAP spec.""" + service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) + + service.add_char(CharacteristicsTypes.ACTIVE, value=False) + + cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) + cur_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + cur_state.value = -1 + + targ_state = service.add_char( + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + targ_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + cur_state.value = 0 + + targ_state = service.add_char( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + targ_state.value = 0 + + return service + + +async def test_humidifier_active_state(hass, utcnow): + """Test that we can turn a HomeKit humidifier on and off again.""" + helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": "humidifier.testdevice"}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": "humidifier.testdevice"}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 0 + + +async def test_humidifier_read_humidity(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + + helper.characteristics[ACTIVE].value = True + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 75 + state = await helper.poll_and_get_state() + assert state.state == "on" + assert state.attributes["humidity"] == 75 + + helper.characteristics[ACTIVE].value = False + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 10 + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["humidity"] == 10 + + +async def test_humidifier_read_only_mode(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "unknown" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "off" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "humidifying" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "dehumidifying" + + +async def test_humidifier_target_humidity_modes(hass, utcnow): + """Test that we can read the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + + helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 37 + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73 + helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + assert state.attributes["humidity"] == 37 + assert state.attributes["current_humidity"] == 51 + assert state.attributes["humidifier_threshold"] == 37 + assert state.attributes["dehumidifier_threshold"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "dehumidifying" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "humidifying" + assert state.attributes["humidity"] == 37 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "off" + assert state.attributes["humidity"] == 37 From c7c80655875e6f9705903f99952f18846877bd47 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 17:50:41 -0400 Subject: [PATCH 02/11] added more test coverage --- .../homekit_controller/test_humidifier.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index 28844d98538ea1..ab8c568e0171b2 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -91,6 +91,42 @@ async def test_humidifier_read_humidity(hass, utcnow): assert state.state == "off" assert state.attributes["humidity"] == 10 + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 + state = await helper.poll_and_get_state() + assert state.attributes["humidity"] == 40 + + +async def test_humidifier_set_humidity(hass, utcnow): + """Test that we can set the state of a HomeKit diffuser accessory.""" + helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_humidity", + {"entity_id": helper.entity_id, "humidity": 20}, + blocking=True, + ) + assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20 + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": "dehumidifying"}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + await helper.poll_and_get_state() + await hass.services.async_call( + DOMAIN, + "set_humidity", + {"entity_id": helper.entity_id, "humidity": 70}, + blocking=True, + ) + assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 70 + async def test_humidifier_read_only_mode(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" From cf18a0fed404be204d520af79486050df7414262 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 22:27:06 -0400 Subject: [PATCH 03/11] simplified char logic Co-authored-by: Quentame --- homeassistant/components/homekit_controller/humidifier.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index b4cf2d9ca79f81..69c964b5df4183 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -53,15 +53,9 @@ def get_characteristic_types(self): ] def _char_threshold_for_current_mode(self): - if self.device_class == DEVICE_CLASS_DEHUMIDIFIER: - return CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - - if self.device_class == DEVICE_CLASS_HUMIDIFIER: - return CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - return ( CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - if self.mode == "dehumidifying" + if self.device_class == DEVICE_CLASS_DEHUMIDIFIER or self.mode == "dehumidifying" else CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD ) From 1759d5995a727f3db21510822d23b2973150ec1c Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 22:43:17 -0400 Subject: [PATCH 04/11] use mode constants --- .../homekit_controller/humidifier.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 69c964b5df4183..876140d8a52dfc 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -12,6 +12,7 @@ ATTR_MODE, DEVICE_CLASS_DEHUMIDIFIER, DEVICE_CLASS_HUMIDIFIER, + MODE_AUTO, SUPPORT_MODES, ) from homeassistant.core import callback @@ -24,17 +25,21 @@ ATTR_HUMIDIFIER_THRESHOLD = "humidifier_threshold" ATTR_CURRENT_HUMIDITY = "current_humidity" +MODE_OFF = "off" +MODE_HUMIDIFYING = "humidifying" +MODE_DEHUMIDIFYING = "dehumidifying" + HK_MODE_TO_HA = { - 0: "off", - 1: "auto", - 2: "humidifying", - 3: "dehumidifying", + 0: MODE_OFF, + 1: MODE_AUTO, + 2: MODE_HUMIDIFYING, + 3: MODE_DEHUMIDIFYING, } HA_MODE_TO_HK = { - "auto": 0, - "humidifying": 1, - "dehumidifying": 2, + MODE_AUTO: 0, + MODE_HUMIDIFYING: 1, + MODE_DEHUMIDIFYING: 2, } @@ -55,7 +60,8 @@ def get_characteristic_types(self): def _char_threshold_for_current_mode(self): return ( CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - if self.device_class == DEVICE_CLASS_DEHUMIDIFIER or self.mode == "dehumidifying" + if self.device_class == DEVICE_CLASS_DEHUMIDIFIER + or self.mode == MODE_DEHUMIDIFYING else CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD ) @@ -157,20 +163,20 @@ def available_modes(self) -> Optional[List[str]]: Requires SUPPORT_MODES. """ available_modes = [ - "off", - "auto", + MODE_OFF, + MODE_AUTO, ] humidifier_threshold = self.service.value( CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD ) if humidifier_threshold is not None: - available_modes.append("humidifying") + available_modes.append(MODE_HUMIDIFYING) dehumidifier_threshold = self.service.value( CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD ) if dehumidifier_threshold is not None: - available_modes.append("dehumidifying") + available_modes.append(MODE_DEHUMIDIFYING) return available_modes @@ -183,7 +189,7 @@ async def async_set_humidity(self, humidity: int) -> None: async def async_set_mode(self, mode: str) -> None: """Set new mode.""" - if mode == "off": + if mode == MODE_OFF: await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) else: new_mode = HA_MODE_TO_HK.get(mode, "unknown") From 32907fb5c6fb7f5116ea928ebc4c1dff01f63cd7 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 22:51:14 -0400 Subject: [PATCH 05/11] Renamed HomeKit Contorller Co-authored-by: Quentame --- homeassistant/components/homekit_controller/humidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 876140d8a52dfc..fd3e37f7b6c27e 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -43,7 +43,7 @@ } -class HomeKitHumidifierDehumidifier(HomeKitEntity, HumidifierEntity): +class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" def get_characteristic_types(self): From b4d8f08afc14741a7acc716b38f9543cda66c935 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 24 Oct 2020 23:03:58 -0400 Subject: [PATCH 06/11] improved threshold logic --- .../homekit_controller/humidifier.py | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 876140d8a52dfc..12cd40ddd4dd11 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -43,9 +43,24 @@ } -class HomeKitHumidifierDehumidifier(HomeKitEntity, HumidifierEntity): +class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" + def __init__(self, accessory, devinfo): + """Initialize a HomeKit humidifier entity.""" + super().__init__(accessory, devinfo) + + dehumidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + + humidifier_threshold = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) + + self._has_dehumidifier_threshold = dehumidifier_threshold is not None + self._has_humidifier_threshold = humidifier_threshold is not None + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -68,20 +83,13 @@ def _char_threshold_for_current_mode(self): @property def device_class(self) -> str: """Return the device class of the device.""" - dehumidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ) - humidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ) - - if dehumidifier_threshold is not None and humidifier_threshold is not None: + if self._has_dehumidifier_threshold and self._has_humidifier_threshold: return None - if dehumidifier_threshold is not None: + if self._has_dehumidifier_threshold: return DEVICE_CLASS_DEHUMIDIFIER - if humidifier_threshold is not None: + if self._has_humidifier_threshold: return DEVICE_CLASS_HUMIDIFIER return None @@ -126,17 +134,15 @@ def state_attributes(self) -> Dict[str, Any]: ), } - dehumidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ) - if dehumidifier_threshold is not None: - data[ATTR_DEHUMIDIFIER_THRESHOLD] = dehumidifier_threshold + if self._has_dehumidifier_threshold: + data[ATTR_DEHUMIDIFIER_THRESHOLD] = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) - humidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ) - if humidifier_threshold is not None: - data[ATTR_HUMIDIFIER_THRESHOLD] = humidifier_threshold + if self._has_humidifier_threshold: + data[ATTR_HUMIDIFIER_THRESHOLD] = self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ) return data @@ -166,16 +172,10 @@ def available_modes(self) -> Optional[List[str]]: MODE_OFF, MODE_AUTO, ] - humidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ) - if humidifier_threshold is not None: + if self._has_humidifier_threshold: available_modes.append(MODE_HUMIDIFYING) - dehumidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ) - if dehumidifier_threshold is not None: + if self._has_dehumidifier_threshold: available_modes.append(MODE_DEHUMIDIFYING) return available_modes @@ -218,7 +218,7 @@ def async_add_service(aid, service): if service["stype"] != "humidifier-dehumidifier": return False info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitHumidifierDehumidifier(conn, info)], True) + async_add_entities([HomeKitHumidifier(conn, info)], True) return True conn.add_listener(async_add_service) From f20e37e1bbfe6b1d7cf4857f3adde02cf0b3b6ef Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 31 Oct 2020 10:36:17 -0400 Subject: [PATCH 07/11] split up homekit humidifier into 2 entities --- .../homekit_controller/humidifier.py | 269 +++++++++++++----- .../homekit_controller/test_humidifier.py | 43 +-- 2 files changed, 209 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 12cd40ddd4dd11..de13f28e5e0db1 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -13,6 +13,7 @@ DEVICE_CLASS_DEHUMIDIFIER, DEVICE_CLASS_HUMIDIFIER, MODE_AUTO, + MODE_NORMAL, SUPPORT_MODES, ) from homeassistant.core import callback @@ -21,45 +22,148 @@ SUPPORT_FLAGS = 0 -ATTR_DEHUMIDIFIER_THRESHOLD = "dehumidifier_threshold" -ATTR_HUMIDIFIER_THRESHOLD = "humidifier_threshold" -ATTR_CURRENT_HUMIDITY = "current_humidity" - -MODE_OFF = "off" -MODE_HUMIDIFYING = "humidifying" -MODE_DEHUMIDIFYING = "dehumidifying" - HK_MODE_TO_HA = { - 0: MODE_OFF, + 0: "off", 1: MODE_AUTO, - 2: MODE_HUMIDIFYING, - 3: MODE_DEHUMIDIFYING, + 2: "humidifying", + 3: "dehumidifying", } HA_MODE_TO_HK = { MODE_AUTO: 0, - MODE_HUMIDIFYING: 1, - MODE_DEHUMIDIFYING: 2, + "humidifying": 1, + "dehumidifying": 2, } class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" - def __init__(self, accessory, devinfo): - """Initialize a HomeKit humidifier entity.""" - super().__init__(accessory, devinfo) + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, + ] + + @property + def device_class(self) -> str: + """Return the device class of the device.""" + return DEVICE_CLASS_HUMIDIFIER - dehumidifier_threshold = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS | SUPPORT_MODES + + @property + def is_on(self): + """Return true if device is on.""" + return self.service.value(CharacteristicsTypes.ACTIVE) + + async def async_turn_on(self, **kwargs): + """Turn the specified valve on.""" + await self.async_put_characteristics( + { + CharacteristicsTypes.ACTIVE: True, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + } ) - humidifier_threshold = self.service.value( + async def async_turn_off(self, **kwargs): + """Turn the specified valve off.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def capability_attributes(self) -> Dict[str, Any]: + """Return capability attributes.""" + data = { + ATTR_MIN_HUMIDITY: self.min_humidity, + ATTR_MAX_HUMIDITY: self.max_humidity, + ATTR_AVAILABLE_MODES: self.available_modes, + } + + return data + + @property + def state_attributes(self) -> Dict[str, Any]: + """Return the optional state attributes.""" + data = { + ATTR_MODE: self.mode, + ATTR_HUMIDITY: self.target_humidity, + } + + return data + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + return self.service.value( CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD ) - self._has_dehumidifier_threshold = dehumidifier_threshold is not None - self._has_humidifier_threshold = humidifier_threshold is not None + @property + def mode(self) -> Optional[str]: + """Return the current mode, e.g., home, auto, baby. + + Requires SUPPORT_MODES. + """ + mode = self.service.value( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + return MODE_AUTO if mode == 1 else MODE_NORMAL + + @property + def available_modes(self) -> Optional[List[str]]: + """Return a list of available modes. + + Requires SUPPORT_MODES. + """ + available_modes = [ + MODE_NORMAL, + MODE_AUTO, + ] + + return available_modes + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.async_put_characteristics( + {CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: humidity} + ) + + async def async_set_mode(self, mode: str) -> None: + """Set new mode.""" + if mode == MODE_AUTO: + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + CharacteristicsTypes.ACTIVE: True, + } + ) + else: + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].minValue + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].maxValue + + +class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): + """Representation of a HomeKit Controller Humidifier.""" def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" @@ -72,27 +176,10 @@ def get_characteristic_types(self): CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD, ] - def _char_threshold_for_current_mode(self): - return ( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - if self.device_class == DEVICE_CLASS_DEHUMIDIFIER - or self.mode == MODE_DEHUMIDIFYING - else CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ) - @property def device_class(self) -> str: """Return the device class of the device.""" - if self._has_dehumidifier_threshold and self._has_humidifier_threshold: - return None - - if self._has_dehumidifier_threshold: - return DEVICE_CLASS_DEHUMIDIFIER - - if self._has_humidifier_threshold: - return DEVICE_CLASS_HUMIDIFIER - - return None + return DEVICE_CLASS_DEHUMIDIFIER @property def supported_features(self): @@ -106,7 +193,12 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the specified valve on.""" - await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) + await self.async_put_characteristics( + { + CharacteristicsTypes.ACTIVE: True, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + } + ) async def async_turn_off(self, **kwargs): """Turn the specified valve off.""" @@ -129,27 +221,16 @@ def state_attributes(self) -> Dict[str, Any]: data = { ATTR_MODE: self.mode, ATTR_HUMIDITY: self.target_humidity, - ATTR_CURRENT_HUMIDITY: self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT - ), } - if self._has_dehumidifier_threshold: - data[ATTR_DEHUMIDIFIER_THRESHOLD] = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ) - - if self._has_humidifier_threshold: - data[ATTR_HUMIDIFIER_THRESHOLD] = self.service.value( - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ) - return data @property def target_humidity(self) -> Optional[int]: """Return the humidity we try to reach.""" - return self.service.value(self._char_threshold_for_current_mode()) + return self.service.value( + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) @property def mode(self) -> Optional[str]: @@ -160,7 +241,7 @@ def mode(self) -> Optional[str]: mode = self.service.value( CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE ) - return HK_MODE_TO_HA.get(mode, "unknown") + return MODE_AUTO if mode == 1 else MODE_NORMAL @property def available_modes(self) -> Optional[List[str]]: @@ -169,43 +250,49 @@ def available_modes(self) -> Optional[List[str]]: Requires SUPPORT_MODES. """ available_modes = [ - MODE_OFF, + MODE_NORMAL, MODE_AUTO, ] - if self._has_humidifier_threshold: - available_modes.append(MODE_HUMIDIFYING) - - if self._has_dehumidifier_threshold: - available_modes.append(MODE_DEHUMIDIFYING) return available_modes async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" await self.async_put_characteristics( - {self._char_threshold_for_current_mode(): humidity} + {CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: humidity} ) async def async_set_mode(self, mode: str) -> None: """Set new mode.""" - - if mode == MODE_OFF: - await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) - else: - new_mode = HA_MODE_TO_HK.get(mode, "unknown") + if mode == MODE_AUTO: await self.async_put_characteristics( - {CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: new_mode} + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + CharacteristicsTypes.ACTIVE: True, + } ) + else: + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @property def min_humidity(self) -> int: """Return the minimum humidity.""" - return self.service[self._char_threshold_for_current_mode()].minValue + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].minValue @property def max_humidity(self) -> int: """Return the maximum humidity.""" - return self.service[self._char_threshold_for_current_mode()].maxValue + return self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].maxValue + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-{self._iid}-{self.device_class}" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -213,12 +300,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + def get_accessory(conn, aid): + for acc in conn.accessories: + if acc.get("aid") == aid: + return acc + return None + + def get_service(acc, iid): + for serv in acc.get("services"): + if serv.get("iid") == iid: + return serv + return None + + def get_char(serv, iid): + try: + type_name = CharacteristicsTypes[iid] + type_uuid = CharacteristicsTypes.get_uuid(type_name) + for char in serv.get("characteristics"): + if char.get("type") == type_uuid: + return char + except KeyError: + return None + return None + @callback def async_add_service(aid, service): if service["stype"] != "humidifier-dehumidifier": return False info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitHumidifier(conn, info)], True) + + acc = get_accessory(conn, aid) + serv = get_service(acc, service["iid"]) + + if ( + get_char(serv, CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD) + is not None + ): + async_add_entities([HomeKitHumidifier(conn, info)], True) + + if ( + get_char( + serv, CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ) + is not None + ): + async_add_entities([HomeKitDehumidifier(conn, info)], True) + return True conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index ab8c568e0171b2..a9c59d4733af43 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -63,13 +63,13 @@ async def test_humidifier_active_state(hass, utcnow): helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": "humidifier.testdevice"}, blocking=True + DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True ) assert helper.characteristics[ACTIVE].value == 1 await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": "humidifier.testdevice"}, blocking=True + DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True ) assert helper.characteristics[ACTIVE].value == 0 @@ -94,7 +94,7 @@ async def test_humidifier_read_humidity(hass, utcnow): helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 state = await helper.poll_and_get_state() - assert state.attributes["humidity"] == 40 + assert state.attributes["humidity"] == 10 async def test_humidifier_set_humidity(hass, utcnow): @@ -109,35 +109,17 @@ async def test_humidifier_set_humidity(hass, utcnow): ) assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20 - await hass.services.async_call( - DOMAIN, - "set_mode", - {"entity_id": helper.entity_id, "mode": "dehumidifying"}, - blocking=True, - ) - assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2 - - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - await helper.poll_and_get_state() - await hass.services.async_call( - DOMAIN, - "set_humidity", - {"entity_id": helper.entity_id, "humidity": 70}, - blocking=True, - ) - assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 70 - async def test_humidifier_read_only_mode(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "unknown" + assert state.attributes["mode"] == "normal" helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "off" + assert state.attributes["mode"] == "normal" helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 state = await helper.poll_and_get_state() @@ -145,11 +127,11 @@ async def test_humidifier_read_only_mode(hass, utcnow): helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "humidifying" + assert state.attributes["mode"] == "normal" helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "dehumidifying" + assert state.attributes["mode"] == "normal" async def test_humidifier_target_humidity_modes(hass, utcnow): @@ -164,21 +146,18 @@ async def test_humidifier_target_humidity_modes(hass, utcnow): state = await helper.poll_and_get_state() assert state.attributes["mode"] == "auto" assert state.attributes["humidity"] == 37 - assert state.attributes["current_humidity"] == 51 - assert state.attributes["humidifier_threshold"] == 37 - assert state.attributes["dehumidifier_threshold"] == 73 helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "dehumidifying" - assert state.attributes["humidity"] == 73 + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 37 helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "humidifying" + assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 state = await helper.poll_and_get_state() - assert state.attributes["mode"] == "off" + assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 From fa2e5bba02d2b9f4c6d16359a35bbcd901ef85ba Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 31 Oct 2020 11:12:49 -0400 Subject: [PATCH 08/11] fixed tests --- .../homekit_controller/test_humidifier.py | 141 ++++++++++++++++-- 1 file changed, 132 insertions(+), 9 deletions(-) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index a9c59d4733af43..c071d1a122bcb7 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -26,7 +26,7 @@ ) -def create_humidifier_dehumidifier_service(accessory): +def create_humidifier_service(accessory): """Define a humidifier characteristics as per page 219 of HAP spec.""" service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) @@ -50,6 +50,28 @@ def create_humidifier_dehumidifier_service(accessory): ) cur_state.value = 0 + return service + + +def create_dehumidifier_service(accessory): + """Define a dehumidifier characteristics as per page 219 of HAP spec.""" + service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) + + service.add_char(CharacteristicsTypes.ACTIVE, value=False) + + cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) + cur_state.value = 0 + + cur_state = service.add_char( + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + cur_state.value = -1 + + targ_state = service.add_char( + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE + ) + targ_state.value = 0 + targ_state = service.add_char( CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD ) @@ -60,7 +82,24 @@ def create_humidifier_dehumidifier_service(accessory): async def test_humidifier_active_state(hass, utcnow): """Test that we can turn a HomeKit humidifier on and off again.""" - helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + helper = await setup_test_component(hass, create_humidifier_service) + + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True + ) + + assert helper.characteristics[ACTIVE].value == 0 + + +async def test_dehumidifier_active_state(hass, utcnow): + """Test that we can turn a HomeKit dehumidifier on and off again.""" + helper = await setup_test_component(hass, create_dehumidifier_service) await hass.services.async_call( DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True @@ -77,7 +116,7 @@ async def test_humidifier_active_state(hass, utcnow): async def test_humidifier_read_humidity(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" - helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + helper = await setup_test_component(hass, create_humidifier_service) helper.characteristics[ACTIVE].value = True helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 75 @@ -92,14 +131,34 @@ async def test_humidifier_read_humidity(hass, utcnow): assert state.attributes["humidity"] == 10 helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 state = await helper.poll_and_get_state() assert state.attributes["humidity"] == 10 +async def test_dehumidifier_read_humidity(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + helper.characteristics[ACTIVE].value = True + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 75 + state = await helper.poll_and_get_state() + assert state.state == "on" + assert state.attributes["humidity"] == 75 + + helper.characteristics[ACTIVE].value = False + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["humidity"] == 40 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["humidity"] == 40 + + async def test_humidifier_set_humidity(hass, utcnow): - """Test that we can set the state of a HomeKit diffuser accessory.""" - helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + """Test that we can set the state of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) await hass.services.async_call( DOMAIN, @@ -110,9 +169,46 @@ async def test_humidifier_set_humidity(hass, utcnow): assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20 +async def test_dehumidifier_set_humidity(hass, utcnow): + """Test that we can set the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_humidity", + {"entity_id": helper.entity_id, "humidity": 20}, + blocking=True, + ) + assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 20 + + async def test_humidifier_read_only_mode(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" - helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + helper = await setup_test_component(hass, create_humidifier_service) + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + + +async def test_dehumidifier_read_only_mode(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) state = await helper.poll_and_get_state() assert state.attributes["mode"] == "normal" @@ -136,10 +232,9 @@ async def test_humidifier_read_only_mode(hass, utcnow): async def test_humidifier_target_humidity_modes(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" - helper = await setup_test_component(hass, create_humidifier_dehumidifier_service) + helper = await setup_test_component(hass, create_humidifier_service) helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 37 - helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73 helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 @@ -161,3 +256,31 @@ async def test_humidifier_target_humidity_modes(hass, utcnow): state = await helper.poll_and_get_state() assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 + + +async def test_dehumidifier_target_humidity_modes(hass, utcnow): + """Test that we can read the state of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73 + helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 + + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "auto" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 + + helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["mode"] == "normal" + assert state.attributes["humidity"] == 73 From bdae947aba83ea089940ce8028e3b068de16132e Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sun, 8 Nov 2020 23:07:40 -0500 Subject: [PATCH 09/11] fixed mode and switch logic --- .../homekit_controller/humidifier.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index de13f28e5e0db1..66b763519bb2c2 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -66,12 +66,7 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the specified valve on.""" - await self.async_put_characteristics( - { - CharacteristicsTypes.ACTIVE: True, - CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, - } - ) + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) async def async_turn_off(self, **kwargs): """Turn the specified valve off.""" @@ -145,7 +140,12 @@ async def async_set_mode(self, mode: str) -> None: } ) else: - await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + CharacteristicsTypes.ACTIVE: True, + } + ) @property def min_humidity(self) -> int: @@ -193,12 +193,7 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the specified valve on.""" - await self.async_put_characteristics( - { - CharacteristicsTypes.ACTIVE: True, - CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, - } - ) + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) async def async_turn_off(self, **kwargs): """Turn the specified valve off.""" @@ -272,7 +267,12 @@ async def async_set_mode(self, mode: str) -> None: } ) else: - await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + CharacteristicsTypes.ACTIVE: True, + } + ) @property def min_humidity(self) -> int: From cbe21aa0f8eb8738bc39ff2ee4aec0321329ab3a Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sun, 8 Nov 2020 23:08:11 -0500 Subject: [PATCH 10/11] added set mode tests --- .../homekit_controller/test_humidifier.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index c071d1a122bcb7..0af795e2ce9416 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -3,6 +3,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.humidifier import DOMAIN +from homeassistant.components.humidifier.const import MODE_AUTO, MODE_NORMAL from tests.components.homekit_controller.common import setup_test_component @@ -182,6 +183,52 @@ async def test_dehumidifier_set_humidity(hass, utcnow): assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 20 +async def test_humidifier_set_mode(hass, utcnow): + """Test that we can set the mode of a HomeKit humidifier accessory.""" + helper = await setup_test_component(hass, create_humidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_AUTO}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 1 + assert helper.characteristics[ACTIVE].value == 1 + + +async def test_dehumidifier_set_mode(hass, utcnow): + """Test that we can set the mode of a HomeKit dehumidifier accessory.""" + helper = await setup_test_component(hass, create_dehumidifier_service) + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_AUTO}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 + assert helper.characteristics[ACTIVE].value == 1 + + await hass.services.async_call( + DOMAIN, + "set_mode", + {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, + blocking=True, + ) + assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2 + assert helper.characteristics[ACTIVE].value == 1 + + async def test_humidifier_read_only_mode(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) From ae9e44158dd2ad8ab26e940bf78af730a184b8d3 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Nov 2020 11:24:04 -0500 Subject: [PATCH 11/11] removed redundant methods present in base class --- .../homekit_controller/humidifier.py | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 66b763519bb2c2..10ffee198e4204 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -1,15 +1,10 @@ """Support for HomeKit Controller humidifier.""" -from typing import Any, Dict, List, Optional +from typing import List, Optional from aiohomekit.model.characteristics import CharacteristicsTypes from homeassistant.components.humidifier import HumidifierEntity from homeassistant.components.humidifier.const import ( - ATTR_AVAILABLE_MODES, - ATTR_HUMIDITY, - ATTR_MAX_HUMIDITY, - ATTR_MIN_HUMIDITY, - ATTR_MODE, DEVICE_CLASS_DEHUMIDIFIER, DEVICE_CLASS_HUMIDIFIER, MODE_AUTO, @@ -72,27 +67,6 @@ async def async_turn_off(self, **kwargs): """Turn the specified valve off.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) - @property - def capability_attributes(self) -> Dict[str, Any]: - """Return capability attributes.""" - data = { - ATTR_MIN_HUMIDITY: self.min_humidity, - ATTR_MAX_HUMIDITY: self.max_humidity, - ATTR_AVAILABLE_MODES: self.available_modes, - } - - return data - - @property - def state_attributes(self) -> Dict[str, Any]: - """Return the optional state attributes.""" - data = { - ATTR_MODE: self.mode, - ATTR_HUMIDITY: self.target_humidity, - } - - return data - @property def target_humidity(self) -> Optional[int]: """Return the humidity we try to reach.""" @@ -199,27 +173,6 @@ async def async_turn_off(self, **kwargs): """Turn the specified valve off.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) - @property - def capability_attributes(self) -> Dict[str, Any]: - """Return capability attributes.""" - data = { - ATTR_MIN_HUMIDITY: self.min_humidity, - ATTR_MAX_HUMIDITY: self.max_humidity, - ATTR_AVAILABLE_MODES: self.available_modes, - } - - return data - - @property - def state_attributes(self) -> Dict[str, Any]: - """Return the optional state attributes.""" - data = { - ATTR_MODE: self.mode, - ATTR_HUMIDITY: self.target_humidity, - } - - return data - @property def target_humidity(self) -> Optional[int]: """Return the humidity we try to reach."""