Merge pull request #2980 from home-assistant/automation-entities

Create entities for automation
This commit is contained in:
Paulus Schoutsen 2016-09-01 09:50:39 +01:00 committed by GitHub
commit 87e332c777
31 changed files with 553 additions and 145 deletions

View File

@ -4,19 +4,26 @@ Allow to setup simple automation rules via the config file.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/ https://home-assistant.io/components/automation/
""" """
from functools import partial
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE)
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'automation' DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEPENDENCIES = ['group'] DEPENDENCIES = ['group']
@ -36,6 +43,10 @@ DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
METHOD_TRIGGER = 'trigger' METHOD_TRIGGER = 'trigger'
METHOD_IF_ACTION = 'if_action' METHOD_IF_ACTION = 'if_action'
ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -88,41 +99,171 @@ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE): vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)), vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
CONF_CONDITION: _CONDITION_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
}) })
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
TRIGGER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict,
})
def is_on(hass, entity_id=None):
"""
Return true if specified automation entity_id is on.
Check all automation if no entity_id specified.
"""
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
return any(hass.states.is_state(entity_id, STATE_ON)
for entity_id in entity_ids)
def turn_on(hass, entity_id=None):
"""Turn on specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None):
"""Turn off specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def toggle(hass, entity_id=None):
"""Toggle specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def trigger(hass, entity_id=None):
"""Trigger specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
def setup(hass, config): def setup(hass, config):
"""Setup the automation.""" """Setup the automation."""
# pylint: disable=too-many-locals
component = EntityComponent(_LOGGER, DOMAIN, hass)
success = False success = False
for config_key in extract_domain_configs(config, DOMAIN): for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key] conf = config[config_key]
for list_no, config_block in enumerate(conf): for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)) list_no)
success = (_setup_automation(hass, config_block, name, config) or
success)
return success action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
if CONF_CONDITION in config_block:
cond_func = _process_if(hass, config, config_block)
def _setup_automation(hass, config_block, name, config): if cond_func is None:
"""Setup one instance of automation.""" continue
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) else:
def cond_func(variables):
"""Condition will always pass."""
return True
if CONF_CONDITION in config_block: attach_triggers = partial(_process_trigger, hass, config,
action = _process_if(hass, config, config_block, action) config_block.get(CONF_TRIGGER, []), name)
entity = AutomationEntity(name, attach_triggers, cond_func, action)
component.add_entities((entity,))
success = True
if action is None: if not success:
return False return False
def trigger_service_handler(service_call):
"""Handle automation triggers."""
for entity in component.extract_from_service(service_call):
entity.trigger(service_call.data.get(ATTR_VARIABLES))
def service_handler(service_call):
"""Handle automation service calls."""
for entity in component.extract_from_service(service_call):
getattr(entity, service_call.service)()
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
schema=TRIGGER_SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE):
hass.services.register(DOMAIN, service, service_handler,
schema=SERVICE_SCHEMA)
_process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name,
action)
return True return True
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
def __init__(self, name, attach_triggers, cond_func, action):
"""Initialize an automation entity."""
self._name = name
self._attach_triggers = attach_triggers
self._detach_triggers = attach_triggers(self.trigger)
self._cond_func = cond_func
self._action = action
self._enabled = True
self._last_triggered = None
@property
def name(self):
"""Name of the automation."""
return self._name
@property
def should_poll(self):
"""No polling needed for automation entities."""
return False
@property
def state_attributes(self):
"""Return the entity state attributes."""
return {
ATTR_LAST_TRIGGERED: self._last_triggered
}
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
return self._enabled
def turn_on(self, **kwargs) -> None:
"""Turn the entity on."""
if self._enabled:
return
self._detach_triggers = self._attach_triggers(self.trigger)
self._enabled = True
self.update_ha_state()
def turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
if not self._enabled:
return
self._detach_triggers()
self._detach_triggers = None
self._enabled = False
self.update_ha_state()
def trigger(self, variables):
"""Trigger automation."""
if self._cond_func(variables):
self._action(variables)
self._last_triggered = utcnow()
self.update_ha_state()
def _get_action(hass, config, name): def _get_action(hass, config, name):
"""Return an action based on a configuration.""" """Return an action based on a configuration."""
script_obj = script.Script(hass, config, name) script_obj = script.Script(hass, config, name)
@ -136,7 +277,7 @@ def _get_action(hass, config, name):
return action return action
def _process_if(hass, config, p_config, action): def _process_if(hass, config, p_config):
"""Process if checks.""" """Process if checks."""
cond_type = p_config.get(CONF_CONDITION_TYPE, cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower() DEFAULT_CONDITION_TYPE).lower()
@ -182,29 +323,43 @@ def _process_if(hass, config, p_config, action):
if cond_type == CONDITION_TYPE_AND: if cond_type == CONDITION_TYPE_AND:
def if_action(variables=None): def if_action(variables=None):
"""AND all conditions.""" """AND all conditions."""
if all(check(hass, variables) for check in checks): return all(check(hass, variables) for check in checks)
action(variables)
else: else:
def if_action(variables=None): def if_action(variables=None):
"""OR all conditions.""" """OR all conditions."""
if any(check(hass, variables) for check in checks): return any(check(hass, variables) for check in checks)
action(variables)
return if_action return if_action
def _process_trigger(hass, config, trigger_configs, name, action): def _process_trigger(hass, config, trigger_configs, name, action):
"""Setup the triggers.""" """Setup the triggers."""
removes = []
for conf in trigger_configs: for conf in trigger_configs:
platform = _resolve_platform(METHOD_TRIGGER, hass, config, platform = _resolve_platform(METHOD_TRIGGER, hass, config,
conf.get(CONF_PLATFORM)) conf.get(CONF_PLATFORM))
if platform is None: if platform is None:
continue continue
if platform.trigger(hass, conf, action): remove = platform.trigger(hass, conf, action)
_LOGGER.info("Initialized rule %s", name)
else: if not remove:
_LOGGER.error("Error setting up rule %s", name) _LOGGER.error("Error setting up rule %s", name)
continue
_LOGGER.info("Initialized rule %s", name)
removes.append(remove)
if not removes:
return None
def remove_triggers():
"""Remove attached triggers."""
for remove in removes:
remove()
return remove_triggers
def _resolve_platform(method, hass, config, platform): def _resolve_platform(method, hass, config, platform):

View File

@ -39,5 +39,4 @@ def trigger(hass, config, action):
}, },
}) })
hass.bus.listen(event_type, handle_event) return hass.bus.listen(event_type, handle_event)
return True

View File

@ -39,6 +39,4 @@ def trigger(hass, config, action):
} }
}) })
mqtt.subscribe(hass, topic, mqtt_automation_listener) return mqtt.subscribe(hass, topic, mqtt_automation_listener)
return True

View File

@ -63,7 +63,4 @@ def trigger(hass, config, action):
action(variables) action(variables)
track_state_change( return track_state_change(hass, entity_id, state_automation_listener)
hass, entity_id, state_automation_listener)
return True

View File

@ -7,8 +7,7 @@ at https://home-assistant.io/components/automation/#state-trigger
import voluptuous as vol import voluptuous as vol
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import MATCH_ALL, CONF_PLATFORM
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
from homeassistant.helpers.event import track_state_change, track_point_in_time from homeassistant.helpers.event import track_state_change, track_point_in_time
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -39,9 +38,13 @@ def 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) or config.get(CONF_STATE) or MATCH_ALL to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
time_delta = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
remove_state_for_cancel = None
remove_state_for_listener = None
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 remove_state_for_cancel, remove_state_for_listener
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
action({ action({
@ -60,26 +63,33 @@ def trigger(hass, config, action):
def state_for_listener(now): def state_for_listener(now):
"""Fire on state changes after a delay and calls action.""" """Fire on state changes after a delay and calls action."""
hass.bus.remove_listener( remove_state_for_cancel()
EVENT_STATE_CHANGED, attached_state_for_cancel)
call_action() call_action()
def state_for_cancel_listener(entity, inner_from_s, inner_to_s): def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
"""Fire on changes and cancel for listener if changed.""" """Fire on changes and cancel for listener if changed."""
if inner_to_s.state == to_s.state: if inner_to_s.state == to_s.state:
return return
hass.bus.remove_listener(EVENT_TIME_CHANGED, remove_state_for_listener()
attached_state_for_listener) remove_state_for_cancel()
hass.bus.remove_listener(EVENT_STATE_CHANGED,
attached_state_for_cancel)
attached_state_for_listener = track_point_in_time( remove_state_for_listener = track_point_in_time(
hass, state_for_listener, dt_util.utcnow() + time_delta) hass, state_for_listener, dt_util.utcnow() + time_delta)
attached_state_for_cancel = track_state_change( remove_state_for_cancel = track_state_change(
hass, entity, state_for_cancel_listener) hass, entity, state_for_cancel_listener)
track_state_change( unsub = track_state_change(hass, entity_id, state_automation_listener,
hass, entity_id, state_automation_listener, from_state, to_state) from_state, to_state)
return True def remove():
"""Remove state listeners."""
unsub()
# pylint: disable=not-callable
if remove_state_for_cancel is not None:
remove_state_for_cancel()
if remove_state_for_listener is not None:
remove_state_for_listener()
return remove

View File

@ -42,8 +42,6 @@ def trigger(hass, config, action):
# Do something to call action # Do something to call action
if event == SUN_EVENT_SUNRISE: if event == SUN_EVENT_SUNRISE:
track_sunrise(hass, call_action, offset) return track_sunrise(hass, call_action, offset)
else: else:
track_sunset(hass, call_action, offset) return track_sunset(hass, call_action, offset)
return True

View File

@ -49,5 +49,4 @@ def trigger(hass, config, action):
elif not template_result: elif not template_result:
already_triggered = False already_triggered = False
track_state_change(hass, MATCH_ALL, state_changed_listener) return track_state_change(hass, MATCH_ALL, state_changed_listener)
return True

View File

@ -47,7 +47,5 @@ def trigger(hass, config, action):
}, },
}) })
track_time_change(hass, time_automation_listener, return track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds) hour=hours, minute=minutes, second=seconds)
return True

View File

@ -58,7 +58,5 @@ def trigger(hass, config, action):
}, },
}) })
track_state_change( return track_state_change(hass, entity_id, zone_automation_listener,
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) MATCH_ALL, MATCH_ALL)
return True

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
@ -32,8 +31,8 @@ class DemoCover(CoverDevice):
self._tilt_position = tilt_position self._tilt_position = tilt_position
self._closing = True self._closing = True
self._closing_tilt = True self._closing_tilt = True
self._listener_cover = None self._unsub_listener_cover = None
self._listener_cover_tilt = None self._unsub_listener_cover_tilt = None
@property @property
def name(self): def name(self):
@ -120,10 +119,9 @@ class DemoCover(CoverDevice):
"""Stop the cover.""" """Stop the cover."""
if self._position is None: if self._position is None:
return return
if self._listener_cover is not None: if self._unsub_listener_cover is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._unsub_listener_cover()
self._listener_cover) self._unsub_listener_cover = None
self._listener_cover = None
self._set_position = None self._set_position = None
def stop_cover_tilt(self, **kwargs): def stop_cover_tilt(self, **kwargs):
@ -131,16 +129,15 @@ class DemoCover(CoverDevice):
if self._tilt_position is None: if self._tilt_position is None:
return return
if self._listener_cover_tilt is not None: if self._unsub_listener_cover_tilt is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._unsub_listener_cover_tilt()
self._listener_cover_tilt) self._unsub_listener_cover_tilt = None
self._listener_cover_tilt = None
self._set_tilt_position = None self._set_tilt_position = None
def _listen_cover(self): def _listen_cover(self):
"""Listen for changes in cover.""" """Listen for changes in cover."""
if self._listener_cover is None: if self._unsub_listener_cover is None:
self._listener_cover = track_utc_time_change( self._unsub_listener_cover = track_utc_time_change(
self.hass, self._time_changed_cover) self.hass, self._time_changed_cover)
def _time_changed_cover(self, now): def _time_changed_cover(self, now):
@ -156,8 +153,8 @@ class DemoCover(CoverDevice):
def _listen_cover_tilt(self): def _listen_cover_tilt(self):
"""Listen for changes in cover tilt.""" """Listen for changes in cover tilt."""
if self._listener_cover_tilt is None: if self._unsub_listener_cover_tilt is None:
self._listener_cover_tilt = track_utc_time_change( self._unsub_listener_cover_tilt = track_utc_time_change(
self.hass, self._time_changed_cover_tilt) self.hass, self._time_changed_cover_tilt)
def _time_changed_cover_tilt(self, now): def _time_changed_cover_tilt(self, now):

View File

@ -175,6 +175,7 @@ class Group(Entity):
self.group_off = None self.group_off = None
self._assumed_state = False self._assumed_state = False
self._lock = threading.Lock() self._lock = threading.Lock()
self._unsub_state_changed = None
if entity_ids is not None: if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids) self.update_tracked_entity_ids(entity_ids)
@ -236,15 +237,16 @@ class Group(Entity):
def start(self): def start(self):
"""Start tracking members.""" """Start tracking members."""
track_state_change( self._unsub_state_changed = track_state_change(
self.hass, self.tracking, self._state_changed_listener) self.hass, self.tracking, self._state_changed_listener)
def stop(self): def stop(self):
"""Unregister the group from Home Assistant.""" """Unregister the group from Home Assistant."""
self.hass.states.remove(self.entity_id) self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener( if self._unsub_state_changed:
ha.EVENT_STATE_CHANGED, self._state_changed_listener) self._unsub_state_changed()
self._unsub_state_changed = None
def update(self): def update(self):
"""Query all members and determine current group state.""" """Query all members and determine current group state."""

View File

@ -170,9 +170,14 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD],
event.data[ATTR_QOS]) event.data[ATTR_QOS])
hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, mqtt_topic_subscriber) remove = hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED,
mqtt_topic_subscriber)
# Future: track subscriber count and unsubscribe in remove
MQTT_CLIENT.subscribe(topic, qos) MQTT_CLIENT.subscribe(topic, qos)
return remove
def _setup_server(hass, config): def _setup_server(hass, config):
"""Try to start embedded MQTT broker.""" """Try to start embedded MQTT broker."""

View File

