Added HomeKit fan speed based on speed_list (#19767)
Speed_list needs to be in ascending order.
This commit is contained in:
parent
208ea6eae4
commit
a94a24f6f8
5 changed files with 194 additions and 20 deletions
|
@ -113,6 +113,7 @@ CHAR_OUTLET_IN_USE = 'OutletInUse'
|
|||
CHAR_ON = 'On'
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||
CHAR_ROTATION_SPEED = 'RotationSpeed'
|
||||
CHAR_SATURATION = 'Saturation'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||
|
|
|
@ -4,17 +4,20 @@ import logging
|
|||
from pyhap.const import CATEGORY_FAN
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
|
||||
DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION,
|
||||
SUPPORT_OSCILLATE)
|
||||
ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST,
|
||||
DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE,
|
||||
SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, SUPPORT_DIRECTION,
|
||||
SUPPORT_OSCILLATE, SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
STATE_OFF, STATE_ON)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2)
|
||||
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE,
|
||||
SERV_FANV2)
|
||||
from .util import HomeKitSpeedMapping
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,12 +44,18 @@ class Fan(HomeAccessory):
|
|||
chars.append(CHAR_ROTATION_DIRECTION)
|
||||
if features & SUPPORT_OSCILLATE:
|
||||
chars.append(CHAR_SWING_MODE)
|
||||
if features & SUPPORT_SET_SPEED:
|
||||
speed_list = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_SPEED_LIST)
|
||||
self.speed_mapping = HomeKitSpeedMapping(speed_list)
|
||||
chars.append(CHAR_ROTATION_SPEED)
|
||||
|
||||
serv_fan = self.add_preload_service(SERV_FANV2, chars)
|
||||
self.char_active = serv_fan.configure_char(
|
||||
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
|
||||
|
||||
self.char_direction = None
|
||||
self.char_speed = None
|
||||
self.char_swing = None
|
||||
|
||||
if CHAR_ROTATION_DIRECTION in chars:
|
||||
|
@ -54,6 +63,10 @@ class Fan(HomeAccessory):
|
|||
CHAR_ROTATION_DIRECTION, value=0,
|
||||
setter_callback=self.set_direction)
|
||||
|
||||
if CHAR_ROTATION_SPEED in chars:
|
||||
self.char_speed = serv_fan.configure_char(
|
||||
CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed)
|
||||
|
||||
if CHAR_SWING_MODE in chars:
|
||||
self.char_swing = serv_fan.configure_char(
|
||||
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
|
||||
|
@ -83,6 +96,15 @@ class Fan(HomeAccessory):
|
|||
ATTR_OSCILLATING: oscillating}
|
||||
self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
|
||||
|
||||
@debounce
|
||||
def set_speed(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set speed to %d', self.entity_id, value)
|
||||
speed = self.speed_mapping.speed_to_states(value)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_SPEED: speed}
|
||||
self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update fan after state change."""
|
||||
# Handle State
|
||||
|
@ -104,6 +126,14 @@ class Fan(HomeAccessory):
|
|||
self.char_direction.set_value(hk_direction)
|
||||
self._flag[CHAR_ROTATION_DIRECTION] = False
|
||||
|
||||
# Handle Speed
|
||||
if self.char_speed is not None:
|
||||
speed = new_state.attributes.get(ATTR_SPEED)
|
||||
hk_speed_value = self.speed_mapping.speed_to_homekit(speed)
|
||||
if hk_speed_value is not None and \
|
||||
self.char_speed.value != hk_speed_value:
|
||||
self.char_speed.set_value(hk_speed_value)
|
||||
|
||||
# Handle Oscillating
|
||||
if self.char_swing is not None:
|
||||
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
"""Collection of useful functions for the HomeKit component."""
|
||||
from collections import namedtuple, OrderedDict
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_player
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.components import fan, media_player
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS)
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.temperature as temp_util
|
||||
|
||||
from .const import (
|
||||
CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF,
|
||||
FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET,
|
||||
CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET,
|
||||
TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -110,6 +112,50 @@ def validate_media_player_features(state, feature_list):
|
|||
return True
|
||||
|
||||
|
||||
SpeedRange = namedtuple('SpeedRange', ('start', 'target'))
|
||||
SpeedRange.__doc__ += """ Maps Home Assistant speed \
|
||||
values to percentage based HomeKit speeds.
|
||||
start: Start of the range (inclusive).
|
||||
target: Percentage to use to determine HomeKit percentages \
|
||||
from HomeAssistant speed.
|
||||
"""
|
||||
|
||||
|
||||
class HomeKitSpeedMapping:
|
||||
"""Supports conversion between Home Assistant and HomeKit fan speeds."""
|
||||
|
||||
def __init__(self, speed_list):
|
||||
"""Initialize a new SpeedMapping object."""
|
||||
if speed_list[0] != fan.SPEED_OFF:
|
||||
_LOGGER.warning("%s does not contain the speed setting "
|
||||
"%s as its first element. "
|
||||
"Assuming that %s is equivalent to 'off'.",
|
||||
speed_list, fan.SPEED_OFF, speed_list[0])
|
||||
self.speed_ranges = OrderedDict()
|
||||
list_size = len(speed_list)
|
||||
for index, speed in enumerate(speed_list):
|
||||
# By dividing by list_size -1 the following
|
||||
# desired attributes hold true:
|
||||
# * index = 0 => 0%, equal to "off"
|
||||
# * index = len(speed_list) - 1 => 100 %
|
||||
# * all other indices are equally distributed
|
||||
target = index * 100 / (list_size - 1)
|
||||
start = index * 100 / list_size
|
||||
self.speed_ranges[speed] = SpeedRange(start, target)
|
||||
|
||||
def speed_to_homekit(self, speed):
|
||||
"""Map Home Assistant speed state to HomeKit speed."""
|
||||
speed_range = self.speed_ranges[speed]
|
||||
return speed_range.target
|
||||
|
||||
def speed_to_states(self, speed):
|
||||
"""Map HomeKit speed to Home Assistant speed state."""
|
||||
for state, speed_range in reversed(self.speed_ranges.items()):
|
||||
if speed_range.start <= speed:
|
||||
return state
|
||||
return list(self.speed_ranges.keys())[0]
|
||||
|
||||
|
||||
def show_setup_message(hass, pincode):
|
||||
"""Display persistent notification with setup information."""
|
||||
pin = pincode.decode()
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
"""Test different accessory types: Fans."""
|
||||
from collections import namedtuple
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
|
||||
DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE)
|
||||
ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST,
|
||||
DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SPEED_HIGH, SPEED_LOW,
|
||||
SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED)
|
||||
from homeassistant.components.homekit.const import ATTR_VALUE
|
||||
from homeassistant.components.homekit.util import HomeKitSpeedMapping
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF,
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
@ -39,6 +42,9 @@ async def test_fan_basic(hass, hk_driver, cls, events):
|
|||
assert acc.category == 3 # Fan
|
||||
assert acc.char_active.value == 0
|
||||
|
||||
# If there are no speed_list values, then HomeKit speed is unsupported
|
||||
assert acc.char_speed is None
|
||||
|
||||
await hass.async_add_job(acc.run)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_active.value == 1
|
||||
|
@ -155,3 +161,40 @@ async def test_fan_oscillate(hass, hk_driver, cls, events):
|
|||
assert call_oscillate[1].data[ATTR_OSCILLATING] is True
|
||||
assert len(events) == 2
|
||||
assert events[-1].data[ATTR_VALUE] is True
|
||||
|
||||
|
||||
async def test_fan_speed(hass, hk_driver, cls, events):
|
||||
"""Test fan with speed."""
|
||||
entity_id = 'fan.demo'
|
||||
speed_list = [SPEED_OFF, SPEED_LOW, SPEED_HIGH]
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, ATTR_SPEED: SPEED_OFF,
|
||||
ATTR_SPEED_LIST: speed_list})
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None)
|
||||
assert acc.char_speed.value == 0
|
||||
|
||||
await hass.async_add_job(acc.run)
|
||||
assert acc.speed_mapping.speed_ranges == \
|
||||
HomeKitSpeedMapping(speed_list).speed_ranges
|
||||
|
||||
acc.speed_mapping.speed_to_homekit = Mock(return_value=42)
|
||||
acc.speed_mapping.speed_to_states = Mock(return_value='ludicrous')
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_SPEED: SPEED_HIGH})
|
||||
await hass.async_block_till_done()
|
||||
acc.speed_mapping.speed_to_homekit.assert_called_with(SPEED_HIGH)
|
||||
assert acc.char_speed.value == 42
|
||||
|
||||
# Set from HomeKit
|
||||
call_set_speed = async_mock_service(hass, DOMAIN, 'set_speed')
|
||||
|
||||
await hass.async_add_job(acc.char_speed.client_update_value, 42)
|
||||
await hass.async_block_till_done()
|
||||
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
||||
assert call_set_speed[0]
|
||||
assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_set_speed[0].data[ATTR_SPEED] == 'ludicrous'
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] == 'ludicrous'
|
||||
|
|
|
@ -3,15 +3,14 @@ import pytest
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.homekit.const import (
|
||||
CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF,
|
||||
FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER,
|
||||
CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||
HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER,
|
||||
TYPE_SWITCH, TYPE_VALVE)
|
||||
from homeassistant.components.homekit.util import (
|
||||
convert_to_float, density_to_air_quality, dismiss_setup_message,
|
||||
show_setup_message, temperature_to_homekit, temperature_to_states,
|
||||
HomeKitSpeedMapping, SpeedRange, convert_to_float, density_to_air_quality,
|
||||
dismiss_setup_message, show_setup_message, temperature_to_homekit,
|
||||
temperature_to_states, validate_entity_config as vec,
|
||||
validate_media_player_features)
|
||||
from homeassistant.components.homekit.util import validate_entity_config \
|
||||
as vec
|
||||
from homeassistant.components.persistent_notification import (
|
||||
ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
|
@ -144,3 +143,58 @@ async def test_dismiss_setup_msg(hass):
|
|||
assert call_dismiss_notification
|
||||
assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \
|
||||
HOMEKIT_NOTIFY_ID
|
||||
|
||||
|
||||
def test_homekit_speed_mapping():
|
||||
"""Test if the SpeedRanges from a speed_list are as expected."""
|
||||
# A standard 2-speed fan
|
||||
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||
assert speed_mapping.speed_ranges == {
|
||||
'off': SpeedRange(0, 0),
|
||||
'low': SpeedRange(100 / 3, 50),
|
||||
'high': SpeedRange(200 / 3, 100),
|
||||
}
|
||||
|
||||
# A standard 3-speed fan
|
||||
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'medium', 'high'])
|
||||
assert speed_mapping.speed_ranges == {
|
||||
'off': SpeedRange(0, 0),
|
||||
'low': SpeedRange(100 / 4, 100 / 3),
|
||||
'medium': SpeedRange(200 / 4, 200 / 3),
|
||||
'high': SpeedRange(300 / 4, 100),
|
||||
}
|
||||
|
||||
# a Dyson-like fan with 10 speeds
|
||||
speed_mapping = HomeKitSpeedMapping([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
assert speed_mapping.speed_ranges == {
|
||||
0: SpeedRange(0, 0),
|
||||
1: SpeedRange(10, 100 / 9),
|
||||
2: SpeedRange(20, 200 / 9),
|
||||
3: SpeedRange(30, 300 / 9),
|
||||
4: SpeedRange(40, 400 / 9),
|
||||
5: SpeedRange(50, 500 / 9),
|
||||
6: SpeedRange(60, 600 / 9),
|
||||
7: SpeedRange(70, 700 / 9),
|
||||
8: SpeedRange(80, 800 / 9),
|
||||
9: SpeedRange(90, 100),
|
||||
}
|
||||
|
||||
|
||||
def test_speed_to_homekit():
|
||||
"""Test speed conversion from HA to Homekit."""
|
||||
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||
assert speed_mapping.speed_to_homekit('off') == 0
|
||||
assert speed_mapping.speed_to_homekit('low') == 50
|
||||
assert speed_mapping.speed_to_homekit('high') == 100
|
||||
|
||||
|
||||
def test_speed_to_states():
|
||||
"""Test speed conversion from Homekit to HA."""
|
||||
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||
assert speed_mapping.speed_to_states(0) == 'off'
|
||||
assert speed_mapping.speed_to_states(33) == 'off'
|
||||
assert speed_mapping.speed_to_states(34) == 'low'
|
||||
assert speed_mapping.speed_to_states(50) == 'low'
|
||||
assert speed_mapping.speed_to_states(66) == 'low'
|
||||
assert speed_mapping.speed_to_states(67) == 'high'
|
||||
assert speed_mapping.speed_to_states(100) == 'high'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue