hass-core/homeassistant/components/template/fan.py
J. Nick Koston 44fefb3216
Improve handling of template platforms when entity extraction fails (#37831)
Most of the the template platforms would check for
extract_entities failing to extract entities and avoid
setting up a state change listner for MATCH_ALL after
extract_entities had warned that it could not extract
the entities and updates would need to be done manually.
This protection has been extended to all template platforms.

Alter the behavior of extract_entities to return the
successfully extracted entities if one or more templates
fail extraction instead of returning MATCH_ALL
2020-07-14 22:34:35 -07:00

434 lines
14 KiB
Python

"""Support for Template fans."""
import logging
import voluptuous as vol
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_SPEED,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
ENTITY_ID_FORMAT,
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
SUPPORT_DIRECTION,
SUPPORT_OSCILLATE,
SUPPORT_SET_SPEED,
FanEntity,
)
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_FRIENDLY_NAME,
CONF_VALUE_TEMPLATE,
EVENT_HOMEASSISTANT_START,
MATCH_ALL,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.script import Script
from . import extract_entities, initialise_templates
from .const import CONF_AVAILABILITY_TEMPLATE
_LOGGER = logging.getLogger(__name__)
CONF_FANS = "fans"
CONF_SPEED_LIST = "speeds"
CONF_SPEED_TEMPLATE = "speed_template"
CONF_OSCILLATING_TEMPLATE = "oscillating_template"
CONF_DIRECTION_TEMPLATE = "direction_template"
CONF_ON_ACTION = "turn_on"
CONF_OFF_ACTION = "turn_off"
CONF_SET_SPEED_ACTION = "set_speed"
CONF_SET_OSCILLATING_ACTION = "set_oscillating"
CONF_SET_DIRECTION_ACTION = "set_direction"
_VALID_STATES = [STATE_ON, STATE_OFF]
_VALID_OSC = [True, False]
_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE]
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.Optional(CONF_DIRECTION_TEMPLATE): cv.template,
vol.Optional(CONF_AVAILABILITY_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_SET_DIRECTION_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 = cv.PLATFORM_SCHEMA.extend(
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_SCHEMA)}
)
async def async_setup_platform(hass, config, async_add_entities, 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)
direction_template = device_config.get(CONF_DIRECTION_TEMPLATE)
availability_template = device_config.get(CONF_AVAILABILITY_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)
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
speed_list = device_config[CONF_SPEED_LIST]
templates = {
CONF_VALUE_TEMPLATE: state_template,
CONF_SPEED_TEMPLATE: speed_template,
CONF_OSCILLATING_TEMPLATE: oscillating_template,
CONF_DIRECTION_TEMPLATE: direction_template,
CONF_AVAILABILITY_TEMPLATE: availability_template,
}
initialise_templates(hass, templates)
entity_ids = extract_entities(device, "fan", None, templates)
fans.append(
TemplateFan(
hass,
device,
friendly_name,
state_template,
speed_template,
oscillating_template,
direction_template,
availability_template,
on_action,
off_action,
set_speed_action,
set_oscillating_action,
set_direction_action,
speed_list,
entity_ids,
)
)
async_add_entities(fans)
class TemplateFan(FanEntity):
"""A template fan component."""
def __init__(
self,
hass,
device_id,
friendly_name,
state_template,
speed_template,
oscillating_template,
direction_template,
availability_template,
on_action,
off_action,
set_speed_action,
set_oscillating_action,
set_direction_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._direction_template = direction_template
self._availability_template = availability_template
self._available = True
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._set_direction_script = None
if set_direction_action:
self._set_direction_script = Script(hass, set_direction_action)
self._state = STATE_OFF
self._speed = None
self._oscillating = None
self._direction = None
if self._speed_template:
self._supported_features |= SUPPORT_SET_SPEED
if self._oscillating_template:
self._supported_features |= SUPPORT_OSCILLATE
if self._direction_template:
self._supported_features |= SUPPORT_DIRECTION
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) -> 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 current_direction(self):
"""Return the oscillation state."""
return self._direction
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def available(self):
"""Return availability of Device."""
return self._available
# pylint: disable=arguments-differ
async def async_turn_on(self, speed: str = None) -> None:
"""Turn on the fan."""
await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context)
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(context=self._context)
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}, context=self._context
)
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
if oscillating in _VALID_OSC:
self._oscillating = oscillating
await self._set_oscillating_script.async_run(
{ATTR_OSCILLATING: oscillating}, context=self._context
)
else:
_LOGGER.error(
"Received invalid oscillating value: %s. Expected: %s",
oscillating,
", ".join(_VALID_OSC),
)
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
if self._set_direction_script is None:
return
if direction in _VALID_DIRECTIONS:
self._direction = direction
await self._set_direction_script.async_run(
{ATTR_DIRECTION: direction}, context=self._context
)
else:
_LOGGER.error(
"Received invalid direction: %s. Expected: %s",
direction,
", ".join(_VALID_DIRECTIONS),
)
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def template_fan_state_listener(event):
"""Handle target device state changes."""
self.async_schedule_update_ha_state(True)
@callback
def template_fan_startup(event):
"""Update template on startup."""
if self._entities != MATCH_ALL:
# Track state change only for valid templates
self.hass.helpers.event.async_track_state_change_event(
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)
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)
oscillating = None
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
# Update direction if 'direction_template' is configured
if self._direction_template is not None:
try:
direction = self._direction_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
direction = None
self._state = None
# Validate speed
if direction in _VALID_DIRECTIONS:
self._direction = direction
elif direction == STATE_UNKNOWN:
self._direction = None
else:
_LOGGER.error(
"Received invalid direction: %s. Expected: %s",
direction,
", ".join(_VALID_DIRECTIONS),
)
self._direction = None
# Update Availability if 'availability_template' is defined
if self._availability_template is not None:
try:
self._available = (
self._availability_template.async_render().lower() == "true"
)
except (TemplateError, ValueError) as ex:
_LOGGER.error(
"Could not render %s template %s: %s",
CONF_AVAILABILITY_TEMPLATE,
self._name,
ex,
)