@ -92,7 +92,8 @@ class States(Base): # type: ignore
else: else:
dbstate.domain = state.domain dbstate.domain = state.domain
dbstate.state = state.state dbstate.state = state.state
dbstate.attributes = json.dumps(dict(state.attributes)) dbstate.attributes = json.dumps(dict(state.attributes),
cls=JSONEncoder)
dbstate.last_changed = state.last_changed dbstate.last_changed = state.last_changed
dbstate.last_updated = state.last_updated dbstate.last_updated = state.last_updated

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
@ -27,7 +26,7 @@ class DemoRollershutter(RollershutterDevice):
self._name = name self._name = name
self._position = position self._position = position
self._moving_up = True self._moving_up = True
self._listener = None self._unsub_listener = None
@property @property
def name(self): def name(self):
@ -70,15 +69,15 @@ class DemoRollershutter(RollershutterDevice):
def stop(self, **kwargs): def stop(self, **kwargs):
"""Stop the roller shutter.""" """Stop the roller shutter."""
if self._listener is not None: if self._unsub_listener is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._listener) self._unsub_listener()
self._listener = None self._unsub_listener = None
def _listen(self): def _listen(self):
"""Listen for changes.""" """Listen for changes."""
if self._listener is None: if self._unsub_listener is None:
self._listener = track_utc_time_change(self.hass, self._unsub_listener = track_utc_time_change(self.hass,
self._time_changed) self._time_changed)
def _time_changed(self, now): def _time_changed(self, now):
"""Track time changes.""" """Track time changes."""

View File

@ -297,6 +297,12 @@ class EventBus(object):
else: else:
self._listeners[event_type] = [listener] self._listeners[event_type] = [listener]
def remove_listener():
"""Remove the listener."""
self.remove_listener(event_type, listener)
return remove_listener
def listen_once(self, event_type, listener): def listen_once(self, event_type, listener):
"""Listen once for event of a specific type. """Listen once for event of a specific type.

View File

@ -14,8 +14,7 @@ def track_state_change(hass, entity_ids, action, from_state=None,
entity_ids, from_state and to_state can be string or list. entity_ids, from_state and to_state can be string or list.
Use list to match multiple. Use list to match multiple.
Returns the listener that listens on the bus for EVENT_STATE_CHANGED. Returns a function that can be called to remove the listener.
Pass the return value into hass.bus.remove_listener to remove it.
""" """
from_state = _process_state_match(from_state) from_state = _process_state_match(from_state)
to_state = _process_state_match(to_state) to_state = _process_state_match(to_state)
@ -50,9 +49,7 @@ def track_state_change(hass, entity_ids, action, from_state=None,
event.data.get('old_state'), event.data.get('old_state'),
event.data.get('new_state')) event.data.get('new_state'))
hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) return hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener)
return state_change_listener
def track_point_in_time(hass, action, point_in_time): def track_point_in_time(hass, action, point_in_time):
@ -77,23 +74,20 @@ def track_point_in_utc_time(hass, action, point_in_time):
"""Listen for matching time_changed events.""" """Listen for matching time_changed events."""
now = event.data[ATTR_NOW] now = event.data[ATTR_NOW]
if now >= point_in_time and \ if now < point_in_time or hasattr(point_in_time_listener, 'run'):
not hasattr(point_in_time_listener, 'run'): return
# Set variable so that we will never run twice. # Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes # Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the # available to execute this listener it might occur that the
# listener gets lined up twice to be executed. This will make # listener gets lined up twice to be executed. This will make
# sure the second time it does nothing. # sure the second time it does nothing.
point_in_time_listener.run = True point_in_time_listener.run = True
remove()
action(now)
hass.bus.remove_listener(EVENT_TIME_CHANGED, remove = hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
point_in_time_listener) return remove
action(now)
hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
return point_in_time_listener
def track_sunrise(hass, action, offset=None): def track_sunrise(hass, action, offset=None):
@ -112,10 +106,18 @@ def track_sunrise(hass, action, offset=None):
def sunrise_automation_listener(now): def sunrise_automation_listener(now):
"""Called when it's time for action.""" """Called when it's time for action."""
nonlocal remove
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
action() action()
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) remove = track_point_in_utc_time(hass, sunrise_automation_listener,
next_rise())
def remove_listener():
"""Remove sunrise listener."""
remove()
return remove_listener
def track_sunset(hass, action, offset=None): def track_sunset(hass, action, offset=None):
@ -134,10 +136,19 @@ def track_sunset(hass, action, offset=None):
def sunset_automation_listener(now): def sunset_automation_listener(now):
"""Called when it's time for action.""" """Called when it's time for action."""
track_point_in_utc_time(hass, sunset_automation_listener, next_set()) nonlocal remove
remove = track_point_in_utc_time(hass, sunset_automation_listener,
next_set())
action() action()
track_point_in_utc_time(hass, sunset_automation_listener, next_set()) remove = track_point_in_utc_time(hass, sunset_automation_listener,
next_set())
def remove_listener():
"""Remove sunset listener."""
remove()
return remove_listener
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
@ -152,8 +163,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
"""Fire every time event that comes in.""" """Fire every time event that comes in."""
action(event.data[ATTR_NOW]) action(event.data[ATTR_NOW])
hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener) return hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener)
return time_change_listener
pmp = _process_time_match pmp = _process_time_match
year, month, day = pmp(year), pmp(month), pmp(day) year, month, day = pmp(year), pmp(month), pmp(day)
@ -178,8 +188,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
action(now) action(now)
hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener) return hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener)
return pattern_time_change_listener
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments

