diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index e6da2ff0fd7..a0079d723d6 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -4,7 +4,9 @@ Provides functionality to interact with fans. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan/ """ +import asyncio from datetime import timedelta +import functools as ft import logging import os @@ -24,6 +26,8 @@ import homeassistant.helpers.config_validation as cv DOMAIN = 'fan' SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) + GROUP_NAME_ALL_FANS = 'all fans' ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS) @@ -88,7 +92,32 @@ FAN_SET_DIRECTION_SCHEMA = vol.Schema({ vol.Optional(ATTR_DIRECTION): cv.string }) # type: dict -_LOGGER = logging.getLogger(__name__) +SERVICE_TO_METHOD = { + SERVICE_TURN_ON: { + 'method': 'async_turn_on', + 'schema': FAN_TURN_ON_SCHEMA, + }, + SERVICE_TURN_OFF: { + 'method': 'async_turn_off', + 'schema': FAN_TURN_OFF_SCHEMA, + }, + SERVICE_TOGGLE: { + 'method': 'async_toggle', + 'schema': FAN_TOGGLE_SCHEMA, + }, + SERVICE_SET_SPEED: { + 'method': 'async_set_speed', + 'schema': FAN_SET_SPEED_SCHEMA, + }, + SERVICE_OSCILLATE: { + 'method': 'async_oscillate', + 'schema': FAN_OSCILLATE_SCHEMA, + }, + SERVICE_SET_DIRECTION: { + 'method': 'async_set_direction', + 'schema': FAN_SET_DIRECTION_SCHEMA, + }, +} def is_on(hass, entity_id: str=None) -> bool: @@ -164,60 +193,53 @@ def set_direction(hass, entity_id: str=None, direction: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) -def setup(hass, config: dict) -> None: +@asyncio.coroutine +def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) - component.setup(config) - def handle_fan_service(service: str) -> None: + yield from component.async_setup(config) + + @asyncio.coroutine + def async_handle_fan_service(service): """Hande service call for fans.""" - # Get the validated data + method = SERVICE_TO_METHOD.get(service.service) params = service.data.copy() # Convert the entity ids to valid fan ids - target_fans = component.extract_from_service(service) + target_fans = component.async_extract_from_service(service) params.pop(ATTR_ENTITY_ID, None) - service_fun = None - for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_SET_SPEED, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION]: - if service_def == service.service: - service_fun = service_def - break + for fan in target_fans: + yield from getattr(fan, method['method'])(**params) - if service_fun: - for fan in target_fans: - getattr(fan, service_fun)(**params) + update_tasks = [] - for fan in target_fans: - if fan.should_poll: - fan.update_ha_state(True) - return + for fan in target_fans: + if not fan.should_poll: + continue + + update_coro = hass.loop.create_task( + fan.async_update_ha_state(True)) + if hasattr(fan, 'async_update'): + update_tasks.append(update_coro) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) # Listen for fan service calls. - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_fan_service, - descriptions.get(SERVICE_TURN_ON), - schema=FAN_TURN_ON_SCHEMA) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_fan_service, - descriptions.get(SERVICE_TURN_OFF), - schema=FAN_TURN_OFF_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_SPEED, handle_fan_service, - descriptions.get(SERVICE_SET_SPEED), - schema=FAN_SET_SPEED_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_OSCILLATE, handle_fan_service, - descriptions.get(SERVICE_OSCILLATE), - schema=FAN_OSCILLATE_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_SET_DIRECTION, handle_fan_service, - descriptions.get(SERVICE_SET_DIRECTION), - schema=FAN_SET_DIRECTION_SCHEMA) + for service_name in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service_name].get('schema') + hass.services.async_register( + DOMAIN, service_name, async_handle_fan_service, + descriptions.get(service_name), schema=schema) return True @@ -225,34 +247,57 @@ def setup(hass, config: dict) -> None: class FanEntity(ToggleEntity): """Representation of a fan.""" - # pylint: disable=no-self-use - 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 async_set_speed(self: ToggleEntity, speed: str): + """Set the speed of the fan. + + This method must be run in the event loop and returns a coroutine. + """ + if speed is SPEED_OFF: + return self.async_turn_off() + return self.hass.loop.run_in_executor(None, self.set_speed, speed) + def set_direction(self: ToggleEntity, direction: str) -> None: """Set the direction of the fan.""" raise NotImplementedError() + def async_set_direction(self: ToggleEntity, direction: str): + """Set the direction of the fan. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, self.set_direction, direction) + 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: - """Turn off the fan.""" - raise NotImplementedError() + def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs): + """Turn on the fan. + + This method must be run in the event loop and returns a coroutine. + """ + if speed is SPEED_OFF: + return self.async_turn_off() + return self.hass.loop.run_in_executor( + None, ft.partial(self.turn_on, speed, **kwargs)) def oscillate(self: ToggleEntity, oscillating: bool) -> None: """Oscillate the fan.""" pass + def async_oscillate(self: ToggleEntity, oscillating: bool): + """Oscillate the fan. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, self.oscillate, oscillating) + @property def is_on(self): """Return true if the entity is on.""" diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index 6d24f8d3048..931f4914552 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -56,8 +56,10 @@ class DemoFan(FanEntity): """Get the list of available speeds.""" return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - def turn_on(self, speed: str=SPEED_MEDIUM) -> None: + def turn_on(self, speed: str=None) -> None: """Turn on the entity.""" + if speed is None: + speed = SPEED_MEDIUM self.set_speed(speed) def turn_off(self) -> None: @@ -68,17 +70,17 @@ class DemoFan(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" self._speed = speed - self.update_ha_state() + self.schedule_update_ha_state() def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" self.direction = direction - self.update_ha_state() + self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" self.oscillating = oscillating - self.update_ha_state() + self.schedule_update_ha_state() @property def current_direction(self) -> str: diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 4ca1fc8bae4..1d5b7609897 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -150,7 +150,7 @@ class MqttFan(FanEntity): elif payload == self._payload[STATE_OFF]: self._state = False - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC], @@ -165,7 +165,7 @@ class MqttFan(FanEntity): self._speed = SPEED_MEDIUM elif payload == self._payload[SPEED_HIGH]: self._speed = SPEED_HIGH - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC], @@ -183,7 +183,7 @@ class MqttFan(FanEntity): self._oscillation = True elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: self._oscillation = False - self.update_ha_state() + self.schedule_update_ha_state() if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: mqtt.subscribe(self._hass, @@ -262,7 +262,7 @@ class MqttFan(FanEntity): self._speed = speed mqtt.publish(self._hass, self._topic[CONF_SPEED_COMMAND_TOPIC], mqtt_payload, self._qos, self._retain) - self.update_ha_state() + self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" @@ -274,4 +274,4 @@ class MqttFan(FanEntity): mqtt.publish(self._hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], payload, self._qos, self._retain) - self.update_ha_state() + self.schedule_update_ha_state()