Fan improvements (#5457)
* Remove SPEED_MED from fan * Correctly use the oscillation on/off payloads for MQTT fan * Add set_direction service documentation * Correct function name for Wink fans * Check for existence of the correct topic * Enable set fan speed in emulated_hue * features -> functions * Final emulated_hue fan fixes * Fix linting issues * Revert to supported features instead of supported functions * Fix logic * Add a test for emulated_hue fan supportpull/5478/head
parent
2efd7d4e4a
commit
074f9315d7
|
@ -17,6 +17,10 @@ from homeassistant.components.media_player import (
|
|||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW,
|
||||
SPEED_MEDIUM, SPEED_HIGH
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -174,7 +178,9 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
if (entity_features &
|
||||
SUPPORT_BRIGHTNESS &
|
||||
(entity.domain == "light")) == SUPPORT_BRIGHTNESS:
|
||||
if brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
|
@ -207,6 +213,23 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||
else:
|
||||
service = SERVICE_CLOSE_COVER
|
||||
|
||||
# If the requested entity is a fan, convert to speed
|
||||
elif entity.domain == "fan":
|
||||
functions = entity.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (functions & SUPPORT_SET_SPEED) == SUPPORT_SET_SPEED:
|
||||
if brightness is not None:
|
||||
domain = entity.domain
|
||||
# Convert 0-100 to a fan speed
|
||||
if brightness == 0:
|
||||
data[ATTR_SPEED] = SPEED_OFF
|
||||
elif brightness <= 33.3 and brightness > 0:
|
||||
data[ATTR_SPEED] = SPEED_LOW
|
||||
elif brightness <= 66.6 and brightness > 33.3:
|
||||
data[ATTR_SPEED] = SPEED_MEDIUM
|
||||
elif brightness <= 100 and brightness > 66.6:
|
||||
data[ATTR_SPEED] = SPEED_HIGH
|
||||
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
# Map the off command to on
|
||||
service = SERVICE_TURN_ON
|
||||
|
@ -269,7 +292,9 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
|
||||
elif entity.domain == "script" or entity.domain == "media_player":
|
||||
elif (entity.domain == "script" or
|
||||
entity.domain == "media_player" or
|
||||
entity.domain == "fan"):
|
||||
# Convert 0-255 to 0-100
|
||||
level = brightness / 255 * 100
|
||||
brightness = round(level)
|
||||
|
@ -299,6 +324,16 @@ def get_entity_state(config, entity):
|
|||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
final_brightness = round(min(1.0, level) * 255)
|
||||
elif entity.domain == "fan":
|
||||
speed = entity.attributes.get(ATTR_SPEED, 0)
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
final_brightness = 0
|
||||
if speed == SPEED_LOW:
|
||||
final_brightness = 85
|
||||
elif speed == SPEED_MEDIUM:
|
||||
final_brightness = 170
|
||||
elif speed == SPEED_HIGH:
|
||||
final_brightness = 255
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
# Make sure brightness is valid
|
||||
|
|
|
@ -41,7 +41,6 @@ SERVICE_SET_DIRECTION = 'set_direction'
|
|||
|
||||
SPEED_OFF = 'off'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MED = 'med'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
|
@ -230,6 +229,9 @@ class FanEntity(ToggleEntity):
|
|||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
|
@ -238,6 +240,9 @@ class FanEntity(ToggleEntity):
|
|||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
|
|
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
|||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
FanEntity, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
@ -54,9 +54,9 @@ class DemoFan(FanEntity):
|
|||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
|
||||
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def turn_on(self, speed: str=SPEED_MED) -> None:
|
||||
def turn_on(self, speed: str=SPEED_MEDIUM) -> None:
|
||||
"""Turn on the entity."""
|
||||
self.set_speed(speed)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import logging
|
|||
from typing import Callable
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
|
||||
SPEED_LOW, SPEED_MED,
|
||||
SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF
|
||||
|
@ -20,8 +20,8 @@ VALUE_TO_STATE = {
|
|||
0: SPEED_OFF,
|
||||
63: SPEED_LOW,
|
||||
64: SPEED_LOW,
|
||||
190: SPEED_MED,
|
||||
191: SPEED_MED,
|
||||
190: SPEED_MEDIUM,
|
||||
191: SPEED_MEDIUM,
|
||||
255: SPEED_HIGH,
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ STATE_TO_VALUE = {}
|
|||
for key in VALUE_TO_STATE:
|
||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||
|
||||
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
|
||||
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM,
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH, FanEntity,
|
||||
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
SPEED_OFF, ATTR_SPEED)
|
||||
|
@ -64,11 +64,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF,
|
||||
default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MED): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string,
|
||||
vol.Optional(CONF_SPEED_LIST,
|
||||
default=[SPEED_OFF, SPEED_LOW,
|
||||
SPEED_MED, SPEED_HIGH]): cv.ensure_list,
|
||||
SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
})
|
||||
|
||||
|
@ -162,7 +162,7 @@ class MqttFan(FanEntity):
|
|||
if payload == self._payload[SPEED_LOW]:
|
||||
self._speed = SPEED_LOW
|
||||
elif payload == self._payload[SPEED_MEDIUM]:
|
||||
self._speed = SPEED_MED
|
||||
self._speed = SPEED_MEDIUM
|
||||
elif payload == self._payload[SPEED_HIGH]:
|
||||
self._speed = SPEED_HIGH
|
||||
self.update_ha_state()
|
||||
|
@ -235,7 +235,7 @@ class MqttFan(FanEntity):
|
|||
"""Return the oscillation state."""
|
||||
return self._oscillation
|
||||
|
||||
def turn_on(self, speed: str=SPEED_MED) -> None:
|
||||
def turn_on(self, speed: str=SPEED_MEDIUM) -> None:
|
||||
"""Turn on the entity."""
|
||||
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_ON], self._qos, self._retain)
|
||||
|
@ -252,7 +252,7 @@ class MqttFan(FanEntity):
|
|||
mqtt_payload = SPEED_OFF
|
||||
if speed == SPEED_LOW:
|
||||
mqtt_payload = self._payload[SPEED_LOW]
|
||||
elif speed == SPEED_MED:
|
||||
elif speed == SPEED_MEDIUM:
|
||||
mqtt_payload = self._payload[SPEED_MEDIUM]
|
||||
elif speed == SPEED_HIGH:
|
||||
mqtt_payload = self._payload[SPEED_HIGH]
|
||||
|
@ -265,9 +265,12 @@ class MqttFan(FanEntity):
|
|||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation."""
|
||||
if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None:
|
||||
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None:
|
||||
self._oscillation = oscillating
|
||||
payload = self._payload[OSCILLATE_ON_PAYLOAD]
|
||||
if oscillating is False:
|
||||
payload = self._payload[OSCILLATE_OFF_PAYLOAD]
|
||||
mqtt.publish(self._hass,
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
self._oscillation, self._qos, self._retain)
|
||||
payload, self._qos, self._retain)
|
||||
self.update_ha_state()
|
||||
|
|
|
@ -50,4 +50,15 @@ toggle:
|
|||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to toggle
|
||||
exampl: 'fan.living_room'
|
||||
exampl: 'fan.living_room'
|
||||
|
||||
set_direction:
|
||||
description: Set the fan rotation direction
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to toggle
|
||||
exampl: 'fan.living_room'
|
||||
direction:
|
||||
description: The direction to rotate
|
||||
example: 'left'
|
||||
|
|
|
@ -32,7 +32,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
|
|||
"""Initialize the fan."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
def set_drection(self: ToggleEntity, direction: str) -> None:
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self.wink.set_fan_direction(direction)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
from homeassistant import bootstrap, const, core
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import (
|
||||
emulated_hue, http, light, script, media_player
|
||||
emulated_hue, http, light, script, media_player, fan
|
||||
)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.emulated_hue.hue_api import (
|
||||
|
@ -83,6 +83,15 @@ def hass_hue(loop, hass):
|
|||
]
|
||||
}))
|
||||
|
||||
loop.run_until_complete(
|
||||
bootstrap.async_setup_component(hass, fan.DOMAIN, {
|
||||
'fan': [
|
||||
{
|
||||
'platform': 'demo',
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
# Kitchen light is explicitly excluded from being exposed
|
||||
kitchen_light_entity = hass.states.get('light.kitchen_lights')
|
||||
attrs = dict(kitchen_light_entity.attributes)
|
||||
|
@ -137,6 +146,7 @@ def test_discover_lights(hue_client):
|
|||
assert 'media_player.bedroom' in devices
|
||||
assert 'media_player.walkman' in devices
|
||||
assert 'media_player.lounge_room' in devices
|
||||
assert 'fan.living_room_fan' in devices
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -281,6 +291,33 @@ def test_put_light_state_media_player(hass_hue, hue_client):
|
|||
assert walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL] == level
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_put_light_state_fan(hass_hue, hue_client):
|
||||
"""Test turning on fan and setting speed."""
|
||||
# Turn the fan off first
|
||||
yield from hass_hue.services.async_call(
|
||||
fan.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'fan.living_room_fan'},
|
||||
blocking=True)
|
||||
|
||||
# Emulated hue converts 0-100% to 0-255.
|
||||
level = 23
|
||||
brightness = round(level * 255 / 100)
|
||||
|
||||
fan_result = yield from perform_put_light_state(
|
||||
hass_hue, hue_client,
|
||||
'fan.living_room_fan', True, brightness)
|
||||
|
||||
fan_result_json = yield from fan_result.json()
|
||||
|
||||
assert fan_result.status == 200
|
||||
assert len(fan_result_json) == 2
|
||||
|
||||
living_room_fan = hass_hue.states.get('fan.living_room_fan')
|
||||
assert living_room_fan.state == 'on'
|
||||
assert living_room_fan.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@asyncio.coroutine
|
||||
def test_put_with_form_urlencoded_content_type(hass_hue, hue_client):
|
||||
|
|
Loading…
Reference in New Issue