View File

@ -7,7 +7,7 @@ from typing import Optional, Sequence
import voluptuous as vol import voluptuous as vol
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_TIME_CHANGED, CONF_CONDITION from homeassistant.const import CONF_CONDITION
from homeassistant.helpers import ( from homeassistant.helpers import (
service, condition, template, config_validation as cv) service, condition, template, config_validation as cv)
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
@ -47,7 +47,7 @@ class Script():
self.can_cancel = any(CONF_DELAY in action for action self.can_cancel = any(CONF_DELAY in action for action
in self.sequence) in self.sequence)
self._lock = threading.Lock() self._lock = threading.Lock()
self._delay_listener = None self._unsub_delay_listener = None
@property @property
def is_running(self) -> bool: def is_running(self) -> bool:
@ -72,7 +72,7 @@ class Script():
# Call ourselves in the future to continue work # Call ourselves in the future to continue work
def script_delay(now): def script_delay(now):
"""Called after delay is done.""" """Called after delay is done."""
self._delay_listener = None self._unsub_delay_listener = None
self.run(variables) self.run(variables)
delay = action[CONF_DELAY] delay = action[CONF_DELAY]
@ -83,7 +83,7 @@ class Script():
cv.positive_timedelta)( cv.positive_timedelta)(
template.render(self.hass, delay)) template.render(self.hass, delay))
self._delay_listener = track_point_in_utc_time( self._unsub_delay_listener = track_point_in_utc_time(
self.hass, script_delay, self.hass, script_delay,
date_util.utcnow() + delay) date_util.utcnow() + delay)
self._cur = cur + 1 self._cur = cur + 1
@ -139,10 +139,9 @@ class Script():
def _remove_listener(self): def _remove_listener(self):
"""Remove point in time listener, if any.""" """Remove point in time listener, if any."""
if self._delay_listener: if self._unsub_delay_listener:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._unsub_delay_listener()
self._delay_listener) self._unsub_delay_listener = None
self._delay_listener = None
def _log(self, msg): def _log(self, msg):
"""Logger helper.""" """Logger helper."""

View File

@ -44,6 +44,7 @@ def get_test_home_assistant(num_threads=None):
hass.config.elevation = 0 hass.config.elevation = 0
hass.config.time_zone = date_util.get_time_zone('US/Pacific') hass.config.time_zone = date_util.get_time_zone('US/Pacific')
hass.config.units = METRIC_SYSTEM hass.config.units = METRIC_SYSTEM
hass.config.skip_pip = True
if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS:
loader.prepare(hass) loader.prepare(hass)

View File

@ -44,6 +44,13 @@ class TestAutomationEvent(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_event_with_data(self): def test_if_fires_on_event_with_data(self):
"""Test the firing of events with data.""" """Test the firing of events with data."""
assert _setup_component(self.hass, automation.DOMAIN, { assert _setup_component(self.hass, automation.DOMAIN, {

View File

@ -1,9 +1,11 @@
"""The tests for the automation component.""" """The tests for the automation component."""
import unittest import unittest
from unittest.mock import patch
from homeassistant.bootstrap import _setup_component from homeassistant.bootstrap import _setup_component
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
import homeassistant.util.dt as dt_util
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant
@ -45,6 +47,7 @@ class TestAutomation(unittest.TestCase):
"""Test service data.""" """Test service data."""
assert _setup_component(self.hass, automation.DOMAIN, { assert _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
'alias': 'hello',
'trigger': { 'trigger': {
'platform': 'event', 'platform': 'event',
'event_type': 'test_event', 'event_type': 'test_event',
@ -59,10 +62,17 @@ class TestAutomation(unittest.TestCase):
} }
}) })
self.hass.bus.fire('test_event') time = dt_util.utcnow()
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) with patch('homeassistant.components.automation.utcnow',
self.assertEqual('event - test_event', self.calls[0].data['some']) return_value=time):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
assert len(self.calls) == 1
assert 'event - test_event' == self.calls[0].data['some']
state = self.hass.states.get('automation.hello')
assert state is not None
assert state.attributes.get('last_triggered') == time
def test_service_specify_entity_id(self): def test_service_specify_entity_id(self):
"""Test service data.""" """Test service data."""
@ -347,3 +357,60 @@ class TestAutomation(unittest.TestCase):
assert len(self.calls) == 2 assert len(self.calls) == 2
assert self.calls[0].data['position'] == 0 assert self.calls[0].data['position'] == 0
assert self.calls[1].data['position'] == 1 assert self.calls[1].data['position'] == 1
def test_services(self):
"""Test the automation services for turning entities on/off."""
entity_id = 'automation.hello'
assert self.hass.states.get(entity_id) is None
assert not automation.is_on(self.hass, entity_id)
assert _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
}
}
})
assert self.hass.states.get(entity_id) is not None
assert automation.is_on(self.hass, entity_id)
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
assert len(self.calls) == 1
automation.turn_off(self.hass, entity_id)
self.hass.pool.block_till_done()
assert not automation.is_on(self.hass, entity_id)
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
assert len(self.calls) == 1
automation.toggle(self.hass, entity_id)
self.hass.pool.block_till_done()
assert automation.is_on(self.hass, entity_id)
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
assert len(self.calls) == 2
automation.trigger(self.hass, entity_id)
self.hass.pool.block_till_done()
assert len(self.calls) == 3
automation.turn_off(self.hass, entity_id)
self.hass.pool.block_till_done()
automation.trigger(self.hass, entity_id)
self.hass.pool.block_till_done()
assert len(self.calls) == 4
automation.turn_on(self.hass, entity_id)
self.hass.pool.block_till_done()
assert automation.is_on(self.hass, entity_id)

View File

@ -50,6 +50,12 @@ class TestAutomationMQTT(unittest.TestCase):
self.assertEqual('mqtt - test-topic - test_payload', self.assertEqual('mqtt - test-topic - test_payload',
self.calls[0].data['some']) self.calls[0].data['some'])
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
fire_mqtt_message(self.hass, 'test-topic', 'test_payload')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_topic_and_payload_match(self): def test_if_fires_on_topic_and_payload_match(self):
"""Test if message is fired on topic and payload match.""" """Test if message is fired on topic and payload match."""
assert _setup_component(self.hass, automation.DOMAIN, { assert _setup_component(self.hass, automation.DOMAIN, {

View File

@ -45,6 +45,14 @@ class TestAutomationNumericState(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
# Set above 12 so the automation will fire again
self.hass.states.set('test.entity', 12)
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
self.hass.states.set('test.entity', 9)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_entity_change_over_to_below(self): def test_if_fires_on_entity_change_over_to_below(self):
""""Test the firing with changed entity.""" """"Test the firing with changed entity."""
self.hass.states.set('test.entity', 11) self.hass.states.set('test.entity', 11)

View File

@ -59,6 +59,12 @@ class TestAutomationState(unittest.TestCase):
'state - test.entity - hello - world - None', 'state - test.entity - hello - world - None',
self.calls[0].data['some']) self.calls[0].data['some'])
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
self.hass.states.set('test.entity', 'planet')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_entity_change_with_from_filter(self): def test_if_fires_on_entity_change_with_from_filter(self):
"""Test for firing on entity change with filter.""" """Test for firing on entity change with filter."""
assert _setup_component(self.hass, automation.DOMAIN, { assert _setup_component(self.hass, automation.DOMAIN, {

View File

@ -54,6 +54,18 @@ class TestAutomationSun(unittest.TestCase):
} }
}) })
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
fire_time_changed(self.hass, trigger_time)
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
with patch('homeassistant.util.dt.utcnow',
return_value=now):
automation.turn_on(self.hass)
self.hass.pool.block_till_done()
fire_time_changed(self.hass, trigger_time) fire_time_changed(self.hass, trigger_time)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))

View File

@ -45,6 +45,13 @@ class TestAutomationTemplate(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
self.hass.states.set('test.entity', 'planet')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_change_str(self): def test_if_fires_on_change_str(self):
"""Test for firing on change.""" """Test for firing on change."""
assert _setup_component(self.hass, automation.DOMAIN, { assert _setup_component(self.hass, automation.DOMAIN, {
@ -149,6 +156,9 @@ class TestAutomationTemplate(unittest.TestCase):
} }
}) })
self.hass.pool.block_till_done()
self.calls = []
self.hass.states.set('test.entity', 'hello') self.hass.states.set('test.entity', 'hello')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
@ -209,9 +219,12 @@ class TestAutomationTemplate(unittest.TestCase):
} }
}) })
self.hass.pool.block_till_done()
self.calls = []
self.hass.states.set('test.entity', 'world') self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) assert len(self.calls) == 0
def test_if_fires_on_change_with_template_advanced(self): def test_if_fires_on_change_with_template_advanced(self):
"""Test for firing on change with template advanced.""" """Test for firing on change with template advanced."""
@ -237,6 +250,9 @@ class TestAutomationTemplate(unittest.TestCase):
} }
}) })
self.hass.pool.block_till_done()
self.calls = []
self.hass.states.set('test.entity', 'world') self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
@ -287,29 +303,32 @@ class TestAutomationTemplate(unittest.TestCase):
} }
}) })
self.hass.pool.block_till_done()
self.calls = []
self.hass.states.set('test.entity', 'world') self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) assert len(self.calls) == 0
self.hass.states.set('test.entity', 'home') self.hass.states.set('test.entity', 'home')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) assert len(self.calls) == 1
self.hass.states.set('test.entity', 'work') self.hass.states.set('test.entity', 'work')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) assert len(self.calls) == 1
self.hass.states.set('test.entity', 'not_home') self.hass.states.set('test.entity', 'not_home')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) assert len(self.calls) == 1
self.hass.states.set('test.entity', 'world') self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) assert len(self.calls) == 1
self.hass.states.set('test.entity', 'home') self.hass.states.set('test.entity', 'home')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls)) assert len(self.calls) == 2
def test_if_action(self): def test_if_action(self):
"""Test for firing if action.""" """Test for firing if action."""

