-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Issue/add template fans #12027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue/add template fans #12027
Changes from all commits
440eed3
42944b2
514aaf6
494b7a4
1e05371
4043a14
e9da52f
38e2540
5bfd46a
5cd86aa
b4d97ab
a1b08d6
2ff6183
a5900b2
ed10ec8
7afca9e
da2e18a
f8437df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| """ | ||
| Support for Template fans. | ||
|
|
||
| For more details about this platform, please refer to the documentation | ||
| https://home-assistant.io/components/fan.template/ | ||
| """ | ||
| import logging | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.core import callback | ||
| from homeassistant.const import ( | ||
| CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, | ||
| STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, | ||
| STATE_UNKNOWN) | ||
|
|
||
| from homeassistant.exceptions import TemplateError | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.helpers.config_validation import PLATFORM_SCHEMA | ||
| from homeassistant.helpers.entity import ToggleEntity | ||
| from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, | ||
| SPEED_HIGH, SUPPORT_SET_SPEED, | ||
| SUPPORT_OSCILLATE, FanEntity, | ||
| ATTR_SPEED, ATTR_OSCILLATING, | ||
| ENTITY_ID_FORMAT) | ||
|
|
||
| from homeassistant.helpers.entity import async_generate_entity_id | ||
| from homeassistant.helpers.script import Script | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| CONF_FANS = 'fans' | ||
| CONF_SPEED_LIST = 'speeds' | ||
| CONF_SPEED_TEMPLATE = 'speed_template' | ||
| CONF_OSCILLATING_TEMPLATE = 'oscillating_template' | ||
| CONF_ON_ACTION = 'turn_on' | ||
| CONF_OFF_ACTION = 'turn_off' | ||
| CONF_SET_SPEED_ACTION = 'set_speed' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you should add _ACTION here, just CONF_SET_SPEED, etc.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am using same naming convention as in |
||
| CONF_SET_OSCILLATING_ACTION = 'set_oscillating' | ||
|
|
||
| _VALID_STATES = [STATE_ON, STATE_OFF] | ||
| _VALID_OSC = [True, False] | ||
|
|
||
| FAN_SCHEMA = vol.Schema({ | ||
| vol.Optional(CONF_FRIENDLY_NAME): cv.string, | ||
| vol.Required(CONF_VALUE_TEMPLATE): cv.template, | ||
| vol.Optional(CONF_SPEED_TEMPLATE): cv.template, | ||
| vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, | ||
|
|
||
| vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, | ||
| vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, | ||
|
|
||
| vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, | ||
| vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, | ||
|
|
||
| vol.Optional( | ||
| CONF_SPEED_LIST, | ||
| default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] | ||
| ): cv.ensure_list, | ||
|
|
||
| vol.Optional(CONF_ENTITY_ID): cv.entity_ids | ||
| }) | ||
|
|
||
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
| vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), | ||
| }) | ||
|
|
||
|
|
||
| async def async_setup_platform( | ||
| hass, config, async_add_devices, discovery_info=None | ||
| ): | ||
| """Set up the Template Fans.""" | ||
| fans = [] | ||
|
|
||
| for device, device_config in config[CONF_FANS].items(): | ||
| friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) | ||
|
|
||
| state_template = device_config[CONF_VALUE_TEMPLATE] | ||
| speed_template = device_config.get(CONF_SPEED_TEMPLATE) | ||
| oscillating_template = device_config.get( | ||
| CONF_OSCILLATING_TEMPLATE | ||
| ) | ||
|
|
||
| on_action = device_config[CONF_ON_ACTION] | ||
| off_action = device_config[CONF_OFF_ACTION] | ||
| set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) | ||
| set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) | ||
|
|
||
| speed_list = device_config[CONF_SPEED_LIST] | ||
|
|
||
| entity_ids = set() | ||
| manual_entity_ids = device_config.get(CONF_ENTITY_ID) | ||
|
|
||
| for template in (state_template, speed_template, oscillating_template): | ||
| if template is None: | ||
| continue | ||
| template.hass = hass | ||
|
|
||
| if entity_ids == MATCH_ALL or manual_entity_ids is not None: | ||
| continue | ||
|
|
||
| template_entity_ids = template.extract_entities() | ||
| if template_entity_ids == MATCH_ALL: | ||
| entity_ids = MATCH_ALL | ||
| else: | ||
| entity_ids |= set(template_entity_ids) | ||
|
|
||
| if manual_entity_ids is not None: | ||
| entity_ids = manual_entity_ids | ||
| elif entity_ids != MATCH_ALL: | ||
| entity_ids = list(entity_ids) | ||
|
|
||
| fans.append( | ||
| TemplateFan( | ||
| hass, device, friendly_name, | ||
| state_template, speed_template, oscillating_template, | ||
| on_action, off_action, set_speed_action, | ||
| set_oscillating_action, speed_list, entity_ids | ||
| ) | ||
| ) | ||
|
|
||
| async_add_devices(fans) | ||
|
|
||
|
|
||
| class TemplateFan(FanEntity): | ||
| """A template fan component.""" | ||
|
|
||
| def __init__(self, hass, device_id, friendly_name, | ||
| state_template, speed_template, oscillating_template, | ||
| on_action, off_action, set_speed_action, | ||
| set_oscillating_action, speed_list, entity_ids): | ||
| """Initialize the fan.""" | ||
| self.hass = hass | ||
| self.entity_id = async_generate_entity_id( | ||
| ENTITY_ID_FORMAT, device_id, hass=hass) | ||
| self._name = friendly_name | ||
|
|
||
| self._template = state_template | ||
| self._speed_template = speed_template | ||
| self._oscillating_template = oscillating_template | ||
| self._supported_features = 0 | ||
|
|
||
| self._on_script = Script(hass, on_action) | ||
| self._off_script = Script(hass, off_action) | ||
|
|
||
| self._set_speed_script = None | ||
| if set_speed_action: | ||
| self._set_speed_script = Script(hass, set_speed_action) | ||
|
|
||
| self._set_oscillating_script = None | ||
| if set_oscillating_action: | ||
| self._set_oscillating_script = Script(hass, set_oscillating_action) | ||
|
|
||
| self._state = STATE_OFF | ||
| self._speed = None | ||
| self._oscillating = None | ||
|
|
||
| self._template.hass = self.hass | ||
| if self._speed_template: | ||
| self._speed_template.hass = self.hass | ||
| self._supported_features |= SUPPORT_SET_SPEED | ||
| if self._oscillating_template: | ||
| self._oscillating_template.hass = self.hass | ||
| self._supported_features |= SUPPORT_OSCILLATE | ||
|
|
||
| self._entities = entity_ids | ||
| # List of valid speeds | ||
| self._speed_list = speed_list | ||
|
|
||
| @property | ||
| def name(self): | ||
| """Return the display name of this fan.""" | ||
| return self._name | ||
|
|
||
| @property | ||
| def supported_features(self) -> int: | ||
| """Flag supported features.""" | ||
| return self._supported_features | ||
|
|
||
| @property | ||
| def speed_list(self: ToggleEntity) -> list: | ||
| """Get the list of available speeds.""" | ||
| return self._speed_list | ||
|
|
||
| @property | ||
| def is_on(self): | ||
| """Return true if device is on.""" | ||
| return self._state == STATE_ON | ||
|
|
||
| @property | ||
| def speed(self): | ||
| """Return the current speed.""" | ||
| return self._speed | ||
|
|
||
| @property | ||
| def oscillating(self): | ||
| """Return the oscillation state.""" | ||
| return self._oscillating | ||
|
|
||
| @property | ||
| def should_poll(self): | ||
| """Return the polling state.""" | ||
| return False | ||
|
|
||
| # pylint: disable=arguments-differ | ||
| async def async_turn_on(self, speed: str = None) -> None: | ||
| """Turn on the fan.""" | ||
| await self._on_script.async_run() | ||
| self._state = STATE_ON | ||
|
|
||
| if speed is not None: | ||
| await self.async_set_speed(speed) | ||
|
|
||
| # pylint: disable=arguments-differ | ||
| async def async_turn_off(self) -> None: | ||
| """Turn off the fan.""" | ||
| await self._off_script.async_run() | ||
| self._state = STATE_OFF | ||
|
|
||
| async def async_set_speed(self, speed: str) -> None: | ||
| """Set the speed of the fan.""" | ||
| if self._set_speed_script is None: | ||
| return | ||
|
|
||
| if speed in self._speed_list: | ||
| self._speed = speed | ||
| await self._set_speed_script.async_run({ATTR_SPEED: speed}) | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid speed: %s. ' + | ||
| 'Expected: %s.', | ||
| speed, self._speed_list) | ||
|
|
||
| async def async_oscillate(self, oscillating: bool) -> None: | ||
| """Set oscillation of the fan.""" | ||
| if self._set_oscillating_script is None: | ||
| return | ||
|
|
||
| await self._set_oscillating_script.async_run( | ||
| {ATTR_OSCILLATING: oscillating} | ||
| ) | ||
| self._oscillating = oscillating | ||
|
|
||
| async def async_added_to_hass(self): | ||
| """Register callbacks.""" | ||
| @callback | ||
| def template_fan_state_listener(entity, old_state, new_state): | ||
| """Handle target device state changes.""" | ||
| self.async_schedule_update_ha_state(True) | ||
|
|
||
| @callback | ||
| def template_fan_startup(event): | ||
| """Update template on startup.""" | ||
| self.hass.helpers.event.async_track_state_change( | ||
| self._entities, template_fan_state_listener) | ||
|
|
||
| self.async_schedule_update_ha_state(True) | ||
|
|
||
| self.hass.bus.async_listen_once( | ||
| EVENT_HOMEASSISTANT_START, template_fan_startup) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why we should only call this on
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am doing the same as in |
||
|
|
||
| async def async_update(self): | ||
| """Update the state from the template.""" | ||
| # Update state | ||
| try: | ||
| state = self._template.async_render() | ||
| except TemplateError as ex: | ||
| _LOGGER.error(ex) | ||
| state = None | ||
| self._state = None | ||
|
|
||
| # Validate state | ||
| if state in _VALID_STATES: | ||
| self._state = state | ||
| elif state == STATE_UNKNOWN: | ||
| self._state = None | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid fan is_on state: %s. ' + | ||
| 'Expected: %s.', | ||
| state, ', '.join(_VALID_STATES)) | ||
| self._state = None | ||
|
|
||
| # Update speed if 'speed_template' is configured | ||
| if self._speed_template is not None: | ||
| try: | ||
| speed = self._speed_template.async_render() | ||
| except TemplateError as ex: | ||
| _LOGGER.error(ex) | ||
| speed = None | ||
| self._state = None | ||
|
|
||
| # Validate speed | ||
| if speed in self._speed_list: | ||
| self._speed = speed | ||
| elif speed == STATE_UNKNOWN: | ||
| self._speed = None | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid speed: %s. ' + | ||
| 'Expected: %s.', | ||
| speed, self._speed_list) | ||
| self._speed = None | ||
|
|
||
| # Update oscillating if 'oscillating_template' is configured | ||
| if self._oscillating_template is not None: | ||
| try: | ||
| oscillating = self._oscillating_template.async_render() | ||
| except TemplateError as ex: | ||
| _LOGGER.error(ex) | ||
| self._state = None | ||
|
|
||
| # Validate osc | ||
| if oscillating == 'True' or oscillating is True: | ||
| self._oscillating = True | ||
| elif oscillating == 'False' or oscillating is False: | ||
| self._oscillating = False | ||
| elif oscillating == STATE_UNKNOWN: | ||
| self._oscillating = None | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid oscillating: %s. ' + | ||
| 'Expected: True/False.', oscillating) | ||
| self._oscillating = None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use the consts COMMAND_ON and COMMAND_OFF to match the other templates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
light/templateis also using same naming convention and some people might already use my PR so I prefer not to change this.