From 074f9315d7f0d20837ed19d1846a295120ed63c9 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 20 Jan 2017 22:21:28 -0800 Subject: [PATCH] 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 --- .../components/emulated_hue/hue_api.py | 39 ++++++++++++++++++- homeassistant/components/fan/__init__.py | 7 +++- homeassistant/components/fan/demo.py | 6 +-- homeassistant/components/fan/isy994.py | 8 ++-- homeassistant/components/fan/mqtt.py | 19 +++++---- homeassistant/components/fan/services.yaml | 13 ++++++- homeassistant/components/fan/wink.py | 2 +- tests/components/emulated_hue/test_hue_api.py | 39 ++++++++++++++++++- 8 files changed, 112 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 9b0a2828394..b56be3484fe 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -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 diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index efb7e0b1496..e6da2ff0fd7 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -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: diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index 7ba6b4d67fb..6d24f8d3048 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -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) diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index fd0690f4253..30c1d2ed2a3 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -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 diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 08db5ead26b..4540ce01532 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -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() diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index e729e7f7e89..7862aa9a7c3 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -50,4 +50,15 @@ toggle: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' \ No newline at end of file + 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' diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 066dbfcb561..74fd06e5516 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -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) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 7c73e933fd3..c3888bd9cf7 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -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):