View File

@ -43,7 +43,13 @@ class TestAutomationTime(unittest.TestCase):
}) })
fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0))
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0))
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))

View File

@ -74,6 +74,24 @@ class TestAutomationZone(unittest.TestCase):
'zone - test.entity - hello - hello - test', 'zone - test.entity - hello - hello - test',
self.calls[0].data['some']) self.calls[0].data['some'])
# Set out of zone again so we can trigger call
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.pool.block_till_done()
automation.turn_off(self.hass)
self.hass.pool.block_till_done()
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_for_enter_on_zone_leave(self): def test_if_not_fires_for_enter_on_zone_leave(self):
"""Test for not firing on zone leave.""" """Test for not firing on zone leave."""
self.hass.states.set('test.entity', 'hello', { self.hass.states.set('test.entity', 'hello', {

View File

@ -149,7 +149,7 @@ class TestMQTT(unittest.TestCase):
def test_subscribe_topic(self): def test_subscribe_topic(self):
"""Test the subscription of a topic.""" """Test the subscription of a topic."""
mqtt.subscribe(self.hass, 'test-topic', self.record_calls) unsub = mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
fire_mqtt_message(self.hass, 'test-topic', 'test-payload') fire_mqtt_message(self.hass, 'test-topic', 'test-payload')
@ -158,6 +158,13 @@ class TestMQTT(unittest.TestCase):
self.assertEqual('test-topic', self.calls[0][0]) self.assertEqual('test-topic', self.calls[0][0])
self.assertEqual('test-payload', self.calls[0][1]) self.assertEqual('test-payload', self.calls[0][1])
unsub()
fire_mqtt_message(self.hass, 'test-topic', 'test-payload')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_subscribe_topic_not_match(self): def test_subscribe_topic_not_match(self):
"""Test if subscribed topic is not a match.""" """Test if subscribed topic is not a match."""
mqtt.subscribe(self.hass, 'test-topic', self.record_calls) mqtt.subscribe(self.hass, 'test-topic', self.record_calls)

View File

@ -65,13 +65,21 @@ class TestEventHelpers(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(runs)) self.assertEqual(2, len(runs))
unsub = track_point_in_time(
self.hass, lambda x: runs.append(1), birthday_paulus)
unsub()
self._send_time_changed(after_birthday)
self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
def test_track_time_change(self): def test_track_time_change(self):
"""Test tracking time change.""" """Test tracking time change."""
wildcard_runs = [] wildcard_runs = []
specific_runs = [] specific_runs = []
track_time_change(self.hass, lambda x: wildcard_runs.append(1)) unsub = track_time_change(self.hass, lambda x: wildcard_runs.append(1))
track_utc_time_change( unsub_utc = track_utc_time_change(
self.hass, lambda x: specific_runs.append(1), second=[0, 30]) self.hass, lambda x: specific_runs.append(1), second=[0, 30])
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
@ -89,6 +97,14 @@ class TestEventHelpers(unittest.TestCase):
self.assertEqual(2, len(specific_runs)) self.assertEqual(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs)) self.assertEqual(3, len(wildcard_runs))
unsub()
unsub_utc()
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
def test_track_state_change(self): def test_track_state_change(self):
"""Test track_state_change.""" """Test track_state_change."""
# 2 lists to track how often our callbacks get called # 2 lists to track how often our callbacks get called
@ -186,11 +202,12 @@ class TestEventHelpers(unittest.TestCase):
# Track sunrise # Track sunrise
runs = [] runs = []
track_sunrise(self.hass, lambda: runs.append(1)) unsub = track_sunrise(self.hass, lambda: runs.append(1))
offset_runs = [] offset_runs = []
offset = timedelta(minutes=30) offset = timedelta(minutes=30)
track_sunrise(self.hass, lambda: offset_runs.append(1), offset) unsub2 = track_sunrise(self.hass, lambda: offset_runs.append(1),
offset)
# run tests # run tests
self._send_time_changed(next_rising - offset) self._send_time_changed(next_rising - offset)
@ -208,6 +225,14 @@ class TestEventHelpers(unittest.TestCase):
self.assertEqual(2, len(runs)) self.assertEqual(2, len(runs))
self.assertEqual(1, len(offset_runs)) self.assertEqual(1, len(offset_runs))
unsub()
unsub2()
self._send_time_changed(next_rising + offset)
self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
self.assertEqual(1, len(offset_runs))
def test_track_sunset(self): def test_track_sunset(self):
"""Test track the sunset.""" """Test track the sunset."""
latitude = 32.87336 latitude = 32.87336
@ -232,11 +257,11 @@ class TestEventHelpers(unittest.TestCase):
# Track sunset # Track sunset
runs = [] runs = []
track_sunset(self.hass, lambda: runs.append(1)) unsub = track_sunset(self.hass, lambda: runs.append(1))
offset_runs = [] offset_runs = []
offset = timedelta(minutes=30) offset = timedelta(minutes=30)
track_sunset(self.hass, lambda: offset_runs.append(1), offset) unsub2 = track_sunset(self.hass, lambda: offset_runs.append(1), offset)
# Run tests # Run tests
self._send_time_changed(next_setting - offset) self._send_time_changed(next_setting - offset)
@ -254,6 +279,14 @@ class TestEventHelpers(unittest.TestCase):
self.assertEqual(2, len(runs)) self.assertEqual(2, len(runs))
self.assertEqual(1, len(offset_runs)) self.assertEqual(1, len(offset_runs))
unsub()
unsub2()
self._send_time_changed(next_setting + offset)
self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
self.assertEqual(1, len(offset_runs))
def _send_time_changed(self, now): def _send_time_changed(self, now):
"""Send a time changed event.""" """Send a time changed event."""
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
@ -262,7 +295,7 @@ class TestEventHelpers(unittest.TestCase):
"""Test periodic tasks per minute.""" """Test periodic tasks per minute."""
specific_runs = [] specific_runs = []
track_utc_time_change( unsub = track_utc_time_change(
self.hass, lambda x: specific_runs.append(1), minute='/5') self.hass, lambda x: specific_runs.append(1), minute='/5')
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
@ -277,11 +310,17 @@ class TestEventHelpers(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs)) self.assertEqual(2, len(specific_runs))
unsub()
self._send_time_changed(datetime(2014, 5, 24, 12, 5, 0))
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
def test_periodic_task_hour(self): def test_periodic_task_hour(self):
"""Test periodic tasks per hour.""" """Test periodic tasks per hour."""
specific_runs = [] specific_runs = []
track_utc_time_change( unsub = track_utc_time_change(
self.hass, lambda x: specific_runs.append(1), hour='/2') self.hass, lambda x: specific_runs.append(1), hour='/2')
self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
@ -304,11 +343,17 @@ class TestEventHelpers(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(3, len(specific_runs)) self.assertEqual(3, len(specific_runs))
unsub()
self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0))
self.hass.pool.block_till_done()
self.assertEqual(3, len(specific_runs))
def test_periodic_task_day(self): def test_periodic_task_day(self):
"""Test periodic tasks per day.""" """Test periodic tasks per day."""
specific_runs = [] specific_runs = []
track_utc_time_change( unsub = track_utc_time_change(
self.hass, lambda x: specific_runs.append(1), day='/2') self.hass, lambda x: specific_runs.append(1), day='/2')
self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
@ -323,11 +368,17 @@ class TestEventHelpers(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs)) self.assertEqual(2, len(specific_runs))
unsub()
self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0))
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
def test_periodic_task_year(self): def test_periodic_task_year(self):
"""Test periodic tasks per year.""" """Test periodic tasks per year."""
specific_runs = [] specific_runs = []
track_utc_time_change( unsub = track_utc_time_change(
self.hass, lambda x: specific_runs.append(1), year='/2') self.hass, lambda x: specific_runs.append(1), year='/2')
self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
@ -342,6 +393,12 @@ class TestEventHelpers(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs)) self.assertEqual(2, len(specific_runs))
unsub()
self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0))
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
def test_periodic_task_wrong_input(self): def test_periodic_task_wrong_input(self):
"""Test periodic tasks with wrong input.""" """Test periodic tasks with wrong input."""
specific_runs = [] specific_runs = []

