core/homeassistant/components/automation/numeric_state.py

159 lines
5.1 KiB
Python
Raw Normal View History

"""Offer numeric state listening automation rules."""
import logging
import voluptuous as vol
from homeassistant import exceptions
from homeassistant.core import callback
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
CONF_VALUE_TEMPLATE,
CONF_PLATFORM,
CONF_ENTITY_ID,
CONF_BELOW,
CONF_ABOVE,
CONF_FOR,
)
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
from homeassistant.helpers import condition, config_validation as cv, template
2019-07-31 19:25:30 +00:00
TRIGGER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_PLATFORM): "numeric_state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FOR): vol.Any(
vol.All(cv.time_period, cv.positive_timedelta),
cv.template,
cv.template_complex,
),
}
),
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
)
_LOGGER = logging.getLogger(__name__)
2015-09-13 11:05:36 +00:00
async def async_trigger(hass, config, action, automation_info):
2016-03-07 16:14:55 +00:00
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
time_delta = config.get(CONF_FOR)
template.attach(hass, time_delta)
2015-12-14 18:08:31 +00:00
value_template = config.get(CONF_VALUE_TEMPLATE)
unsub_track_same = {}
entities_triggered = set()
period = {}
if value_template is not None:
value_template.hass = hass
@callback
def check_numeric_state(entity, from_s, to_s):
"""Return True if criteria are now met."""
if to_s is None:
return False
variables = {
2019-07-31 19:25:30 +00:00
"trigger": {
"platform": "numeric_state",
"entity_id": entity,
"below": below,
"above": above,
}
}
return condition.async_numeric_state(
2019-07-31 19:25:30 +00:00
hass, to_s, below, above, value_template, variables
)
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
2019-07-31 19:25:30 +00:00
@callback
def call_action():
"""Call action with right context."""
2019-07-31 19:25:30 +00:00
hass.async_run_job(
action(
{
"trigger": {
"platform": "numeric_state",
"entity_id": entity,
"below": below,
"above": above,
"from_state": from_s,
"to_state": to_s,
"for": time_delta if not time_delta else period[entity],
}
},
context=to_s.context,
)
)
matching = check_numeric_state(entity, from_s, to_s)
if not matching:
entities_triggered.discard(entity)
elif entity not in entities_triggered:
entities_triggered.add(entity)
if time_delta:
variables = {
2019-07-31 19:25:30 +00:00
"trigger": {
"platform": "numeric_state",
"entity_id": entity,
"below": below,
"above": above,
}
}
try:
if isinstance(time_delta, template.Template):
2019-07-31 19:25:30 +00:00
period[entity] = vol.All(cv.time_period, cv.positive_timedelta)(
time_delta.async_render(variables)
)
elif isinstance(time_delta, dict):
time_delta_data = {}
time_delta_data.update(
2019-07-31 19:25:30 +00:00
template.render_complex(time_delta, variables)
)
period[entity] = vol.All(cv.time_period, cv.positive_timedelta)(
time_delta_data
)
else:
period[entity] = time_delta
except (exceptions.TemplateError, vol.Invalid) as ex:
2019-07-31 19:25:30 +00:00
_LOGGER.error(
"Error rendering '%s' for template: %s",
automation_info["name"],
ex,
)
entities_triggered.discard(entity)
return
unsub_track_same[entity] = async_track_same_state(
2019-07-31 19:25:30 +00:00
hass,
period[entity],
call_action,
entity_ids=entity,
async_check_same_func=check_numeric_state,
)
else:
call_action()
2019-07-31 19:25:30 +00:00
unsub = async_track_state_change(hass, entity_id, state_automation_listener)
@callback
def async_remove():
"""Remove state listeners async."""
unsub()
for async_remove in unsub_track_same.values():
async_remove()
unsub_track_same.clear()
return async_remove