From fbd68b6f89af1d8022b1f00d66a651472251bb3d Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 23 Jan 2016 19:39:59 -0500 Subject: [PATCH 01/15] Created automation decorator prototype Created an initial iteration of an Automation decorator. --- .../components/automation/__init__.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9c464f6954e..839cc71c37f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,7 +6,11 @@ Allows to setup simple automation rules via the config file. For more details about this component, please refer to the documentation at https://home-assistant.io/components/automation/ """ +from datetime import datetime +import functools +import inspect import logging +import yaml from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM @@ -31,6 +35,8 @@ CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND +CUSTOM_AUTOMATIONS = [] + _LOGGER = logging.getLogger(__name__) @@ -63,6 +69,115 @@ def setup(hass, config): return True +def activate(hass, config, domain): + """ Activate the automations for specified domain """ + for auto_rule in CUSTOM_AUTOMATIONS: + if auto_rule.domain == domain: + try: + success = auto_rule.activate(hass, config) + except Exception: + _LOGGER.exception('Error activating automation %s', + auto_rule.alias) + success = True + + if not success: + _LOGGER.error('Error activating automation %s', + auto_rule.alias) + + +class Automation(object): + """ Decorator for automation functions """ + + hass = None + + def __init__(self, action): + # store action and config + self.action = action + self.config = yaml.load(inspect.getdoc(action)) + self._activated = False + self._last_run = None + self._running = 0 + + # register the automation + module = inspect.getmodule(action) + self._domain = module.DOMAIN + CUSTOM_AUTOMATIONS.append(self) + + functools.update_wrapper(self, action) + + def __call__(self): + """ Call the action """ + if not self.activated: + return + + self._running += 1 + + _LOGGER.info('Executing %s', self.alias) + logbook.log_entry(self.hass, self.alias, 'has been triggered', DOMAIN) + + try: + self.action(self) + except Exception: + _LOGGER.exception('Error running Python automation: %s', + self.alias) + else: + self._last_run = datetime.now() + + self._running -= 1 + + @property + def alias(self): + """ The alias for the function """ + if CONF_ALIAS in self.config: + return self.config[CONF_ALIAS] + return None + + @property + def domain(self): + """ The domain to which this automation belongs """ + return self._domain + + @property + def is_running(self): + """ Boolean if the automation is running """ + return self._running > 0 + + @property + def num_running(self): + """ Integer of how many instances of the automation are running """ + return self._running + + @property + def activated(self): + """ Boolean indicating if the automation has been activated """ + return self._activated + + @property + def last_run(self): + """ Datetime object of the last automation completion """ + return self._last_run + + def activate(self, hass, config): + """ Activates the automation with HASS """ + self.hass = hass + + if self.activated: + return True + + if CONF_CONDITION in self.config or CONF_CONDITION_TYPE in self.config: + action = _process_if(hass, config, self.config, self.action) + + if action is None: + return False + self.action = action + + _process_trigger(hass, config, self.config.get(CONF_TRIGGER, []), + self.alias, self) + + self._activated = True + return True + + def _setup_automation(hass, config_block, name, config): """ Setup one instance of automation """ From a65d0f05496d837dbfb8aea88f6da3d12d7b2ac5 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 14:44:48 -0500 Subject: [PATCH 02/15] Reverting Automation decorator in favor of a new approach. --- .../components/automation/__init__.py | 115 ------------------ 1 file changed, 115 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 839cc71c37f..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,11 +6,7 @@ Allows to setup simple automation rules via the config file. For more details about this component, please refer to the documentation at https://home-assistant.io/components/automation/ """ -from datetime import datetime -import functools -import inspect import logging -import yaml from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM @@ -35,8 +31,6 @@ CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND -CUSTOM_AUTOMATIONS = [] - _LOGGER = logging.getLogger(__name__) @@ -69,115 +63,6 @@ def setup(hass, config): return True -def activate(hass, config, domain): - """ Activate the automations for specified domain """ - for auto_rule in CUSTOM_AUTOMATIONS: - if auto_rule.domain == domain: - try: - success = auto_rule.activate(hass, config) - except Exception: - _LOGGER.exception('Error activating automation %s', - auto_rule.alias) - success = True - - if not success: - _LOGGER.error('Error activating automation %s', - auto_rule.alias) - - -class Automation(object): - """ Decorator for automation functions """ - - hass = None - - def __init__(self, action): - # store action and config - self.action = action - self.config = yaml.load(inspect.getdoc(action)) - self._activated = False - self._last_run = None - self._running = 0 - - # register the automation - module = inspect.getmodule(action) - self._domain = module.DOMAIN - CUSTOM_AUTOMATIONS.append(self) - - functools.update_wrapper(self, action) - - def __call__(self): - """ Call the action """ - if not self.activated: - return - - self._running += 1 - - _LOGGER.info('Executing %s', self.alias) - logbook.log_entry(self.hass, self.alias, 'has been triggered', DOMAIN) - - try: - self.action(self) - except Exception: - _LOGGER.exception('Error running Python automation: %s', - self.alias) - else: - self._last_run = datetime.now() - - self._running -= 1 - - @property - def alias(self): - """ The alias for the function """ - if CONF_ALIAS in self.config: - return self.config[CONF_ALIAS] - return None - - @property - def domain(self): - """ The domain to which this automation belongs """ - return self._domain - - @property - def is_running(self): - """ Boolean if the automation is running """ - return self._running > 0 - - @property - def num_running(self): - """ Integer of how many instances of the automation are running """ - return self._running - - @property - def activated(self): - """ Boolean indicating if the automation has been activated """ - return self._activated - - @property - def last_run(self): - """ Datetime object of the last automation completion """ - return self._last_run - - def activate(self, hass, config): - """ Activates the automation with HASS """ - self.hass = hass - - if self.activated: - return True - - if CONF_CONDITION in self.config or CONF_CONDITION_TYPE in self.config: - action = _process_if(hass, config, self.config, self.action) - - if action is None: - return False - self.action = action - - _process_trigger(hass, config, self.config.get(CONF_TRIGGER, []), - self.alias, self) - - self._activated = True - return True - - def _setup_automation(hass, config_block, name, config): """ Setup one instance of automation """ From 81dd1515ae8d3c18b7f1d48b4874d95b3c6e3eec Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:07:09 -0500 Subject: [PATCH 03/15] Moved sunrise/sunset tracking to helpers The automation component contained some very handy and generic functions for tracking sunset and sunrise. This was moved to helpers/event.py. --- homeassistant/components/automation/sun.py | 44 ++------------------ homeassistant/helpers/event.py | 48 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 0616c0a48e6..6abb59eede6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -10,7 +10,7 @@ import logging from datetime import timedelta from homeassistant.components import sun -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import track_sunrise, track_sunset import homeassistant.util.dt as dt_util DEPENDENCIES = ['sun'] @@ -47,9 +47,9 @@ def trigger(hass, config, action): # Do something to call action if event == EVENT_SUNRISE: - trigger_sunrise(hass, action, offset) + track_sunrise(hass, action, offset) else: - trigger_sunset(hass, action, offset) + track_sunset(hass, action, offset) return True @@ -125,44 +125,6 @@ def if_action(hass, config): return time_if -def trigger_sunrise(hass, action, offset): - """ Trigger action at next sun rise. """ - def next_rise(): - """ Returns next sunrise. """ - next_time = sun.next_rising_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunrise_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - action() - - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - - -def trigger_sunset(hass, action, offset): - """ Trigger action at next sun set. """ - def next_set(): - """ Returns next sunrise. """ - next_time = sun.next_setting_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunset_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - action() - - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - - def _parse_offset(raw_offset): if raw_offset is None: return timedelta(0) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3934a6c52ef..e8c9d0048b0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,11 +1,13 @@ """ Helpers for listening to events """ +from datetime import timedelta import functools as ft from ..util import dt as dt_util from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) +from homeassistant.components import sun def track_state_change(hass, entity_ids, action, from_state=None, @@ -95,6 +97,52 @@ def track_point_in_utc_time(hass, action, point_in_time): return point_in_time_listener +def track_sunrise(hass, action, offset=None): + """ + Adds a listener that will fire a specified offset from sunrise daily. + """ + offset = offset or timedelta() + + def next_rise(): + """ Returns next sunrise. """ + next_time = sun.next_rising_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunrise_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + action() + + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + + +def track_sunset(hass, action, offset=None): + """ + Adds a listener that will fire a specified offset from sunset daily. + """ + offset = offset or timedelta() + + def next_set(): + """ Returns next sunrise. """ + next_time = sun.next_setting_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunset_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + action() + + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + + # pylint: disable=too-many-arguments def track_utc_time_change(hass, action, year=None, month=None, day=None, hour=None, minute=None, second=None, local=False): From 0f937cad7452e1b606dddd4f25666572dbf43b97 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:28:09 -0500 Subject: [PATCH 04/15] Initial pass at event decorators Created event decorators for custom components. Decorators were created for the events: track_state_change, track_sunrise, track_sunset, and track_time_change. --- homeassistant/bootstrap.py | 4 + homeassistant/helpers/event_decorators.py | 146 ++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 homeassistant/helpers/event_decorators.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b704fc082ac..e78d70fd11a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,6 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistnat.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -203,6 +204,9 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, for domain in loader.load_order_components(components): _setup_component(hass, domain, config) + # activate event decorators + event_decorators.activate(hass) + return hass diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py new file mode 100644 index 00000000000..35b1247cf60 --- /dev/null +++ b/homeassistant/helpers/event_decorators.py @@ -0,0 +1,146 @@ +""" Event Decorators for custom components """ + +from datetime import datetime +import functools +import inspect +import logging + +from homeassistant.helpers import event +from homeassistant.components import logbook + +REGISTERED_DECORATORS = [] +_LOGGER = logging.getLogger(__name__) + + +def track_state_change(entity_ids, from_state=None, to_state=None): + """ Decorator factory to track state changes for entity id """ + + def track_state_change_decorator(action): + """ Decorator to track state changes """ + return Automation(action, event.track_state_change, + {"entity_ids": entity_ids, "from_state": from_state, + "to_state": to_state}) + + return track_state_change_decorator + + +def track_sunrise(offset=None): + """ Decorator factory to track sunrise events """ + + def track_sunrise_decorator(action): + """ Decorator to track sunrise events """ + return Automation(action, event.track_sunrise, {"offset": offset}) + + return track_sunrise_decorator + + +def track_sunset(offset=None): + """ Decorator factory to track sunset events """ + + def track_sunset_decorator(action): + """ Decorator to track sunset events """ + return Automation(action, event.track_sunset, {"offset": offset}) + + return track_sunset_decorator + + +# pylint: disable=too-many-arguments +def track_time_change(year=None, month=None, day=None, hour=None, minute=None, + second=None): + """ Decorator factory to track time changes """ + + def track_time_change_decorator(action): + """ Decorator to track time changes """ + return Automation(action, event.track_time_change, + {"year": year, "month": month, "day": day, + "hour": hour, "minute": minute, "second": second}) + + return track_time_change_decorator + + +def activate(hass): + """ Activate all event decorators """ + Automation.hass = hass + + return all([rule.activate() for rule in REGISTERED_DECORATORS]) + + +class Automation(object): + """ Base Decorator for automation functions """ + + hass = None + + def __init__(self, action, event, event_args): + # store action and config + self.action = action + self._event = (event, event_args) + self._activated = False + self._last_run = None + self._running = 0 + module = inspect.getmodule(action) + self._domain = module.DOMAIN + + REGISTERED_DECORATORS.append(self) + + functools.update_wrapper(self, action) + + def __call__(self, *args, **kwargs): + """ Call the action """ + if not self.activated: + return + + self._running += 1 + + _LOGGER.info('Executing %s', self.alias) + logbook.log_entry(self.hass, self.alias, 'has been triggered', + self._domain) + + try: + self.action(*args, **kwargs) + except Exception: + _LOGGER.exception('Error running Python automation: %s', + self.alias) + else: + self._last_run = datetime.now() + + self._running -= 1 + + @property + def alias(self): + """ The name of the action """ + return self.action.__name__ + + @property + def domain(self): + """ The domain to which this automation belongs """ + return self._domain + + @property + def is_running(self): + """ Boolean if the automation is running """ + return self._running > 0 + + @property + def num_running(self): + """ Integer of how many instances of the automation are running """ + return self._running + + @property + def activated(self): + """ Boolean indicating if the automation has been activated """ + return self._activated + + @property + def last_run(self): + """ Datetime object of the last automation completion """ + return self._last_run + + def activate(self): + """ Activates the automation with HASS """ + if self.activated: + return True + + self._event[0](hass=self.hass, action=self.action, **self._event[1]) + + self._activated = True + return True From 02e634c6a2df7a03dbdb6edcf00003d0df85f20c Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:55:47 -0500 Subject: [PATCH 05/15] Fixed bugs to allow HA to boot again 1) helpers/event should not import the sun component unless it is requested. This prevents circular import. 2) fixed import typo in bootstrap 2) bootstrap cannot import event_decorators until it is needed because this leads to a circular import. --- homeassistant/bootstrap.py | 2 +- homeassistant/helpers/event.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e78d70fd11a..aa649c400a0 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,6 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistnat.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -205,6 +204,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, _setup_component(hass, domain, config) # activate event decorators + from homeassistant.helpers import event_decorators event_decorators.activate(hass) return hass diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e8c9d0048b0..42725b8eea9 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -7,7 +7,6 @@ import functools as ft from ..util import dt as dt_util from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) -from homeassistant.components import sun def track_state_change(hass, entity_ids, action, from_state=None, @@ -101,6 +100,7 @@ def track_sunrise(hass, action, offset=None): """ Adds a listener that will fire a specified offset from sunrise daily. """ + from homeassistant.components import sun offset = offset or timedelta() def next_rise(): @@ -124,6 +124,7 @@ def track_sunset(hass, action, offset=None): """ Adds a listener that will fire a specified offset from sunset daily. """ + from homeassistant.components import sun offset = offset or timedelta() def next_set(): From ef92940ffb4980dc490bd91799299718c7b34a63 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 16:45:35 -0500 Subject: [PATCH 06/15] A few lint fixes to event decorators. --- homeassistant/helpers/event_decorators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index 35b1247cf60..fbf979eaf47 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -70,10 +70,10 @@ class Automation(object): hass = None - def __init__(self, action, event, event_args): + def __init__(self, action, event_fun, event_args): # store action and config self.action = action - self._event = (event, event_args) + self._event = (event_fun, event_args) self._activated = False self._last_run = None self._running = 0 @@ -86,6 +86,7 @@ class Automation(object): def __call__(self, *args, **kwargs): """ Call the action """ + # pylint: disable=broad-except if not self.activated: return From 40dbeb0b60aaafc6909c477626851094659a4f4e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 17:46:05 -0500 Subject: [PATCH 07/15] Another revision on event decorators This revision of event decorators removes much of the complexity. The decorated functions are no longer wrapped with a class that tracks last_run, etc. Bootstrap now gives hass to the event_decorators module before initializing components so the decorators no longer require activation. --- homeassistant/bootstrap.py | 8 +- homeassistant/helpers/event_decorators.py | 116 ++-------------------- 2 files changed, 15 insertions(+), 109 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index aa649c400a0..132178361e0 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,6 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistant.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -199,14 +200,13 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, _LOGGER.info('Home Assistant core initialized') + # give event decorators access to HASS + event_decorators.HASS = hass + # Setup the components for domain in loader.load_order_components(components): _setup_component(hass, domain, config) - # activate event decorators - from homeassistant.helpers import event_decorators - event_decorators.activate(hass) - return hass diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index fbf979eaf47..e48cf4bf88d 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -1,15 +1,8 @@ """ Event Decorators for custom components """ -from datetime import datetime -import functools -import inspect -import logging - from homeassistant.helpers import event -from homeassistant.components import logbook -REGISTERED_DECORATORS = [] -_LOGGER = logging.getLogger(__name__) +HASS = None def track_state_change(entity_ids, from_state=None, to_state=None): @@ -17,9 +10,9 @@ def track_state_change(entity_ids, from_state=None, to_state=None): def track_state_change_decorator(action): """ Decorator to track state changes """ - return Automation(action, event.track_state_change, - {"entity_ids": entity_ids, "from_state": from_state, - "to_state": to_state}) + event.track_state_change(HASS, entity_ids, action, + from_state, to_state) + return action return track_state_change_decorator @@ -29,7 +22,8 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ - return Automation(action, event.track_sunrise, {"offset": offset}) + event.track_sunrise(HASS, action, offset) + return action return track_sunrise_decorator @@ -39,7 +33,8 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ - return Automation(action, event.track_sunset, {"offset": offset}) + event.track_sunset(HASS, action, offset) + return action return track_sunset_decorator @@ -51,97 +46,8 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ - return Automation(action, event.track_time_change, - {"year": year, "month": month, "day": day, - "hour": hour, "minute": minute, "second": second}) + event.track_time_change(HASS, action, year, month, day, hour, + minute, second) + return action return track_time_change_decorator - - -def activate(hass): - """ Activate all event decorators """ - Automation.hass = hass - - return all([rule.activate() for rule in REGISTERED_DECORATORS]) - - -class Automation(object): - """ Base Decorator for automation functions """ - - hass = None - - def __init__(self, action, event_fun, event_args): - # store action and config - self.action = action - self._event = (event_fun, event_args) - self._activated = False - self._last_run = None - self._running = 0 - module = inspect.getmodule(action) - self._domain = module.DOMAIN - - REGISTERED_DECORATORS.append(self) - - functools.update_wrapper(self, action) - - def __call__(self, *args, **kwargs): - """ Call the action """ - # pylint: disable=broad-except - if not self.activated: - return - - self._running += 1 - - _LOGGER.info('Executing %s', self.alias) - logbook.log_entry(self.hass, self.alias, 'has been triggered', - self._domain) - - try: - self.action(*args, **kwargs) - except Exception: - _LOGGER.exception('Error running Python automation: %s', - self.alias) - else: - self._last_run = datetime.now() - - self._running -= 1 - - @property - def alias(self): - """ The name of the action """ - return self.action.__name__ - - @property - def domain(self): - """ The domain to which this automation belongs """ - return self._domain - - @property - def is_running(self): - """ Boolean if the automation is running """ - return self._running > 0 - - @property - def num_running(self): - """ Integer of how many instances of the automation are running """ - return self._running - - @property - def activated(self): - """ Boolean indicating if the automation has been activated """ - return self._activated - - @property - def last_run(self): - """ Datetime object of the last automation completion """ - return self._last_run - - def activate(self): - """ Activates the automation with HASS """ - if self.activated: - return True - - self._event[0](hass=self.hass, action=self.action, **self._event[1]) - - self._activated = True - return True From 57725136c0413f156b093ee0049752ed3f30cd25 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 19:52:22 -0500 Subject: [PATCH 08/15] Many updates regarding event decorators 1. Added HASS to the arguments for callbacks that are created with event decorators. 2. Added a service decorator. 3. Updated example.py in the example config to use the event decorators. --- config/custom_components/example.py | 145 +++++++++++----------- homeassistant/helpers/event_decorators.py | 35 +++++- 2 files changed, 104 insertions(+), 76 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index ee7f18f437a..dc29d4b1967 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -29,9 +29,12 @@ import time import logging from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF -import homeassistant.loader as loader from homeassistant.helpers import validate_config +from homeassistant.helpers.event_decorators import \ + track_state_change, track_time_change, service import homeassistant.components as core +from homeassistant.components import device_tracker +from homeassistant.components import light # The domain of your component. Should be equal to the name of your component DOMAIN = "example" @@ -39,11 +42,14 @@ DOMAIN = "example" # List of component names (string) your component depends upon # We depend on group because group will be loaded after all the components that # initialize devices have been setup. -DEPENDENCIES = ['group'] +DEPENDENCIES = ['group', 'device_tracker', 'light'] # Configuration key for the entity id we are targetting CONF_TARGET = 'target' +# Variable for storing configuration parameters +CONFIG = {} + # Name of the service that we expose SERVICE_FLASH = 'flash' @@ -58,79 +64,76 @@ def setup(hass, config): if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False - target_id = config[DOMAIN][CONF_TARGET] + CONFIG['target_id'] = config[DOMAIN][CONF_TARGET] # Validate that the target entity id exists - if hass.states.get(target_id) is None: - _LOGGER.error("Target entity id %s does not exist", target_id) + if hass.states.get(config['target_id']) is None: + _LOGGER.error("Target entity id %s does not exist", + CONFIG['target_id']) # Tell the bootstrapper that we failed to initialize return False - # We will use the component helper methods to check the states. - device_tracker = loader.get_component('device_tracker') - light = loader.get_component('light') - - def track_devices(entity_id, old_state, new_state): - """ Called when the group.all devices change state. """ - - # If anyone comes home and the core is not on, turn it on. - if new_state.state == STATE_HOME and not core.is_on(hass, target_id): - - core.turn_on(hass, target_id) - - # If all people leave the house and the core is on, turn it off - elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): - - core.turn_off(hass, target_id) - - # Register our track_devices method to receive state changes of the - # all tracked devices group. - hass.states.track_change( - device_tracker.ENTITY_ID_ALL_DEVICES, track_devices) - - def wake_up(now): - """ Turn it on in the morning if there are people home and - it is not already on. """ - - if device_tracker.is_on(hass) and not core.is_on(hass, target_id): - _LOGGER.info('People home at 7AM, turning it on') - core.turn_on(hass, target_id) - - # Register our wake_up service to be called at 7AM in the morning - hass.track_time_change(wake_up, hour=7, minute=0, second=0) - - def all_lights_off(entity_id, old_state, new_state): - """ If all lights turn off, turn off. """ - - if core.is_on(hass, target_id): - _LOGGER.info('All lights have been turned off, turning it off') - core.turn_off(hass, target_id) - - # Register our all_lights_off method to be called when all lights turn off - hass.states.track_change( - light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF) - - def flash_service(call): - """ Service that will turn the target off for 10 seconds - if on and vice versa. """ - - if core.is_on(hass, target_id): - core.turn_off(hass, target_id) - - time.sleep(10) - - core.turn_on(hass, target_id) - - else: - core.turn_on(hass, target_id) - - time.sleep(10) - - core.turn_off(hass, target_id) - - # Register our service with HASS. - hass.services.register(DOMAIN, SERVICE_FLASH, flash_service) - - # Tells the bootstrapper that the component was successfully initialized + # Tell the bootstrapper that we initialized successfully return True + + +@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) +def track_devices(hass, entity_id, old_state, new_state): + """ Called when the group.all devices change state. """ + target_id = CONFIG['target_id'] + + # If anyone comes home and the entity is not on, turn it on. + if new_state.state == STATE_HOME and not core.is_on(hass, target_id): + + core.turn_on(hass, target_id) + + # If all people leave the house and the entity is on, turn it off + elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): + + core.turn_off(hass, target_id) + + +@track_time_change(hour=7, minute=0, second=0) +def wake_up(hass, now): + """ + Turn it on in the morning (7 AM) if there are people home and + it is not already on. + """ + target_id = CONFIG['target_id'] + + if device_tracker.is_on(hass) and not core.is_on(hass, target_id): + _LOGGER.info('People home at 7AM, turning it on') + core.turn_on(hass, target_id) + + +@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) +def all_lights_off(hass, entity_id, old_state, new_state): + """ If all lights turn off, turn off. """ + target_id = CONFIG['target_id'] + + if core.is_on(hass, target_id): + _LOGGER.info('All lights have been turned off, turning it off') + core.turn_off(hass, target_id) + + +@service(DOMAIN, SERVICE_FLASH) +def flash_service(hass, call): + """ + Service that will turn the target off for 10 seconds if on and vice versa. + """ + target_id = CONFIG['target_id'] + + if core.is_on(hass, target_id): + core.turn_off(hass, target_id) + + time.sleep(10) + + core.turn_on(hass, target_id) + + else: + core.turn_on(hass, target_id) + + time.sleep(10) + + core.turn_off(hass, target_id) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index e48cf4bf88d..0fcd002c169 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -1,16 +1,36 @@ """ Event Decorators for custom components """ +import functools + from homeassistant.helpers import event HASS = None +def _callback(action, *args, **kwargs): + """ adds HASS to callback arguments """ + action(HASS, *args, **kwargs) + + +def service(domain, service): + """ Decorator factory to register a service """ + + def register_service_decorator(action): + """ Decorator to register a service """ + HASS.services.register(domain, service, + functools.partial(_callback, action)) + return action + + return register_service_decorator + + def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ def track_state_change_decorator(action): """ Decorator to track state changes """ - event.track_state_change(HASS, entity_ids, action, + event.track_state_change(HASS, entity_ids, + functools.partial(_callback, action), from_state, to_state) return action @@ -22,7 +42,9 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ - event.track_sunrise(HASS, action, offset) + event.track_sunrise(HASS, + functools.partial(_callback, action), + action, offset) return action return track_sunrise_decorator @@ -33,7 +55,9 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ - event.track_sunset(HASS, action, offset) + event.track_sunset(HASS, + functools.partial(_callback, action), + offset) return action return track_sunset_decorator @@ -46,8 +70,9 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ - event.track_time_change(HASS, action, year, month, day, hour, - minute, second) + event.track_time_change(HASS, + functools.partial(_callback, action), + year, month, day, hour, minute, second) return action return track_time_change_decorator From 2fa98167c2d4d80f74207b1cfb137783a1b8872a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 20:05:40 -0500 Subject: [PATCH 09/15] Updated example.py component Cleaned up example.py to better handle failed loads. --- config/custom_components/example.py | 52 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index dc29d4b1967..3fb46d18792 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -48,7 +48,7 @@ DEPENDENCIES = ['group', 'device_tracker', 'light'] CONF_TARGET = 'target' # Variable for storing configuration parameters -CONFIG = {} +TARGET_ID = None # Name of the service that we expose SERVICE_FLASH = 'flash' @@ -59,19 +59,22 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup example component. """ + global TARGET_ID # Validate that all required config options are given if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False - CONFIG['target_id'] = config[DOMAIN][CONF_TARGET] + TARGET_ID = config[DOMAIN][CONF_TARGET] # Validate that the target entity id exists - if hass.states.get(config['target_id']) is None: + if hass.states.get(TARGET_ID) is None: _LOGGER.error("Target entity id %s does not exist", - CONFIG['target_id']) + TARGET_ID) - # Tell the bootstrapper that we failed to initialize + # Tell the bootstrapper that we failed to initialize and clear the + # stored target id so our functions don't run. + TARGET_ID = None return False # Tell the bootstrapper that we initialized successfully @@ -81,17 +84,19 @@ def setup(hass, config): @track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) def track_devices(hass, entity_id, old_state, new_state): """ Called when the group.all devices change state. """ - target_id = CONFIG['target_id'] + # If the target id is not set, return + if not TARGET_ID: + return # If anyone comes home and the entity is not on, turn it on. - if new_state.state == STATE_HOME and not core.is_on(hass, target_id): + if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID): - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) # If all people leave the house and the entity is on, turn it off - elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): + elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID): - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) @track_time_change(hour=7, minute=0, second=0) @@ -100,21 +105,23 @@ def wake_up(hass, now): Turn it on in the morning (7 AM) if there are people home and it is not already on. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if device_tracker.is_on(hass) and not core.is_on(hass, target_id): + if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID): _LOGGER.info('People home at 7AM, turning it on') - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) @track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) def all_lights_off(hass, entity_id, old_state, new_state): """ If all lights turn off, turn off. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if core.is_on(hass, target_id): + if core.is_on(hass, TARGET_ID): _LOGGER.info('All lights have been turned off, turning it off') - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) @service(DOMAIN, SERVICE_FLASH) @@ -122,18 +129,19 @@ def flash_service(hass, call): """ Service that will turn the target off for 10 seconds if on and vice versa. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if core.is_on(hass, target_id): - core.turn_off(hass, target_id) + if core.is_on(hass, TARGET_ID): + core.turn_off(hass, TARGET_ID) time.sleep(10) - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) else: - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) time.sleep(10) - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) From 54b82ecd91e1dcc000eb020bbabb4a3aa2b0e1cc Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 21:06:15 -0500 Subject: [PATCH 10/15] Lint fixes and additions to event decorators 1. service decorator was overwriting the function name with one of its arguments. 2. Accidentally left an extra argument in track_sunrise. 3. Added track_utc_time_change decorator. --- homeassistant/helpers/event_decorators.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index 0fcd002c169..f7aee82631c 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -12,12 +12,12 @@ def _callback(action, *args, **kwargs): action(HASS, *args, **kwargs) -def service(domain, service): +def service(domain, service_name): """ Decorator factory to register a service """ def register_service_decorator(action): """ Decorator to register a service """ - HASS.services.register(domain, service, + HASS.services.register(domain, service_name, functools.partial(_callback, action)) return action @@ -44,7 +44,7 @@ def track_sunrise(offset=None): """ Decorator to track sunrise events """ event.track_sunrise(HASS, functools.partial(_callback, action), - action, offset) + offset) return action return track_sunrise_decorator @@ -76,3 +76,18 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, return action return track_time_change_decorator + + +# pylint: disable=too-many-arguments +def track_utc_time_change(year=None, month=None, day=None, hour=None, + minute=None, second=None): + """ Decorator factory to track time changes """ + + def track_utc_time_change_decorator(action): + """ Decorator to track time changes """ + event.track_utc_time_change(HASS, + functools.partial(_callback, action), + year, month, day, hour, minute, second) + return action + + return track_utc_time_change_decorator From f66aeb2e7332f6fb8062717cb56cbffa46d50e58 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 22:23:56 -0500 Subject: [PATCH 11/15] Added event helper tests 1. Added tests for all event decorators 2. Added tests for sunrise and sunset event helpers --- tests/helpers/test_event.py | 95 +++++++++++ tests/helpers/test_event_decorators.py | 211 +++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 tests/helpers/test_event_decorators.py diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 89711e2584e..e12ca0c4124 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -9,8 +9,11 @@ Tests event helpers. import unittest from datetime import datetime +from astral import Astral + import homeassistant.core as ha from homeassistant.helpers.event import * +from homeassistant.components import sun class TestEventHelpers(unittest.TestCase): @@ -121,6 +124,98 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(1, len(specific_runs)) self.assertEqual(3, len(wildcard_runs)) + def test_track_sunrise(self): + """ Test track sunrise """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + # track sunrise + runs = [] + track_sunrise(self.hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + track_sunrise(self.hass, lambda: offset_runs.append(1), offset) + + # run tests + self._send_time_changed(next_rising - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + 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): + """ Test track sunset """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_setting = (astral.sunset_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + # track sunset + runs = [] + track_sunset(self.hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + track_sunset(self.hass, lambda: offset_runs.append(1), offset) + + # run tests + self._send_time_changed(next_setting - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + 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): """ Send a time changed event. """ self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py new file mode 100644 index 00000000000..d246cf1844c --- /dev/null +++ b/tests/helpers/test_event_decorators.py @@ -0,0 +1,211 @@ +""" +tests.helpers.test_event_decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests event decorator helpers. +""" +# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=too-few-public-methods +import unittest +from datetime import datetime, timedelta + +from astral import Astral + +import homeassistant.core as ha +import homeassistant.util.dt as dt_util +from homeassistant.helpers import event_decorators +from homeassistant.helpers.event_decorators import ( + track_time_change, track_utc_time_change, track_state_change, service, + track_sunrise, track_sunset) +from homeassistant.components import sun + + +class TestEventDecoratorHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = ha.HomeAssistant() + self.hass.states.set("light.Bowl", "on") + self.hass.states.set("switch.AC", "off") + + event_decorators.HASS = self.hass + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_service(self): + """ Test service registration decorator. """ + runs = [] + + decor = service('test', 'test') + decor(lambda x, y: runs.append(1)) + + self.hass.services.call('test', 'test') + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + def test_track_sunrise(self): + """ Test track sunrise decorator """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + # use decorator + runs = [] + decor = track_sunrise() + decor(lambda x: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + decor = track_sunrise(offset) + decor(lambda x: offset_runs.append(1)) + + # run tests + self._send_time_changed(next_rising - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + 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): + """ Test track sunset decorator """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_setting = (astral.sunset_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + # use decorator + runs = [] + decor = track_sunset() + decor(lambda x: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + decor = track_sunset(offset) + decor(lambda x: offset_runs.append(1)) + + # run tests + self._send_time_changed(next_setting - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + 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 test_track_time_change(self): + """ Test tracking time change. """ + wildcard_runs = [] + specific_runs = [] + + decor = track_time_change() + decor(lambda x, y: wildcard_runs.append(1)) + + decor = track_utc_time_change(second=[0, 30]) + decor(lambda x, y: specific_runs.append(1)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + 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): + """ Test track_state_change. """ + # 2 lists to track how often our callbacks get called + specific_runs = [] + wildcard_runs = [] + + decor = track_state_change('light.Bowl', 'on', 'off') + decor(lambda a, b, c, d: specific_runs.append(1)) + + decor = track_state_change('light.Bowl', ha.MATCH_ALL, ha.MATCH_ALL) + decor(lambda a, b, c, d: wildcard_runs.append(1)) + + # Set same state should not trigger a state change/listener + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(0, len(specific_runs)) + self.assertEqual(0, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'off') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + # State change off -> off + self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + def _send_time_changed(self, now): + """ Send a time changed event. """ + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) From 5830da63b134fbc83de6e177ac1f19055d8ead2e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 22:46:30 -0500 Subject: [PATCH 12/15] Moved service decorator to service helpers Moved the service decorator to the service helpers module and moved the associated tests. --- config/custom_components/example.py | 3 ++- homeassistant/bootstrap.py | 3 ++- homeassistant/helpers/service.py | 24 ++++++++++++++++++++++-- tests/helpers/test_event_decorators.py | 13 +------------ tests/helpers/test_service.py | 14 +++++++++++++- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index 3fb46d18792..08b3f4c2a83 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -31,7 +31,8 @@ import logging from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF from homeassistant.helpers import validate_config from homeassistant.helpers.event_decorators import \ - track_state_change, track_time_change, service + track_state_change, track_time_change +from homeassistant.helpers.service import service import homeassistant.components as core from homeassistant.components import device_tracker from homeassistant.components import light diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 132178361e0..dbec25b99b6 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistant.helpers import event_decorators +from homeassistant.helpers import event_decorators, service from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -202,6 +202,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, # give event decorators access to HASS event_decorators.HASS = hass + service.HASS = hass # Setup the components for domain in loader.load_order_components(components): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 15cfe9b97c6..952de383444 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,9 +1,12 @@ """Service calling related helpers.""" +import functools import logging from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID +HASS = None + CONF_SERVICE = 'service' CONF_SERVICE_ENTITY_ID = 'entity_id' CONF_SERVICE_DATA = 'data' @@ -11,6 +14,23 @@ CONF_SERVICE_DATA = 'data' _LOGGER = logging.getLogger(__name__) +def _callback(action, *args, **kwargs): + """ adds HASS to callback arguments """ + action(HASS, *args, **kwargs) + + +def service(domain, service_name): + """ Decorator factory to register a service """ + + def register_service_decorator(action): + """ Decorator to register a service """ + HASS.services.register(domain, service_name, + functools.partial(_callback, action)) + return action + + return register_service_decorator + + def call_from_config(hass, config, blocking=False): """Call a service based on a config hash.""" if not isinstance(config, dict) or CONF_SERVICE not in config: @@ -18,7 +38,7 @@ def call_from_config(hass, config, blocking=False): return try: - domain, service = split_entity_id(config[CONF_SERVICE]) + domain, service_name = split_entity_id(config[CONF_SERVICE]) except ValueError: _LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE]) return @@ -40,4 +60,4 @@ def call_from_config(hass, config, blocking=False): elif entity_id is not None: service_data[ATTR_ENTITY_ID] = entity_id - hass.services.call(domain, service, service_data, blocking) + hass.services.call(domain, service_name, service_data, blocking) diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py index d246cf1844c..db836e372ae 100644 --- a/tests/helpers/test_event_decorators.py +++ b/tests/helpers/test_event_decorators.py @@ -15,7 +15,7 @@ import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.helpers import event_decorators from homeassistant.helpers.event_decorators import ( - track_time_change, track_utc_time_change, track_state_change, service, + track_time_change, track_utc_time_change, track_state_change, track_sunrise, track_sunset) from homeassistant.components import sun @@ -37,17 +37,6 @@ class TestEventDecoratorHelpers(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_service(self): - """ Test service registration decorator. """ - runs = [] - - decor = service('test', 'test') - decor(lambda x, y: runs.append(1)) - - self.hass.services.call('test', 'test') - self.hass.pool.block_till_done() - self.assertEqual(1, len(runs)) - def test_track_sunrise(self): """ Test track sunrise decorator """ latitude = 32.87336 diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index aa2cab07d0d..d0bd1669f07 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -7,7 +7,6 @@ Test service helpers. import unittest from unittest.mock import patch -from homeassistant.const import SERVICE_TURN_ON from homeassistant.helpers import service from tests.common import get_test_home_assistant, mock_service @@ -23,10 +22,23 @@ class TestServiceHelpers(unittest.TestCase): self.hass = get_test_home_assistant() self.calls = mock_service(self.hass, 'test_domain', 'test_service') + service.HASS = self.hass + def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() + def test_service(self): + """ Test service registration decorator. """ + runs = [] + + decor = service.service('test', 'test') + decor(lambda x, y: runs.append(1)) + + self.hass.services.call('test', 'test') + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + def test_split_entity_string(self): service.call_from_config(self.hass, { 'service': 'test_domain.test_service', From 3b89102338bfd8c7576d5d37a97a51f4bc101c2d Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 23:00:43 -0500 Subject: [PATCH 13/15] Fixed lint issue from merge extract_entity_ids from the service helpers was overwriting the service decorator with one of its attributes. This was fixed. --- homeassistant/helpers/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ccab891eedb..6617d0e1514 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -64,18 +64,18 @@ def call_from_config(hass, config, blocking=False): hass.services.call(domain, service_name, service_data, blocking) -def extract_entity_ids(hass, service): +def extract_entity_ids(hass, service_call): """ Helper method to extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. """ - if not (service.data and ATTR_ENTITY_ID in service.data): + if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] group = get_component('group') # Entity ID attr can be a list or a string - service_ent_id = service.data[ATTR_ENTITY_ID] + service_ent_id = service_call.data[ATTR_ENTITY_ID] if isinstance(service_ent_id, str): return group.expand_entity_ids(hass, [service_ent_id]) From bcdfc555e0e6da26d05df98a213f97af7787567a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 23:09:09 -0500 Subject: [PATCH 14/15] Removed service decorator from event decorators --- homeassistant/helpers/event_decorators.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index f7aee82631c..b1a1e1f0304 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -12,18 +12,6 @@ def _callback(action, *args, **kwargs): action(HASS, *args, **kwargs) -def service(domain, service_name): - """ Decorator factory to register a service """ - - def register_service_decorator(action): - """ Decorator to register a service """ - HASS.services.register(domain, service_name, - functools.partial(_callback, action)) - return action - - return register_service_decorator - - def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ From 8406f8181172036047cdfbce4d6fab1d342e86b3 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Mon, 25 Jan 2016 00:14:16 -0500 Subject: [PATCH 15/15] Removed decorator callback The decorator callback was not actually necessary so it was removed and replaced with a partial function instead. --- homeassistant/helpers/event_decorators.py | 15 +++++---------- homeassistant/helpers/service.py | 7 +------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index b1a1e1f0304..e98f912ef64 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -7,18 +7,13 @@ from homeassistant.helpers import event HASS = None -def _callback(action, *args, **kwargs): - """ adds HASS to callback arguments """ - action(HASS, *args, **kwargs) - - def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ def track_state_change_decorator(action): """ Decorator to track state changes """ event.track_state_change(HASS, entity_ids, - functools.partial(_callback, action), + functools.partial(action, HASS), from_state, to_state) return action @@ -31,7 +26,7 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ event.track_sunrise(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), offset) return action @@ -44,7 +39,7 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ event.track_sunset(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), offset) return action @@ -59,7 +54,7 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ event.track_time_change(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), year, month, day, hour, minute, second) return action @@ -74,7 +69,7 @@ def track_utc_time_change(year=None, month=None, day=None, hour=None, def track_utc_time_change_decorator(action): """ Decorator to track time changes """ event.track_utc_time_change(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), year, month, day, hour, minute, second) return action diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 6617d0e1514..2d198910408 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -15,18 +15,13 @@ CONF_SERVICE_DATA = 'data' _LOGGER = logging.getLogger(__name__) -def _callback(action, *args, **kwargs): - """ adds HASS to callback arguments """ - action(HASS, *args, **kwargs) - - def service(domain, service_name): """ Decorator factory to register a service """ def register_service_decorator(action): """ Decorator to register a service """ HASS.services.register(domain, service_name, - functools.partial(_callback, action)) + functools.partial(action, HASS)) return action return register_service_decorator