diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 7eca4ae4c66..ed7b3d6b293 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -215,6 +215,7 @@ CHAR_SWING_MODE = "SwingMode" CHAR_TARGET_DOOR_STATE = "TargetDoorState" CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" CHAR_TARGET_POSITION = "TargetPosition" +CHAR_TARGET_FAN_STATE = "TargetFanState" CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER = "TargetHumidifierDehumidifierState" CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity" CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 54d6424e8d5..82d254cb9a5 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -39,6 +39,7 @@ from .const import ( CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + CHAR_TARGET_FAN_STATE, PROP_MIN_STEP, SERV_FANV2, SERV_SWITCH, @@ -58,35 +59,38 @@ class Fan(HomeAccessory): def __init__(self, *args): """Initialize a new Fan accessory object.""" super().__init__(*args, category=CATEGORY_FAN) - chars = [] + self.chars = [] state = self.hass.states.get(self.entity_id) 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) + self.preset_modes = state.attributes.get(ATTR_PRESET_MODES) if features & SUPPORT_DIRECTION: - chars.append(CHAR_ROTATION_DIRECTION) + self.chars.append(CHAR_ROTATION_DIRECTION) if features & SUPPORT_OSCILLATE: - chars.append(CHAR_SWING_MODE) + self.chars.append(CHAR_SWING_MODE) if features & SUPPORT_SET_SPEED: - chars.append(CHAR_ROTATION_SPEED) + self.chars.append(CHAR_ROTATION_SPEED) + if self.preset_modes and len(self.preset_modes) == 1: + self.chars.append(CHAR_TARGET_FAN_STATE) - serv_fan = self.add_preload_service(SERV_FANV2, chars) + serv_fan = self.add_preload_service(SERV_FANV2, self.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.char_target_fan_state = None self.preset_mode_chars = {} - if CHAR_ROTATION_DIRECTION in chars: + if CHAR_ROTATION_DIRECTION in self.chars: self.char_direction = serv_fan.configure_char( CHAR_ROTATION_DIRECTION, value=0 ) - if CHAR_ROTATION_SPEED in chars: + if CHAR_ROTATION_SPEED in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. @@ -96,8 +100,13 @@ class Fan(HomeAccessory): properties={PROP_MIN_STEP: percentage_step}, ) - if preset_modes: - for preset_mode in preset_modes: + if self.preset_modes and len(self.preset_modes) == 1: + self.char_target_fan_state = serv_fan.configure_char( + CHAR_TARGET_FAN_STATE, + value=0, + ) + elif self.preset_modes: + for preset_mode in self.preset_modes: preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_fan.add_linked_service(preset_serv) preset_serv.configure_char( @@ -115,7 +124,7 @@ class Fan(HomeAccessory): ), ) - if CHAR_SWING_MODE in chars: + if CHAR_SWING_MODE in self.chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) self.async_update_state(state) serv_fan.setter_callback = self._set_chars @@ -148,6 +157,24 @@ class Fan(HomeAccessory): # get the speed they asked for if CHAR_ROTATION_SPEED in char_values: self.set_percentage(char_values[CHAR_ROTATION_SPEED]) + if CHAR_TARGET_FAN_STATE in char_values: + self.set_single_preset_mode(char_values[CHAR_TARGET_FAN_STATE]) + + def set_single_preset_mode(self, value): + """Set auto call came from HomeKit.""" + params = {ATTR_ENTITY_ID: self.entity_id} + if value: + _LOGGER.debug( + "%s: Set auto to 1 (%s)", self.entity_id, self.preset_modes[0] + ) + params[ATTR_PRESET_MODE] = self.preset_modes[0] + self.async_call_service(DOMAIN, SERVICE_SET_PRESET_MODE, params) + else: + current_state = self.hass.states.get(self.entity_id) + percentage = current_state.attributes.get(ATTR_PERCENTAGE) or 50 + params[ATTR_PERCENTAGE] = percentage + _LOGGER.debug("%s: Set auto to 0", self.entity_id) + self.async_call_service(DOMAIN, SERVICE_TURN_ON, params) def set_preset_mode(self, value, preset_mode): """Set preset_mode if call came from HomeKit.""" @@ -193,6 +220,7 @@ class Fan(HomeAccessory): """Update fan after state change.""" # Handle State state = new_state.state + attributes = new_state.attributes if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 self.char_active.set_value(self._state) @@ -208,7 +236,7 @@ class Fan(HomeAccessory): if self.char_speed is not None and state != STATE_OFF: # We do not change the homekit speed when turning off # as it will clear the restore state - percentage = new_state.attributes.get(ATTR_PERCENTAGE) + percentage = attributes.get(ATTR_PERCENTAGE) # If the homeassistant component reports its speed as the first entry # in its speed list but is not off, the hk_speed_value is 0. But 0 # is a special value in homekit. When you turn on a homekit accessory @@ -227,12 +255,18 @@ class Fan(HomeAccessory): # Handle Oscillating if self.char_swing is not None: - oscillating = new_state.attributes.get(ATTR_OSCILLATING) + oscillating = attributes.get(ATTR_OSCILLATING) if isinstance(oscillating, bool): hk_oscillating = 1 if oscillating else 0 self.char_swing.set_value(hk_oscillating) - current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) + current_preset_mode = attributes.get(ATTR_PRESET_MODE) + if self.char_target_fan_state is not None: + # Handle single preset mode + self.char_target_fan_state.set_value(int(current_preset_mode is not None)) + return + + # Handle multiple preset modes for preset_mode, char in self.preset_mode_chars.items(): hk_value = 1 if preset_mode == current_preset_mode else 0 char.set_value(hk_value) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index c1ce1ffaddb..b520eb7f874 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -567,8 +567,8 @@ async def test_fan_restore(hass, hk_driver, events): assert acc.char_swing is not None -async def test_fan_preset_modes(hass, hk_driver, events): - """Test fan with direction.""" +async def test_fan_multiple_preset_modes(hass, hk_driver, events): + """Test fan with multiple preset modes.""" entity_id = "fan.demo" hass.states.async_set( @@ -645,3 +645,84 @@ async def test_fan_preset_modes(hass, hk_driver, events): assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert events[-1].data["service"] == "turn_on" assert len(events) == 2 + + +async def test_fan_single_preset_mode(hass, hk_driver, events): + """Test fan with a single preset mode.""" + entity_id = "fan.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 42, + ATTR_PRESET_MODE: "smart", + ATTR_PRESET_MODES: ["smart"], + }, + ) + await hass.async_block_till_done() + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_target_fan_state.value == 1 + + await acc.run() + await hass.async_block_till_done() + + # 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_target_fan_state_iid = acc.char_target_fan_state.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_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 call_turn_on[0].data[ATTR_PERCENTAGE] == 42 + assert len(events) == 1 + assert events[-1].data["service"] == "turn_on" + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_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] == "smart" + assert events[-1].data["service"] == "set_preset_mode" + assert len(events) == 2 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 42, + ATTR_PRESET_MODE: None, + ATTR_PRESET_MODES: ["smart"], + }, + ) + await hass.async_block_till_done() + assert acc.char_target_fan_state.value == 0