Core track same state for a period / Allow on platforms (#9273)
* Core track state period / Allow on platforms * Add tests * fix lint * fix tests * add new tracker to automation state * update schema * fix bug * revert validate string * Fix bug * Set arguments to async_check_funct * add logic into numeric_state * fix numeric_state * Add tests * fix retrigger state * cleanup * Add delay function to template binary_sensor * Fix tests & lint * add more tests * fix lint * Address comments * fix test & lint
This commit is contained in:
parent
67828cb7a2
commit
ed699896cb
8 changed files with 548 additions and 88 deletions
|
@ -12,16 +12,18 @@ import voluptuous as vol
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||||
CONF_BELOW, CONF_ABOVE)
|
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change, async_track_same_state)
|
||||||
from homeassistant.helpers import condition, config_validation as cv
|
from homeassistant.helpers import condition, config_validation as cv
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
CONF_BELOW: vol.Coerce(float),
|
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||||
CONF_ABOVE: vol.Coerce(float),
|
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -33,15 +35,18 @@ def async_trigger(hass, config, action):
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
below = config.get(CONF_BELOW)
|
below = config.get(CONF_BELOW)
|
||||||
above = config.get(CONF_ABOVE)
|
above = config.get(CONF_ABOVE)
|
||||||
|
time_delta = config.get(CONF_FOR)
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
async_remove_track_same = None
|
||||||
|
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def check_numeric_state(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Return True if they should trigger."""
|
||||||
if to_s is None:
|
if to_s is None:
|
||||||
return
|
return False
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
|
@ -55,17 +60,56 @@ def async_trigger(hass, config, action):
|
||||||
# If new one doesn't match, nothing to do
|
# If new one doesn't match, nothing to do
|
||||||
if not condition.async_numeric_state(
|
if not condition.async_numeric_state(
|
||||||
hass, to_s, below, above, value_template, variables):
|
hass, to_s, below, above, value_template, variables):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
|
"""Listen for state changes and calls action."""
|
||||||
|
nonlocal async_remove_track_same
|
||||||
|
|
||||||
|
if not check_numeric_state(entity, from_s, to_s):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': entity,
|
||||||
|
'below': below,
|
||||||
|
'above': above,
|
||||||
|
'from_state': from_s,
|
||||||
|
'to_state': to_s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Only match if old didn't exist or existed but didn't match
|
# Only match if old didn't exist or existed but didn't match
|
||||||
# Written as: skip if old one did exist and matched
|
# Written as: skip if old one did exist and matched
|
||||||
if from_s is not None and condition.async_numeric_state(
|
if from_s is not None and condition.async_numeric_state(
|
||||||
hass, from_s, below, above, value_template, variables):
|
hass, from_s, below, above, value_template, variables):
|
||||||
return
|
return
|
||||||
|
|
||||||
variables['trigger']['from_state'] = from_s
|
@callback
|
||||||
variables['trigger']['to_state'] = to_s
|
def call_action():
|
||||||
|
"""Call action with right context."""
|
||||||
|
hass.async_run_job(action, variables)
|
||||||
|
|
||||||
hass.async_run_job(action, variables)
|
if not time_delta:
|
||||||
|
call_action()
|
||||||
|
return
|
||||||
|
|
||||||
return async_track_state_change(hass, entity_id, state_automation_listener)
|
async_remove_track_same = async_track_same_state(
|
||||||
|
hass, True, time_delta, call_action, entity_ids=entity_id,
|
||||||
|
async_check_func=check_numeric_state)
|
||||||
|
|
||||||
|
unsub = async_track_state_change(
|
||||||
|
hass, entity_id, state_automation_listener)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove():
|
||||||
|
"""Remove state listeners async."""
|
||||||
|
unsub()
|
||||||
|
if async_remove_track_same:
|
||||||
|
async_remove_track_same() # pylint: disable=not-callable
|
||||||
|
|
||||||
|
return async_remove
|
||||||
|
|
|
@ -8,28 +8,23 @@ import asyncio
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.util.dt as dt_util
|
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_state_change, async_track_point_in_utc_time)
|
async_track_state_change, async_track_same_state)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
CONF_ENTITY_ID = 'entity_id'
|
CONF_ENTITY_ID = 'entity_id'
|
||||||
CONF_FROM = 'from'
|
CONF_FROM = 'from'
|
||||||
CONF_TO = 'to'
|
CONF_TO = 'to'
|
||||||
CONF_FOR = 'for'
|
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(
|
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||||
vol.Schema({
|
vol.Required(CONF_PLATFORM): 'state',
|
||||||
vol.Required(CONF_PLATFORM): 'state',
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
# These are str on purpose. Want to catch YAML conversions
|
||||||
# These are str on purpose. Want to catch YAML conversions
|
vol.Optional(CONF_FROM): str,
|
||||||
CONF_FROM: str,
|
vol.Optional(CONF_TO): str,
|
||||||
CONF_TO: str,
|
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
|
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||||
}),
|
|
||||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -39,28 +34,15 @@ def async_trigger(hass, config, action):
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||||
time_delta = config.get(CONF_FOR)
|
time_delta = config.get(CONF_FOR)
|
||||||
async_remove_state_for_cancel = None
|
|
||||||
async_remove_state_for_listener = None
|
|
||||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||||
|
async_remove_track_same = None
|
||||||
@callback
|
|
||||||
def clear_listener():
|
|
||||||
"""Clear all unsub listener."""
|
|
||||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
|
||||||
|
|
||||||
# pylint: disable=not-callable
|
|
||||||
if async_remove_state_for_listener is not None:
|
|
||||||
async_remove_state_for_listener()
|
|
||||||
async_remove_state_for_listener = None
|
|
||||||
if async_remove_state_for_cancel is not None:
|
|
||||||
async_remove_state_for_cancel()
|
|
||||||
async_remove_state_for_cancel = None
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
nonlocal async_remove_track_same
|
||||||
|
|
||||||
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(action, {
|
||||||
|
@ -78,33 +60,12 @@ def async_trigger(hass, config, action):
|
||||||
from_s.last_changed == to_s.last_changed):
|
from_s.last_changed == to_s.last_changed):
|
||||||
return
|
return
|
||||||
|
|
||||||
if time_delta is None:
|
if not time_delta:
|
||||||
call_action()
|
call_action()
|
||||||
return
|
return
|
||||||
|
|
||||||
@callback
|
async_remove_track_same = async_track_same_state(
|
||||||
def state_for_listener(now):
|
hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
|
||||||
"""Fire on state changes after a delay and calls action."""
|
|
||||||
nonlocal async_remove_state_for_listener
|
|
||||||
async_remove_state_for_listener = None
|
|
||||||
clear_listener()
|
|
||||||
call_action()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
|
||||||
"""Fire on changes and cancel for listener if changed."""
|
|
||||||
if inner_to_s.state == to_s.state:
|
|
||||||
return
|
|
||||||
clear_listener()
|
|
||||||
|
|
||||||
# cleanup previous listener
|
|
||||||
clear_listener()
|
|
||||||
|
|
||||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
|
||||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
|
||||||
|
|
||||||
async_remove_state_for_cancel = async_track_state_change(
|
|
||||||
hass, entity, state_for_cancel_listener)
|
|
||||||
|
|
||||||
unsub = async_track_state_change(
|
unsub = async_track_state_change(
|
||||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||||
|
@ -113,6 +74,7 @@ def async_trigger(hass, config, action):
|
||||||
def async_remove():
|
def async_remove():
|
||||||
"""Remove state listeners async."""
|
"""Remove state listeners async."""
|
||||||
unsub()
|
unsub()
|
||||||
clear_listener()
|
if async_remove_track_same:
|
||||||
|
async_remove_track_same() # pylint: disable=not-callable
|
||||||
|
|
||||||
return async_remove
|
return async_remove
|
||||||
|
|
|
@ -19,16 +19,24 @@ from homeassistant.const import (
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import async_generate_entity_id
|
from homeassistant.helpers.entity import async_generate_entity_id
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change, async_track_same_state)
|
||||||
from homeassistant.helpers.restore_state import async_get_last_state
|
from homeassistant.helpers.restore_state import async_get_last_state
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_DELAY_ON = 'delay_on'
|
||||||
|
CONF_DELAY_OFF = 'delay_off'
|
||||||
|
|
||||||
SENSOR_SCHEMA = vol.Schema({
|
SENSOR_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
|
vol.Optional(CONF_DELAY_ON):
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
|
vol.Optional(CONF_DELAY_OFF):
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
})
|
})
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -47,6 +55,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
value_template.extract_entities())
|
value_template.extract_entities())
|
||||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||||
|
delay_on = device_config.get(CONF_DELAY_ON)
|
||||||
|
delay_off = device_config.get(CONF_DELAY_OFF)
|
||||||
|
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
@ -54,13 +64,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
sensors.append(
|
sensors.append(
|
||||||
BinarySensorTemplate(
|
BinarySensorTemplate(
|
||||||
hass, device, friendly_name, device_class, value_template,
|
hass, device, friendly_name, device_class, value_template,
|
||||||
entity_ids)
|
entity_ids, delay_on, delay_off)
|
||||||
)
|
)
|
||||||
if not sensors:
|
if not sensors:
|
||||||
_LOGGER.error("No sensors added")
|
_LOGGER.error("No sensors added")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async_add_devices(sensors, True)
|
async_add_devices(sensors)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +78,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
"""A virtual binary sensor that triggers from another sensor."""
|
"""A virtual binary sensor that triggers from another sensor."""
|
||||||
|
|
||||||
def __init__(self, hass, device, friendly_name, device_class,
|
def __init__(self, hass, device, friendly_name, device_class,
|
||||||
value_template, entity_ids):
|
value_template, entity_ids, delay_on, delay_off):
|
||||||
"""Initialize the Template binary sensor."""
|
"""Initialize the Template binary sensor."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = async_generate_entity_id(
|
self.entity_id = async_generate_entity_id(
|
||||||
|
@ -78,6 +88,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
self._template = value_template
|
self._template = value_template
|
||||||
self._state = None
|
self._state = None
|
||||||
self._entities = entity_ids
|
self._entities = entity_ids
|
||||||
|
self._delay_on = delay_on
|
||||||
|
self._delay_off = delay_off
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
|
@ -89,7 +101,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
@callback
|
@callback
|
||||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||||
"""Handle the target device state changes."""
|
"""Handle the target device state changes."""
|
||||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
self.async_check_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def template_bsensor_startup(event):
|
def template_bsensor_startup(event):
|
||||||
|
@ -97,7 +109,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
self.hass, self._entities, template_bsensor_state_listener)
|
self.hass, self._entities, template_bsensor_state_listener)
|
||||||
|
|
||||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
self.hass.async_add_job(self.async_check_state)
|
||||||
|
|
||||||
self.hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||||
|
@ -122,11 +134,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
"""No polling needed."""
|
"""No polling needed."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@asyncio.coroutine
|
@callback
|
||||||
def async_update(self):
|
def _async_render(self, *args):
|
||||||
"""Update the state from the template."""
|
"""Get the state of template."""
|
||||||
try:
|
try:
|
||||||
self._state = self._template.async_render().lower() == 'true'
|
return self._template.async_render().lower() == 'true'
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
if ex.args and ex.args[0].startswith(
|
if ex.args and ex.args[0].startswith(
|
||||||
"UndefinedError: 'None' has no attribute"):
|
"UndefinedError: 'None' has no attribute"):
|
||||||
|
@ -135,4 +147,29 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||||
"the state is unknown", self._name)
|
"the state is unknown", self._name)
|
||||||
return
|
return
|
||||||
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
||||||
self._state = False
|
|
||||||
|
@callback
|
||||||
|
def async_check_state(self):
|
||||||
|
"""Update the state from the template."""
|
||||||
|
state = self._async_render()
|
||||||
|
|
||||||
|
# return if the state don't change or is invalid
|
||||||
|
if state is None or state == self.state:
|
||||||
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def set_state():
|
||||||
|
"""Set state of template binary sensor."""
|
||||||
|
self._state = state
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
# state without delay
|
||||||
|
if (state and not self._delay_on) or \
|
||||||
|
(not state and not self._delay_off):
|
||||||
|
set_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
period = self._delay_on if state else self._delay_off
|
||||||
|
async_track_same_state(
|
||||||
|
self.hass, state, period, set_state, entity_ids=self._entities,
|
||||||
|
async_check_func=self._async_render)
|
||||||
|
|
|
@ -101,6 +101,7 @@ CONF_EVENT = 'event'
|
||||||
CONF_EXCLUDE = 'exclude'
|
CONF_EXCLUDE = 'exclude'
|
||||||
CONF_FILE_PATH = 'file_path'
|
CONF_FILE_PATH = 'file_path'
|
||||||
CONF_FILENAME = 'filename'
|
CONF_FILENAME = 'filename'
|
||||||
|
CONF_FOR = 'for'
|
||||||
CONF_FRIENDLY_NAME = 'friendly_name'
|
CONF_FRIENDLY_NAME = 'friendly_name'
|
||||||
CONF_HEADERS = 'headers'
|
CONF_HEADERS = 'headers'
|
||||||
CONF_HOST = 'host'
|
CONF_HOST = 'host'
|
||||||
|
|
|
@ -113,6 +113,62 @@ def async_track_template(hass, template, action, variables=None):
|
||||||
track_template = threaded_listener_factory(async_track_template)
|
track_template = threaded_listener_factory(async_track_template)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_track_same_state(hass, orig_value, period, action,
|
||||||
|
async_check_func=None, entity_ids=MATCH_ALL):
|
||||||
|
"""Track the state of entities for a period and run a action.
|
||||||
|
|
||||||
|
If async_check_func is None it use the state of orig_value.
|
||||||
|
Without entity_ids we track all state changes.
|
||||||
|
"""
|
||||||
|
async_remove_state_for_cancel = None
|
||||||
|
async_remove_state_for_listener = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def clear_listener():
|
||||||
|
"""Clear all unsub listener."""
|
||||||
|
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||||
|
|
||||||
|
# pylint: disable=not-callable
|
||||||
|
if async_remove_state_for_listener is not None:
|
||||||
|
async_remove_state_for_listener()
|
||||||
|
async_remove_state_for_listener = None
|
||||||
|
if async_remove_state_for_cancel is not None:
|
||||||
|
async_remove_state_for_cancel()
|
||||||
|
async_remove_state_for_cancel = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def state_for_listener(now):
|
||||||
|
"""Fire on state changes after a delay and calls action."""
|
||||||
|
nonlocal async_remove_state_for_listener
|
||||||
|
async_remove_state_for_listener = None
|
||||||
|
clear_listener()
|
||||||
|
hass.async_run_job(action)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def state_for_cancel_listener(entity, from_state, to_state):
|
||||||
|
"""Fire on changes and cancel for listener if changed."""
|
||||||
|
if async_check_func:
|
||||||
|
value = async_check_func(entity, from_state, to_state)
|
||||||
|
else:
|
||||||
|
value = to_state.state
|
||||||
|
|
||||||
|
if orig_value == value:
|
||||||
|
return
|
||||||
|
clear_listener()
|
||||||
|
|
||||||
|
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||||
|
hass, state_for_listener, dt_util.utcnow() + period)
|
||||||
|
|
||||||
|
async_remove_state_for_cancel = async_track_state_change(
|
||||||
|
hass, entity_ids, state_for_cancel_listener)
|
||||||
|
|
||||||
|
return clear_listener
|
||||||
|
|
||||||
|
|
||||||
|
track_same_state = threaded_listener_factory(async_track_same_state)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_track_point_in_time(hass, action, point_in_time):
|
def async_track_point_in_time(hass, action, point_in_time):
|
||||||
"""Add a listener that fires once after a specific point in time."""
|
"""Add a listener that fires once after a specific point in time."""
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
"""The tests for numeric state automation."""
|
"""The tests for numeric state automation."""
|
||||||
|
from datetime import timedelta
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import homeassistant.components.automation as automation
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.setup import setup_component
|
from homeassistant.setup import setup_component
|
||||||
import homeassistant.components.automation as automation
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant, mock_component
|
from tests.common import (
|
||||||
|
get_test_home_assistant, mock_component, fire_time_changed,
|
||||||
|
assert_setup_component)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
@ -576,3 +581,126 @@ class TestAutomationNumericState(unittest.TestCase):
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(2, len(self.calls))
|
self.assertEqual(2, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fails_setup_bad_for(self):
|
||||||
|
"""Test for setup failure for bad for."""
|
||||||
|
with assert_setup_component(0):
|
||||||
|
assert setup_component(self.hass, automation.DOMAIN, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'above': 8,
|
||||||
|
'below': 12,
|
||||||
|
'for': {
|
||||||
|
'invalid': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'homeassistant.turn_on',
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
def test_if_fails_setup_for_without_above_below(self):
|
||||||
|
"""Test for setup failures for missing above or below."""
|
||||||
|
with assert_setup_component(0):
|
||||||
|
assert setup_component(self.hass, automation.DOMAIN, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'for': {
|
||||||
|
'seconds': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'homeassistant.turn_on',
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
def test_if_not_fires_on_entity_change_with_for(self):
|
||||||
|
"""Test for not firing on entity change with for."""
|
||||||
|
assert setup_component(self.hass, automation.DOMAIN, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'above': 8,
|
||||||
|
'below': 12,
|
||||||
|
'for': {
|
||||||
|
'seconds': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 9)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.hass.states.set('test.entity', 15)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_entity_change_with_for_attribute_change(self):
|
||||||
|
"""Test for firing on entity change with for and attribute change."""
|
||||||
|
assert setup_component(self.hass, automation.DOMAIN, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'above': 8,
|
||||||
|
'below': 12,
|
||||||
|
'for': {
|
||||||
|
'seconds': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
utcnow = dt_util.utcnow()
|
||||||
|
with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow:
|
||||||
|
mock_utcnow.return_value = utcnow
|
||||||
|
self.hass.states.set('test.entity', 9)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
mock_utcnow.return_value += timedelta(seconds=4)
|
||||||
|
fire_time_changed(self.hass, mock_utcnow.return_value)
|
||||||
|
self.hass.states.set('test.entity', 9,
|
||||||
|
attributes={"mock_attr": "attr_change"})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
mock_utcnow.return_value += timedelta(seconds=4)
|
||||||
|
fire_time_changed(self.hass, mock_utcnow.return_value)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_entity_change_with_for(self):
|
||||||
|
"""Test for firing on entity change with for."""
|
||||||
|
assert setup_component(self.hass, automation.DOMAIN, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'above': 8,
|
||||||
|
'below': 12,
|
||||||
|
'for': {
|
||||||
|
'seconds': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 9)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""The tests for the Template Binary sensor platform."""
|
"""The tests for the Template Binary sensor platform."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
@ -10,10 +11,12 @@ from homeassistant.components.binary_sensor import template
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import template as template_hlpr
|
from homeassistant.helpers import template as template_hlpr
|
||||||
from homeassistant.util.async import run_callback_threadsafe
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE
|
from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
get_test_home_assistant, assert_setup_component, mock_component)
|
get_test_home_assistant, assert_setup_component, mock_component,
|
||||||
|
async_fire_time_changed)
|
||||||
|
|
||||||
|
|
||||||
class TestBinarySensorTemplate(unittest.TestCase):
|
class TestBinarySensorTemplate(unittest.TestCase):
|
||||||
|
@ -103,19 +106,20 @@ class TestBinarySensorTemplate(unittest.TestCase):
|
||||||
vs = run_callback_threadsafe(
|
vs = run_callback_threadsafe(
|
||||||
self.hass.loop, template.BinarySensorTemplate,
|
self.hass.loop, template.BinarySensorTemplate,
|
||||||
self.hass, 'parent', 'Parent', 'motion',
|
self.hass, 'parent', 'Parent', 'motion',
|
||||||
template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL
|
template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL,
|
||||||
|
None, None
|
||||||
).result()
|
).result()
|
||||||
self.assertFalse(vs.should_poll)
|
self.assertFalse(vs.should_poll)
|
||||||
self.assertEqual('motion', vs.device_class)
|
self.assertEqual('motion', vs.device_class)
|
||||||
self.assertEqual('Parent', vs.name)
|
self.assertEqual('Parent', vs.name)
|
||||||
|
|
||||||
vs.update()
|
run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
|
||||||
self.assertFalse(vs.is_on)
|
self.assertFalse(vs.is_on)
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
vs._template = template_hlpr.Template("{{ 2 > 1 }}", self.hass)
|
vs._template = template_hlpr.Template("{{ 2 > 1 }}", self.hass)
|
||||||
|
|
||||||
vs.update()
|
run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
|
||||||
self.assertTrue(vs.is_on)
|
self.assertTrue(vs.is_on)
|
||||||
|
|
||||||
def test_event(self):
|
def test_event(self):
|
||||||
|
@ -155,13 +159,14 @@ class TestBinarySensorTemplate(unittest.TestCase):
|
||||||
vs = run_callback_threadsafe(
|
vs = run_callback_threadsafe(
|
||||||
self.hass.loop, template.BinarySensorTemplate,
|
self.hass.loop, template.BinarySensorTemplate,
|
||||||
self.hass, 'parent', 'Parent', 'motion',
|
self.hass, 'parent', 'Parent', 'motion',
|
||||||
template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL
|
template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL,
|
||||||
|
None, None
|
||||||
).result()
|
).result()
|
||||||
mock_render.side_effect = TemplateError('foo')
|
mock_render.side_effect = TemplateError('foo')
|
||||||
vs.update()
|
run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
|
||||||
mock_render.side_effect = TemplateError(
|
mock_render.side_effect = TemplateError(
|
||||||
"UndefinedError: 'None' has no attribute")
|
"UndefinedError: 'None' has no attribute")
|
||||||
vs.update()
|
run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -197,3 +202,124 @@ def test_restore_state(hass):
|
||||||
|
|
||||||
state = hass.states.get('binary_sensor.test')
|
state = hass.states.get('binary_sensor.test')
|
||||||
assert state.state == 'off'
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_template_delay_on(hass):
|
||||||
|
"""Test binary sensor template delay on."""
|
||||||
|
config = {
|
||||||
|
'binary_sensor': {
|
||||||
|
'platform': 'template',
|
||||||
|
'sensors': {
|
||||||
|
'test': {
|
||||||
|
'friendly_name': 'virtual thingy',
|
||||||
|
'value_template':
|
||||||
|
"{{ states.sensor.test_state.state == 'on' }}",
|
||||||
|
'device_class': 'motion',
|
||||||
|
'delay_on': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
yield from setup.async_setup_component(hass, 'binary_sensor', config)
|
||||||
|
yield from hass.async_start()
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'on')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
future = dt_util.utcnow() + timedelta(seconds=5)
|
||||||
|
async_fire_time_changed(hass, future)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
||||||
|
# check with time changes
|
||||||
|
hass.states.async_set('sensor.test_state', 'off')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'on')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'off')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
future = dt_util.utcnow() + timedelta(seconds=5)
|
||||||
|
async_fire_time_changed(hass, future)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_template_delay_off(hass):
|
||||||
|
"""Test binary sensor template delay off."""
|
||||||
|
config = {
|
||||||
|
'binary_sensor': {
|
||||||
|
'platform': 'template',
|
||||||
|
'sensors': {
|
||||||
|
'test': {
|
||||||
|
'friendly_name': 'virtual thingy',
|
||||||
|
'value_template':
|
||||||
|
"{{ states.sensor.test_state.state == 'on' }}",
|
||||||
|
'device_class': 'motion',
|
||||||
|
'delay_off': 5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hass.states.async_set('sensor.test_state', 'on')
|
||||||
|
yield from setup.async_setup_component(hass, 'binary_sensor', config)
|
||||||
|
yield from hass.async_start()
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'off')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
||||||
|
future = dt_util.utcnow() + timedelta(seconds=5)
|
||||||
|
async_fire_time_changed(hass, future)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'off'
|
||||||
|
|
||||||
|
# check with time changes
|
||||||
|
hass.states.async_set('sensor.test_state', 'on')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'off')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
||||||
|
hass.states.async_set('sensor.test_state', 'on')
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
||||||
|
future = dt_util.utcnow() + timedelta(seconds=5)
|
||||||
|
async_fire_time_changed(hass, future)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('binary_sensor.test')
|
||||||
|
assert state.state == 'on'
|
||||||
|
|
|
@ -17,6 +17,7 @@ from homeassistant.helpers.event import (
|
||||||
track_state_change,
|
track_state_change,
|
||||||
track_time_interval,
|
track_time_interval,
|
||||||
track_template,
|
track_template,
|
||||||
|
track_same_state,
|
||||||
track_sunrise,
|
track_sunrise,
|
||||||
track_sunset,
|
track_sunset,
|
||||||
)
|
)
|
||||||
|
@ -24,7 +25,7 @@ from homeassistant.helpers.template import Template
|
||||||
from homeassistant.components import sun
|
from homeassistant.components import sun
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant
|
from tests.common import get_test_home_assistant, fire_time_changed
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
@ -262,6 +263,111 @@ class TestEventHelpers(unittest.TestCase):
|
||||||
self.assertEqual(2, len(wildcard_runs))
|
self.assertEqual(2, len(wildcard_runs))
|
||||||
self.assertEqual(2, len(wildercard_runs))
|
self.assertEqual(2, len(wildercard_runs))
|
||||||
|
|
||||||
|
def test_track_same_state_simple_trigger(self):
|
||||||
|
"""Test track_same_change with trigger simple."""
|
||||||
|
thread_runs = []
|
||||||
|
callback_runs = []
|
||||||
|
coroutine_runs = []
|
||||||
|
period = timedelta(minutes=1)
|
||||||
|
|
||||||
|
def thread_run_callback():
|
||||||
|
thread_runs.append(1)
|
||||||
|
|
||||||
|
track_same_state(
|
||||||
|
self.hass, 'on', period, thread_run_callback,
|
||||||
|
entity_ids='light.Bowl')
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def callback_run_callback():
|
||||||
|
callback_runs.append(1)
|
||||||
|
|
||||||
|
track_same_state(
|
||||||
|
self.hass, 'on', period, callback_run_callback,
|
||||||
|
entity_ids='light.Bowl')
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def coroutine_run_callback():
|
||||||
|
coroutine_runs.append(1)
|
||||||
|
|
||||||
|
track_same_state(
|
||||||
|
self.hass, 'on', period, coroutine_run_callback)
|
||||||
|
|
||||||
|
# Adding state to state machine
|
||||||
|
self.hass.states.set("light.Bowl", "on")
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(thread_runs))
|
||||||
|
self.assertEqual(0, len(callback_runs))
|
||||||
|
self.assertEqual(0, len(coroutine_runs))
|
||||||
|
|
||||||
|
# change time to track and see if they trigger
|
||||||
|
future = dt_util.utcnow() + period
|
||||||
|
fire_time_changed(self.hass, future)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(1, len(thread_runs))
|
||||||
|
self.assertEqual(1, len(callback_runs))
|
||||||
|
self.assertEqual(1, len(coroutine_runs))
|
||||||
|
|
||||||
|
def test_track_same_state_simple_no_trigger(self):
|
||||||
|
"""Test track_same_change with no trigger."""
|
||||||
|
callback_runs = []
|
||||||
|
period = timedelta(minutes=1)
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def callback_run_callback():
|
||||||
|
callback_runs.append(1)
|
||||||
|
|
||||||
|
track_same_state(
|
||||||
|
self.hass, 'on', period, callback_run_callback,
|
||||||
|
entity_ids='light.Bowl')
|
||||||
|
|
||||||
|
# Adding state to state machine
|
||||||
|
self.hass.states.set("light.Bowl", "on")
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(callback_runs))
|
||||||
|
|
||||||
|
# Change state on state machine
|
||||||
|
self.hass.states.set("light.Bowl", "off")
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(callback_runs))
|
||||||
|
|
||||||
|
# change time to track and see if they trigger
|
||||||
|
future = dt_util.utcnow() + period
|
||||||
|
fire_time_changed(self.hass, future)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(callback_runs))
|
||||||
|
|
||||||
|
def test_track_same_state_simple_trigger_check_funct(self):
|
||||||
|
"""Test track_same_change with trigger and check funct."""
|
||||||
|
callback_runs = []
|
||||||
|
check_func = []
|
||||||
|
period = timedelta(minutes=1)
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def callback_run_callback():
|
||||||
|
callback_runs.append(1)
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def async_check_func(entity, from_s, to_s):
|
||||||
|
check_func.append((entity, from_s, to_s))
|
||||||
|
return 'on'
|
||||||
|
|
||||||
|
track_same_state(
|
||||||
|
self.hass, 'on', period, callback_run_callback,
|
||||||
|
entity_ids='light.Bowl', async_check_func=async_check_func)
|
||||||
|
|
||||||
|
# Adding state to state machine
|
||||||
|
self.hass.states.set("light.Bowl", "on")
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(callback_runs))
|
||||||
|
self.assertEqual('on', check_func[-1][2].state)
|
||||||
|
self.assertEqual('light.bowl', check_func[-1][0])
|
||||||
|
|
||||||
|
# change time to track and see if they trigger
|
||||||
|
future = dt_util.utcnow() + period
|
||||||
|
fire_time_changed(self.hass, future)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(1, len(callback_runs))
|
||||||
|
|
||||||
def test_track_time_interval(self):
|
def test_track_time_interval(self):
|
||||||
"""Test tracking time interval."""
|
"""Test tracking time interval."""
|
||||||
specific_runs = []
|
specific_runs = []
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue