Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for preset modes in homekit fans #45962

Merged
merged 2 commits into from Feb 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 43 additions & 0 deletions homeassistant/components/homekit/type_fans.py
Expand Up @@ -8,12 +8,15 @@
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,
Expand All @@ -31,11 +34,14 @@
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__)
Expand All @@ -56,6 +62,7 @@ def __init__(self, *args):

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)
Expand All @@ -65,11 +72,13 @@ def __init__(self, *args):
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(
Expand All @@ -86,6 +95,22 @@ def __init__(self, *args):
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)
Expand Down Expand Up @@ -120,6 +145,18 @@ def _set_chars(self, char_values):
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If value is not truthy, what are we achieving by calling turn on, reset preset mode ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no unset preset mode, we are accomplishing this by turning on the fan to its default on state which in turn makes the percentage slider active instead of the preset switches.


def set_state(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
Expand Down Expand Up @@ -193,3 +230,9 @@ def async_update_state(self, new_state):
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)
83 changes: 83 additions & 0 deletions tests/components/homekit/test_type_fans.py
Expand Up @@ -7,11 +7,14 @@
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
Expand Down Expand Up @@ -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