Add support for auto target fan state in HomeKit fans (#66383)

pull/66869/head^2
J. Nick Koston 2022-02-21 08:08:09 -10:00 committed by GitHub
parent 16cc2b790b
commit f2f2a08966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 132 additions and 16 deletions

View File

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

View File

@ -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)

View File

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