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 support
pull/5478/head
Robbie Trencheny 2017-01-20 22:21:28 -08:00 committed by Paulus Schoutsen
parent 2efd7d4e4a
commit 074f9315d7
8 changed files with 112 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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