From d9ab1482bc5e4e0e2eea675036aa352e8e38b8d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Feb 2021 17:24:14 -1000 Subject: [PATCH] Add support for preset modes in homekit fans (#45962) --- homeassistant/components/homekit/type_fans.py | 43 ++++++++++ tests/components/homekit/test_type_fans.py | 83 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 306beb89c48..1efb3b6c8be 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -8,12 +8,15 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, @@ -31,11 +34,14 @@ from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, + CHAR_NAME, + CHAR_ON, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, PROP_MIN_STEP, SERV_FANV2, + SERV_SWITCH, ) _LOGGER = logging.getLogger(__name__) @@ -56,6 +62,7 @@ class Fan(HomeAccessory): features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1) + preset_modes = state.attributes.get(ATTR_PRESET_MODES) if features & SUPPORT_DIRECTION: chars.append(CHAR_ROTATION_DIRECTION) @@ -65,11 +72,13 @@ class Fan(HomeAccessory): chars.append(CHAR_ROTATION_SPEED) serv_fan = self.add_preload_service(SERV_FANV2, chars) + self.set_primary_service(serv_fan) self.char_active = serv_fan.configure_char(CHAR_ACTIVE, value=0) self.char_direction = None self.char_speed = None self.char_swing = None + self.preset_mode_chars = {} if CHAR_ROTATION_DIRECTION in chars: self.char_direction = serv_fan.configure_char( @@ -86,6 +95,22 @@ class Fan(HomeAccessory): properties={PROP_MIN_STEP: percentage_step}, ) + if preset_modes: + for preset_mode in preset_modes: + preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_fan.add_linked_service(preset_serv) + preset_serv.configure_char( + CHAR_NAME, value=f"{self.display_name} {preset_mode}" + ) + + self.preset_mode_chars[preset_mode] = preset_serv.configure_char( + CHAR_ON, + value=False, + setter_callback=lambda value, preset_mode=preset_mode: self.set_preset_mode( + value, preset_mode + ), + ) + if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) self.async_update_state(state) @@ -120,6 +145,18 @@ class Fan(HomeAccessory): if CHAR_ROTATION_SPEED in char_values: self.set_percentage(char_values[CHAR_ROTATION_SPEED]) + def set_preset_mode(self, value, preset_mode): + """Set preset_mode if call came from HomeKit.""" + _LOGGER.debug( + "%s: Set preset_mode %s to %d", self.entity_id, preset_mode, value + ) + params = {ATTR_ENTITY_ID: self.entity_id} + if value: + params[ATTR_PRESET_MODE] = preset_mode + self.async_call_service(DOMAIN, SERVICE_SET_PRESET_MODE, params) + else: + self.async_call_service(DOMAIN, SERVICE_TURN_ON, params) + def set_state(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set state to %d", self.entity_id, value) @@ -193,3 +230,9 @@ class Fan(HomeAccessory): hk_oscillating = 1 if oscillating else 0 if self.char_swing.value != hk_oscillating: self.char_swing.set_value(hk_oscillating) + + current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) + for preset_mode, char in self.preset_mode_chars.items(): + hk_value = 1 if preset_mode == current_preset_mode else 0 + if char.value != hk_value: + char.set_value(hk_value) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index baa47462cdc..ba660f2f12d 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -7,11 +7,14 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, ) from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP @@ -557,3 +560,83 @@ async def test_fan_restore(hass, hk_driver, events): assert acc.char_direction is not None assert acc.char_speed is not None assert acc.char_swing is not None + + +async def test_fan_preset_modes(hass, hk_driver, events): + """Test fan with direction.""" + entity_id = "fan.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_PRESET_MODE: "auto", + ATTR_PRESET_MODES: ["auto", "smart"], + }, + ) + await hass.async_block_till_done() + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.preset_mode_chars["auto"].value == 1 + assert acc.preset_mode_chars["smart"].value == 0 + + await acc.run() + await hass.async_block_till_done() + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_PRESET_MODE: "smart", + ATTR_PRESET_MODES: ["auto", "smart"], + }, + ) + await hass.async_block_till_done() + + assert acc.preset_mode_chars["auto"].value == 0 + assert acc.preset_mode_chars["smart"].value == 1 + # Set from HomeKit + call_set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_auto_iid = acc.preset_mode_chars["auto"].to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_auto_iid, + HAP_REPR_VALUE: 1, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_set_preset_mode[0] + assert call_set_preset_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_preset_mode[0].data[ATTR_PRESET_MODE] == "auto" + assert len(events) == 1 + assert events[-1].data["service"] == "set_preset_mode" + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_auto_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert events[-1].data["service"] == "turn_on" + assert len(events) == 2