View File

@ -177,6 +177,7 @@ class TestBootstrap:
return_value=False) return_value=False)
def test_component_not_installed_if_requirement_fails(self, mock_install): def test_component_not_installed_if_requirement_fails(self, mock_install):
"""Component setup should fail if requirement can't install.""" """Component setup should fail if requirement can't install."""
self.hass.config.skip_pip = False
loader.set_component( loader.set_component(
'comp', MockModule('comp', requirements=['package==0.0.1'])) 'comp', MockModule('comp', requirements=['package==0.0.1']))

View File

@ -175,6 +175,29 @@ class TestEventBus(unittest.TestCase):
# Try deleting listener while category doesn't exist either # Try deleting listener while category doesn't exist either
self.bus.remove_listener('test', listener) self.bus.remove_listener('test', listener)
def test_unsubscribe_listener(self):
"""Test unsubscribe listener from returned function."""
self.bus._pool.add_worker()
calls = []
def listener(event):
"""Mock listener."""
calls.append(event)
unsub = self.bus.listen('test', listener)
self.bus.fire('test')
self.bus._pool.block_till_done()
assert len(calls) == 1
unsub()
self.bus.fire('event')
self.bus._pool.block_till_done()
assert len(calls) == 1
def test_listen_once_event(self): def test_listen_once_event(self):
"""Test listen_once_event method.""" """Test listen_once_event method."""
runs = [] runs = []