From 5fe73cf33eb5b27a9317e33979639524fe8799ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 15:34:42 -0800 Subject: [PATCH 01/60] Made dependency loading more robust --- homeassistant/bootstrap.py | 104 +++---------------------------------- homeassistant/loader.py | 98 ++++++++++++++++++++++++++++++++++ homeassistant/util.py | 71 +++++++++++++++++++++++++ test/helper.py | 10 ++++ test/test_loader.py | 54 ++++++++++++++++++- 5 files changed, 238 insertions(+), 99 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 81eaefdba24..3875c23b4ce 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -13,12 +13,10 @@ import os import configparser import logging from collections import defaultdict -from itertools import chain import homeassistant import homeassistant.loader as loader import homeassistant.components as core_components -import homeassistant.components.group as group # pylint: disable=too-many-branches, too-many-statements @@ -33,112 +31,22 @@ def from_config_dict(config, hass=None): logger = logging.getLogger(__name__) + loader.prepare(hass) + # Make a copy because we are mutating it. # Convert it to defaultdict so components can always have config dict config = defaultdict(dict, config) - # List of loaded components - components = {} - - # List of components to validate - to_validate = [] - - # List of validated components - validated = [] - - # List of components we are going to load - to_load = [key for key in config.keys() if key != homeassistant.DOMAIN] - - loader.prepare(hass) - - # Load required components - while to_load: - domain = to_load.pop() - - component = loader.get_component(domain) - - # if None it does not exist, error already thrown by get_component - if component is not None: - components[domain] = component - - # Special treatment for GROUP, we want to load it as late as - # possible. We do this by loading it if all other to be loaded - # modules depend on it. - if component.DOMAIN == group.DOMAIN: - pass - - # Components with no dependencies are valid - elif not component.DEPENDENCIES: - validated.append(domain) - - # If dependencies we'll validate it later - else: - to_validate.append(domain) - - # Make sure to load all dependencies that are not being loaded - for dependency in component.DEPENDENCIES: - if dependency not in chain(components.keys(), to_load): - to_load.append(dependency) - - # Validate dependencies - group_added = False - - while to_validate: - newly_validated = [] - - for domain in to_validate: - if all(domain in validated for domain - in components[domain].DEPENDENCIES): - - newly_validated.append(domain) - - # We validated new domains this iteration, add them to validated - if newly_validated: - - # Add newly validated domains to validated - validated.extend(newly_validated) - - # remove domains from to_validate - for domain in newly_validated: - to_validate.remove(domain) - - newly_validated.clear() - - # Nothing validated this iteration. Add group dependency and try again. - elif not group_added: - group_added = True - validated.append(group.DOMAIN) - - # Group has already been added and we still can't validate all. - # Report missing deps as error and skip loading of these domains - else: - for domain in to_validate: - missing_deps = [dep for dep in components[domain].DEPENDENCIES - if dep not in validated] - - logger.error( - "Could not validate all dependencies for %s: %s", - domain, ", ".join(missing_deps)) - - break - - # Make sure we load groups if not in list yet. - if not group_added: - validated.append(group.DOMAIN) - - if group.DOMAIN not in components: - components[group.DOMAIN] = \ - loader.get_component(group.DOMAIN) + # Filter out the common config section [homeassistant] + components = (key for key in config.keys() if key != homeassistant.DOMAIN) # Setup the components if core_components.setup(hass, config): logger.info("Home Assistant core initialized") - for domain in validated: - component = components[domain] - + for domain in loader.load_order_components(components): try: - if component.setup(hass, config): + if loader.get_component(domain).setup(hass, config): logger.info("component %s initialized", domain) else: logger.error("component %s failed to initialize", domain) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 87a76d9eb7d..e537eb96450 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -19,6 +19,10 @@ import pkgutil import importlib import logging +from homeassistant.util import OrderedSet + +PREPARED = False + # List of available components AVAILABLE_COMPONENTS = [] @@ -30,6 +34,8 @@ _LOGGER = logging.getLogger(__name__) def prepare(hass): """ Prepares the loading of components. """ + global PREPARED # pylint: disable=global-statement + # Load the built-in components import homeassistant.components as components @@ -62,9 +68,13 @@ def prepare(hass): AVAILABLE_COMPONENTS.append( 'custom_components.{}'.format(fil[0:-3])) + PREPARED = True + def set_component(comp_name, component): """ Sets a component in the cache. """ + _check_prepared() + _COMPONENT_CACHE[comp_name] = component @@ -76,6 +86,8 @@ def get_component(comp_name): if comp_name in _COMPONENT_CACHE: return _COMPONENT_CACHE[comp_name] + _check_prepared() + # If we ie. try to load custom_components.switch.wemo but the parent # custom_components.switch does not exist, importing it will trigger # an exception because it will try to import the parent. @@ -125,3 +137,89 @@ def get_component(comp_name): _LOGGER.error("Unable to find component %s", comp_name) return None + + +def load_order_components(components): + """ + Takes in a list of components we want to load: + - filters out components we cannot load + - filters out components that have invalid/circular dependencies + - Will ensure that all components that do not directly depend on + the group component will be loaded before the group component. + - returns an OrderedSet load order. + """ + _check_prepared() + + group = get_component('group') + + load_order = OrderedSet() + + # Sort the list of modules on if they depend on group component or not. + # We do this because the components that do not depend on the group + # component usually set up states that the group component requires to be + # created before it can group them. + # This does not matter in the future if we can setup groups without the + # states existing yet. + for comp_load_order in sorted((load_order_component(component) + for component in components), + # Test if group component exists in case + # above get_component call had an error. + key=lambda order: + group and group.DOMAIN in order): + load_order.update(comp_load_order) + + return load_order + + +def load_order_component(comp_name): + """ + Returns an OrderedSet of components in the correct order of loading. + Raises HomeAssistantError if a circular dependency is detected. + Returns an empty list if component could not be loaded. + """ + return _load_order_component(comp_name, OrderedSet(), set()) + + +def _load_order_component(comp_name, load_order, loading): + """ Recursive function to get load order of components. """ + component = get_component(comp_name) + + # if None it does not exist, error already thrown by get_component + if component is None: + return OrderedSet() + + loading.add(comp_name) + + for dependency in component.DEPENDENCIES: + # Check not already loaded + if dependency not in load_order: + # If we are already loading it, we have a circular dependency + if dependency in loading: + _LOGGER.error('Circular dependency detected: %s -> %s', + comp_name, dependency) + + return OrderedSet() + + dep_load_order = _load_order_component( + dependency, load_order, loading) + + # length == 0 means error loading dependency or children + if len(dep_load_order) == 0: + _LOGGER.error('Error loading %s dependency: %s', + comp_name, dependency) + return OrderedSet() + + load_order.update(dep_load_order) + + load_order.add(comp_name) + loading.remove(comp_name) + + return load_order + + +def _check_prepared(): + """ Issues a warning if loader.prepare() has never been called. """ + if not PREPARED: + _LOGGER.warning(( + "You did not call loader.prepare() yet. " + "Certain functionality might not be working.")) diff --git a/homeassistant/util.py b/homeassistant/util.py index 67188926546..42b6e668096 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -4,6 +4,8 @@ homeassistant.util Helper methods for various modules. """ +import collections +from itertools import chain import threading import queue import datetime @@ -176,6 +178,75 @@ class OrderedEnum(enum.Enum): return NotImplemented +class OrderedSet(collections.MutableSet): + """ Ordered set taken from http://code.activestate.com/recipes/576694/ """ + + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + """ Add an element to the set. """ + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + """ Discard an element from the set. """ + if key in self.map: + key, prev_item, next_item = self.map.pop(key) + prev_item[2] = next_item + next_item[1] = prev_item + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): # pylint: disable=arguments-differ + """ Pops element of the beginning of the set. + Set last=True to pop from the back. """ + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def update(self, *args): + """ Add elements from args to the set. """ + for item in chain(*args): + self.add(item) + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + def validate_config(config, items, logger): """ Validates if all items are available in the configuration. diff --git a/test/helper.py b/test/helper.py index 3e5c9caa761..ed0c2c74e82 100644 --- a/test/helper.py +++ b/test/helper.py @@ -28,3 +28,13 @@ def mock_service(hass, domain, service): domain, service, lambda call: calls.append(call)) return calls + + +class MockModule(object): + """ Provides a fake module. """ + + def __init__(self, domain, dependencies=[], setup=None): + self.DOMAIN = domain + self.DEPENDENCIES = dependencies + # Setup a mock setup if none given. + self.setup = lambda hass, config: False if setup is None else setup diff --git a/test/test_loader.py b/test/test_loader.py index 8d9cad71303..ceb398815b4 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -6,13 +6,14 @@ Provides tests to verify that we can load components. """ # pylint: disable=too-many-public-methods,protected-access import unittest +from collections import namedtuple import homeassistant as ha import homeassistant.loader as loader import homeassistant.components.http as http import mock_toggledevice_platform -from helper import get_test_home_assistant +from helper import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): @@ -37,3 +38,54 @@ class TestLoader(unittest.TestCase): self.assertEqual(http, loader.get_component('http')) self.assertIsNotNone(loader.get_component('custom_one')) + + def test_load_order_component(self): + """ Test if we can get the proper load order of components. """ + loader.set_component('mod1', MockModule('mod1')) + loader.set_component('mod2', MockModule('mod2', ['mod1'])) + loader.set_component('mod3', MockModule('mod3', ['mod2'])) + + self.assertEqual( + ['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3')) + + # Create circular dependency + loader.set_component('mod1', MockModule('mod1', ['mod3'])) + + self.assertEqual([], loader.load_order_component('mod3')) + + # Depend on non-existing component + loader.set_component('mod1', MockModule('mod1', ['nonexisting'])) + + self.assertEqual([], loader.load_order_component('mod1')) + + # Try to get load order for non-existing component + self.assertEqual([], loader.load_order_component('mod1')) + + def test_load_order_components(self): + loader.set_component('mod1', MockModule('mod1', ['group'])) + loader.set_component('mod2', MockModule('mod2', ['mod1', 'sun'])) + loader.set_component('mod3', MockModule('mod3', ['mod2'])) + loader.set_component('mod4', MockModule('mod4', ['group'])) + + self.assertEqual( + ['group', 'mod4', 'mod1', 'sun', 'mod2', 'mod3'], + loader.load_order_components(['mod4', 'mod3', 'mod2'])) + + loader.set_component('mod1', MockModule('mod1')) + loader.set_component('mod2', MockModule('mod2', ['group'])) + + self.assertEqual( + ['mod1', 'group', 'mod2'], + loader.load_order_components(['mod2', 'mod1'])) + + # Add a non existing one + self.assertEqual( + ['mod1', 'group', 'mod2'], + loader.load_order_components(['mod2', 'nonexisting', 'mod1'])) + + # Depend on a non existing one + loader.set_component('mod1', MockModule('mod1', ['nonexisting'])) + + self.assertEqual( + ['group', 'mod2'], + loader.load_order_components(['mod2', 'mod1'])) From 89102b5652e32821a2836788100d907488cdfa84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 19:42:16 -0800 Subject: [PATCH 02/60] Increase homeassistant.util test coverage --- homeassistant/util.py | 6 +-- test/test_util.py | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/homeassistant/util.py b/homeassistant/util.py index 42b6e668096..a4b812803d4 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -223,8 +223,8 @@ class OrderedSet(collections.MutableSet): curr = curr[1] def pop(self, last=True): # pylint: disable=arguments-differ - """ Pops element of the beginning of the set. - Set last=True to pop from the back. """ + """ Pops element of the end of the set. + Set last=False to pop from the beginning. """ if not self: raise KeyError('set is empty') key = self.end[1][0] if last else self.end[2][0] @@ -318,7 +318,7 @@ class ThreadPool(object): """ Add a job to be sent to the workers. """ with self._lock: if not self.running: - raise Exception("We are shutting down the ") + raise RuntimeError("ThreadPool not running") self.work_queue.put(PriorityQueueItem(priority, job)) diff --git a/test/test_util.py b/test/test_util.py index 727034ac409..1edb4b92a8c 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -43,6 +43,7 @@ class TestUtil(unittest.TestCase): """ Test str_to_datetime. """ self.assertEqual(datetime(1986, 7, 9, 12, 0, 0), util.str_to_datetime("12:00:00 09-07-1986")) + self.assertIsNone(util.str_to_datetime("not a datetime string")) def test_split_entity_id(self): """ Test split_entity_id. """ @@ -87,3 +88,105 @@ class TestUtil(unittest.TestCase): self.assertEqual( "Beer_3", util.ensure_unique_string("Beer", ["Beer", "Beer_2"])) + self.assertEqual( + "Beer", + util.ensure_unique_string("Beer", ["Wine", "Soda"])) + + def test_ordered_enum(self): + """ Test the ordered enum class. """ + + class TestEnum(util.OrderedEnum): + """ Test enum that can be ordered. """ + FIRST = 1 + SECOND = 2 + THIRD = 3 + + self.assertTrue(TestEnum.SECOND >= TestEnum.FIRST) + self.assertTrue(TestEnum.SECOND >= TestEnum.SECOND) + self.assertFalse(TestEnum.SECOND >= TestEnum.THIRD) + + self.assertTrue(TestEnum.SECOND > TestEnum.FIRST) + self.assertFalse(TestEnum.SECOND > TestEnum.SECOND) + self.assertFalse(TestEnum.SECOND > TestEnum.THIRD) + + self.assertFalse(TestEnum.SECOND <= TestEnum.FIRST) + self.assertTrue(TestEnum.SECOND <= TestEnum.SECOND) + self.assertTrue(TestEnum.SECOND <= TestEnum.THIRD) + + self.assertFalse(TestEnum.SECOND < TestEnum.FIRST) + self.assertFalse(TestEnum.SECOND < TestEnum.SECOND) + self.assertTrue(TestEnum.SECOND < TestEnum.THIRD) + + # Python will raise a TypeError if the <, <=, >, >= methods + # raise a NotImplemented error. + self.assertRaises(TypeError, + lambda x, y: x < y, TestEnum.FIRST, 1) + + self.assertRaises(TypeError, + lambda x, y: x <= y, TestEnum.FIRST, 1) + + self.assertRaises(TypeError, + lambda x, y: x > y, TestEnum.FIRST, 1) + + self.assertRaises(TypeError, + lambda x, y: x >= y, TestEnum.FIRST, 1) + + def test_ordered_set(self): + set1 = util.OrderedSet([1, 2, 3, 4]) + set2 = util.OrderedSet([3, 4, 5]) + + self.assertEqual(4, len(set1)) + self.assertEqual(3, len(set2)) + + self.assertIn(1, set1) + self.assertIn(2, set1) + self.assertIn(3, set1) + self.assertIn(4, set1) + self.assertNotIn(5, set1) + + self.assertNotIn(1, set2) + self.assertNotIn(2, set2) + self.assertIn(3, set2) + self.assertIn(4, set2) + self.assertIn(5, set2) + + set1.add(5) + self.assertIn(5, set1) + + set1.discard(5) + self.assertNotIn(5, set1) + + # Try again while key is not in + set1.discard(5) + self.assertNotIn(5, set1) + + self.assertEqual([1, 2, 3, 4], list(set1)) + self.assertEqual([4, 3, 2, 1], list(reversed(set1))) + + self.assertEqual(1, set1.pop(False)) + self.assertEqual([2, 3, 4], list(set1)) + + self.assertEqual(4, set1.pop()) + self.assertEqual([2, 3], list(set1)) + + self.assertEqual('OrderedSet()', str(util.OrderedSet())) + self.assertEqual('OrderedSet([2, 3])', str(set1)) + + self.assertEqual(set1, util.OrderedSet([2, 3])) + self.assertNotEqual(set1, util.OrderedSet([3, 2])) + self.assertEqual(set1, set([2, 3])) + self.assertEqual(set1, set([3, 2])) + self.assertEqual(set1, [2, 3]) + self.assertEqual(set1, [3, 2]) + self.assertNotEqual(set1, set([2])) + + set3 = util.OrderedSet(set1) + set3.update(set2) + + self.assertEqual([3, 4, 5, 2], set3) + self.assertEqual([3, 4, 5, 2], set1 | set2) + self.assertEqual([3], set1 & set2) + self.assertEqual([2], set1 - set2) + + set1.update([1, 2], [5, 6]) + self.assertEqual([2, 3, 1, 5, 6], set1) From 006310c883836d907dc6d4bb8f511e048f1ba9f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 20:22:08 -0800 Subject: [PATCH 03/60] Home Assistant stop is more robust --- homeassistant/__init__.py | 10 +++++----- homeassistant/components/http/__init__.py | 8 ++++---- test/test_component_chromecast.py | 2 +- test/test_component_core.py | 2 +- test/test_component_light.py | 2 +- test/test_component_sun.py | 2 +- test/test_component_switch.py | 2 +- test/test_core.py | 2 +- test/test_loader.py | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 7b836af8d05..8ebbe16f41d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -610,7 +610,7 @@ class Timer(threading.Thread): threading.Thread.__init__(self) self.daemon = True - self._bus = hass.bus + self.hass = hass self.interval = interval or TIMER_INTERVAL self._stop = threading.Event() @@ -622,12 +622,12 @@ class Timer(threading.Thread): hass.listen_once_event(EVENT_HOMEASSISTANT_START, lambda event: self.start()) - hass.listen_once_event(EVENT_HOMEASSISTANT_STOP, - lambda event: self._stop.set()) - def run(self): """ Start the timer. """ + self.hass.listen_once_event(EVENT_HOMEASSISTANT_STOP, + lambda event: self._stop.set()) + _LOGGER.info("Timer:starting") last_fired_on_second = -1 @@ -658,7 +658,7 @@ class Timer(threading.Thread): last_fired_on_second = now.second - self._bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) class HomeAssistantError(Exception): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8c6ddc1e5e0..7f054190128 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -136,10 +136,6 @@ def setup(hass, config): lambda event: threading.Thread(target=server.start, daemon=True).start()) - hass.listen_once_event( - ha.EVENT_HOMEASSISTANT_STOP, - lambda event: server.shutdown()) - # If no local api set, set one with known information if isinstance(hass, rem.HomeAssistant) and hass.local_api is None: hass.local_api = \ @@ -173,6 +169,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): def start(self): """ Starts the server. """ + self.hass.listen_once_event( + ha.EVENT_HOMEASSISTANT_STOP, + lambda event: self.shutdown()) + _LOGGER.info( "Starting web interface at http://%s:%d", *self.server_address) diff --git a/test/test_component_chromecast.py b/test/test_component_chromecast.py index 27e5bbb0370..82cb511c5b7 100644 --- a/test/test_component_chromecast.py +++ b/test/test_component_chromecast.py @@ -33,7 +33,7 @@ class TestChromecast(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_is_on(self): """ Test is_on method. """ diff --git a/test/test_component_core.py b/test/test_component_core.py index eb56112f906..24d66a6a3bb 100644 --- a/test/test_component_core.py +++ b/test/test_component_core.py @@ -26,7 +26,7 @@ class TestComponentsCore(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_is_on(self): """ Test is_on method. """ diff --git a/test/test_component_light.py b/test/test_component_light.py index 04db6d9ec13..255187ea253 100644 --- a/test/test_component_light.py +++ b/test/test_component_light.py @@ -29,7 +29,7 @@ class TestLight(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) diff --git a/test/test_component_sun.py b/test/test_component_sun.py index f7cf7c90dab..d37c8f678f9 100644 --- a/test/test_component_sun.py +++ b/test/test_component_sun.py @@ -22,7 +22,7 @@ class TestSun(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_is_on(self): """ Test is_on method. """ diff --git a/test/test_component_switch.py b/test/test_component_switch.py index ca207972720..f81fcdd8f89 100644 --- a/test/test_component_switch.py +++ b/test/test_component_switch.py @@ -34,7 +34,7 @@ class TestSwitch(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_methods(self): """ Test is_on, turn_on, turn_off methods. """ diff --git a/test/test_core.py b/test/test_core.py index 91a56df14fa..d8b4e2d1283 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -30,7 +30,7 @@ class TestHomeAssistant(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_get_config_path(self): """ Test get_config_path method. """ diff --git a/test/test_loader.py b/test/test_loader.py index ceb398815b4..bbabc44ed59 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -24,7 +24,7 @@ class TestLoader(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass._pool.stop() + self.hass.stop() def test_set_component(self): """ Test if set_component works. """ From cfae4c667a1fe2cf9862064adcd8e4b7cd2acd91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 20:22:29 -0800 Subject: [PATCH 04/60] Minor improvements to homeassistant.remote tests --- homeassistant/remote.py | 3 ++- test/test_remote.py | 33 +++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 3ee38d15af8..7b28de0fa9e 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -124,7 +124,8 @@ class HomeAssistant(ha.HomeAssistant): # pylint: disable=too-many-format-args random_password = '%030x'.format(random.randrange(16**30)) - http.setup(self, random_password) + http.setup( + self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}}) ha.Timer(self) diff --git a/test/test_remote.py b/test/test_remote.py index 0d0f92c6a33..753b9319a8e 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -6,6 +6,7 @@ Tests Home Assistant remote methods and classes. """ # pylint: disable=protected-access,too-many-public-methods import unittest +import json import homeassistant as ha import homeassistant.remote as remote @@ -35,19 +36,15 @@ def setUpModule(): # pylint: disable=invalid-name hass.states.set('test.test', 'a_state') http.setup(hass, - {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD}}) + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: 8122}}) hass.start() - master_api = remote.API("127.0.0.1", API_PASSWORD) + master_api = remote.API("127.0.0.1", API_PASSWORD, 8122) # Start slave - local_api = remote.API("127.0.0.1", API_PASSWORD, 8124) - slave = remote.HomeAssistant(master_api, local_api) - - http.setup(slave, - {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: 8124}}) + slave = remote.HomeAssistant(master_api) slave.start() @@ -71,6 +68,10 @@ class TestRemoteMethods(unittest.TestCase): remote.validate_api( remote.API("127.0.0.1", API_PASSWORD + "A"))) + self.assertEqual(remote.APIStatus.CANNOT_CONNECT, + remote.validate_api( + remote.API("127.0.0.1", API_PASSWORD, 8125))) + def test_get_event_listeners(self): """ Test Python API get_event_listeners. """ local_data = hass.bus.listeners @@ -156,10 +157,16 @@ class TestRemoteClasses(unittest.TestCase): def test_home_assistant_init(self): """ Test HomeAssistant init. """ + # Wrong password self.assertRaises( ha.HomeAssistantError, remote.HomeAssistant, remote.API('127.0.0.1', API_PASSWORD + 'A', 8124)) + # Wrong port + self.assertRaises( + ha.HomeAssistantError, remote.HomeAssistant, + remote.API('127.0.0.1', API_PASSWORD, 8125)) + def test_statemachine_init(self): """ Tests if remote.StateMachine copies all states on init. """ self.assertEqual(len(hass.states.all()), @@ -199,3 +206,13 @@ class TestRemoteClasses(unittest.TestCase): hass._pool.block_till_done() self.assertEqual(1, len(test_value)) + + def test_json_encoder(self): + """ Test the JSON Encoder. """ + ha_json_enc = remote.JSONEncoder() + state = hass.states.get('test.test') + + self.assertEqual(state.as_dict(), ha_json_enc.default(state)) + + # Default method raises TypeError if non HA object + self.assertRaises(TypeError, ha_json_enc.default, 1) From 63f8f2ee7feaa06d2d09d9c47520e5d2d9482ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 20:37:23 -0800 Subject: [PATCH 05/60] Remote instance closes event forwarding on shutdown --- homeassistant/remote.py | 3 +++ test/test_remote.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 7b28de0fa9e..626e7779d06 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -142,6 +142,9 @@ class HomeAssistant(ha.HomeAssistant): self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) + # Disconnect master event forwarding + disconnect_remote_events(self.remote_api, self.local_api) + # Wait till all responses to homeassistant_stop are done self._pool.block_till_done() diff --git a/test/test_remote.py b/test/test_remote.py index 753b9319a8e..d05dda025e0 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -3,6 +3,7 @@ test.remote ~~~~~~~~~~~ Tests Home Assistant remote methods and classes. +Uses port 8122 for master, 8123 for slave """ # pylint: disable=protected-access,too-many-public-methods import unittest @@ -53,8 +54,8 @@ def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server and slave. """ global hass, slave - hass.stop() slave.stop() + hass.stop() class TestRemoteMethods(unittest.TestCase): From cdccdb432a42416175d4788c7c9943dca2892256 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 21:01:44 -0800 Subject: [PATCH 06/60] Test remote methods for errors --- homeassistant/remote.py | 4 ++-- test/test_remote.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 626e7779d06..c0aca8700f3 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -340,7 +340,7 @@ def fire_event(api, event_type, data=None): req.status_code, req.text) except ha.HomeAssistantError: - pass + _LOGGER.exception("Error firing event") def get_state(api, entity_id): @@ -376,7 +376,7 @@ def get_states(api): # ValueError if req.json() can't parse the json _LOGGER.exception("Error fetching states") - return {} + return [] def set_state(api, entity_id, new_state, attributes=None): diff --git a/test/test_remote.py b/test/test_remote.py index d05dda025e0..07e7a23416d 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -4,10 +4,10 @@ test.remote Tests Home Assistant remote methods and classes. Uses port 8122 for master, 8123 for slave +Uses port 8125 as a port that nothing runs on """ # pylint: disable=protected-access,too-many-public-methods import unittest -import json import homeassistant as ha import homeassistant.remote as remote @@ -19,7 +19,7 @@ HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT) HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD} -hass, slave, master_api = None, None, None +hass, slave, master_api, broken_api = None, None, None, None def _url(path=""): @@ -29,7 +29,7 @@ def _url(path=""): def setUpModule(): # pylint: disable=invalid-name """ Initalizes a Home Assistant server and Slave instance. """ - global hass, slave, master_api + global hass, slave, master_api, broken_api hass = ha.HomeAssistant() @@ -49,6 +49,9 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() + # Setup API pointing at nothing + broken_api = remote.API("127.0.0.1", "", 8125) + def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server and slave. """ @@ -70,8 +73,7 @@ class TestRemoteMethods(unittest.TestCase): remote.API("127.0.0.1", API_PASSWORD + "A"))) self.assertEqual(remote.APIStatus.CANNOT_CONNECT, - remote.validate_api( - remote.API("127.0.0.1", API_PASSWORD, 8125))) + remote.validate_api(broken_api)) def test_get_event_listeners(self): """ Test Python API get_event_listeners. """ @@ -84,6 +86,8 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual(len(local_data), 0) + self.assertEqual({}, remote.get_event_listeners(broken_api)) + def test_fire_event(self): """ Test Python API fire_event. """ test_value = [] @@ -100,6 +104,9 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual(1, len(test_value)) + # Should not trigger any exception + remote.fire_event(broken_api, "test.event_no_data") + def test_get_state(self): """ Test Python API get_state. """ @@ -107,11 +114,13 @@ class TestRemoteMethods(unittest.TestCase): hass.states.get('test.test'), remote.get_state(master_api, 'test.test')) + self.assertEqual(None, remote.get_state(broken_api, 'test.test')) + def test_get_states(self): """ Test Python API get_state_entity_ids. """ - self.assertEqual( - remote.get_states(master_api), hass.states.all()) + self.assertEqual(hass.states.all(), remote.get_states(master_api)) + self.assertEqual([], remote.get_states(broken_api)) def test_set_state(self): """ Test Python API set_state. """ @@ -119,6 +128,8 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual('set_test', hass.states.get('test.test').state) + self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) + def test_is_state(self): """ Test Python API is_state. """ @@ -126,6 +137,10 @@ class TestRemoteMethods(unittest.TestCase): remote.is_state(master_api, 'test.test', hass.states.get('test.test').state)) + self.assertFalse( + remote.is_state(broken_api, 'test.test', + hass.states.get('test.test').state)) + def test_get_services(self): """ Test Python API get_services. """ @@ -136,6 +151,8 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual(local, serv_domain["services"]) + self.assertEqual({}, remote.get_services(broken_api)) + def test_call_service(self): """ Test Python API call_service. """ test_value = [] @@ -152,6 +169,9 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual(1, len(test_value)) + # Should not raise an exception + remote.call_service(broken_api, "test_domain", "test_service") + class TestRemoteClasses(unittest.TestCase): """ Test the homeassistant.remote module. """ From 014abdba395a556d44157d1ae8ae6549869a82e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 21:02:03 -0800 Subject: [PATCH 07/60] Remove unused imports in tests --- test/test_loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_loader.py b/test/test_loader.py index bbabc44ed59..a00fc02e250 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -6,9 +6,7 @@ Provides tests to verify that we can load components. """ # pylint: disable=too-many-public-methods,protected-access import unittest -from collections import namedtuple -import homeassistant as ha import homeassistant.loader as loader import homeassistant.components.http as http From a4eb975b595a5bf0a41f6a39b0cd6b9c01970e06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 22:27:44 -0800 Subject: [PATCH 08/60] HTTP more robust and increased test coverage --- homeassistant/components/http/__init__.py | 117 +++++++++--------- homeassistant/remote.py | 62 +++++++--- test/test_component_http.py | 139 ++++++++++++++++++++-- test/test_remote.py | 2 +- 4 files changed, 232 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7f054190128..79bd17044f4 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -108,6 +108,8 @@ CONF_SERVER_HOST = "server_host" CONF_SERVER_PORT = "server_port" CONF_DEVELOPMENT = "development" +DATA_API_PASSWORD = 'api_password' + _LOGGER = logging.getLogger(__name__) @@ -118,7 +120,7 @@ def setup(hass, config): _LOGGER): return False - api_password = config[DOMAIN]['api_password'] + api_password = config[DOMAIN][CONF_API_PASSWORD] # If no server host is given, accept all incoming requests server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') @@ -192,7 +194,6 @@ class RequestHandler(SimpleHTTPRequestHandler): PATHS = [ # debug interface ('GET', URL_ROOT, '_handle_get_root'), - ('POST', URL_ROOT, '_handle_get_root'), # /api - for validation purposes ('GET', rem.URL_API, '_handle_get_api'), @@ -228,8 +229,10 @@ class RequestHandler(SimpleHTTPRequestHandler): ('DELETE', rem.URL_API_EVENT_FORWARD, '_handle_delete_api_event_forward'), - # Statis files + # Static files ('GET', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'), + '_handle_get_static'), + ('HEAD', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'), '_handle_get_static') ] @@ -255,24 +258,22 @@ class RequestHandler(SimpleHTTPRequestHandler): if content_length: body_content = self.rfile.read(content_length).decode("UTF-8") - if self.use_json: - try: - data.update(json.loads(body_content)) - except ValueError: - _LOGGER.exception("Exception parsing JSON: %s", - body_content) + try: + data.update(json.loads(body_content)) + except (TypeError, ValueError): + # TypeError is JSON object is not a dict + # ValueError if we could not parse JSON + _LOGGER.exception("Exception parsing JSON: %s", + body_content) - self._message( - "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) - return - else: - data.update({key: value[-1] for key, value in - parse_qs(body_content).items()}) + self._json_message( + "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) + return api_password = self.headers.get(rem.AUTH_HEADER) - if not api_password and 'api_password' in data: - api_password = data['api_password'] + if not api_password and DATA_API_PASSWORD in data: + api_password = data[DATA_API_PASSWORD] if '_METHOD' in data: method = data.pop('_METHOD') @@ -307,7 +308,7 @@ class RequestHandler(SimpleHTTPRequestHandler): # For API calls we need a valid password if self.use_json and api_password != self.server.api_password: - self._message( + self._json_message( "API password missing or incorrect.", HTTP_UNAUTHORIZED) else: @@ -315,9 +316,11 @@ class RequestHandler(SimpleHTTPRequestHandler): elif path_matched_but_not_method: self.send_response(HTTP_METHOD_NOT_ALLOWED) + self.end_headers() else: self.send_response(HTTP_NOT_FOUND) + self.end_headers() def do_HEAD(self): # pylint: disable=invalid-name """ HEAD request handler. """ @@ -377,7 +380,7 @@ class RequestHandler(SimpleHTTPRequestHandler): # pylint: disable=unused-argument def _handle_get_api(self, path_match, data): """ Renders the debug interface. """ - self._message("API running.") + self._json_message("API running.") # pylint: disable=unused-argument def _handle_get_api_states(self, path_match, data): @@ -394,7 +397,7 @@ class RequestHandler(SimpleHTTPRequestHandler): if state: self._write_json(state) else: - self._message("State does not exist.", HTTP_NOT_FOUND) + self._json_message("State does not exist.", HTTP_NOT_FOUND) def _handle_post_state_entity(self, path_match, data): """ Handles updating the state of an entity. @@ -407,7 +410,7 @@ class RequestHandler(SimpleHTTPRequestHandler): try: new_state = data['state'] except KeyError: - self._message("state not specified", HTTP_BAD_REQUEST) + self._json_message("state not specified", HTTP_BAD_REQUEST) return attributes = data['attributes'] if 'attributes' in data else None @@ -417,19 +420,14 @@ class RequestHandler(SimpleHTTPRequestHandler): # Write state self.server.hass.states.set(entity_id, new_state, attributes) - # Return state if json, else redirect to main page - if self.use_json: - state = self.server.hass.states.get(entity_id) + state = self.server.hass.states.get(entity_id) - status_code = HTTP_CREATED if is_new_state else HTTP_OK + status_code = HTTP_CREATED if is_new_state else HTTP_OK - self._write_json( - state.as_dict(), - status_code=status_code, - location=rem.URL_API_STATES_ENTITY.format(entity_id)) - else: - self._message( - "State of {} changed to {}".format(entity_id, new_state)) + self._write_json( + state.as_dict(), + status_code=status_code, + location=rem.URL_API_STATES_ENTITY.format(entity_id)) def _handle_get_api_events(self, path_match, data): """ Handles getting overview of event listeners. """ @@ -448,8 +446,8 @@ class RequestHandler(SimpleHTTPRequestHandler): event_type = path_match.group('event_type') if event_data is not None and not isinstance(event_data, dict): - self._message("event_data should be an object", - HTTP_UNPROCESSABLE_ENTITY) + self._json_message("event_data should be an object", + HTTP_UNPROCESSABLE_ENTITY) event_origin = ha.EventOrigin.remote @@ -464,7 +462,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self.server.hass.bus.fire(event_type, event_data, event_origin) - self._message("Event {} fired.".format(event_type)) + self._json_message("Event {} fired.".format(event_type)) def _handle_get_api_services(self, path_match, data): """ Handles getting overview of services. """ @@ -485,7 +483,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self.server.hass.call_service(domain, service, data) - self._message("Service {}/{} called.".format(domain, service)) + self._json_message("Service {}/{} called.".format(domain, service)) # pylint: disable=invalid-name def _handle_post_api_event_forward(self, path_match, data): @@ -495,26 +493,31 @@ class RequestHandler(SimpleHTTPRequestHandler): host = data['host'] api_password = data['api_password'] except KeyError: - self._message("No host or api_password received.", - HTTP_BAD_REQUEST) + self._json_message("No host or api_password received.", + HTTP_BAD_REQUEST) return try: port = int(data['port']) if 'port' in data else None except ValueError: - self._message( + self._json_message( "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) return + api = rem.API(host, api_password, port) + + if not api.validate_api(): + self._json_message( + "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY) + return + if self.server.event_forwarder is None: self.server.event_forwarder = \ rem.EventForwarder(self.server.hass) - api = rem.API(host, api_password, port) - self.server.event_forwarder.connect(api) - self._message("Event forwarding setup.") + self._json_message("Event forwarding setup.") def _handle_delete_api_event_forward(self, path_match, data): """ Handles deleting an event forwarding target. """ @@ -522,14 +525,14 @@ class RequestHandler(SimpleHTTPRequestHandler): try: host = data['host'] except KeyError: - self._message("No host received.", - HTTP_BAD_REQUEST) + self._json_message("No host received.", + HTTP_BAD_REQUEST) return try: port = int(data['port']) if 'port' in data else None except ValueError: - self._message( + self._json_message( "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) return @@ -538,7 +541,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self.server.event_forwarder.disconnect(api) - self._message("Event forwarding cancelled.") + self._json_message("Event forwarding cancelled.") def _handle_get_static(self, path_match, data): """ Returns a static file. """ @@ -585,7 +588,10 @@ class RequestHandler(SimpleHTTPRequestHandler): self.end_headers() - if do_gzip: + if self.command == 'HEAD': + return + + elif do_gzip: self.wfile.write(gzip_data) else: @@ -599,22 +605,9 @@ class RequestHandler(SimpleHTTPRequestHandler): if inp: inp.close() - def _message(self, message, status_code=HTTP_OK): + def _json_message(self, message, status_code=HTTP_OK): """ Helper method to return a message to the caller. """ - if self.use_json: - self._write_json({'message': message}, status_code=status_code) - else: - self.send_error(status_code, message) - - def _redirect(self, location): - """ Helper method to redirect caller. """ - self.send_response(HTTP_MOVED_PERMANENTLY) - - self.send_header( - "Location", "{}?api_password={}".format( - location, self.server.api_password)) - - self.end_headers() + self._write_json({'message': message}, status_code=status_code) def _write_json(self, data=None, status_code=HTTP_OK, location=None): """ Helper method to return JSON to the caller. """ diff --git a/homeassistant/remote.py b/homeassistant/remote.py index c0aca8700f3..bcfe6e68e65 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -34,6 +34,7 @@ URL_API_EVENT_FORWARD = "/api/event_forwarding" METHOD_GET = "get" METHOD_POST = "post" +METHOD_DELETE = "delete" _LOGGER = logging.getLogger(__name__) @@ -94,6 +95,10 @@ class API(object): _LOGGER.exception(error) raise ha.HomeAssistantError(error) + def __repr__(self): + return "API({}, {}, {})".format( + self.host, self.api_password, self.port) + class HomeAssistant(ha.HomeAssistant): """ Home Assistant that forwards work. """ @@ -122,19 +127,23 @@ class HomeAssistant(ha.HomeAssistant): import random # pylint: disable=too-many-format-args - random_password = '%030x'.format(random.randrange(16**30)) + random_password = '{:30}'.format(random.randrange(16**30)) http.setup( self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}}) ha.Timer(self) - # Setup that events from remote_api get forwarded to local_api - connect_remote_events(self.remote_api, self.local_api) - self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) + # Setup that events from remote_api get forwarded to local_api + # Do this after we fire START, otherwise HTTP is not started + if not connect_remote_events(self.remote_api, self.local_api): + raise ha.HomeAssistantError(( + 'Could not setup event forwarding from api {} to ' + 'local api {}').format(self.remote_api, self.local_api)) + def stop(self): """ Stops Home Assistant and shuts down all threads. """ _LOGGER.info("Stopping") @@ -289,30 +298,51 @@ def validate_api(api): def connect_remote_events(from_api, to_api): """ Sets up from_api to forward all events to to_api. """ - data = {'host': to_api.host, 'api_password': to_api.api_password} - - if to_api.port is not None: - data['port'] = to_api.port + data = { + 'host': to_api.host, + 'api_password': to_api.api_password, + 'port': to_api.port + } try: - from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) + req = from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) + + if req.status_code == 200: + return True + else: + _LOGGER.error( + "Error settign up event forwarding: %s - %s", + req.status_code, req.text) + + return False except ha.HomeAssistantError: - pass + _LOGGER.exception("Error setting up event forwarding") + return False def disconnect_remote_events(from_api, to_api): """ Disconnects forwarding events from from_api to to_api. """ - data = {'host': to_api.host, '_METHOD': 'DELETE'} - - if to_api.port is not None: - data['port'] = to_api.port + data = { + 'host': to_api.host, + 'port': to_api.port + } try: - from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) + req = from_api(METHOD_DELETE, URL_API_EVENT_FORWARD, data) + + if req.status_code == 200: + return True + else: + _LOGGER.error( + "Error removing event forwarding: %s - %s", + req.status_code, req.text) + + return False except ha.HomeAssistantError: - pass + _LOGGER.exception("Error removing an event forwarder") + return False def get_event_listeners(api): diff --git a/test/test_component_http.py b/test/test_component_http.py index 640600becca..f4080daaa8b 100644 --- a/test/test_component_http.py +++ b/test/test_component_http.py @@ -52,30 +52,50 @@ def setUpModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server. """ - global hass - hass.stop() class TestHTTP(unittest.TestCase): """ Test the HTTP debug interface and API. """ - def test_get_frontend(self): + def test_setup(self): + """ Test http.setup. """ + self.assertFalse(http.setup(hass, {})) + self.assertFalse(http.setup(hass, {http.DOMAIN: {}})) + + def test_frontend_and_static(self): """ Tests if we can get the frontend. """ req = requests.get(_url("")) self.assertEqual(200, req.status_code) + # Test we can retrieve frontend.js frontendjs = re.search( r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)', - req.text).groups(0)[0] + req.text) self.assertIsNotNone(frontendjs) - req = requests.get(_url(frontendjs)) + req = requests.head(_url(frontendjs.groups(0)[0])) self.assertEqual(200, req.status_code) + # Test auto filling in api password + req = requests.get( + _url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD))) + + self.assertEqual(200, req.status_code) + + auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text) + + self.assertIsNotNone(auth_text) + + # Test 404 + self.assertEqual(404, requests.get(_url("/not-existing")).status_code) + + # Test we cannot POST to / + self.assertEqual(405, requests.post(_url("")).status_code) + def test_api_password(self): """ Test if we get access denied if we omit or provide a wrong api password. """ @@ -127,8 +147,8 @@ class TestHTTP(unittest.TestCase): hass.states.set("test.test", "not_to_be_set") requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")), - data=json.dumps({"state": "debug_state_change2", - "api_password": API_PASSWORD})) + data=json.dumps({"state": "debug_state_change2"}), + headers=HA_HEADERS) self.assertEqual("debug_state_change2", hass.states.get("test.test").state) @@ -143,8 +163,8 @@ class TestHTTP(unittest.TestCase): req = requests.post( _url(remote.URL_API_STATES_ENTITY.format( "test_entity.that_does_not_exist")), - data=json.dumps({"state": new_state, - "api_password": API_PASSWORD})) + data=json.dumps({'state': new_state}), + headers=HA_HEADERS) cur_state = (hass.states. get("test_entity.that_does_not_exist").state) @@ -152,6 +172,20 @@ class TestHTTP(unittest.TestCase): self.assertEqual(201, req.status_code) self.assertEqual(cur_state, new_state) + # pylint: disable=invalid-name + def test_api_state_change_with_bad_data(self): + """ Test if API sends appropriate error if we omit state. """ + + new_state = "debug_state_change" + + req = requests.post( + _url(remote.URL_API_STATES_ENTITY.format( + "test_entity.that_does_not_exist")), + data=json.dumps({}), + headers=HA_HEADERS) + + self.assertEqual(400, req.status_code) + # pylint: disable=invalid-name def test_api_fire_event_with_no_data(self): """ Test if the API allows us to fire an event. """ @@ -214,6 +248,17 @@ class TestHTTP(unittest.TestCase): self.assertEqual(422, req.status_code) self.assertEqual(0, len(test_value)) + # Try now with valid but unusable JSON + req = requests.post( + _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")), + data=json.dumps([1, 2, 3]), + headers=HA_HEADERS) + + hass._pool.block_till_done() + + self.assertEqual(422, req.status_code) + self.assertEqual(0, len(test_value)) + def test_api_get_event_listeners(self): """ Test if we can get the list of events being listened for. """ req = requests.get(_url(remote.URL_API_EVENTS), @@ -279,3 +324,79 @@ class TestHTTP(unittest.TestCase): hass._pool.block_till_done() self.assertEqual(1, len(test_value)) + + def test_api_event_forward(self): + """ Test setting up event forwarding. """ + + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) + + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({'host': '127.0.0.1'}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) + + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({'api_password': 'bla-di-bla'}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) + + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': 'bla-di-bla', + 'host': '127.0.0.1', + 'port': 'abcd' + }), + headers=HA_HEADERS) + self.assertEqual(422, req.status_code) + + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': 'bla-di-bla', + 'host': '127.0.0.1', + 'port': '8125' + }), + headers=HA_HEADERS) + self.assertEqual(422, req.status_code) + + # Setup a real one + req = requests.post( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': API_PASSWORD, + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) + + # Delete it again.. + req = requests.delete( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) + + req = requests.delete( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'host': '127.0.0.1', + 'port': 'abcd' + }), + headers=HA_HEADERS) + self.assertEqual(422, req.status_code) + + req = requests.delete( + _url(remote.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) diff --git a/test/test_remote.py b/test/test_remote.py index 07e7a23416d..948a84a7600 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -15,7 +15,7 @@ import homeassistant.components.http as http API_PASSWORD = "test1234" -HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT) +HTTP_BASE_URL = "http://127.0.0.1:8122" HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD} From 6f05548ec8436dd393325af055cf0a752fcb58c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 22:49:29 -0800 Subject: [PATCH 09/60] Add test coverage for demo component --- homeassistant/components/demo.py | 27 ++++++++--- test/test_component_demo.py | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 test/test_component_demo.py diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 67127f7e3cd..961feea705e 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -8,11 +8,11 @@ import random import homeassistant as ha import homeassistant.loader as loader -from homeassistant.components import (SERVICE_TURN_ON, SERVICE_TURN_OFF, - STATE_ON, STATE_OFF, ATTR_ENTITY_PICTURE, - extract_entity_ids) -from homeassistant.components.light import (ATTR_XY_COLOR, ATTR_BRIGHTNESS, - GROUP_NAME_ALL_LIGHTS) +from homeassistant.components import ( + SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, + ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, extract_entity_ids) +from homeassistant.components.light import ( + ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS) from homeassistant.util import split_entity_id DOMAIN = "demo" @@ -24,6 +24,9 @@ def setup(hass, config): """ Setup a demo environment. """ group = loader.get_component('group') + config.setdefault(ha.DOMAIN, {}) + config.setdefault(DOMAIN, {}) + if config[DOMAIN].get('hide_demo_state') != '1': hass.states.set('a.Demo_Mode', 'Enabled') @@ -35,7 +38,12 @@ def setup(hass, config): def mock_turn_on(service): """ Will fake the component has been turned on. """ - for entity_id in extract_entity_ids(hass, service): + if service.data and ATTR_ENTITY_ID in service.data: + entity_ids = extract_entity_ids(hass, service) + else: + entity_ids = hass.get_entity_ids(service.domain) + + for entity_id in entity_ids: domain, _ = split_entity_id(entity_id) if domain == "light": @@ -48,7 +56,12 @@ def setup(hass, config): def mock_turn_off(service): """ Will fake the component has been turned off. """ - for entity_id in extract_entity_ids(hass, service): + if service.data and ATTR_ENTITY_ID in service.data: + entity_ids = extract_entity_ids(hass, service) + else: + entity_ids = hass.get_entity_ids(service.domain) + + for entity_id in entity_ids: hass.states.set(entity_id, STATE_OFF) # Setup sun diff --git a/test/test_component_demo.py b/test/test_component_demo.py new file mode 100644 index 00000000000..901d62b4f60 --- /dev/null +++ b/test/test_component_demo.py @@ -0,0 +1,77 @@ +""" +test.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest +import datetime as dt + +import ephem + +import homeassistant as ha +import homeassistant.components.demo as demo +from homeassistant.components import ( + SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, ATTR_ENTITY_ID) + + +class TestDemo(unittest.TestCase): + """ Test the demo module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_services(self): + """ Test the demo services. """ + # Test turning on and off different types + demo.setup(self.hass, {}) + + for domain in ('light', 'switch'): + # Focus on 1 entity + entity_id = self.hass.get_entity_ids(domain)[0] + + self.hass.call_service( + domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) + + self.hass._pool.block_till_done() + + self.assertEqual(STATE_ON, self.hass.states.get(entity_id).state) + + self.hass.call_service( + domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + self.hass._pool.block_till_done() + + self.assertEqual(STATE_OFF, self.hass.states.get(entity_id).state) + + # Act on all + self.hass.call_service(domain, SERVICE_TURN_ON) + + self.hass._pool.block_till_done() + + for entity_id in self.hass.get_entity_ids(domain): + self.assertEqual( + STATE_ON, self.hass.states.get(entity_id).state) + + self.hass.call_service(domain, SERVICE_TURN_OFF) + + self.hass._pool.block_till_done() + + for entity_id in self.hass.get_entity_ids(domain): + self.assertEqual( + STATE_OFF, self.hass.states.get(entity_id).state) + + def test_hiding_demo_state(self): + """ Test if you can hide the demo card. """ + demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': '1'}}) + + self.assertIsNone(self.hass.states.get('a.Demo_Mode')) + + demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': '0'}}) + + self.assertIsNotNone(self.hass.states.get('a.Demo_Mode')) From c08676aa81e31d4ccd0892667fb5d5326b6c2ba8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Nov 2014 23:19:59 -0800 Subject: [PATCH 10/60] Reorganized some core methods --- homeassistant/__init__.py | 101 +++++++++++------- homeassistant/components/__init__.py | 2 +- homeassistant/components/chromecast.py | 2 +- homeassistant/components/demo.py | 4 +- .../components/device_sun_light_trigger.py | 2 +- homeassistant/components/http/__init__.py | 4 +- test/test_component_demo.py | 9 +- test/test_component_group.py | 4 +- test/test_component_http.py | 8 +- test/test_core.py | 56 +++++----- test/test_remote.py | 4 +- 11 files changed, 106 insertions(+), 90 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 8ebbe16f41d..c9e8013b4eb 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -100,14 +100,6 @@ class HomeAssistant(object): self.bus.fire(EVENT_CALL_SERVICE, event_data) - def get_entity_ids(self, domain_filter=None): - """ Returns known entity ids. """ - if domain_filter: - return [entity_id for entity_id in self.states.entity_ids - if entity_id.startswith(domain_filter)] - else: - return self.states.entity_ids - def track_state_change(self, entity_ids, action, from_state=None, to_state=None): """ @@ -202,31 +194,6 @@ class HomeAssistant(object): self.bus.listen(EVENT_TIME_CHANGED, time_listener) - def listen_once_event(self, event_type, listener): - """ Listen once for event of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - - Note: at the moment it is impossible to remove a one time listener. - """ - @ft.wraps(listener) - def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ - if not hasattr(onetime_listener, 'run'): - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. - # This will make sure the second time it does nothing. - onetime_listener.run = True - - self.bus.remove_listener(event_type, onetime_listener) - - listener(event) - - self.bus.listen(event_type, onetime_listener) - def stop(self): """ Stops Home Assistant and shuts down all threads. """ _LOGGER.info("Stopping") @@ -238,6 +205,32 @@ class HomeAssistant(object): self._pool.stop() + def get_entity_ids(self, domain_filter=None): + """ + Returns known entity ids. + + THIS METHOD IS DEPRECATED. Use hass.states.entity_ids + """ + _LOGGER.warning( + "hass.get_entiy_ids is deprecated. Use hass.states.entity_ids") + + return self.states.entity_ids(domain_filter) + + def listen_once_event(self, event_type, listener): + """ Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Note: at the moment it is impossible to remove a one time listener. + + THIS METHOD IS DEPRECATED. Please use hass.events.listen_once. + """ + _LOGGER.warning( + "hass.listen_once_event is deprecated. Use hass.bus.listen_once") + + self.bus.listen_once(event_type, listener) + def _process_match_param(parameter): """ Wraps parameter in a list if it is not one and returns it. """ @@ -390,6 +383,31 @@ class EventBus(object): else: self._listeners[event_type] = [listener] + def listen_once(self, event_type, listener): + """ Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Note: at the moment it is impossible to remove a one time listener. + """ + @ft.wraps(listener) + def onetime_listener(event): + """ Removes listener from eventbus and then fires listener. """ + if not hasattr(onetime_listener, 'run'): + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. + # This will make sure the second time it does nothing. + onetime_listener.run = True + + self.remove_listener(event_type, onetime_listener) + + listener(event) + + self.listen(event_type, onetime_listener) + def remove_listener(self, event_type, listener): """ Removes a listener of a specific event_type. """ with self._lock: @@ -487,10 +505,13 @@ class StateMachine(object): self._bus = bus self._lock = threading.Lock() - @property - def entity_ids(self): + def entity_ids(self, domain_filter=None): """ List of entity ids that are being tracked. """ - return list(self._states.keys()) + if domain_filter is not None: + return [entity_id for entity_id in self._states.keys() + if util.split_entity_id(entity_id)[0] == domain_filter] + else: + return list(self._states.keys()) def all(self): """ Returns a list of all states. """ @@ -619,14 +640,14 @@ class Timer(threading.Thread): # every minute. assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!" - hass.listen_once_event(EVENT_HOMEASSISTANT_START, - lambda event: self.start()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + lambda event: self.start()) def run(self): """ Start the timer. """ - self.hass.listen_once_event(EVENT_HOMEASSISTANT_STOP, - lambda event: self._stop.set()) + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: self._stop.set()) _LOGGER.info("Timer:starting") diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index a7eeadabc69..9b04d266668 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -61,7 +61,7 @@ def is_on(hass, entity_id=None): entity_ids = group.expand_entity_ids(hass, [entity_id]) else: - entity_ids = hass.states.entity_ids + entity_ids = hass.states.entity_ids() for entity_id in entity_ids: domain = util.split_entity_id(entity_id)[0] diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index c5b0c180d99..c7baee64496 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -38,7 +38,7 @@ def is_on(hass, entity_id=None): """ Returns true if specified ChromeCast entity_id is on. Will check all chromecasts if no entity_id specified. """ - entity_ids = [entity_id] if entity_id else hass.get_entity_ids(DOMAIN) + entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) return any(not hass.states.is_state(entity_id, STATE_NO_APP) for entity_id in entity_ids) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 961feea705e..ca04d90e7fc 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -41,7 +41,7 @@ def setup(hass, config): if service.data and ATTR_ENTITY_ID in service.data: entity_ids = extract_entity_ids(hass, service) else: - entity_ids = hass.get_entity_ids(service.domain) + entity_ids = hass.states.entity_ids(service.domain) for entity_id in entity_ids: domain, _ = split_entity_id(entity_id) @@ -59,7 +59,7 @@ def setup(hass, config): if service.data and ATTR_ENTITY_ID in service.data: entity_ids = extract_entity_ids(hass, service) else: - entity_ids = hass.get_entity_ids(service.domain) + entity_ids = hass.states.entity_ids(service.domain) for entity_id in entity_ids: hass.states.set(entity_id, STATE_OFF) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 0fc2b99ee31..7bb1f9cbddc 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -36,7 +36,7 @@ def setup(hass, config): logger = logging.getLogger(__name__) - device_entity_ids = hass.get_entity_ids(device_tracker.DOMAIN) + device_entity_ids = hass.states.entity_ids(device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 79bd17044f4..0d115d95524 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -133,7 +133,7 @@ def setup(hass, config): RequestHandler, hass, api_password, development) - hass.listen_once_event( + hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_START, lambda event: threading.Thread(target=server.start, daemon=True).start()) @@ -171,7 +171,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): def start(self): """ Starts the server. """ - self.hass.listen_once_event( + self.hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_STOP, lambda event: self.shutdown()) diff --git a/test/test_component_demo.py b/test/test_component_demo.py index 901d62b4f60..54619266078 100644 --- a/test/test_component_demo.py +++ b/test/test_component_demo.py @@ -6,9 +6,6 @@ Tests demo component. """ # pylint: disable=too-many-public-methods,protected-access import unittest -import datetime as dt - -import ephem import homeassistant as ha import homeassistant.components.demo as demo @@ -33,7 +30,7 @@ class TestDemo(unittest.TestCase): for domain in ('light', 'switch'): # Focus on 1 entity - entity_id = self.hass.get_entity_ids(domain)[0] + entity_id = self.hass.states.entity_ids(domain)[0] self.hass.call_service( domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) @@ -54,7 +51,7 @@ class TestDemo(unittest.TestCase): self.hass._pool.block_till_done() - for entity_id in self.hass.get_entity_ids(domain): + for entity_id in self.hass.states.entity_ids(domain): self.assertEqual( STATE_ON, self.hass.states.get(entity_id).state) @@ -62,7 +59,7 @@ class TestDemo(unittest.TestCase): self.hass._pool.block_till_done() - for entity_id in self.hass.get_entity_ids(domain): + for entity_id in self.hass.states.entity_ids(domain): self.assertEqual( STATE_OFF, self.hass.states.get(entity_id).state) diff --git a/test/test_component_group.py b/test/test_component_group.py index 2af99e3c209..80c63ae9b0c 100644 --- a/test/test_component_group.py +++ b/test/test_component_group.py @@ -44,7 +44,7 @@ class TestComponentsGroup(unittest.TestCase): """ Test setup_group method. """ # Test if group setup in our init mode is ok - self.assertIn(self.group_name, self.hass.states.entity_ids) + self.assertIn(self.group_name, self.hass.states.entity_ids()) group_state = self.hass.states.get(self.group_name) self.assertEqual(comps.STATE_ON, group_state.state) @@ -73,7 +73,7 @@ class TestComponentsGroup(unittest.TestCase): ['light.Bowl', 'device_tracker.Paulus'])) # Try to setup a group with a non existing state - self.assertNotIn('non.existing', self.hass.states.entity_ids) + self.assertNotIn('non.existing', self.hass.states.entity_ids()) self.assertFalse(group.setup_group( self.hass, 'light_and_nothing', ['light.Bowl', 'non.existing'])) diff --git a/test/test_component_http.py b/test/test_component_http.py index f4080daaa8b..2a840d39499 100644 --- a/test/test_component_http.py +++ b/test/test_component_http.py @@ -176,8 +176,6 @@ class TestHTTP(unittest.TestCase): def test_api_state_change_with_bad_data(self): """ Test if API sends appropriate error if we omit state. """ - new_state = "debug_state_change" - req = requests.post( _url(remote.URL_API_STATES_ENTITY.format( "test_entity.that_does_not_exist")), @@ -195,7 +193,7 @@ class TestHTTP(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - hass.listen_once_event("test.event_no_data", listener) + hass.bus.listen_once("test.event_no_data", listener) requests.post( _url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")), @@ -216,7 +214,7 @@ class TestHTTP(unittest.TestCase): if "test" in event.data: test_value.append(1) - hass.listen_once_event("test_event_with_data", listener) + hass.bus.listen_once("test_event_with_data", listener) requests.post( _url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")), @@ -236,7 +234,7 @@ class TestHTTP(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - hass.listen_once_event("test_event_bad_data", listener) + hass.bus.listen_once("test_event_bad_data", listener) req = requests.post( _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")), diff --git a/test/test_core.py b/test/test_core.py index d8b4e2d1283..0c046ed0cc2 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -64,17 +64,6 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) - def test_get_entity_ids(self): - """ Test get_entity_ids method. """ - ent_ids = self.hass.get_entity_ids() - self.assertEqual(2, len(ent_ids)) - self.assertTrue('light.Bowl' in ent_ids) - self.assertTrue('switch.AC' in ent_ids) - - ent_ids = self.hass.get_entity_ids('light') - self.assertEqual(1, len(ent_ids)) - self.assertTrue('light.Bowl' in ent_ids) - def test_track_state_change(self): """ Test track_state_change. """ # 2 lists to track how often our callbacks got called @@ -112,21 +101,6 @@ class TestHomeAssistant(unittest.TestCase): self.assertEqual(1, len(specific_runs)) self.assertEqual(3, len(wildcard_runs)) - def test_listen_once_event(self): - """ Test listen_once_event method. """ - runs = [] - - self.hass.listen_once_event('test_event', lambda x: runs.append(1)) - - self.hass.bus.fire('test_event') - self.hass._pool.block_till_done() - self.assertEqual(1, len(runs)) - - # Second time it should not increase runs - self.hass.bus.fire('test_event') - self.hass._pool.block_till_done() - self.assertEqual(1, len(runs)) - def test_track_point_in_time(self): """ Test track point in time. """ before_birthday = datetime(1985, 7, 9, 12, 0, 0) @@ -234,6 +208,21 @@ class TestEventBus(unittest.TestCase): # Try deleting listener while category doesn't exist either self.bus.remove_listener('test', listener) + def test_listen_once_event(self): + """ Test listen_once_event method. """ + runs = [] + + self.bus.listen_once('test_event', lambda x: runs.append(1)) + + self.bus.fire('test_event') + self.bus._pool.block_till_done() + self.assertEqual(1, len(runs)) + + # Second time it should not increase runs + self.bus.fire('test_event') + self.bus._pool.block_till_done() + self.assertEqual(1, len(runs)) + class TestState(unittest.TestCase): """ Test EventBus methods. """ @@ -276,11 +265,22 @@ class TestStateMachine(unittest.TestCase): self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Non_existing', 'on')) + def test_entity_ids(self): + """ Test get_entity_ids method. """ + ent_ids = self.states.entity_ids() + self.assertEqual(2, len(ent_ids)) + self.assertTrue('light.Bowl' in ent_ids) + self.assertTrue('switch.AC' in ent_ids) + + ent_ids = self.states.entity_ids('light') + self.assertEqual(1, len(ent_ids)) + self.assertTrue('light.Bowl' in ent_ids) + def test_remove(self): """ Test remove method. """ - self.assertTrue('light.Bowl' in self.states.entity_ids) + self.assertTrue('light.Bowl' in self.states.entity_ids()) self.assertTrue(self.states.remove('light.Bowl')) - self.assertFalse('light.Bowl' in self.states.entity_ids) + self.assertFalse('light.Bowl' in self.states.entity_ids()) # If it does not exist, we should get False self.assertFalse(self.states.remove('light.Bowl')) diff --git a/test/test_remote.py b/test/test_remote.py index 948a84a7600..adeeaaa8e72 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -96,7 +96,7 @@ class TestRemoteMethods(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - hass.listen_once_event("test.event_no_data", listener) + hass.bus.listen_once("test.event_no_data", listener) remote.fire_event(master_api, "test.event_no_data") @@ -217,7 +217,7 @@ class TestRemoteClasses(unittest.TestCase): """ Helper method that will verify our event got called. """ test_value.append(1) - slave.listen_once_event("test.event_no_data", listener) + slave.bus.listen_once("test.event_no_data", listener) slave.bus.fire("test.event_no_data") From 5835d502c7dd3f1e86012bd3bf2edd80eda2f1c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Nov 2014 18:42:52 -0800 Subject: [PATCH 11/60] Moved more methods out of HomeAssistant object --- config/custom_components/example.py | 2 +- homeassistant/__init__.py | 110 ++++++++++++------ homeassistant/components/__init__.py | 6 +- homeassistant/components/chromecast.py | 16 +-- .../components/device_sun_light_trigger.py | 16 +-- homeassistant/components/group.py | 2 +- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/keyboard.py | 12 +- homeassistant/components/light/__init__.py | 4 +- homeassistant/components/switch/__init__.py | 4 +- test/test_component_demo.py | 8 +- test/test_core.py | 76 ++++++------ test/test_remote.py | 2 +- 13 files changed, 149 insertions(+), 111 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index f7ece4db5ea..51565d00d89 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -25,7 +25,7 @@ def setup(hass, config): hass.track_time_change(print, second=[0, 30]) # See also (defined in homeassistant/__init__.py): - # hass.track_state_change + # hass.states.track_change # hass.track_point_in_time # Tells the bootstrapper that the component was succesfully initialized diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index c9e8013b4eb..3ee3e0d4539 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -27,7 +27,7 @@ EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_STATE_CHANGED = "state_changed" EVENT_TIME_CHANGED = "time_changed" -EVENT_CALL_SERVICE = "call_service" +EVENT_CALL_SERVICE = "services.call" ATTR_NOW = "now" ATTR_DOMAIN = "domain" @@ -92,42 +92,6 @@ class HomeAssistant(object): self.stop() - def call_service(self, domain, service, service_data=None): - """ Fires event to call specified service. """ - event_data = service_data or {} - event_data[ATTR_DOMAIN] = domain - event_data[ATTR_SERVICE] = service - - self.bus.fire(EVENT_CALL_SERVICE, event_data) - - def track_state_change(self, entity_ids, action, - from_state=None, to_state=None): - """ - Track specific state changes. - entity_ids, from_state and to_state can be string or list. - Use list to match multiple. - """ - from_state = _process_match_param(from_state) - to_state = _process_match_param(to_state) - - # Ensure it is a list with entity ids we want to match on - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - @ft.wraps(action) - def state_listener(event): - """ The listener that listens for specific state changes. """ - if event.data['entity_id'] in entity_ids and \ - 'old_state' in event.data and \ - _matcher(event.data['old_state'].state, from_state) and \ - _matcher(event.data['new_state'].state, to_state): - - action(event.data['entity_id'], - event.data['old_state'], - event.data['new_state']) - - self.bus.listen(EVENT_STATE_CHANGED, state_listener) - def track_point_in_time(self, action, point_in_time): """ Adds a listener that fires once at or after a spefic point in time. @@ -231,6 +195,33 @@ class HomeAssistant(object): self.bus.listen_once(event_type, listener) + def track_state_change(self, entity_ids, action, + from_state=None, to_state=None): + """ + Track specific state changes. + entity_ids, from_state and to_state can be string or list. + Use list to match multiple. + + THIS METHOD IS DEPRECATED. Use hass.states.track_change + """ + _LOGGER.warning(( + "hass.track_state_change is deprecated. " + "Use hass.states.track_change")) + + self.states.track_change(entity_ids, action, from_state, to_state) + + def call_service(self, domain, service, service_data=None): + """ + Fires event to call specified service. + + THIS METHOD IS DEPRECATED. Use hass.services.call + """ + _LOGGER.warning(( + "hass.services.call is deprecated. " + "Use hass.services.call")) + + self.services.call(domain, service, service_data) + def _process_match_param(parameter): """ Wraps parameter in a list if it is not one and returns it. """ @@ -561,6 +552,33 @@ class StateMachine(object): self._bus.fire(EVENT_STATE_CHANGED, event_data) + def track_change(self, entity_ids, action, from_state=None, to_state=None): + """ + Track specific state changes. + entity_ids, from_state and to_state can be string or list. + Use list to match multiple. + """ + from_state = _process_match_param(from_state) + to_state = _process_match_param(to_state) + + # Ensure it is a list with entity ids we want to match on + if isinstance(entity_ids, str): + entity_ids = [entity_ids] + + @ft.wraps(action) + def state_listener(event): + """ The listener that listens for specific state changes. """ + if event.data['entity_id'] in entity_ids and \ + 'old_state' in event.data and \ + _matcher(event.data['old_state'].state, from_state) and \ + _matcher(event.data['new_state'].state, to_state): + + action(event.data['entity_id'], + event.data['old_state'], + event.data['new_state']) + + self._bus.listen(EVENT_STATE_CHANGED, state_listener) + # pylint: disable=too-few-public-methods class ServiceCall(object): @@ -588,6 +606,7 @@ class ServiceRegistry(object): self._services = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() + self._bus = bus bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) @property @@ -609,6 +628,23 @@ class ServiceRegistry(object): else: self._services[domain] = {service: service_func} + def call(self, domain, service, service_data=None): + """ + Fires event to call specified service. + + This method will fire an event to call the service. + This event will be picked up by this ServiceRegistry and any + other ServiceRegistry that is listening on the EventBus. + + Because the service is sent as an event you are not allowed to use + the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. + """ + event_data = service_data or {} + event_data[ATTR_DOMAIN] = domain + event_data[ATTR_SERVICE] = service + + self._bus.fire(EVENT_CALL_SERVICE, event_data) + def _event_to_service_call(self, event): """ Calls a service from an event. """ service_data = dict(event.data) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 9b04d266668..3acaf11a4fd 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -85,7 +85,7 @@ def turn_on(hass, entity_id=None, **service_data): if entity_id is not None: service_data[ATTR_ENTITY_ID] = entity_id - hass.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data) + hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data) def turn_off(hass, entity_id=None, **service_data): @@ -93,7 +93,7 @@ def turn_off(hass, entity_id=None, **service_data): if entity_id is not None: service_data[ATTR_ENTITY_ID] = entity_id - hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data) + hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) def extract_entity_ids(hass, service): @@ -195,7 +195,7 @@ def setup(hass, config): # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.call_service(domain, service.service, data) + hass.services.call(domain, service.service, data) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index c7baee64496..d6e9971dc0d 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -48,56 +48,56 @@ def turn_off(hass, entity_id=None): """ Will turn off specified Chromecast or all. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_TURN_OFF, data) + hass.services.call(DOMAIN, components.SERVICE_TURN_OFF, data) def volume_up(hass, entity_id=None): """ Send the chromecast the command for volume up. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data) + hass.services.call(DOMAIN, components.SERVICE_VOLUME_UP, data) def volume_down(hass, entity_id=None): """ Send the chromecast the command for volume down. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data) + hass.services.call(DOMAIN, components.SERVICE_VOLUME_DOWN, data) def media_play_pause(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data) def media_play(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY, data) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY, data) def media_pause(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PAUSE, data) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PAUSE, data) def media_next_track(hass, entity_id=None): """ Send the chromecast the command for next track. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data) def media_prev_track(hass, entity_id=None): """ Send the chromecast the command for prev track. """ data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data) # pylint: disable=too-many-locals, too-many-branches diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 7bb1f9cbddc..5e2b99e64a7 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -92,8 +92,8 @@ def setup(hass, config): # Track every time sun rises so we can schedule a time-based # pre-sun set event - hass.track_state_change(sun.ENTITY_ID, schedule_light_on_sun_rise, - sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon # schedule the time-based pre-sun set event @@ -152,12 +152,14 @@ def setup(hass, config): light.turn_off(hass) # Track home coming of each device - hass.track_state_change(device_entity_ids, check_light_on_dev_state_change, - components.STATE_NOT_HOME, components.STATE_HOME) + hass.states.track_change( + device_entity_ids, check_light_on_dev_state_change, + components.STATE_NOT_HOME, components.STATE_HOME) # Track when all devices are gone to shut down lights - hass.track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES, - check_light_on_dev_state_change, - components.STATE_HOME, components.STATE_NOT_HOME) + hass.states.track_change( + device_tracker.ENTITY_ID_ALL_DEVICES, + check_light_on_dev_state_change, + components.STATE_HOME, components.STATE_NOT_HOME) return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 8c0ff0b763a..5376b65131a 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -186,7 +186,7 @@ def setup_group(hass, name, entity_ids, user_defined=True): if entity_id != ent_id]): hass.states.set(group_entity_id, group_off, state_attr) - hass.track_state_change(entity_ids, update_group_state) + hass.states.track_change(entity_ids, update_group_state) hass.states.set(group_entity_id, group_state, state_attr) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0d115d95524..02a1bea0272 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -481,7 +481,7 @@ class RequestHandler(SimpleHTTPRequestHandler): domain = path_match.group('domain') service = path_match.group('service') - self.server.hass.call_service(domain, service, data) + self.server.hass.services.call(domain, service, data) self._json_message("Service {}/{} called.".format(domain, service)) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 40d4b938e47..e2d156d5c7e 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -14,32 +14,32 @@ DEPENDENCIES = [] def volume_up(hass): """ Press the keyboard button for volume up. """ - hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP) + hass.services.call(DOMAIN, components.SERVICE_VOLUME_UP) def volume_down(hass): """ Press the keyboard button for volume down. """ - hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN) + hass.services.call(DOMAIN, components.SERVICE_VOLUME_DOWN) def volume_mute(hass): """ Press the keyboard button for muting volume. """ - hass.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE) + hass.services.call(DOMAIN, components.SERVICE_VOLUME_MUTE) def media_play_pause(hass): """ Press the keyboard button for play/pause. """ - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) def media_next_track(hass): """ Press the keyboard button for next track. """ - hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) def media_prev_track(hass): """ Press the keyboard button for prev track. """ - hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) + hass.services.call(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) # pylint: disable=unused-argument diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b7e6a4fd348..957a318ce3c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -118,7 +118,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, if xy_color: data[ATTR_XY_COLOR] = xy_color - hass.call_service(DOMAIN, SERVICE_TURN_ON, data) + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None, transition=None): @@ -131,7 +131,7 @@ def turn_off(hass, entity_id=None, transition=None): if transition is not None: data[ATTR_TRANSITION] = transition - hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) # pylint: disable=too-many-branches, too-many-locals diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 96325b92fac..87e50eaa41c 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -43,14 +43,14 @@ def turn_on(hass, entity_id=None): """ Turns all or specified switch on. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.call_service(DOMAIN, SERVICE_TURN_ON, data) + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None): """ Turns all or specified switch off. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) # pylint: disable=too-many-branches diff --git a/test/test_component_demo.py b/test/test_component_demo.py index 54619266078..dd3e47cb97e 100644 --- a/test/test_component_demo.py +++ b/test/test_component_demo.py @@ -32,14 +32,14 @@ class TestDemo(unittest.TestCase): # Focus on 1 entity entity_id = self.hass.states.entity_ids(domain)[0] - self.hass.call_service( + self.hass.services.call( domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) self.hass._pool.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(entity_id).state) - self.hass.call_service( + self.hass.services.call( domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) self.hass._pool.block_till_done() @@ -47,7 +47,7 @@ class TestDemo(unittest.TestCase): self.assertEqual(STATE_OFF, self.hass.states.get(entity_id).state) # Act on all - self.hass.call_service(domain, SERVICE_TURN_ON) + self.hass.services.call(domain, SERVICE_TURN_ON) self.hass._pool.block_till_done() @@ -55,7 +55,7 @@ class TestDemo(unittest.TestCase): self.assertEqual( STATE_ON, self.hass.states.get(entity_id).state) - self.hass.call_service(domain, SERVICE_TURN_OFF) + self.hass.services.call(domain, SERVICE_TURN_OFF) self.hass._pool.block_till_done() diff --git a/test/test_core.py b/test/test_core.py index 0c046ed0cc2..012aef2a1f3 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -52,7 +52,7 @@ class TestHomeAssistant(unittest.TestCase): self.assertTrue(blocking_thread.is_alive()) - self.hass.call_service(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) + self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) self.hass._pool.block_till_done() # hass.block_till_stopped checks every second if it should quit @@ -64,43 +64,6 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) - def test_track_state_change(self): - """ Test track_state_change. """ - # 2 lists to track how often our callbacks got called - specific_runs = [] - wildcard_runs = [] - - self.hass.track_state_change( - 'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off') - - self.hass.track_state_change( - 'light.Bowl', lambda a, b, c: wildcard_runs.append(1), - ha.MATCH_ALL, ha.MATCH_ALL) - - # 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 test_track_point_in_time(self): """ Test track point in time. """ before_birthday = datetime(1985, 7, 9, 12, 0, 0) @@ -285,6 +248,43 @@ class TestStateMachine(unittest.TestCase): # If it does not exist, we should get False self.assertFalse(self.states.remove('light.Bowl')) + def test_track_change(self): + """ Test states.track_change. """ + # 2 lists to track how often our callbacks got called + specific_runs = [] + wildcard_runs = [] + + self.states.track_change( + 'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off') + + self.states.track_change( + 'light.Bowl', lambda a, b, c: wildcard_runs.append(1), + ha.MATCH_ALL, ha.MATCH_ALL) + + # Set same state should not trigger a state change/listener + self.states.set('light.Bowl', 'on') + self.bus._pool.block_till_done() + self.assertEqual(0, len(specific_runs)) + self.assertEqual(0, len(wildcard_runs)) + + # State change off -> on + self.states.set('light.Bowl', 'off') + self.bus._pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + # State change off -> off + self.states.set('light.Bowl', 'off', {"some_attr": 1}) + self.bus._pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + # State change off -> on + self.states.set('light.Bowl', 'on') + self.bus._pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + class TestServiceCall(unittest.TestCase): """ Test ServiceCall class. """ diff --git a/test/test_remote.py b/test/test_remote.py index adeeaaa8e72..1b093b1c56a 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -154,7 +154,7 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual({}, remote.get_services(broken_api)) def test_call_service(self): - """ Test Python API call_service. """ + """ Test Python API services.call. """ test_value = [] def listener(service_call): # pylint: disable=unused-argument From ed150b8ea5c4b0be853cebaade4a9cf96693d314 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Nov 2014 23:14:08 -0800 Subject: [PATCH 12/60] Renamed test to ha_test because of conflict with built-in python test package --- .../config/custom_components/light/test.py | 29 +++++++++ .../config/custom_components/switch/test.py | 29 +++++++++ .../helper.py | 64 +++++++++++-------- .../test_component_chromecast.py | 4 +- {test => ha_test}/test_component_core.py | 4 +- {test => ha_test}/test_component_demo.py | 4 +- {test => ha_test}/test_component_group.py | 4 +- {test => ha_test}/test_component_http.py | 4 +- {test => ha_test}/test_component_light.py | 60 +++++++++-------- {test => ha_test}/test_component_sun.py | 4 +- {test => ha_test}/test_component_switch.py | 27 ++++---- {test => ha_test}/test_core.py | 4 +- {test => ha_test}/test_loader.py | 12 ++-- {test => ha_test}/test_remote.py | 4 +- {test => ha_test}/test_util.py | 4 +- homeassistant/loader.py | 5 +- run_tests.sh | 2 +- test/config/custom_components/custom_one.py | 3 - test/helper.py | 40 ------------ 19 files changed, 170 insertions(+), 137 deletions(-) create mode 100644 ha_test/config/custom_components/light/test.py create mode 100644 ha_test/config/custom_components/switch/test.py rename test/mock_toggledevice_platform.py => ha_test/helper.py (53%) rename {test => ha_test}/test_component_chromecast.py (97%) rename {test => ha_test}/test_component_core.py (97%) rename {test => ha_test}/test_component_demo.py (97%) rename {test => ha_test}/test_component_group.py (99%) rename {test => ha_test}/test_component_http.py (99%) rename {test => ha_test}/test_component_light.py (82%) rename {test => ha_test}/test_component_sun.py (98%) rename {test => ha_test}/test_component_switch.py (84%) rename {test => ha_test}/test_core.py (99%) rename {test => ha_test}/test_loader.py (90%) rename {test => ha_test}/test_remote.py (99%) rename {test => ha_test}/test_util.py (99%) delete mode 100644 test/config/custom_components/custom_one.py delete mode 100644 test/helper.py diff --git a/ha_test/config/custom_components/light/test.py b/ha_test/config/custom_components/light/test.py new file mode 100644 index 00000000000..757099ddca0 --- /dev/null +++ b/ha_test/config/custom_components/light/test.py @@ -0,0 +1,29 @@ +""" +custom_components.light.test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a mock switch platform. + +Call init before using it in your tests to ensure clean test data. +""" +import homeassistant.components as components +from ha_test.helper import MockToggleDevice + + +DEVICES = [] + + +def init(empty=False): + """ (re-)initalizes the platform with devices. """ + global DEVICES + + DEVICES = [] if empty else [ + MockToggleDevice('Ceiling', components.STATE_ON), + MockToggleDevice('Ceiling', components.STATE_OFF), + MockToggleDevice(None, components.STATE_OFF) + ] + + +def get_lights(hass, config): + """ Returns mock devices. """ + return DEVICES diff --git a/ha_test/config/custom_components/switch/test.py b/ha_test/config/custom_components/switch/test.py new file mode 100644 index 00000000000..927aca24feb --- /dev/null +++ b/ha_test/config/custom_components/switch/test.py @@ -0,0 +1,29 @@ +""" +custom_components.switch.test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a mock switch platform. + +Call init before using it in your tests to ensure clean test data. +""" +import homeassistant.components as components +from ha_test.helper import MockToggleDevice + + +DEVICES = [] + + +def init(empty=False): + """ (re-)initalizes the platform with devices. """ + global DEVICES + + DEVICES = [] if empty else [ + MockToggleDevice('AC', components.STATE_ON), + MockToggleDevice('AC', components.STATE_OFF), + MockToggleDevice(None, components.STATE_OFF) + ] + + +def get_switches(hass, config): + """ Returns mock devices. """ + return DEVICES diff --git a/test/mock_toggledevice_platform.py b/ha_test/helper.py similarity index 53% rename from test/mock_toggledevice_platform.py rename to ha_test/helper.py index f864301ade4..109b4185ba5 100644 --- a/test/mock_toggledevice_platform.py +++ b/ha_test/helper.py @@ -1,16 +1,48 @@ """ -test.mock.switch_platform -~~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.helper +~~~~~~~~~~~~~ -Provides a mock switch platform. - -Call init before using it in your tests to ensure clean test data. +Helper method for writing tests. """ +import os + +import homeassistant as ha import homeassistant.components as components +def get_test_home_assistant(): + """ Returns a Home Assistant object pointing at test config dir. """ + hass = ha.HomeAssistant() + hass.config_dir = os.path.join(os.path.dirname(__file__), "config") + + return hass + + +def mock_service(hass, domain, service): + """ + Sets up a fake service. + Returns a list that logs all calls to fake service. + """ + calls = [] + + hass.services.register( + domain, service, lambda call: calls.append(call)) + + return calls + + +class MockModule(object): + """ Provides a fake module. """ + + def __init__(self, domain, dependencies=[], setup=None): + self.DOMAIN = domain + self.DEPENDENCIES = dependencies + # Setup a mock setup if none given. + self.setup = lambda hass, config: False if setup is None else setup + + class MockToggleDevice(components.ToggleDevice): - """ Fake switch. """ + """ Provides a mock toggle device. """ def __init__(self, name, state): self.name = name self.state = state @@ -42,23 +74,3 @@ class MockToggleDevice(components.ToggleDevice): else: return next(call for call in reversed(self.calls) if call[0] == method) - -DEVICES = [] - - -def init(empty=False): - """ (re-)initalizes the platform with devices. """ - global DEVICES - - DEVICES = [] if empty else [ - MockToggleDevice('AC', components.STATE_ON), - MockToggleDevice('AC', components.STATE_OFF), - MockToggleDevice(None, components.STATE_OFF) - ] - - -def get_switches(hass, config): - """ Returns mock devices. """ - return DEVICES - -get_lights = get_switches diff --git a/test/test_component_chromecast.py b/ha_test/test_component_chromecast.py similarity index 97% rename from test/test_component_chromecast.py rename to ha_test/test_component_chromecast.py index 82cb511c5b7..3b80c700606 100644 --- a/test/test_component_chromecast.py +++ b/ha_test/test_component_chromecast.py @@ -1,6 +1,6 @@ """ -test.test_component_chromecast -~~~~~~~~~~~ +ha_test.test_component_chromecast +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Chromecast component. """ diff --git a/test/test_component_core.py b/ha_test/test_component_core.py similarity index 97% rename from test/test_component_core.py rename to ha_test/test_component_core.py index 24d66a6a3bb..2a4a942a5ba 100644 --- a/test/test_component_core.py +++ b/ha_test/test_component_core.py @@ -1,6 +1,6 @@ """ -test.test_component_core -~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_core +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests core compoments. """ diff --git a/test/test_component_demo.py b/ha_test/test_component_demo.py similarity index 97% rename from test/test_component_demo.py rename to ha_test/test_component_demo.py index dd3e47cb97e..a510759a8ea 100644 --- a/test/test_component_demo.py +++ b/ha_test/test_component_demo.py @@ -1,6 +1,6 @@ """ -test.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests demo component. """ diff --git a/test/test_component_group.py b/ha_test/test_component_group.py similarity index 99% rename from test/test_component_group.py rename to ha_test/test_component_group.py index 80c63ae9b0c..d1d9dccbb27 100644 --- a/test/test_component_group.py +++ b/ha_test/test_component_group.py @@ -1,6 +1,6 @@ """ -test.test_component_group -~~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_group +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests the group compoments. """ diff --git a/test/test_component_http.py b/ha_test/test_component_http.py similarity index 99% rename from test/test_component_http.py rename to ha_test/test_component_http.py index 2a840d39499..15c2966292a 100644 --- a/test/test_component_http.py +++ b/ha_test/test_component_http.py @@ -1,6 +1,6 @@ """ -test.test_component_http -~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_http +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Home Assistant HTTP component does what it should do. """ diff --git a/test/test_component_light.py b/ha_test/test_component_light.py similarity index 82% rename from test/test_component_light.py rename to ha_test/test_component_light.py index 255187ea253..3d4b5c1f3eb 100644 --- a/test/test_component_light.py +++ b/ha_test/test_component_light.py @@ -1,6 +1,6 @@ """ -test.test_component_switch -~~~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_switch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests switch component. """ @@ -11,11 +11,11 @@ import os import homeassistant as ha import homeassistant.loader as loader import homeassistant.util as util -import homeassistant.components as components +from homeassistant.components import ( + get_component, ATTR_ENTITY_ID, STATE_ON, STATE_OFF, + SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components.light as light -import mock_toggledevice_platform - from helper import mock_service, get_test_home_assistant @@ -25,7 +25,6 @@ class TestLight(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant() loader.prepare(self.hass) - loader.set_component('light.test', mock_toggledevice_platform) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -39,21 +38,21 @@ class TestLight(unittest.TestCase): def test_methods(self): """ Test if methods call the services as expected. """ # Test is_on - self.hass.states.set('light.test', components.STATE_ON) + self.hass.states.set('light.test', STATE_ON) self.assertTrue(light.is_on(self.hass, 'light.test')) - self.hass.states.set('light.test', components.STATE_OFF) + self.hass.states.set('light.test', STATE_OFF) self.assertFalse(light.is_on(self.hass, 'light.test')) - self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_ON) + self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_ON) self.assertTrue(light.is_on(self.hass)) - self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_OFF) + self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_OFF) self.assertFalse(light.is_on(self.hass)) # Test turn_on turn_on_calls = mock_service( - self.hass, light.DOMAIN, components.SERVICE_TURN_ON) + self.hass, light.DOMAIN, SERVICE_TURN_ON) light.turn_on( self.hass, @@ -70,17 +69,19 @@ class TestLight(unittest.TestCase): call = turn_on_calls[-1] self.assertEqual(light.DOMAIN, call.domain) - self.assertEqual(components.SERVICE_TURN_ON, call.service) - self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID]) - self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) - self.assertEqual('brightness_val', call.data[light.ATTR_BRIGHTNESS]) - self.assertEqual('rgb_color_val', call.data[light.ATTR_RGB_COLOR]) - self.assertEqual('xy_color_val', call.data[light.ATTR_XY_COLOR]) - self.assertEqual('profile_val', call.data[light.ATTR_PROFILE]) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual('entity_id_val', call.data.get(ATTR_ENTITY_ID)) + self.assertEqual( + 'transition_val', call.data.get(light.ATTR_TRANSITION)) + self.assertEqual( + 'brightness_val', call.data.get(light.ATTR_BRIGHTNESS)) + self.assertEqual('rgb_color_val', call.data.get(light.ATTR_RGB_COLOR)) + self.assertEqual('xy_color_val', call.data.get(light.ATTR_XY_COLOR)) + self.assertEqual('profile_val', call.data.get(light.ATTR_PROFILE)) # Test turn_off turn_off_calls = mock_service( - self.hass, light.DOMAIN, components.SERVICE_TURN_OFF) + self.hass, light.DOMAIN, SERVICE_TURN_OFF) light.turn_off( self.hass, entity_id='entity_id_val', transition='transition_val') @@ -91,17 +92,19 @@ class TestLight(unittest.TestCase): call = turn_off_calls[-1] self.assertEqual(light.DOMAIN, call.domain) - self.assertEqual(components.SERVICE_TURN_OFF, call.service) - self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID]) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) def test_services(self): """ Test the provided services. """ - mock_toggledevice_platform.init() + platform = get_component('light.test') + + platform.init() self.assertTrue( light.setup(self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}})) - dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None) + dev1, dev2, dev3 = platform.get_lights(None, None) # Test init self.assertTrue(light.is_on(self.hass, dev1.entity_id)) @@ -224,10 +227,10 @@ class TestLight(unittest.TestCase): )) # Test if light component returns 0 lightes - mock_toggledevice_platform.init(True) + platform = get_component('light.test') + platform.init(True) - self.assertEqual( - [], mock_toggledevice_platform.get_lights(None, None)) + self.assertEqual([], platform.get_lights(None, None)) self.assertFalse(light.setup( self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} @@ -235,7 +238,8 @@ class TestLight(unittest.TestCase): def test_light_profiles(self): """ Test light profiles. """ - mock_toggledevice_platform.init() + platform = get_component('light.test') + platform.init() user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) @@ -259,7 +263,7 @@ class TestLight(unittest.TestCase): self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} )) - dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None) + dev1, dev2, dev3 = platform.get_lights(None, None) light.turn_on(self.hass, dev1.entity_id, profile='test') diff --git a/test/test_component_sun.py b/ha_test/test_component_sun.py similarity index 98% rename from test/test_component_sun.py rename to ha_test/test_component_sun.py index d37c8f678f9..a83f8de51a7 100644 --- a/test/test_component_sun.py +++ b/ha_test/test_component_sun.py @@ -1,6 +1,6 @@ """ -test.test_component_sun -~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_sun +~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Sun component. """ diff --git a/test/test_component_switch.py b/ha_test/test_component_switch.py similarity index 84% rename from test/test_component_switch.py rename to ha_test/test_component_switch.py index f81fcdd8f89..0df05f0617b 100644 --- a/test/test_component_switch.py +++ b/ha_test/test_component_switch.py @@ -1,6 +1,6 @@ """ -test.test_component_switch -~~~~~~~~~~~~~~~~~~~~~~~~~~ +ha_test.test_component_switch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests switch component. """ @@ -9,28 +9,29 @@ import unittest import homeassistant as ha import homeassistant.loader as loader -import homeassistant.components as components +from homeassistant.components import get_component, STATE_ON, STATE_OFF import homeassistant.components.switch as switch -import mock_toggledevice_platform +from helper import get_test_home_assistant class TestSwitch(unittest.TestCase): """ Test the switch module. """ def setUp(self): # pylint: disable=invalid-name - self.hass = ha.HomeAssistant() + self.hass = get_test_home_assistant() loader.prepare(self.hass) - loader.set_component('switch.test', mock_toggledevice_platform) - mock_toggledevice_platform.init() + platform = get_component('switch.test') + + platform.init() self.assertTrue(switch.setup( self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}} )) # Switch 1 is ON, switch 2 is OFF self.switch_1, self.switch_2, self.switch_3 = \ - mock_toggledevice_platform.get_switches(None, None) + platform.get_switches(None, None) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -40,7 +41,7 @@ class TestSwitch(unittest.TestCase): """ Test is_on, turn_on, turn_off methods. """ self.assertTrue(switch.is_on(self.hass)) self.assertEqual( - components.STATE_ON, + STATE_ON, self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id)) self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id)) @@ -62,7 +63,7 @@ class TestSwitch(unittest.TestCase): self.assertFalse(switch.is_on(self.hass)) self.assertEqual( - components.STATE_OFF, + STATE_OFF, self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id)) self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id)) @@ -75,7 +76,7 @@ class TestSwitch(unittest.TestCase): self.assertTrue(switch.is_on(self.hass)) self.assertEqual( - components.STATE_ON, + STATE_ON, self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id)) self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id)) @@ -93,10 +94,10 @@ class TestSwitch(unittest.TestCase): )) # Test if switch component returns 0 switches - mock_toggledevice_platform.init(True) + get_component('switch.test').init(True) self.assertEqual( - [], mock_toggledevice_platform.get_switches(None, None)) + [], get_component('switch.test').get_switches(None, None)) self.assertFalse(switch.setup( self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}} diff --git a/test/test_core.py b/ha_test/test_core.py similarity index 99% rename from test/test_core.py rename to ha_test/test_core.py index 012aef2a1f3..7c6dc2a902c 100644 --- a/test/test_core.py +++ b/ha_test/test_core.py @@ -1,6 +1,6 @@ """ -test.test_core -~~~~~~~~~~~~~~ +ha_test.test_core +~~~~~~~~~~~~~~~~~ Provides tests to verify that Home Assistant core works. """ diff --git a/test/test_loader.py b/ha_test/test_loader.py similarity index 90% rename from test/test_loader.py rename to ha_test/test_loader.py index a00fc02e250..4d9bc190145 100644 --- a/test/test_loader.py +++ b/ha_test/test_loader.py @@ -1,6 +1,6 @@ """ -test.test_loader -~~~~~~~~~~~~~~~~~~ +ha_ha_test.test_loader +~~~~~~~~~~~~~~~~~~~~~~ Provides tests to verify that we can load components. """ @@ -10,7 +10,6 @@ import unittest import homeassistant.loader as loader import homeassistant.components.http as http -import mock_toggledevice_platform from helper import get_test_home_assistant, MockModule @@ -26,16 +25,15 @@ class TestLoader(unittest.TestCase): def test_set_component(self): """ Test if set_component works. """ - loader.set_component('switch.test', mock_toggledevice_platform) + loader.set_component('switch.test', http) - self.assertEqual( - mock_toggledevice_platform, loader.get_component('switch.test')) + self.assertEqual(http, loader.get_component('switch.test')) def test_get_component(self): """ Test if get_component works. """ self.assertEqual(http, loader.get_component('http')) - self.assertIsNotNone(loader.get_component('custom_one')) + self.assertIsNotNone(loader.get_component('switch.test')) def test_load_order_component(self): """ Test if we can get the proper load order of components. """ diff --git a/test/test_remote.py b/ha_test/test_remote.py similarity index 99% rename from test/test_remote.py rename to ha_test/test_remote.py index 1b093b1c56a..cc317d63960 100644 --- a/test/test_remote.py +++ b/ha_test/test_remote.py @@ -1,6 +1,6 @@ """ -test.remote -~~~~~~~~~~~ +ha_test.remote +~~~~~~~~~~~~~~ Tests Home Assistant remote methods and classes. Uses port 8122 for master, 8123 for slave diff --git a/test/test_util.py b/ha_test/test_util.py similarity index 99% rename from test/test_util.py rename to ha_test/test_util.py index 1edb4b92a8c..ee48ef3a784 100644 --- a/test/test_util.py +++ b/ha_test/test_util.py @@ -1,6 +1,6 @@ """ -test.test_util -~~~~~~~~~~~~~~ +ha_test.test_util +~~~~~~~~~~~~~~~~~ Tests Home Assistant util methods. """ diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e537eb96450..ad865589694 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -62,9 +62,12 @@ def prepare(hass): # just might output more errors. for fil in os.listdir(custom_path): if os.path.isdir(os.path.join(custom_path, fil)): - AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) + if fil != '__pycache__': + AVAILABLE_COMPONENTS.append( + 'custom_components.{}'.format(fil)) else: + # For files we will strip out .py extension AVAILABLE_COMPONENTS.append( 'custom_components.{}'.format(fil[0:-3])) diff --git a/run_tests.sh b/run_tests.sh index 26934d045d4..c5b93d4440d 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,4 +2,4 @@ pylint homeassistant flake8 homeassistant --exclude bower_components,external -python3 -m unittest discover test +python3 -m unittest discover ha_test diff --git a/test/config/custom_components/custom_one.py b/test/config/custom_components/custom_one.py deleted file mode 100644 index 53371c59046..00000000000 --- a/test/config/custom_components/custom_one.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Module to be loaded by the Loader test. -""" diff --git a/test/helper.py b/test/helper.py deleted file mode 100644 index ed0c2c74e82..00000000000 --- a/test/helper.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -test.helper -~~~~~~~~~~~ - -Helper method for writing tests. -""" -import os - -import homeassistant as ha - - -def get_test_home_assistant(): - """ Returns a Home Assistant object pointing at test config dir. """ - hass = ha.HomeAssistant() - hass.config_dir = os.path.join(os.path.dirname(__file__), "config") - - return hass - - -def mock_service(hass, domain, service): - """ - Sets up a fake service. - Returns a list that logs all calls to fake service. - """ - calls = [] - - hass.services.register( - domain, service, lambda call: calls.append(call)) - - return calls - - -class MockModule(object): - """ Provides a fake module. """ - - def __init__(self, domain, dependencies=[], setup=None): - self.DOMAIN = domain - self.DEPENDENCIES = dependencies - # Setup a mock setup if none given. - self.setup = lambda hass, config: False if setup is None else setup From 12c734fa48f86ee18124e10ba9f3823d84c95d08 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Dec 2014 07:59:08 -0800 Subject: [PATCH 13/60] Update travis.yml to point at new test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ff38ef3ced1..61ed87bf6b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,6 @@ install: script: - flake8 homeassistant --exclude bower_components,external - pylint homeassistant - - coverage run --source=homeassistant -m unittest discover test + - coverage run --source=homeassistant -m unittest discover ha_test after_success: - coveralls From eef4817804335612d67c147540f395795dd421dd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Dec 2014 21:53:00 -0800 Subject: [PATCH 14/60] Cleaned up device_tracker and added tests --- .../custom_components/device_tracker/test.py | 41 +++ ha_test/test_component_device_scanner.py | 190 +++++++++++ homeassistant/__init__.py | 5 + .../components/device_tracker/__init__.py | 304 +++++++++--------- homeassistant/components/group.py | 60 ++-- 5 files changed, 429 insertions(+), 171 deletions(-) create mode 100644 ha_test/config/custom_components/device_tracker/test.py create mode 100644 ha_test/test_component_device_scanner.py diff --git a/ha_test/config/custom_components/device_tracker/test.py b/ha_test/config/custom_components/device_tracker/test.py new file mode 100644 index 00000000000..50cc2bff9d4 --- /dev/null +++ b/ha_test/config/custom_components/device_tracker/test.py @@ -0,0 +1,41 @@ +""" +custom_components.device_tracker.test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a mock device scanner. +""" + + +def get_scanner(hass, config): + """ Returns a mock scanner. """ + return SCANNER + + +class MockScanner(object): + """ Mock device scanner. """ + + def __init__(self): + """ Initialize the MockScanner. """ + self.devices_home = [] + + def come_home(self, device): + """ Make a device come home. """ + self.devices_home.append(device) + + def leave_home(self, device): + """ Make a device leave the house. """ + self.devices_home.remove(device) + + def scan_devices(self): + """ Returns a list of fake devices. """ + + return list(self.devices_home) + + def get_device_name(self, device): + """ + Returns a name for a mock device. + Returns None for dev1 for testing. + """ + return None if device == 'dev1' else device.upper() + +SCANNER = MockScanner() diff --git a/ha_test/test_component_device_scanner.py b/ha_test/test_component_device_scanner.py new file mode 100644 index 00000000000..26a7ddb5590 --- /dev/null +++ b/ha_test/test_component_device_scanner.py @@ -0,0 +1,190 @@ +""" +ha_test.test_component_group +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the group compoments. +""" +# pylint: disable=protected-access,too-many-public-methods +import unittest +from datetime import datetime, timedelta +import logging +import os + +import homeassistant as ha +import homeassistant.loader as loader +from homeassistant.components import ( + STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE) +import homeassistant.components.device_tracker as device_tracker + +from helper import get_test_home_assistant + + +def setUpModule(): # pylint: disable=invalid-name + """ Setup to ignore group errors. """ + logging.disable(logging.CRITICAL) + + +class TestComponentsDeviceTracker(unittest.TestCase): + """ Tests homeassistant.components.device_tracker module. """ + + def setUp(self): # pylint: disable=invalid-name + """ Init needed objects. """ + self.hass = get_test_home_assistant() + loader.prepare(self.hass) + + self.known_dev_path = self.hass.get_config_path( + device_tracker.KNOWN_DEVICES_FILE) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + if os.path.isfile(self.known_dev_path): + os.remove(self.known_dev_path) + + def test_is_on(self): + """ Test is_on method. """ + entity_id = device_tracker.ENTITY_ID_FORMAT.format('test') + + self.hass.states.set(entity_id, STATE_HOME) + + self.assertTrue(device_tracker.is_on(self.hass, entity_id)) + + self.hass.states.set(entity_id, STATE_NOT_HOME) + + self.assertFalse(device_tracker.is_on(self.hass, entity_id)) + + def test_setup(self): + """ Test setup method. """ + # Bogus config + self.assertFalse(device_tracker.setup(self.hass, {})) + + self.assertFalse( + device_tracker.setup(self.hass, {device_tracker.DOMAIN: {}})) + + # Test with non-existing component + self.assertFalse(device_tracker.setup( + self.hass, {device_tracker.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}} + )) + + # Test with a bad known device file around + with open(self.known_dev_path, 'w') as fil: + fil.write("bad data\nbad data\n") + + self.assertFalse(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'} + })) + + def test_device_tracker(self): + """ Test the device tracker class. """ + scanner = loader.get_component( + 'device_tracker.test').get_scanner(None, None) + + scanner.come_home('dev1') + scanner.come_home('dev2') + + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'} + })) + + # Ensure a new known devices file has been created. + # Since the device_tracker uses a set internally we cannot + # know what the order of the devices in the known devices file is. + # To ensure all the three expected lines are there, we sort the file + with open(self.known_dev_path) as fil: + self.assertEqual( + ['dev1,unknown_device,0,\n', 'dev2,DEV2,0,\n', + 'device,name,track,picture\n'], + sorted(fil)) + + # Write one where we track dev1, dev2 + with open(self.known_dev_path, 'w') as fil: + fil.write('device,name,track,picture\n') + fil.write('dev1,Device 1,1,http://example.com/dev1.jpg\n') + fil.write('dev2,Device 2,1,http://example.com/dev2.jpg\n') + + scanner.leave_home('dev1') + scanner.come_home('dev3') + + self.hass.services.call( + device_tracker.DOMAIN, + device_tracker.SERVICE_DEVICE_TRACKER_RELOAD) + + self.hass._pool.block_till_done() + + dev1 = device_tracker.ENTITY_ID_FORMAT.format('Device_1') + dev2 = device_tracker.ENTITY_ID_FORMAT.format('Device_2') + dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3') + + now = datetime.now() + nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL) + nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND - + timedelta(seconds=1)) + nowMinGone = nowAlmostMinGone + timedelta(seconds=2) + + # Test initial is correct + self.assertTrue(device_tracker.is_on(self.hass)) + self.assertFalse(device_tracker.is_on(self.hass, dev1)) + self.assertTrue(device_tracker.is_on(self.hass, dev2)) + self.assertIsNone(self.hass.states.get(dev3)) + + self.assertEqual( + 'http://example.com/dev1.jpg', + self.hass.states.get(dev1).attributes.get(ATTR_ENTITY_PICTURE)) + self.assertEqual( + 'http://example.com/dev2.jpg', + self.hass.states.get(dev2).attributes.get(ATTR_ENTITY_PICTURE)) + + # Test if dev3 got added to known dev file + with open(self.known_dev_path) as fil: + self.assertEqual('dev3,DEV3,0,\n', list(fil)[-1]) + + # Change dev3 to track + with open(self.known_dev_path, 'w') as fil: + fil.write("device,name,track,picture\n") + fil.write('dev1,Device 1,1,http://example.com/picture.jpg\n') + fil.write('dev2,Device 2,1,http://example.com/picture.jpg\n') + fil.write('dev3,DEV3,1,\n') + + # reload dev file + scanner.come_home('dev1') + scanner.leave_home('dev2') + + self.hass.services.call( + device_tracker.DOMAIN, + device_tracker.SERVICE_DEVICE_TRACKER_RELOAD) + + self.hass._pool.block_till_done() + + # Test what happens if a device comes home and another leaves + self.assertTrue(device_tracker.is_on(self.hass)) + self.assertTrue(device_tracker.is_on(self.hass, dev1)) + # Dev2 will still be home because of the error margin on time + self.assertTrue(device_tracker.is_on(self.hass, dev2)) + # dev3 should be tracked now after we reload the known devices + self.assertTrue(device_tracker.is_on(self.hass, dev3)) + + self.assertIsNone( + self.hass.states.get(dev3).attributes.get(ATTR_ENTITY_PICTURE)) + + # Test if device leaves what happens, test the time span + self.hass.bus.fire( + ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowAlmostMinGone}) + + self.hass._pool.block_till_done() + + self.assertTrue(device_tracker.is_on(self.hass)) + self.assertTrue(device_tracker.is_on(self.hass, dev1)) + # Dev2 will still be home because of the error time + self.assertTrue(device_tracker.is_on(self.hass, dev2)) + self.assertTrue(device_tracker.is_on(self.hass, dev3)) + + # Now test if gone for longer then error margin + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowMinGone}) + + self.hass._pool.block_till_done() + + self.assertTrue(device_tracker.is_on(self.hass)) + self.assertTrue(device_tracker.is_on(self.hass, dev1)) + self.assertFalse(device_tracker.is_on(self.hass, dev2)) + self.assertTrue(device_tracker.is_on(self.hass, dev3)) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 3ee3e0d4539..6fbe1a2c3e3 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -557,6 +557,9 @@ class StateMachine(object): Track specific state changes. entity_ids, from_state and to_state can be string or list. Use list to match multiple. + + Returns the listener that listens on the bus for EVENT_STATE_CHANGED. + Pass the return value into hass.bus.remove_listener to remove it. """ from_state = _process_match_param(from_state) to_state = _process_match_param(to_state) @@ -579,6 +582,8 @@ class StateMachine(object): self._bus.listen(EVENT_STATE_CHANGED, state_listener) + return state_listener + # pylint: disable=too-few-public-methods class ServiceCall(object): diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 85e7d206e3e..08f1abd9ff1 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,6 +1,6 @@ """ homeassistant.components.tracker -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to keep track of devices. """ @@ -13,9 +13,9 @@ from datetime import datetime, timedelta import homeassistant as ha from homeassistant.loader import get_component import homeassistant.util as util -import homeassistant.components as components -from homeassistant.components import group +from homeassistant.components import ( + group, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME) DOMAIN = "device_tracker" DEPENDENCIES = [] @@ -30,7 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' # After how much time do we consider a device not home if # it does not show up on scans -TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3) +TIME_DEVICE_NOT_FOUND = timedelta(minutes=3) # Filename to save known devices to KNOWN_DEVICES_FILE = "known_devices.csv" @@ -43,7 +43,7 @@ def is_on(hass, entity_id=None): """ Returns if any or specified device is home. """ entity = entity_id or ENTITY_ID_ALL_DEVICES - return hass.states.is_state(entity, components.STATE_HOME) + return hass.states.is_state(entity, STATE_HOME) def setup(hass, config): @@ -70,223 +70,231 @@ def setup(hass, config): return False - DeviceTracker(hass, device_scanner) + tracker = DeviceTracker(hass, device_scanner) - return True + # We only succeeded if we got to parse the known devices file + return not tracker.invalid_known_devices_file -# pylint: disable=too-many-instance-attributes class DeviceTracker(object): """ Class that tracks which devices are home and which are not. """ def __init__(self, hass, device_scanner): - self.states = hass.states + self.hass = hass self.device_scanner = device_scanner - self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING - self.lock = threading.Lock() - self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE) - # Dictionary to keep track of known devices and devices we track - self.known_devices = {} + self.tracked = {} + self.untracked_devices = set() # Did we encounter an invalid known devices file self.invalid_known_devices_file = False self._read_known_devices_file() + if self.invalid_known_devices_file: + return + # Wrap it in a func instead of lambda so it can be identified in # the bus by its __name__ attribute. - def update_device_state(time): # pylint: disable=unused-argument + def update_device_state(now): """ Triggers update of the device states. """ - self.update_devices() + self.update_devices(now) + + # pylint: disable=unused-argument + def reload_known_devices_service(service): + """ Reload known devices file. """ + group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES) + + self._read_known_devices_file() + + self.update_devices(datetime.now()) + + if self.tracked: + group.setup_group( + self.hass, GROUP_NAME_ALL_DEVICES, + self.device_entity_ids, False) hass.track_time_change(update_device_state) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, - lambda service: self._read_known_devices_file()) + reload_known_devices_service) - self.update_devices() - - group.setup_group( - hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False) + reload_known_devices_service(None) @property def device_entity_ids(self): """ Returns a set containing all device entity ids that are being tracked. """ - return set([self.known_devices[device]['entity_id'] for device - in self.known_devices - if self.known_devices[device]['track']]) + return set(device['entity_id'] for device in self.tracked.values()) - def update_devices(self, found_devices=None): + def _update_state(self, now, device, is_home): + """ Update the state of a device. """ + dev_info = self.tracked[device] + + if is_home: + # Update last seen if at home + dev_info['last_seen'] = now + else: + # State remains at home if it has been seen in the last + # TIME_DEVICE_NOT_FOUND + is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND + + state = STATE_HOME if is_home else STATE_NOT_HOME + + self.hass.states.set( + dev_info['entity_id'], state, + dev_info['state_attr']) + + def update_devices(self, now): """ Update device states based on the found devices. """ self.lock.acquire() - found_devices = found_devices or self.device_scanner.scan_devices() + found_devices = set(self.device_scanner.scan_devices()) - now = datetime.now() + for device in self.tracked: + is_home = device in found_devices - known_dev = self.known_devices + self._update_state(now, device, is_home) - temp_tracking_devices = [device for device in known_dev - if known_dev[device]['track']] + if is_home: + found_devices.remove(device) - for device in found_devices: - # Are we tracking this device? - if device in temp_tracking_devices: - temp_tracking_devices.remove(device) + # Did we find any devices that we didn't know about yet? + new_devices = found_devices - self.untracked_devices - known_dev[device]['last_seen'] = now + # Write new devices to known devices file + if not self.invalid_known_devices_file and new_devices: - self.states.set( - known_dev[device]['entity_id'], components.STATE_HOME, - known_dev[device]['default_state_attr']) + known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) - # For all devices we did not find, set state to NH - # But only if they have been gone for longer then the error time span - # Because we do not want to have stuff happening when the device does - # not show up for 1 scan beacuse of reboot etc - for device in temp_tracking_devices: - if now - known_dev[device]['last_seen'] > self.error_scanning: + try: + # If file does not exist we will write the header too + is_new_file = not os.path.isfile(known_dev_path) - self.states.set(known_dev[device]['entity_id'], - components.STATE_NOT_HOME, - known_dev[device]['default_state_attr']) + with open(known_dev_path, 'a') as outp: + _LOGGER.info( + "Found %d new devices, updating %s", + len(new_devices), known_dev_path) - # If we come along any unknown devices we will write them to the - # known devices file but only if we did not encounter an invalid - # known devices file - if not self.invalid_known_devices_file: + writer = csv.writer(outp) - known_dev_path = self.path_known_devices_file + if is_new_file: + writer.writerow(( + "device", "name", "track", "picture")) - unknown_devices = [device for device in found_devices - if device not in known_dev] + for device in new_devices: + # See if the device scanner knows the name + # else defaults to unknown device + name = (self.device_scanner.get_device_name(device) + or "unknown_device") - if unknown_devices: - try: - # If file does not exist we will write the header too - is_new_file = not os.path.isfile(known_dev_path) + writer.writerow((device, name, 0, "")) - with open(known_dev_path, 'a') as outp: - _LOGGER.info( - "Found %d new devices, updating %s", - len(unknown_devices), known_dev_path) - - writer = csv.writer(outp) - - if is_new_file: - writer.writerow(( - "device", "name", "track", "picture")) - - for device in unknown_devices: - # See if the device scanner knows the name - # else defaults to unknown device - name = (self.device_scanner.get_device_name(device) - or "unknown_device") - - writer.writerow((device, name, 0, "")) - known_dev[device] = {'name': name, - 'track': False, - 'picture': ""} - - except IOError: - _LOGGER.exception( - "Error updating %s with %d new devices", - known_dev_path, len(unknown_devices)) + except IOError: + _LOGGER.exception( + "Error updating %s with %d new devices", + known_dev_path, len(new_devices)) self.lock.release() + # pylint: disable=too-many-branches def _read_known_devices_file(self): """ Parse and process the known devices file. """ + known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) - # Read known devices if file exists - if os.path.isfile(self.path_known_devices_file): - self.lock.acquire() + # Return if no known devices file exists + if not os.path.isfile(known_dev_path): + return - known_devices = {} + self.lock.acquire() - with open(self.path_known_devices_file) as inp: - default_last_seen = datetime(1990, 1, 1) + self.untracked_devices.clear() - # Temp variable to keep track of which entity ids we use - # so we can ensure we have unique entity ids. - used_entity_ids = [] + with open(known_dev_path) as inp: + default_last_seen = datetime(1990, 1, 1) - try: - for row in csv.DictReader(inp): - device = row['device'] + # To track which devices need an entity_id assigned + need_entity_id = [] - row['track'] = True if row['track'] == '1' else False + # All devices that are still in this set after we read the CSV file + # have been removed from the file and thus need to be cleaned up. + removed_devices = set(self.tracked.keys()) + + try: + for row in csv.DictReader(inp): + device = row['device'] + + if row['track'] == '1': + if device in self.tracked: + # Device exists + removed_devices.remove(device) + else: + # We found a new device + need_entity_id.append(device) + + self.tracked[device] = { + 'name': row['name'], + 'last_seen': default_last_seen + } + + # Update state_attr with latest from file + state_attr = { + ATTR_FRIENDLY_NAME: row['name'] + } if row['picture']: - row['default_state_attr'] = { - components.ATTR_ENTITY_PICTURE: row['picture']} + state_attr[ATTR_ENTITY_PICTURE] = row['picture'] - else: - row['default_state_attr'] = None + self.tracked[device]['state_attr'] = state_attr - # If we track this device setup tracking variables - if row['track']: - row['last_seen'] = default_last_seen + else: + self.untracked_devices.add(device) - # Make sure that each device is mapped - # to a unique entity_id name - name = util.slugify(row['name']) if row['name'] \ - else "unnamed_device" + # Remove existing devices that we no longer track + for device in removed_devices: + entity_id = self.tracked[device]['entity_id'] - entity_id = ENTITY_ID_FORMAT.format(name) - tries = 1 + _LOGGER.info("Removing entity %s", entity_id) - while entity_id in used_entity_ids: - tries += 1 + self.hass.states.remove(entity_id) - suffix = "_{}".format(tries) + self.tracked.pop(device) - entity_id = ENTITY_ID_FORMAT.format( - name + suffix) + # Setup entity_ids for the new devices + used_entity_ids = [info['entity_id'] for device, info + in self.tracked.items() + if device not in need_entity_id] - row['entity_id'] = entity_id - used_entity_ids.append(entity_id) + for device in need_entity_id: + name = self.tracked[device]['name'] - row['picture'] = row['picture'] + entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(name)), + used_entity_ids) - known_devices[device] = row + used_entity_ids.append(entity_id) - if not known_devices: - _LOGGER.warning( - "No devices to track. Please update %s.", - self.path_known_devices_file) + self.tracked[device]['entity_id'] = entity_id - # Remove entities that are no longer maintained - new_entity_ids = set([known_devices[dev]['entity_id'] - for dev in known_devices - if known_devices[dev]['track']]) - - for entity_id in \ - self.device_entity_ids - new_entity_ids: - - _LOGGER.info("Removing entity %s", entity_id) - self.states.remove(entity_id) - - # File parsed, warnings given if necessary - # entities cleaned up, make it available - self.known_devices = known_devices - - _LOGGER.info("Loaded devices from %s", - self.path_known_devices_file) - - except KeyError: - self.invalid_known_devices_file = True + if not self.tracked: _LOGGER.warning( - ("Invalid known devices file: %s. " - "We won't update it with new found devices."), - self.path_known_devices_file) + "No devices to track. Please update %s.", + known_dev_path) - finally: - self.lock.release() + _LOGGER.info("Loaded devices from %s", known_dev_path) + + except KeyError: + self.invalid_known_devices_file = True + + _LOGGER.warning( + ("Invalid known devices file: %s. " + "We won't update it with new found devices."), + known_dev_path) + + finally: + self.lock.release() diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 5376b65131a..f0ce5cc0f94 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,6 +7,7 @@ Provides functionality to group devices that can be turned on or off. import logging +import homeassistant as ha import homeassistant.util as util from homeassistant.components import (STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, @@ -24,6 +25,8 @@ _GROUP_TYPES = { "home_not_home": (STATE_HOME, STATE_NOT_HOME) } +_GROUPS = {} + def _get_group_type(state): """ Determine the group type based on the given group type. """ @@ -105,7 +108,6 @@ def setup(hass, config): def setup_group(hass, name, entity_ids, user_defined=True): """ Sets up a group state that is the combined state of several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ - # In case an iterable is passed in entity_ids = list(entity_ids) @@ -159,35 +161,47 @@ def setup_group(hass, name, entity_ids, user_defined=True): return False - else: - group_entity_id = ENTITY_ID_FORMAT.format(name) - state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined} + group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) + state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined} - # pylint: disable=unused-argument - def update_group_state(entity_id, old_state, new_state): - """ Updates the group state based on a state change by - a tracked entity. """ + # pylint: disable=unused-argument + def update_group_state(entity_id, old_state, new_state): + """ Updates the group state based on a state change by + a tracked entity. """ - cur_gr_state = hass.states.get(group_entity_id).state + cur_gr_state = hass.states.get(group_entity_id).state - # if cur_gr_state = OFF and new_state = ON: set ON - # if cur_gr_state = ON and new_state = OFF: research - # else: ignore + # if cur_gr_state = OFF and new_state = ON: set ON + # if cur_gr_state = ON and new_state = OFF: research + # else: ignore - if cur_gr_state == group_off and new_state.state == group_on: + if cur_gr_state == group_off and new_state.state == group_on: - hass.states.set(group_entity_id, group_on, state_attr) + hass.states.set(group_entity_id, group_on, state_attr) - elif cur_gr_state == group_on and new_state.state == group_off: + elif cur_gr_state == group_on and new_state.state == group_off: - # Check if any of the other states is still on - if not any([hass.states.is_state(ent_id, group_on) - for ent_id in entity_ids - if entity_id != ent_id]): - hass.states.set(group_entity_id, group_off, state_attr) + # Check if any of the other states is still on + if not any([hass.states.is_state(ent_id, group_on) + for ent_id in entity_ids + if entity_id != ent_id]): + hass.states.set(group_entity_id, group_off, state_attr) - hass.states.track_change(entity_ids, update_group_state) + _GROUPS[group_entity_id] = hass.states.track_change( + entity_ids, update_group_state) - hass.states.set(group_entity_id, group_state, state_attr) + hass.states.set(group_entity_id, group_state, state_attr) - return True + return True + + +def remove_group(hass, name): + """ Remove a group and its state listener from Home Assistant. """ + group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) + + if hass.states.get(group_entity_id) is not None: + hass.states.remove(group_entity_id) + + if group_entity_id in _GROUPS: + hass.bus.remove_listener( + ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id)) From 31b9f65513ea2fb59a91facc08550698f9697408 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Dec 2014 01:14:27 -0800 Subject: [PATCH 15/60] Added initial version of AddCooldown decorator --- ha_test/test_util.py | 33 ++++++++++++++- .../components/device_tracker/luci.py | 27 +++++-------- .../components/device_tracker/netgear.py | 24 ++--------- .../components/device_tracker/tomato.py | 27 ++----------- homeassistant/util.py | 40 +++++++++++++++++++ 5 files changed, 90 insertions(+), 61 deletions(-) diff --git a/ha_test/test_util.py b/ha_test/test_util.py index ee48ef3a784..c7f0b848ab2 100644 --- a/ha_test/test_util.py +++ b/ha_test/test_util.py @@ -6,7 +6,8 @@ Tests Home Assistant util methods. """ # pylint: disable=too-many-public-methods import unittest -from datetime import datetime +import time +from datetime import datetime, timedelta import homeassistant.util as util @@ -190,3 +191,33 @@ class TestUtil(unittest.TestCase): set1.update([1, 2], [5, 6]) self.assertEqual([2, 3, 1, 5, 6], set1) + + def test_add_cooldown(self): + """ Test the add cooldown decorator. """ + calls = [] + + @util.AddCooldown(timedelta(milliseconds=500)) + def test_cooldown(): + calls.append(1) + + self.assertEqual(0, len(calls)) + + test_cooldown() + + self.assertEqual(1, len(calls)) + + test_cooldown() + + self.assertEqual(1, len(calls)) + + time.sleep(.3) + + test_cooldown() + + self.assertEqual(1, len(calls)) + + time.sleep(.2) + + test_cooldown() + + self.assertEqual(2, len(calls)) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 89d50f0239f..5409babacb9 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -1,7 +1,7 @@ """ Supports scanning a OpenWRT router. """ import logging import json -from datetime import datetime, timedelta +from datetime import timedelta import re import threading import requests @@ -52,7 +52,6 @@ class LuciDeviceScanner(object): self.lock = threading.Lock() - self.date_updated = None self.last_results = {} self.token = _get_token(host, username, password) @@ -88,29 +87,25 @@ class LuciDeviceScanner(object): return return self.mac2name.get(device, None) + @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Ensures the information from the Luci router is up to date. Returns boolean if scanning successful. """ if not self.success_init: return False + with self.lock: - # if date_updated is None or the date is too old we scan - # for new data - if not self.date_updated or \ - datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: + _LOGGER.info("Checking ARP") - _LOGGER.info("Checking ARP") + url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) + result = _req_json_rpc(url, 'net.arptable', + params={'auth': self.token}) + if result: + self.last_results = [x['HW address'] for x in result] - url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) - result = _req_json_rpc(url, 'net.arptable', - params={'auth': self.token}) - if result: - self.last_results = [x['HW address'] for x in result] - self.date_updated = datetime.now() - return True - return False + return True - return True + return False def _req_json_rpc(url, method, *args, **kwargs): diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 23eda17fff8..98485afefd7 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -1,6 +1,6 @@ """ Supports scanning a Netgear router. """ import logging -from datetime import datetime, timedelta +from datetime import timedelta import threading import homeassistant as ha @@ -34,7 +34,6 @@ class NetgearDeviceScanner(object): host = config[ha.CONF_HOST] username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] - self.date_updated = None self.last_results = [] try: @@ -75,10 +74,6 @@ class NetgearDeviceScanner(object): def get_device_name(self, mac): """ Returns the name of the given device or None if we don't know. """ - # Make sure there are results - if not self.date_updated: - self._update_info() - filter_named = [device.name for device in self.last_results if device.mac == mac] @@ -87,6 +82,7 @@ class NetgearDeviceScanner(object): else: return None + @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Retrieves latest information from the Netgear router. Returns boolean if scanning successful. """ @@ -94,18 +90,6 @@ class NetgearDeviceScanner(object): return with self.lock: - # if date_updated is None or the date is too old we scan for - # new data - if not self.date_updated or \ - datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: + _LOGGER.info("Scanning") - _LOGGER.info("Scanning") - - self.last_results = self._api.get_attached_devices() - - self.date_updated = datetime.now() - - return - - else: - return + self.last_results = self._api.get_attached_devices() diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 748ad53f534..0a5eb1c4fa5 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -1,7 +1,7 @@ """ Supports scanning a Tomato router. """ import logging import json -from datetime import datetime, timedelta +from datetime import timedelta import re import threading @@ -55,7 +55,6 @@ class TomatoDeviceScanner(object): self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato")) self.lock = threading.Lock() - self.date_updated = None self.last_results = {"wldev": [], "dhcpd_lease": []} self.success_init = self._update_tomato_info() @@ -71,10 +70,6 @@ class TomatoDeviceScanner(object): def get_device_name(self, device): """ Returns the name of the given device or None if we don't know. """ - # Make sure there are results - if not self.date_updated: - self._update_tomato_info() - filter_named = [item[0] for item in self.last_results['dhcpd_lease'] if item[2] == device] @@ -83,16 +78,12 @@ class TomatoDeviceScanner(object): else: return filter_named[0] + @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) def _update_tomato_info(self): """ Ensures the information from the Tomato router is up to date. Returns boolean if scanning successful. """ - self.lock.acquire() - - # if date_updated is None or the date is too old we scan for new data - if not self.date_updated or \ - datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: - + with self.lock: self.logger.info("Scanning") try: @@ -111,8 +102,6 @@ class TomatoDeviceScanner(object): self.last_results[param] = \ json.loads(value.replace("'", '"')) - self.date_updated = datetime.now() - return True elif response.status_code == 401: @@ -146,13 +135,3 @@ class TomatoDeviceScanner(object): "Failed to parse response from router") return False - - finally: - self.lock.release() - - else: - # We acquired the lock before the IF check, - # release it before we return True - self.lock.release() - - return True diff --git a/homeassistant/util.py b/homeassistant/util.py index a4b812803d4..40cb463b37e 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -12,6 +12,7 @@ import datetime import re import enum import socket +from functools import wraps RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') @@ -273,6 +274,45 @@ def validate_config(config, items, logger): return not errors_found +class AddCooldown(object): + """ + A method decorator to add a cooldown to a method. + + If you set a cooldown of 5 seconds. Then if you call a method twice the + underlaying method will not be called if the second call was within + 5 seconds of the first. None will be returned instead. + + Makes a last_call attribute available on the wrapped method. + """ + # pylint: disable=too-few-public-methods + + def __init__(self, min_time): + self.min_time = min_time + + def __call__(self, method): + lock = threading.Lock() + + @wraps(method) + def wrapper(*args, **kwargs): + """ + Wrapper that allows wrapped to be called only once per min_time. + """ + with lock: + now = datetime.datetime.now() + last_call = wrapper.last_call + + if last_call is None or now - last_call > self.min_time: + result = method(*args, **kwargs) + wrapper.last_call = now + return result + else: + return None + + wrapper.last_call = None + + return wrapper + + # Reason why I decided to roll my own ThreadPool instead of using # multiprocessing.dummy.pool or even better, use multiprocessing.pool and # not be hurt by the GIL in the cpython interpreter: From 48089b01ab38848b07517a4d3e847cc0f79cf435 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Dec 2014 21:06:45 -0800 Subject: [PATCH 16/60] Renamed AddCooldown to Throttle and added bypass functionality --- ha_test/test_util.py | 63 ++++++++++++++----- .../components/device_tracker/luci.py | 2 +- .../components/device_tracker/netgear.py | 2 +- .../components/device_tracker/tomato.py | 2 +- homeassistant/components/light/hue.py | 28 +++------ homeassistant/components/switch/__init__.py | 19 +++--- homeassistant/util.py | 40 +++++++----- 7 files changed, 91 insertions(+), 65 deletions(-) diff --git a/ha_test/test_util.py b/ha_test/test_util.py index c7f0b848ab2..8b986e76399 100644 --- a/ha_test/test_util.py +++ b/ha_test/test_util.py @@ -192,32 +192,65 @@ class TestUtil(unittest.TestCase): set1.update([1, 2], [5, 6]) self.assertEqual([2, 3, 1, 5, 6], set1) - def test_add_cooldown(self): + def test_throttle(self): """ Test the add cooldown decorator. """ - calls = [] + calls1 = [] - @util.AddCooldown(timedelta(milliseconds=500)) - def test_cooldown(): - calls.append(1) + @util.Throttle(timedelta(milliseconds=500)) + def test_throttle1(): + calls1.append(1) - self.assertEqual(0, len(calls)) + calls2 = [] - test_cooldown() + @util.Throttle( + timedelta(milliseconds=500), timedelta(milliseconds=250)) + def test_throttle2(): + calls2.append(1) - self.assertEqual(1, len(calls)) + # Ensure init is ok + self.assertEqual(0, len(calls1)) + self.assertEqual(0, len(calls2)) - test_cooldown() + # Call first time and ensure methods got called + test_throttle1() + test_throttle2() - self.assertEqual(1, len(calls)) + self.assertEqual(1, len(calls1)) + self.assertEqual(1, len(calls2)) + # Call second time. Methods should not get called + test_throttle1() + test_throttle2() + + self.assertEqual(1, len(calls1)) + self.assertEqual(1, len(calls2)) + + # Call again, overriding throttle, only first one should fire + test_throttle1(no_throttle=True) + test_throttle2(no_throttle=True) + + self.assertEqual(2, len(calls1)) + self.assertEqual(1, len(calls2)) + + # Sleep past the no throttle interval for throttle2 time.sleep(.3) - test_cooldown() + test_throttle1() + test_throttle2() - self.assertEqual(1, len(calls)) + self.assertEqual(2, len(calls1)) + self.assertEqual(1, len(calls2)) - time.sleep(.2) + test_throttle1(no_throttle=True) + test_throttle2(no_throttle=True) - test_cooldown() + self.assertEqual(3, len(calls1)) + self.assertEqual(2, len(calls2)) - self.assertEqual(2, len(calls)) + time.sleep(.5) + + test_throttle1() + test_throttle2() + + self.assertEqual(4, len(calls1)) + self.assertEqual(3, len(calls2)) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 5409babacb9..dba41c11c03 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -87,7 +87,7 @@ class LuciDeviceScanner(object): return return self.mac2name.get(device, None) - @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) + @util.Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Ensures the information from the Luci router is up to date. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 98485afefd7..a57e2f28c70 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -82,7 +82,7 @@ class NetgearDeviceScanner(object): else: return None - @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) + @util.Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Retrieves latest information from the Netgear router. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 0a5eb1c4fa5..c3b7e5dec94 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -78,7 +78,7 @@ class TomatoDeviceScanner(object): else: return filter_named[0] - @util.AddCooldown(MIN_TIME_BETWEEN_SCANS) + @util.Throttle(MIN_TIME_BETWEEN_SCANS) def _update_tomato_info(self): """ Ensures the information from the Tomato router is up to date. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index fb1e8a42ff5..c7db591a4f7 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -1,14 +1,16 @@ """ Support for Hue lights. """ import logging import socket -from datetime import datetime, timedelta +from datetime import timedelta import homeassistant as ha +import homeassistant.util as util from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) PHUE_CONFIG_FILE = "phue.conf" @@ -37,25 +39,9 @@ def get_lights(hass, config): lights = {} - def update_lights(force_reload=False): - """ Updates the light states. """ - now = datetime.now() - - try: - time_scans = now - update_lights.last_updated - - # force_reload == True, return if updated in last second - # force_reload == False, return if last update was less then - # MIN_TIME_BETWEEN_SCANS ago - if force_reload and time_scans.seconds < 1 or \ - not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS: - return - except AttributeError: - # First time we run last_updated is not set, continue as usual - pass - - update_lights.last_updated = now - + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update_lights(): + """ Updates the Hue light objects with latest info from the bridge. """ try: api = bridge.get_api() except socket.error: @@ -142,4 +128,4 @@ class HueLight(ToggleDevice): def update(self): """ Synchronize state with bridge. """ - self.update_lights(True) + self.update_lights(no_throttle=True) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 87e50eaa41c..98da108484e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,7 +4,7 @@ homeassistant.components.switch Component to interface with various switches that can be controlled remotely. """ import logging -from datetime import datetime, timedelta +from datetime import timedelta import homeassistant as ha import homeassistant.util as util @@ -96,21 +96,16 @@ def setup(hass, config): ent_to_switch[entity_id] = switch # pylint: disable=unused-argument - def update_states(time, force_reload=False): + @util.Throttle(MIN_TIME_BETWEEN_SCANS) + def update_states(now): """ Update states of all switches. """ - # First time this method gets called, force_reload should be True - if force_reload or \ - datetime.now() - update_states.last_updated > \ - MIN_TIME_BETWEEN_SCANS: + logger.info("Updating switch states") - logger.info("Updating switch states") - update_states.last_updated = datetime.now() + for switch in switches: + switch.update_ha_state(hass) - for switch in switches: - switch.update_ha_state(hass) - - update_states(None, True) + update_states(None) def handle_switch_service(service): """ Handles calls to the switch services. """ diff --git a/homeassistant/util.py b/homeassistant/util.py index 40cb463b37e..d6bb9423a42 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -8,7 +8,7 @@ import collections from itertools import chain import threading import queue -import datetime +from datetime import datetime import re import enum import socket @@ -52,7 +52,7 @@ def str_to_datetime(dt_str): @rtype: datetime """ try: - return datetime.datetime.strptime(dt_str, DATE_STR_FORMAT) + return datetime.strptime(dt_str, DATE_STR_FORMAT) except ValueError: # If dt_str did not match our format return None @@ -68,7 +68,7 @@ def repr_helper(inp): return ", ".join( repr_helper(key)+"="+repr_helper(item) for key, item in inp.items()) - elif isinstance(inp, datetime.datetime): + elif isinstance(inp, datetime): return datetime_to_str(inp) else: return str(inp) @@ -274,36 +274,48 @@ def validate_config(config, items, logger): return not errors_found -class AddCooldown(object): +class Throttle(object): """ - A method decorator to add a cooldown to a method. + A method decorator to add a cooldown to a method to prevent it from being + called more then 1 time within the timedelta interval `min_time` after it + returned its result. - If you set a cooldown of 5 seconds. Then if you call a method twice the - underlaying method will not be called if the second call was within - 5 seconds of the first. None will be returned instead. + Calling a method a second time during the interval will return None. - Makes a last_call attribute available on the wrapped method. + Pass keyword argument `no_throttle=True` to the wrapped method to make + the call not throttled. + + Decorator takes in an optional second timedelta interval to throttle the + 'no_throttle' calls. + + Adds a datetime attribute `last_call` to the method. """ # pylint: disable=too-few-public-methods - def __init__(self, min_time): + def __init__(self, min_time, limit_no_throttle=None): self.min_time = min_time + self.limit_no_throttle = limit_no_throttle def __call__(self, method): lock = threading.Lock() + if self.limit_no_throttle is not None: + method = Throttle(self.limit_no_throttle)(method) + @wraps(method) def wrapper(*args, **kwargs): """ Wrapper that allows wrapped to be called only once per min_time. """ with lock: - now = datetime.datetime.now() last_call = wrapper.last_call + # Check if method is never called or no_throttle is given + force = last_call is None or kwargs.pop('no_throttle', False) + + if force or datetime.now() - last_call > self.min_time: - if last_call is None or now - last_call > self.min_time: result = method(*args, **kwargs) - wrapper.last_call = now + wrapper.last_call = datetime.now() return result else: return None @@ -418,7 +430,7 @@ def _threadpool_worker(work_queue, current_jobs, job_handler, quit_task): return # Add to current running jobs - job_log = (datetime.datetime.now(), job) + job_log = (datetime.now(), job) current_jobs.append(job_log) # Do the job From 513a03fb46cb08bd040f1599d2c8a950f6cea7e9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 6 Dec 2014 16:16:11 -0800 Subject: [PATCH 17/60] Updated API docs --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 956176e4962..523ce94582c 100644 --- a/README.md +++ b/README.md @@ -234,9 +234,9 @@ Home Assistent runs a webserver accessible on port 8123. In the package `homeassistant.remote` a Python API on top of the HTTP API can be found. -All API calls have to be accompanied by the header "HA-Access" with as value the api password (as specified in `home-assistant.conf`). The API returns only JSON encoded objects. Successful calls will return status code 200 or 201. +The API accepts and returns only JSON encoded objects. All API calls have to be accompanied by the header "HA-Access" with as value the api password (as specified in `home-assistant.conf`). -Other status codes that can occur are: +Successful calls will return status code 200 or 201. Other status codes that can return are: - 400 (Bad Request) - 401 (Unauthorized) - 404 (Not Found) @@ -254,7 +254,7 @@ Returns message if API is up and running. ``` **/api/events - GET**
-Returns a dict with as keys the events and as value the number of listeners. +Returns an array of event objects. Each event object contain event name and listener count. ```json [ @@ -270,7 +270,7 @@ Returns a dict with as keys the events and as value the number of listeners. ``` **/api/services - GET**
-Returns a dict with as keys the domain and as value a list of published services. +Returns an array of service objects. Each object contains the domain and which services it contains. ```json [ @@ -291,7 +291,7 @@ Returns a dict with as keys the domain and as value a list of published services ``` **/api/states - GET**
-Returns a dict with as keys the entity_ids and as value the state. +Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes. ```json [ @@ -314,7 +314,7 @@ Returns a dict with as keys the entity_ids and as value the state. ``` **/api/states/<entity_id>** - GET
-Returns the current state from an entity +Returns a state object for specified entity_id. Returns 404 if not found. ```json { @@ -329,9 +329,12 @@ Returns the current state from an entity ``` **/api/states/<entity_id>** - POST
-Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.
-parameter: new_state - string
-optional parameter: attributes - JSON encoded object +Updates or creates the current state of an entity. + +Return code is 200 if the entity existed, 201 if the state of a new entity was set. A location header will be returned with the url of the new resource. The response body will contain a JSON encoded State object.
+
+parameter: state - string
+optional parameter: attributes - JSON object ```json { From 0527760e9bd7760c0c441aad09f70b246e08ac11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 6 Dec 2014 23:57:02 -0800 Subject: [PATCH 18/60] Refactor: code moved to new helper and constants file. Also adds support for multiple types for switch/light components. --- README.md | 6 +- config/home-assistant.conf.example | 6 +- .../config/custom_components/light/test.py | 10 +- .../config/custom_components/switch/test.py | 10 +- ha_test/{helper.py => helpers.py} | 11 +- ha_test/test_component_chromecast.py | 28 +-- ha_test/test_component_core.py | 24 +-- ha_test/test_component_demo.py | 2 +- ha_test/test_component_device_scanner.py | 12 +- ha_test/test_component_group.py | 32 ++-- ha_test/test_component_light.py | 22 +-- ha_test/test_component_sun.py | 23 +-- ha_test/test_component_switch.py | 28 ++- ha_test/test_helpers.py | 49 +++++ ha_test/test_loader.py | 2 +- homeassistant/__init__.py | 26 +-- homeassistant/bootstrap.py | 5 +- homeassistant/components/__init__.py | 105 +---------- homeassistant/components/chromecast.py | 65 ++++--- homeassistant/components/demo.py | 13 +- .../components/device_sun_light_trigger.py | 10 +- .../components/device_tracker/__init__.py | 22 ++- .../components/device_tracker/luci.py | 18 +- .../components/device_tracker/netgear.py | 18 +- .../components/device_tracker/tomato.py | 19 +- homeassistant/components/downloader.py | 9 +- homeassistant/components/group.py | 5 +- homeassistant/components/http/__init__.py | 25 +-- homeassistant/components/keyboard.py | 32 ++-- homeassistant/components/light/__init__.py | 30 +-- homeassistant/components/light/hue.py | 8 +- homeassistant/components/process.py | 2 +- homeassistant/components/sun.py | 23 +-- homeassistant/components/switch/__init__.py | 30 +-- homeassistant/components/switch/tellstick.py | 5 +- homeassistant/components/switch/wemo.py | 10 +- homeassistant/components/tellstick_sensor.py | 3 +- homeassistant/const.py | 78 ++++++++ homeassistant/helpers.py | 176 ++++++++++++++++++ homeassistant/remote.py | 16 +- homeassistant/util.py | 27 +-- 41 files changed, 603 insertions(+), 442 deletions(-) rename ha_test/{helper.py => helpers.py} (88%) create mode 100644 ha_test/test_helpers.py create mode 100644 homeassistant/const.py create mode 100644 homeassistant/helpers.py diff --git a/README.md b/README.md index 523ce94582c..08564802e77 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ After you got the demo mode running it is time to enable some real components an *Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically. +*Note:* for the light and switch component, you can specify multiple types by using sequential sections: [switch], [switch 2], [switch 3] etc + ### Philips Hue To get Philips Hue working you will have to connect Home Assistant to the Hue bridge. @@ -68,7 +70,7 @@ After that add the following lines to your `home-assistant.conf`: ``` [light] -type=hue +platform=hue ``` ### Wireless router @@ -77,7 +79,7 @@ Your wireless router is used to track which devices are connected. Three differe ``` [device_tracker] -type=netgear +platform=netgear host=192.168.1.1 username=admin password=MY_PASSWORD diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example index 9eb0cf4a90e..5a919ac38b0 100644 --- a/config/home-assistant.conf.example +++ b/config/home-assistant.conf.example @@ -9,11 +9,11 @@ api_password=mypass # development=1 [light] -type=hue +platform=hue [device_tracker] # The following types are available: netgear, tomato, luci -type=netgear +platform=netgear host=192.168.1.1 username=admin password=PASSWORD @@ -26,7 +26,7 @@ password=PASSWORD # hosts=192.168.1.9,192.168.1.12 [switch] -type=wemo +platform=wemo # Optional: hard code the hosts (comma seperated) to avoid scanning the network # hosts=192.168.1.9,192.168.1.12 diff --git a/ha_test/config/custom_components/light/test.py b/ha_test/config/custom_components/light/test.py index 757099ddca0..0ed04a21717 100644 --- a/ha_test/config/custom_components/light/test.py +++ b/ha_test/config/custom_components/light/test.py @@ -6,8 +6,8 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ -import homeassistant.components as components -from ha_test.helper import MockToggleDevice +from homeassistant.const import STATE_ON, STATE_OFF +from ha_test.helpers import MockToggleDevice DEVICES = [] @@ -18,9 +18,9 @@ def init(empty=False): global DEVICES DEVICES = [] if empty else [ - MockToggleDevice('Ceiling', components.STATE_ON), - MockToggleDevice('Ceiling', components.STATE_OFF), - MockToggleDevice(None, components.STATE_OFF) + MockToggleDevice('Ceiling', STATE_ON), + MockToggleDevice('Ceiling', STATE_OFF), + MockToggleDevice(None, STATE_OFF) ] diff --git a/ha_test/config/custom_components/switch/test.py b/ha_test/config/custom_components/switch/test.py index 927aca24feb..682c27f695f 100644 --- a/ha_test/config/custom_components/switch/test.py +++ b/ha_test/config/custom_components/switch/test.py @@ -6,8 +6,8 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ -import homeassistant.components as components -from ha_test.helper import MockToggleDevice +from homeassistant.const import STATE_ON, STATE_OFF +from ha_test.helpers import MockToggleDevice DEVICES = [] @@ -18,9 +18,9 @@ def init(empty=False): global DEVICES DEVICES = [] if empty else [ - MockToggleDevice('AC', components.STATE_ON), - MockToggleDevice('AC', components.STATE_OFF), - MockToggleDevice(None, components.STATE_OFF) + MockToggleDevice('AC', STATE_ON), + MockToggleDevice('AC', STATE_OFF), + MockToggleDevice(None, STATE_OFF) ] diff --git a/ha_test/helper.py b/ha_test/helpers.py similarity index 88% rename from ha_test/helper.py rename to ha_test/helpers.py index 109b4185ba5..f04dac72553 100644 --- a/ha_test/helper.py +++ b/ha_test/helpers.py @@ -7,7 +7,8 @@ Helper method for writing tests. import os import homeassistant as ha -import homeassistant.components as components +from homeassistant.helpers import ToggleDevice +from homeassistant.const import STATE_ON, STATE_OFF def get_test_home_assistant(): @@ -41,7 +42,7 @@ class MockModule(object): self.setup = lambda hass, config: False if setup is None else setup -class MockToggleDevice(components.ToggleDevice): +class MockToggleDevice(ToggleDevice): """ Provides a mock toggle device. """ def __init__(self, name, state): self.name = name @@ -56,17 +57,17 @@ class MockToggleDevice(components.ToggleDevice): def turn_on(self, **kwargs): """ Turn the device on. """ self.calls.append(('turn_on', kwargs)) - self.state = components.STATE_ON + self.state = STATE_ON def turn_off(self, **kwargs): """ Turn the device off. """ self.calls.append(('turn_off', kwargs)) - self.state = components.STATE_OFF + self.state = STATE_OFF def is_on(self): """ True if device is on. """ self.calls.append(('is_on', {})) - return self.state == components.STATE_ON + return self.state == STATE_ON def last_call(self, method=None): if method is None: diff --git a/ha_test/test_component_chromecast.py b/ha_test/test_component_chromecast.py index 3b80c700606..3d9733648c9 100644 --- a/ha_test/test_component_chromecast.py +++ b/ha_test/test_component_chromecast.py @@ -9,9 +9,13 @@ import logging import unittest import homeassistant as ha -import homeassistant.components as components +from homeassistant.const import ( + SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID, + CONF_HOSTS) import homeassistant.components.chromecast as chromecast -from helper import mock_service +from helpers import mock_service def setUpModule(): # pylint: disable=invalid-name @@ -45,14 +49,14 @@ class TestChromecast(unittest.TestCase): Test if the call service methods conver to correct service calls. """ services = { - components.SERVICE_TURN_OFF: chromecast.turn_off, - components.SERVICE_VOLUME_UP: chromecast.volume_up, - components.SERVICE_VOLUME_DOWN: chromecast.volume_down, - components.SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause, - components.SERVICE_MEDIA_PLAY: chromecast.media_play, - components.SERVICE_MEDIA_PAUSE: chromecast.media_pause, - components.SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track, - components.SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track + SERVICE_TURN_OFF: chromecast.turn_off, + SERVICE_VOLUME_UP: chromecast.volume_up, + SERVICE_VOLUME_DOWN: chromecast.volume_down, + SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause, + SERVICE_MEDIA_PLAY: chromecast.media_play, + SERVICE_MEDIA_PAUSE: chromecast.media_pause, + SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track, + SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track } for service_name, service_method in services.items(): @@ -75,7 +79,7 @@ class TestChromecast(unittest.TestCase): self.assertEqual(call.domain, chromecast.DOMAIN) self.assertEqual(call.service, service_name) self.assertEqual(call.data, - {components.ATTR_ENTITY_ID: self.test_entity}) + {ATTR_ENTITY_ID: self.test_entity}) def test_setup(self): """ @@ -84,4 +88,4 @@ class TestChromecast(unittest.TestCase): In an ideal world we would create a mock pychromecast API.. """ self.assertFalse(chromecast.setup( - self.hass, {chromecast.DOMAIN: {ha.CONF_HOSTS: '127.0.0.1'}})) + self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}})) diff --git a/ha_test/test_component_core.py b/ha_test/test_component_core.py index 2a4a942a5ba..927bfb98a7c 100644 --- a/ha_test/test_component_core.py +++ b/ha_test/test_component_core.py @@ -9,6 +9,8 @@ import unittest import homeassistant as ha import homeassistant.loader as loader +from homeassistant.const import ( + STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components as comps @@ -21,8 +23,8 @@ class TestComponentsCore(unittest.TestCase): loader.prepare(self.hass) self.assertTrue(comps.setup(self.hass, {})) - self.hass.states.set('light.Bowl', comps.STATE_ON) - self.hass.states.set('light.Ceiling', comps.STATE_OFF) + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -38,7 +40,7 @@ class TestComponentsCore(unittest.TestCase): """ Test turn_on method. """ runs = [] self.hass.services.register( - 'light', comps.SERVICE_TURN_ON, lambda x: runs.append(1)) + 'light', SERVICE_TURN_ON, lambda x: runs.append(1)) comps.turn_on(self.hass, 'light.Ceiling') @@ -50,24 +52,10 @@ class TestComponentsCore(unittest.TestCase): """ Test turn_off method. """ runs = [] self.hass.services.register( - 'light', comps.SERVICE_TURN_OFF, lambda x: runs.append(1)) + 'light', SERVICE_TURN_OFF, lambda x: runs.append(1)) comps.turn_off(self.hass, 'light.Bowl') self.hass._pool.block_till_done() self.assertEqual(1, len(runs)) - - def test_extract_entity_ids(self): - """ Test extract_entity_ids method. """ - call = ha.ServiceCall('light', 'turn_on', - {comps.ATTR_ENTITY_ID: 'light.Bowl'}) - - self.assertEqual(['light.Bowl'], - comps.extract_entity_ids(self.hass, call)) - - call = ha.ServiceCall('light', 'turn_on', - {comps.ATTR_ENTITY_ID: ['light.Bowl']}) - - self.assertEqual(['light.Bowl'], - comps.extract_entity_ids(self.hass, call)) diff --git a/ha_test/test_component_demo.py b/ha_test/test_component_demo.py index a510759a8ea..72be2d12525 100644 --- a/ha_test/test_component_demo.py +++ b/ha_test/test_component_demo.py @@ -9,7 +9,7 @@ import unittest import homeassistant as ha import homeassistant.components.demo as demo -from homeassistant.components import ( +from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, ATTR_ENTITY_ID) diff --git a/ha_test/test_component_device_scanner.py b/ha_test/test_component_device_scanner.py index 26a7ddb5590..09900951eeb 100644 --- a/ha_test/test_component_device_scanner.py +++ b/ha_test/test_component_device_scanner.py @@ -12,11 +12,11 @@ import os import homeassistant as ha import homeassistant.loader as loader -from homeassistant.components import ( - STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE) +from homeassistant.const import ( + STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker -from helper import get_test_home_assistant +from helpers import get_test_home_assistant def setUpModule(): # pylint: disable=invalid-name @@ -64,7 +64,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): # Test with non-existing component self.assertFalse(device_tracker.setup( - self.hass, {device_tracker.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}} + self.hass, {device_tracker.DOMAIN: {CONF_PLATFORM: 'nonexisting'}} )) # Test with a bad known device file around @@ -72,7 +72,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): fil.write("bad data\nbad data\n") self.assertFalse(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'} + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} })) def test_device_tracker(self): @@ -84,7 +84,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): scanner.come_home('dev2') self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'} + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} })) # Ensure a new known devices file has been created. diff --git a/ha_test/test_component_group.py b/ha_test/test_component_group.py index d1d9dccbb27..d83596cee9d 100644 --- a/ha_test/test_component_group.py +++ b/ha_test/test_component_group.py @@ -9,7 +9,7 @@ import unittest import logging import homeassistant as ha -import homeassistant.components as comps +from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME import homeassistant.components.group as group @@ -25,9 +25,9 @@ class TestComponentsGroup(unittest.TestCase): """ Init needed objects. """ self.hass = ha.HomeAssistant() - self.hass.states.set('light.Bowl', comps.STATE_ON) - self.hass.states.set('light.Ceiling', comps.STATE_OFF) - self.hass.states.set('switch.AC', comps.STATE_OFF) + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + self.hass.states.set('switch.AC', STATE_OFF) group.setup_group(self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) group.setup_group(self.hass, 'mixed_group', @@ -47,27 +47,27 @@ class TestComponentsGroup(unittest.TestCase): self.assertIn(self.group_name, self.hass.states.entity_ids()) group_state = self.hass.states.get(self.group_name) - self.assertEqual(comps.STATE_ON, group_state.state) + self.assertEqual(STATE_ON, group_state.state) self.assertTrue(group_state.attributes[group.ATTR_AUTO]) # Turn the Bowl off and see if group turns off - self.hass.states.set('light.Bowl', comps.STATE_OFF) + self.hass.states.set('light.Bowl', STATE_OFF) self.hass._pool.block_till_done() group_state = self.hass.states.get(self.group_name) - self.assertEqual(comps.STATE_OFF, group_state.state) + self.assertEqual(STATE_OFF, group_state.state) # Turn the Ceiling on and see if group turns on - self.hass.states.set('light.Ceiling', comps.STATE_ON) + self.hass.states.set('light.Ceiling', STATE_ON) self.hass._pool.block_till_done() group_state = self.hass.states.get(self.group_name) - self.assertEqual(comps.STATE_ON, group_state.state) + self.assertEqual(STATE_ON, group_state.state) # Try to setup a group with mixed groupable states - self.hass.states.set('device_tracker.Paulus', comps.STATE_HOME) + self.hass.states.set('device_tracker.Paulus', STATE_HOME) self.assertFalse(group.setup_group( self.hass, 'person_and_light', ['light.Bowl', 'device_tracker.Paulus'])) @@ -91,12 +91,12 @@ class TestComponentsGroup(unittest.TestCase): def test__get_group_type(self): """ Test _get_group_type method. """ - self.assertEqual('on_off', group._get_group_type(comps.STATE_ON)) - self.assertEqual('on_off', group._get_group_type(comps.STATE_OFF)) + self.assertEqual('on_off', group._get_group_type(STATE_ON)) + self.assertEqual('on_off', group._get_group_type(STATE_OFF)) self.assertEqual('home_not_home', - group._get_group_type(comps.STATE_HOME)) + group._get_group_type(STATE_HOME)) self.assertEqual('home_not_home', - group._get_group_type(comps.STATE_NOT_HOME)) + group._get_group_type(STATE_NOT_HOME)) # Unsupported state self.assertIsNone(group._get_group_type('unsupported_state')) @@ -104,7 +104,7 @@ class TestComponentsGroup(unittest.TestCase): def test_is_on(self): """ Test is_on method. """ self.assertTrue(group.is_on(self.hass, self.group_name)) - self.hass.states.set('light.Bowl', comps.STATE_OFF) + self.hass.states.set('light.Bowl', STATE_OFF) self.hass._pool.block_till_done() self.assertFalse(group.is_on(self.hass, self.group_name)) @@ -159,5 +159,5 @@ class TestComponentsGroup(unittest.TestCase): group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('second_group')) - self.assertEqual(comps.STATE_ON, group_state.state) + self.assertEqual(STATE_ON, group_state.state) self.assertFalse(group_state.attributes[group.ATTR_AUTO]) diff --git a/ha_test/test_component_light.py b/ha_test/test_component_light.py index 3d4b5c1f3eb..4d781c70132 100644 --- a/ha_test/test_component_light.py +++ b/ha_test/test_component_light.py @@ -11,12 +11,12 @@ import os import homeassistant as ha import homeassistant.loader as loader import homeassistant.util as util -from homeassistant.components import ( - get_component, ATTR_ENTITY_ID, STATE_ON, STATE_OFF, +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components.light as light -from helper import mock_service, get_test_home_assistant +from helpers import mock_service, get_test_home_assistant class TestLight(unittest.TestCase): @@ -98,11 +98,11 @@ class TestLight(unittest.TestCase): def test_services(self): """ Test the provided services. """ - platform = get_component('light.test') + platform = loader.get_component('light.test') platform.init() self.assertTrue( - light.setup(self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}})) + light.setup(self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}})) dev1, dev2, dev3 = platform.get_lights(None, None) @@ -223,22 +223,22 @@ class TestLight(unittest.TestCase): # Test with non-existing component self.assertFalse(light.setup( - self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}} + self.hass, {light.DOMAIN: {CONF_TYPE: 'nonexisting'}} )) # Test if light component returns 0 lightes - platform = get_component('light.test') + platform = loader.get_component('light.test') platform.init(True) self.assertEqual([], platform.get_lights(None, None)) self.assertFalse(light.setup( - self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} + self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}} )) def test_light_profiles(self): """ Test light profiles. """ - platform = get_component('light.test') + platform = loader.get_component('light.test') platform.init() user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) @@ -249,7 +249,7 @@ class TestLight(unittest.TestCase): user_file.write('I,WILL,NOT,WORK\n') self.assertFalse(light.setup( - self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} + self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}} )) # Clean up broken file @@ -260,7 +260,7 @@ class TestLight(unittest.TestCase): user_file.write('test,.4,.6,100\n') self.assertTrue(light.setup( - self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} + self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}} )) dev1, dev2, dev3 = platform.get_lights(None, None) diff --git a/ha_test/test_component_sun.py b/ha_test/test_component_sun.py index a83f8de51a7..daf8970f406 100644 --- a/ha_test/test_component_sun.py +++ b/ha_test/test_component_sun.py @@ -11,6 +11,7 @@ import datetime as dt import ephem import homeassistant as ha +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE import homeassistant.components.sun as sun @@ -37,8 +38,8 @@ class TestSun(unittest.TestCase): self.assertTrue(sun.setup( self.hass, {ha.DOMAIN: { - ha.CONF_LATITUDE: '32.87336', - ha.CONF_LONGITUDE: '117.22743' + CONF_LATITUDE: '32.87336', + CONF_LONGITUDE: '117.22743' }})) observer = ephem.Observer() @@ -76,8 +77,8 @@ class TestSun(unittest.TestCase): self.assertTrue(sun.setup( self.hass, {ha.DOMAIN: { - ha.CONF_LATITUDE: '32.87336', - ha.CONF_LONGITUDE: '117.22743' + CONF_LATITUDE: '32.87336', + CONF_LONGITUDE: '117.22743' }})) if sun.is_on(self.hass): @@ -101,24 +102,24 @@ class TestSun(unittest.TestCase): self.assertFalse(sun.setup(self.hass, {})) self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}})) self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: '32.87336'}})) + self.hass, {ha.DOMAIN: {CONF_LATITUDE: '32.87336'}})) self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: '117.22743'}})) + self.hass, {ha.DOMAIN: {CONF_LONGITUDE: '117.22743'}})) self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: 'hello'}})) + self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}})) self.assertFalse(sun.setup( - self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: 'how are you'}})) + self.hass, {ha.DOMAIN: {CONF_LONGITUDE: 'how are you'}})) self.assertFalse(sun.setup( self.hass, {ha.DOMAIN: { - ha.CONF_LATITUDE: 'wrong', ha.CONF_LONGITUDE: '117.22743' + CONF_LATITUDE: 'wrong', CONF_LONGITUDE: '117.22743' }})) self.assertFalse(sun.setup( self.hass, {ha.DOMAIN: { - ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: 'wrong' + CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong' }})) # Test with correct config self.assertTrue(sun.setup( self.hass, {ha.DOMAIN: { - ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: '117.22743' + CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743' }})) diff --git a/ha_test/test_component_switch.py b/ha_test/test_component_switch.py index 0df05f0617b..daab9cde4d1 100644 --- a/ha_test/test_component_switch.py +++ b/ha_test/test_component_switch.py @@ -9,10 +9,10 @@ import unittest import homeassistant as ha import homeassistant.loader as loader -from homeassistant.components import get_component, STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM import homeassistant.components.switch as switch -from helper import get_test_home_assistant +from helpers import get_test_home_assistant class TestSwitch(unittest.TestCase): @@ -22,11 +22,11 @@ class TestSwitch(unittest.TestCase): self.hass = get_test_home_assistant() loader.prepare(self.hass) - platform = get_component('switch.test') + platform = loader.get_component('switch.test') platform.init() self.assertTrue(switch.setup( - self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}} + self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}} )) # Switch 1 is ON, switch 2 is OFF @@ -90,15 +90,27 @@ class TestSwitch(unittest.TestCase): # Test with non-existing component self.assertFalse(switch.setup( - self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}} + self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}} )) # Test if switch component returns 0 switches - get_component('switch.test').init(True) + test_platform = loader.get_component('switch.test') + test_platform.init(True) self.assertEqual( - [], get_component('switch.test').get_switches(None, None)) + [], test_platform.get_switches(None, None)) self.assertFalse(switch.setup( - self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}} + self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}} + )) + + # Test if we can load 2 platforms + loader.set_component('switch.test2', test_platform) + test_platform.init(False) + + self.assertTrue(switch.setup( + self.hass, { + switch.DOMAIN: {CONF_PLATFORM: 'test'}, + '{} 2'.format(switch.DOMAIN): {CONF_PLATFORM: 'test2'}, + } )) diff --git a/ha_test/test_helpers.py b/ha_test/test_helpers.py new file mode 100644 index 00000000000..f61204c837f --- /dev/null +++ b/ha_test/test_helpers.py @@ -0,0 +1,49 @@ +""" +ha_test.test_helpers +~~~~~~~~~~~~~~~~~~~~ + +Tests component helpers. +""" +# pylint: disable=protected-access,too-many-public-methods +import unittest + +from helpers import get_test_home_assistant + +import homeassistant as ha +import homeassistant.loader as loader +from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID +from homeassistant.helpers import extract_entity_ids + + +class TestComponentsCore(unittest.TestCase): + """ Tests homeassistant.components module. """ + + def setUp(self): # pylint: disable=invalid-name + """ Init needed objects. """ + self.hass = get_test_home_assistant() + loader.prepare(self.hass) + + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + self.hass.states.set('light.Kitchen', STATE_OFF) + + loader.get_component('group').setup_group( + self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_extract_entity_ids(self): + """ Test extract_entity_ids method. """ + call = ha.ServiceCall('light', 'turn_on', + {ATTR_ENTITY_ID: 'light.Bowl'}) + + self.assertEqual(['light.Bowl'], + extract_entity_ids(self.hass, call)) + + call = ha.ServiceCall('light', 'turn_on', + {ATTR_ENTITY_ID: 'group.test'}) + + self.assertEqual(['light.Ceiling', 'light.Kitchen'], + extract_entity_ids(self.hass, call)) diff --git a/ha_test/test_loader.py b/ha_test/test_loader.py index 4d9bc190145..b7ae75c0e2a 100644 --- a/ha_test/test_loader.py +++ b/ha_test/test_loader.py @@ -10,7 +10,7 @@ import unittest import homeassistant.loader as loader import homeassistant.components.http as http -from helper import get_test_home_assistant, MockModule +from helpers import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 6fbe1a2c3e3..74f27b9f6f5 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -15,32 +15,14 @@ import re import datetime as dt import functools as ft +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, + EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL) import homeassistant.util as util -MATCH_ALL = '*' - DOMAIN = "homeassistant" -SERVICE_HOMEASSISTANT_STOP = "stop" - -EVENT_HOMEASSISTANT_START = "homeassistant_start" -EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" -EVENT_STATE_CHANGED = "state_changed" -EVENT_TIME_CHANGED = "time_changed" -EVENT_CALL_SERVICE = "services.call" - -ATTR_NOW = "now" -ATTR_DOMAIN = "domain" -ATTR_SERVICE = "service" - -CONF_LATITUDE = "latitude" -CONF_LONGITUDE = "longitude" -CONF_TYPE = "type" -CONF_HOST = "host" -CONF_HOSTS = "hosts" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" - # How often time_changed event should fire TIMER_INTERVAL = 10 # seconds diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3875c23b4ce..7675d3f12f3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -37,8 +37,9 @@ def from_config_dict(config, hass=None): # Convert it to defaultdict so components can always have config dict config = defaultdict(dict, config) - # Filter out the common config section [homeassistant] - components = (key for key in config.keys() if key != homeassistant.DOMAIN) + # Filter out the repeating and common config section [homeassistant] + components = (key for key in config.keys() + if ' ' not in key and key != homeassistant.DOMAIN) # Setup the components if core_components.setup(hass, config): diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 3acaf11a4fd..0680b5c67af 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -19,36 +19,10 @@ import logging import homeassistant as ha import homeassistant.util as util +from homeassistant.helpers import extract_entity_ids from homeassistant.loader import get_component - -# Contains one string or a list of strings, each being an entity id -ATTR_ENTITY_ID = 'entity_id' - -# String with a friendly name for the entity -ATTR_FRIENDLY_NAME = "friendly_name" - -# A picture to represent entity -ATTR_ENTITY_PICTURE = "entity_picture" - -# The unit of measurement if applicable -ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" - -STATE_ON = 'on' -STATE_OFF = 'off' -STATE_HOME = 'home' -STATE_NOT_HOME = 'not_home' - -SERVICE_TURN_ON = 'turn_on' -SERVICE_TURN_OFF = 'turn_off' - -SERVICE_VOLUME_UP = "volume_up" -SERVICE_VOLUME_DOWN = "volume_down" -SERVICE_VOLUME_MUTE = "volume_mute" -SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" -SERVICE_MEDIA_PLAY = "media_play" -SERVICE_MEDIA_PAUSE = "media_pause" -SERVICE_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_MEDIA_PREV_TRACK = "media_prev_track" +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) _LOGGER = logging.getLogger(__name__) @@ -96,79 +70,6 @@ def turn_off(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) -def extract_entity_ids(hass, service): - """ - Helper method to extract a list of entity ids from a service call. - Will convert group entity ids to the entity ids it represents. - """ - entity_ids = [] - - if service.data and ATTR_ENTITY_ID in service.data: - group = get_component('group') - - # Entity ID attr can be a list or a string - service_ent_id = service.data[ATTR_ENTITY_ID] - if isinstance(service_ent_id, list): - ent_ids = service_ent_id - else: - ent_ids = [service_ent_id] - - entity_ids.extend( - ent_id for ent_id - in group.expand_entity_ids(hass, ent_ids) - if ent_id not in entity_ids) - - return entity_ids - - -class ToggleDevice(object): - """ ABC for devices that can be turned on and off. """ - # pylint: disable=no-self-use - - entity_id = None - - def get_name(self): - """ Returns the name of the device if any. """ - return None - - def turn_on(self, **kwargs): - """ Turn the device on. """ - pass - - def turn_off(self, **kwargs): - """ Turn the device off. """ - pass - - def is_on(self): - """ True if device is on. """ - return False - - def get_state_attributes(self): - """ Returns optional state attributes. """ - return {} - - def update(self): - """ Retrieve latest state from the real device. """ - pass - - def update_ha_state(self, hass, force_refresh=False): - """ - Updates Home Assistant with current state of device. - If force_refresh == True will update device before setting state. - """ - if self.entity_id is None: - raise ha.NoEntitySpecifiedError( - "No entity specified for device {}".format(self.get_name())) - - if force_refresh: - self.update() - - state = STATE_ON if self.is_on() else STATE_OFF - - return hass.states.set(self.entity_id, state, - self.get_state_attributes()) - - # pylint: disable=unused-argument def setup(hass, config): """ Setup general services related to homeassistant. """ diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index d6e9971dc0d..fc5f7e73dc3 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -6,9 +6,14 @@ Provides functionality to interact with Chromecasts. """ import logging -import homeassistant as ha import homeassistant.util as util -import homeassistant.components as components +from homeassistant.helpers import extract_entity_ids +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, + SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, + CONF_HOSTS) + DOMAIN = 'chromecast' DEPENDENCIES = [] @@ -46,58 +51,58 @@ def is_on(hass, entity_id=None): def turn_off(hass, entity_id=None): """ Will turn off specified Chromecast or all. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_TURN_OFF, data) + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) def volume_up(hass, entity_id=None): """ Send the chromecast the command for volume up. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_VOLUME_UP, data) + hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) def volume_down(hass, entity_id=None): """ Send the chromecast the command for volume down. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_VOLUME_DOWN, data) + hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) def media_play_pause(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data) + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) def media_play(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY, data) + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) def media_pause(hass, entity_id=None): """ Send the chromecast the command for play/pause. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PAUSE, data) + hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) def media_next_track(hass, entity_id=None): """ Send the chromecast the command for next track. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data) + hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) def media_prev_track(hass, entity_id=None): """ Send the chromecast the command for prev track. """ - data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {} + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data) + hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) # pylint: disable=too-many-locals, too-many-branches @@ -114,8 +119,8 @@ def setup(hass, config): return False - if ha.CONF_HOSTS in config[DOMAIN]: - hosts = config[DOMAIN][ha.CONF_HOSTS].split(",") + if CONF_HOSTS in config[DOMAIN]: + hosts = config[DOMAIN][CONF_HOSTS].split(",") # If no hosts given, scan for chromecasts else: @@ -131,7 +136,7 @@ def setup(hass, config): entity_id = util.ensure_unique_string( ENTITY_ID_FORMAT.format( util.slugify(cast.device.friendly_name)), - list(casts.keys())) + casts.keys()) casts[entity_id] = cast @@ -148,7 +153,7 @@ def setup(hass, config): status = chromecast.app - state_attr = {components.ATTR_FRIENDLY_NAME: + state_attr = {ATTR_FRIENDLY_NAME: chromecast.device.friendly_name} if status and status.app_id != pychromecast.APP_ID['HOME']: @@ -196,7 +201,7 @@ def setup(hass, config): def _service_to_entities(service): """ Helper method to get entities from service. """ - entity_ids = components.extract_entity_ids(hass, service) + entity_ids = extract_entity_ids(hass, service) if entity_ids: for entity_id in entity_ids: @@ -274,25 +279,25 @@ def setup(hass, config): hass.track_time_change(update_chromecast_states) - hass.services.register(DOMAIN, components.SERVICE_TURN_OFF, + hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service) - hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, + hass.services.register(DOMAIN, SERVICE_VOLUME_UP, volume_up_service) - hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, + hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN, volume_down_service) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, + hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, media_play_pause_service) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY, + hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY, media_play_service) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_PAUSE, + hass.services.register(DOMAIN, SERVICE_MEDIA_PAUSE, media_pause_service) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, + hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, media_next_track_service) hass.services.register(DOMAIN, "start_fireplace", diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index ca04d90e7fc..3bd7d2f1e33 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -8,9 +8,10 @@ import random import homeassistant as ha import homeassistant.loader as loader -from homeassistant.components import ( +from homeassistant.helpers import extract_entity_ids +from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, - ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, extract_entity_ids) + ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE) from homeassistant.components.light import ( ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS) from homeassistant.util import split_entity_id @@ -65,11 +66,11 @@ def setup(hass, config): hass.states.set(entity_id, STATE_OFF) # Setup sun - if ha.CONF_LATITUDE not in config[ha.DOMAIN]: - config[ha.DOMAIN][ha.CONF_LATITUDE] = '32.87336' + if CONF_LATITUDE not in config[ha.DOMAIN]: + config[ha.DOMAIN][CONF_LATITUDE] = '32.87336' - if ha.CONF_LONGITUDE not in config[ha.DOMAIN]: - config[ha.DOMAIN][ha.CONF_LONGITUDE] = '-117.22743' + if CONF_LONGITUDE not in config[ha.DOMAIN]: + config[ha.DOMAIN][CONF_LONGITUDE] = '-117.22743' loader.get_component('sun').setup(hass, config) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 5e2b99e64a7..5136340832f 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -8,7 +8,7 @@ the state of the sun and devices. import logging from datetime import datetime, timedelta -import homeassistant.components as components +from homeassistant.const import STATE_HOME, STATE_NOT_HOME from . import light, sun, device_tracker, group DOMAIN = "device_sun_light_trigger" @@ -108,7 +108,7 @@ def setup(hass, config): # Specific device came home ? if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \ - new_state.state == components.STATE_HOME: + new_state.state == STATE_HOME: # These variables are needed for the elif check now = datetime.now() @@ -143,7 +143,7 @@ def setup(hass, config): # Did all devices leave the house? elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and - new_state.state == components.STATE_NOT_HOME and lights_are_on + new_state.state == STATE_NOT_HOME and lights_are_on and not disable_turn_off): logger.info( @@ -154,12 +154,12 @@ def setup(hass, config): # Track home coming of each device hass.states.track_change( device_entity_ids, check_light_on_dev_state_change, - components.STATE_NOT_HOME, components.STATE_HOME) + STATE_NOT_HOME, STATE_HOME) # Track when all devices are gone to shut down lights hass.states.track_change( device_tracker.ENTITY_ID_ALL_DEVICES, check_light_on_dev_state_change, - components.STATE_HOME, components.STATE_NOT_HOME) + STATE_HOME, STATE_NOT_HOME) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 08f1abd9ff1..fa96a5b3c28 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -10,12 +10,14 @@ import os import csv from datetime import datetime, timedelta -import homeassistant as ha from homeassistant.loader import get_component +from homeassistant.helpers import validate_config import homeassistant.util as util -from homeassistant.components import ( - group, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME) +from homeassistant.const import ( + STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, + CONF_PLATFORM, CONF_TYPE) +from homeassistant.components import group DOMAIN = "device_tracker" DEPENDENCIES = [] @@ -49,10 +51,20 @@ def is_on(hass, entity_id=None): def setup(hass, config): """ Sets up the device tracker. """ - if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER): + # CONF_TYPE is deprecated for CONF_PLATOFRM. We keep supporting it for now. + if not (validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER) + or validate_config(config, {DOMAIN: [CONF_TYPE]}, _LOGGER)): + return False - tracker_type = config[DOMAIN][ha.CONF_TYPE] + tracker_type = config[DOMAIN].get(CONF_PLATFORM) + + if tracker_type is None: + tracker_type = config[DOMAIN][CONF_TYPE] + + _LOGGER.warning(( + "Please update your config for %s to use 'platform' " + "instead of 'type'"), tracker_type) tracker_implementation = get_component( 'device_tracker.{}'.format(tracker_type)) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index dba41c11c03..9ed73f21375 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -6,8 +6,9 @@ import re import threading import requests -import homeassistant as ha -import homeassistant.util as util +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago @@ -19,10 +20,9 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Luci scanner. """ - if not util.validate_config(config, - {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, - ha.CONF_PASSWORD]}, - _LOGGER): + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): return None scanner = LuciDeviceScanner(config[DOMAIN]) @@ -45,8 +45,8 @@ class LuciDeviceScanner(object): """ def __init__(self, config): - host = config[ha.CONF_HOST] - username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] + host = config[CONF_HOST] + username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") @@ -87,7 +87,7 @@ class LuciDeviceScanner(object): return return self.mac2name.get(device, None) - @util.Throttle(MIN_TIME_BETWEEN_SCANS) + @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Ensures the information from the Luci router is up to date. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index a57e2f28c70..0b0f1107b21 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -3,8 +3,9 @@ import logging from datetime import timedelta import threading -import homeassistant as ha -import homeassistant.util as util +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago @@ -16,10 +17,9 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Netgear scanner. """ - if not util.validate_config(config, - {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, - ha.CONF_PASSWORD]}, - _LOGGER): + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): return None scanner = NetgearDeviceScanner(config[DOMAIN]) @@ -31,8 +31,8 @@ class NetgearDeviceScanner(object): """ This class queries a Netgear wireless router using the SOAP-api. """ def __init__(self, config): - host = config[ha.CONF_HOST] - username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] + host = config[CONF_HOST] + username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.last_results = [] @@ -82,7 +82,7 @@ class NetgearDeviceScanner(object): else: return None - @util.Throttle(MIN_TIME_BETWEEN_SCANS) + @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Retrieves latest information from the Netgear router. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index c3b7e5dec94..81755f42c66 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -7,8 +7,9 @@ import threading import requests -import homeassistant as ha -import homeassistant.util as util +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago @@ -22,10 +23,10 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Tomato scanner. """ - if not util.validate_config(config, - {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, - ha.CONF_PASSWORD, CONF_HTTP_ID]}, - _LOGGER): + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, + CONF_PASSWORD, CONF_HTTP_ID]}, + _LOGGER): return None return TomatoDeviceScanner(config[DOMAIN]) @@ -40,8 +41,8 @@ class TomatoDeviceScanner(object): """ def __init__(self, config): - host, http_id = config[ha.CONF_HOST], config[CONF_HTTP_ID] - username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] + host, http_id = config[CONF_HOST], config[CONF_HTTP_ID] + username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.req = requests.Request('POST', 'http://{}/update.cgi'.format(host), @@ -78,7 +79,7 @@ class TomatoDeviceScanner(object): else: return filter_named[0] - @util.Throttle(MIN_TIME_BETWEEN_SCANS) + @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_tomato_info(self): """ Ensures the information from the Tomato router is up to date. Returns boolean if scanning successful. """ diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 362f7d43e04..6978dbd7fa9 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -9,7 +9,8 @@ import logging import re import threading -import homeassistant.util as util +from homeassistant.helpers import validate_config +from homeassistant.util import sanitize_filename DOMAIN = "downloader" DEPENDENCIES = [] @@ -36,7 +37,7 @@ def setup(hass, config): return False - if not util.validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger): + if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger): return False download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] @@ -64,7 +65,7 @@ def setup(hass, config): subdir = service.data.get(ATTR_SUBDIR) if subdir: - subdir = util.sanitize_filename(subdir) + subdir = sanitize_filename(subdir) final_path = None @@ -88,7 +89,7 @@ def setup(hass, config): filename = "ha_download" # Remove stuff to ruin paths - filename = util.sanitize_filename(filename) + filename = sanitize_filename(filename) # Do we want to download to subdir, create if needed if subdir: diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index f0ce5cc0f94..59033b02b68 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -9,9 +9,8 @@ import logging import homeassistant as ha import homeassistant.util as util -from homeassistant.components import (STATE_ON, STATE_OFF, - STATE_HOME, STATE_NOT_HOME, - ATTR_ENTITY_ID) +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME) DOMAIN = "group" DEPENDENCIES = [] diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 02a1bea0272..0149130c0fc 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -83,6 +83,10 @@ from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs import homeassistant as ha +from homeassistant.const import ( + SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, + URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) +from homeassistant.helpers import validate_config import homeassistant.remote as rem import homeassistant.util as util from . import frontend @@ -116,8 +120,7 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Sets up the HTTP API and debug interface. """ - if not util.validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, - _LOGGER): + if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER): return False api_password = config[DOMAIN][CONF_API_PASSWORD] @@ -125,7 +128,7 @@ def setup(hass, config): # If no server host is given, accept all incoming requests server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') - server_port = config[DOMAIN].get(CONF_SERVER_PORT, rem.SERVER_PORT) + server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT) development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1" @@ -196,10 +199,10 @@ class RequestHandler(SimpleHTTPRequestHandler): ('GET', URL_ROOT, '_handle_get_root'), # /api - for validation purposes - ('GET', rem.URL_API, '_handle_get_api'), + ('GET', URL_API, '_handle_get_api'), # /states - ('GET', rem.URL_API_STATES, '_handle_get_api_states'), + ('GET', URL_API_STATES, '_handle_get_api_states'), ('GET', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), '_handle_get_api_states_entity'), @@ -211,13 +214,13 @@ class RequestHandler(SimpleHTTPRequestHandler): '_handle_post_state_entity'), # /events - ('GET', rem.URL_API_EVENTS, '_handle_get_api_events'), + ('GET', URL_API_EVENTS, '_handle_get_api_events'), ('POST', re.compile(r'/api/events/(?P[a-zA-Z\._0-9]+)'), '_handle_api_post_events_event'), # /services - ('GET', rem.URL_API_SERVICES, '_handle_get_api_services'), + ('GET', URL_API_SERVICES, '_handle_get_api_services'), ('POST', re.compile((r'/api/services/' r'(?P[a-zA-Z\._0-9]+)/' @@ -225,8 +228,8 @@ class RequestHandler(SimpleHTTPRequestHandler): '_handle_post_api_services_domain_service'), # /event_forwarding - ('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'), - ('DELETE', rem.URL_API_EVENT_FORWARD, + ('POST', URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'), + ('DELETE', URL_API_EVENT_FORWARD, '_handle_delete_api_event_forward'), # Static files @@ -270,7 +273,7 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - api_password = self.headers.get(rem.AUTH_HEADER) + api_password = self.headers.get(AUTH_HEADER) if not api_password and DATA_API_PASSWORD in data: api_password = data[DATA_API_PASSWORD] @@ -427,7 +430,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self._write_json( state.as_dict(), status_code=status_code, - location=rem.URL_API_STATES_ENTITY.format(entity_id)) + location=URL_API_STATES_ENTITY.format(entity_id)) def _handle_get_api_events(self, path_match, data): """ Handles getting overview of event listeners. """ diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index e2d156d5c7e..2d8c3995a2f 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -1,12 +1,16 @@ """ -homeassistant.components.keyboard +homeassistant.keyboard ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to emulate keyboard presses on host machine. """ import logging -import homeassistant.components as components +from homeassistant.const import ( + SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, + SERVICE_MEDIA_PLAY_PAUSE) + DOMAIN = "keyboard" DEPENDENCIES = [] @@ -14,32 +18,32 @@ DEPENDENCIES = [] def volume_up(hass): """ Press the keyboard button for volume up. """ - hass.services.call(DOMAIN, components.SERVICE_VOLUME_UP) + hass.services.call(DOMAIN, SERVICE_VOLUME_UP) def volume_down(hass): """ Press the keyboard button for volume down. """ - hass.services.call(DOMAIN, components.SERVICE_VOLUME_DOWN) + hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN) def volume_mute(hass): """ Press the keyboard button for muting volume. """ - hass.services.call(DOMAIN, components.SERVICE_VOLUME_MUTE) + hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE) def media_play_pause(hass): """ Press the keyboard button for play/pause. """ - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE) def media_next_track(hass): """ Press the keyboard button for next track. """ - hass.services.call(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) + hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK) def media_prev_track(hass): """ Press the keyboard button for prev track. """ - hass.services.call(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) + hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK) # pylint: disable=unused-argument @@ -56,27 +60,27 @@ def setup(hass, config): keyboard = pykeyboard.PyKeyboard() keyboard.special_key_assignment() - hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, + hass.services.register(DOMAIN, SERVICE_VOLUME_UP, lambda service: keyboard.tap_key(keyboard.volume_up_key)) - hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, + hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN, lambda service: keyboard.tap_key(keyboard.volume_down_key)) - hass.services.register(DOMAIN, components.SERVICE_VOLUME_MUTE, + hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, lambda service: keyboard.tap_key(keyboard.volume_mute_key)) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, + hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, lambda service: keyboard.tap_key(keyboard.media_play_pause_key)) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, + hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, lambda service: keyboard.tap_key(keyboard.media_next_track_key)) - hass.services.register(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, + hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK, lambda service: keyboard.tap_key(keyboard.media_prev_track_key)) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 957a318ce3c..55bbdb307f2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -52,12 +52,12 @@ import logging import os import csv -import homeassistant as ha -from homeassistant.loader import get_component import homeassistant.util as util -from homeassistant.components import ( - group, extract_entity_ids, STATE_ON, - SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.const import ( + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.helpers import ( + extract_entity_ids, platform_devices_from_config) +from homeassistant.components import group DOMAIN = "light" @@ -138,9 +138,6 @@ def turn_off(hass, entity_id=None, transition=None): def setup(hass, config): """ Exposes light control via statemachine and services. """ - if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER): - return False - # Load built-in profiles and custom profiles profile_paths = [os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), @@ -169,20 +166,9 @@ def setup(hass, config): return False - # Load platform - light_type = config[DOMAIN][ha.CONF_TYPE] + lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER) - light_init = get_component('light.{}'.format(light_type)) - - if light_init is None: - _LOGGER.error("Unknown light type specified: %s", light_type) - - return False - - lights = light_init.get_lights(hass, config[DOMAIN]) - - if len(lights) == 0: - _LOGGER.error("No lights found") + if not lights: return False ent_to_light = {} @@ -198,7 +184,7 @@ def setup(hass, config): entity_id = util.ensure_unique_string( ENTITY_ID_FORMAT.format(util.slugify(name)), - list(ent_to_light.keys())) + ent_to_light.keys()) light.entity_id = entity_id ent_to_light[entity_id] = light diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index c7db591a4f7..19f4034eb39 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -3,9 +3,9 @@ import logging import socket from datetime import timedelta -import homeassistant as ha import homeassistant.util as util -from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME +from homeassistant.helpers import ToggleDevice +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION) @@ -15,7 +15,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) PHUE_CONFIG_FILE = "phue.conf" -def get_lights(hass, config): +def get_devices(hass, config): """ Gets the Hue lights. """ logger = logging.getLogger(__name__) try: @@ -25,7 +25,7 @@ def get_lights(hass, config): return [] - host = config.get(ha.CONF_HOST, None) + host = config.get(CONF_HOST, None) try: bridge = phue.Bridge( diff --git a/homeassistant/components/process.py b/homeassistant/components/process.py index 1c0e6b62373..5d18557f998 100644 --- a/homeassistant/components/process.py +++ b/homeassistant/components/process.py @@ -10,7 +10,7 @@ Author: Markus Stenberg import os -from homeassistant.components import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.util as util DOMAIN = 'process' diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index c6acbc05230..c6bacb45f87 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -8,7 +8,9 @@ import logging from datetime import datetime, timedelta import homeassistant as ha -import homeassistant.util as util +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers import validate_config +from homeassistant.util import str_to_datetime, datetime_to_str DEPENDENCIES = [] DOMAIN = "sun" @@ -35,7 +37,7 @@ def next_setting(hass, entity_id=None): state = hass.states.get(ENTITY_ID) try: - return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING]) + return str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING]) except (AttributeError, KeyError): # AttributeError if state is None # KeyError if STATE_ATTR_NEXT_SETTING does not exist @@ -49,7 +51,7 @@ def next_rising(hass, entity_id=None): state = hass.states.get(ENTITY_ID) try: - return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) + return str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) except (AttributeError, KeyError): # AttributeError if state is None # KeyError if STATE_ATTR_NEXT_RISING does not exist @@ -60,10 +62,9 @@ def setup(hass, config): """ Tracks the state of the sun. """ logger = logging.getLogger(__name__) - if not util.validate_config(config, - {ha.DOMAIN: [ha.CONF_LATITUDE, - ha.CONF_LONGITUDE]}, - logger): + if not validate_config(config, + {ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]}, + logger): return False try: @@ -74,8 +75,8 @@ def setup(hass, config): sun = ephem.Sun() # pylint: disable=no-member - latitude = config[ha.DOMAIN][ha.CONF_LATITUDE] - longitude = config[ha.DOMAIN][ha.CONF_LONGITUDE] + latitude = config[ha.DOMAIN][CONF_LATITUDE] + longitude = config[ha.DOMAIN][CONF_LONGITUDE] # Validate latitude and longitude observer = ephem.Observer() @@ -123,8 +124,8 @@ def setup(hass, config): new_state, next_change.strftime("%H:%M")) state_attributes = { - STATE_ATTR_NEXT_RISING: util.datetime_to_str(next_rising_dt), - STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt) + STATE_ATTR_NEXT_RISING: datetime_to_str(next_rising_dt), + STATE_ATTR_NEXT_SETTING: datetime_to_str(next_setting_dt) } hass.states.set(ENTITY_ID, new_state, state_attributes) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 98da108484e..7f7a70e1842 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -6,12 +6,12 @@ Component to interface with various switches that can be controlled remotely. import logging from datetime import timedelta -import homeassistant as ha import homeassistant.util as util -from homeassistant.loader import get_component -from homeassistant.components import ( - group, extract_entity_ids, STATE_ON, - SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.const import ( + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.helpers import ( + extract_entity_ids, platform_devices_from_config) +from homeassistant.components import group DOMAIN = 'switch' DEPENDENCIES = [] @@ -53,27 +53,13 @@ def turn_off(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) -# pylint: disable=too-many-branches def setup(hass, config): """ Track states and offer events for switches. """ logger = logging.getLogger(__name__) - if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger): - return False + switches = platform_devices_from_config(config, DOMAIN, hass, logger) - switch_type = config[DOMAIN][ha.CONF_TYPE] - - switch_init = get_component('switch.{}'.format(switch_type)) - - if switch_init is None: - logger.error("Error loading switch component %s", switch_type) - - return False - - switches = switch_init.get_switches(hass, config[DOMAIN]) - - if len(switches) == 0: - logger.error("No switches found") + if not switches: return False # Setup a dict mapping entity IDs to devices @@ -90,7 +76,7 @@ def setup(hass, config): entity_id = util.ensure_unique_string( ENTITY_ID_FORMAT.format(util.slugify(name)), - list(ent_to_switch.keys())) + ent_to_switch.keys()) switch.entity_id = entity_id ent_to_switch[entity_id] = switch diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index b34aea38e67..54b85b3ecb4 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -1,7 +1,8 @@ """ Support for Tellstick switches. """ import logging -from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME +from homeassistant.helpers import ToggleDevice +from homeassistant.const import ATTR_FRIENDLY_NAME try: import tellcore.constants as tc_constants @@ -11,7 +12,7 @@ except ImportError: # pylint: disable=unused-argument -def get_switches(hass, config): +def get_devices(hass, config): """ Find and return Tellstick switches. """ try: import tellcore.telldus as telldus diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 5eb3d7c14f0..b6392ec0f0c 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -1,12 +1,12 @@ """ Support for WeMo switchces. """ import logging -import homeassistant as ha -from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME +from homeassistant.helpers import ToggleDevice +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOSTS # pylint: disable=unused-argument -def get_switches(hass, config): +def get_devices(hass, config): """ Find and return WeMo switches. """ try: @@ -21,9 +21,9 @@ def get_switches(hass, config): return [] - if ha.CONF_HOSTS in config: + if CONF_HOSTS in config: switches = (pywemo.device_from_host(host) for host - in config[ha.CONF_HOSTS].split(",")) + in config[CONF_HOSTS].split(",")) else: logging.getLogger(__name__).info("Scanning for WeMo devices") diff --git a/homeassistant/components/tellstick_sensor.py b/homeassistant/components/tellstick_sensor.py index 89a32f1218b..e021632095b 100644 --- a/homeassistant/components/tellstick_sensor.py +++ b/homeassistant/components/tellstick_sensor.py @@ -26,8 +26,7 @@ import logging from collections import namedtuple import homeassistant.util as util -from homeassistant.components import (ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT) +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT # The domain of your component. Should be equal to the name of your component DOMAIN = "tellstick_sensor" diff --git a/homeassistant/const.py b/homeassistant/const.py new file mode 100644 index 00000000000..8930ae5649c --- /dev/null +++ b/homeassistant/const.py @@ -0,0 +1,78 @@ +""" Constants used by Home Assistant components. """ +# Can be used to specify a catch all when registering state or event listeners. +MATCH_ALL = '*' + +# #### CONFIG #### +CONF_LATITUDE = "latitude" +CONF_LONGITUDE = "longitude" + +# This one is deprecated. Use platform instead. +CONF_TYPE = "type" + +CONF_PLATFORM = "platform" +CONF_HOST = "host" +CONF_HOSTS = "hosts" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" + +# #### EVENTS #### +EVENT_HOMEASSISTANT_START = "homeassistant_start" +EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" +EVENT_STATE_CHANGED = "state_changed" +EVENT_TIME_CHANGED = "time_changed" +EVENT_CALL_SERVICE = "services.call" + +# #### STATES #### +STATE_ON = 'on' +STATE_OFF = 'off' +STATE_HOME = 'home' +STATE_NOT_HOME = 'not_home' + +# #### STATE ATTRIBUTES #### +# Contains current time for a TIME_CHANGED event +ATTR_NOW = "now" + +# Contains domain, service for a SERVICE_CALL event +ATTR_DOMAIN = "domain" +ATTR_SERVICE = "service" + +# Contains one string or a list of strings, each being an entity id +ATTR_ENTITY_ID = 'entity_id' + +# String with a friendly name for the entity +ATTR_FRIENDLY_NAME = "friendly_name" + +# A picture to represent entity +ATTR_ENTITY_PICTURE = "entity_picture" + +# The unit of measurement if applicable +ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" + +# #### SERVICES #### +SERVICE_HOMEASSISTANT_STOP = "stop" + +SERVICE_TURN_ON = 'turn_on' +SERVICE_TURN_OFF = 'turn_off' + +SERVICE_VOLUME_UP = "volume_up" +SERVICE_VOLUME_DOWN = "volume_down" +SERVICE_VOLUME_MUTE = "volume_mute" +SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" +SERVICE_MEDIA_PLAY = "media_play" +SERVICE_MEDIA_PAUSE = "media_pause" +SERVICE_MEDIA_NEXT_TRACK = "media_next_track" +SERVICE_MEDIA_PREV_TRACK = "media_prev_track" + +# #### API / REMOTE #### +SERVER_PORT = 8123 + +AUTH_HEADER = "HA-access" + +URL_API = "/api/" +URL_API_STATES = "/api/states" +URL_API_STATES_ENTITY = "/api/states/{}" +URL_API_EVENTS = "/api/events" +URL_API_EVENTS_EVENT = "/api/events/{}" +URL_API_SERVICES = "/api/services" +URL_API_SERVICES_SERVICE = "/api/services/{}/{}" +URL_API_EVENT_FORWARD = "/api/event_forwarding" diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py new file mode 100644 index 00000000000..f673cc5345f --- /dev/null +++ b/homeassistant/helpers.py @@ -0,0 +1,176 @@ +""" +Helper methods for components within Home Assistant. +""" +from homeassistant import NoEntitySpecifiedError + +from homeassistant.loader import get_component +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE) + + +def extract_entity_ids(hass, service): + """ + Helper method to extract a list of entity ids from a service call. + Will convert group entity ids to the entity ids it represents. + """ + entity_ids = [] + + if service.data and ATTR_ENTITY_ID in service.data: + group = get_component('group') + + # Entity ID attr can be a list or a string + service_ent_id = service.data[ATTR_ENTITY_ID] + if isinstance(service_ent_id, list): + ent_ids = service_ent_id + else: + ent_ids = [service_ent_id] + + entity_ids.extend( + ent_id for ent_id + in group.expand_entity_ids(hass, ent_ids) + if ent_id not in entity_ids) + + return entity_ids + + +def validate_config(config, items, logger): + """ + Validates if all items are available in the configuration. + + config is the general dictionary with all the configurations. + items is a dict with per domain which attributes we require. + logger is the logger from the caller to log the errors to. + + Returns True if all required items were found. + """ + errors_found = False + for domain in items.keys(): + config.setdefault(domain, {}) + + errors = [item for item in items[domain] if item not in config[domain]] + + if errors: + logger.error( + "Missing required configuration items in {}: {}".format( + domain, ", ".join(errors))) + + errors_found = True + + return not errors_found + + +def config_per_platform(config, domain, logger): + """ + Generator to break a component config into different platforms. + For example, will find 'switch', 'switch 2', 'switch 3', .. etc + """ + config_key = domain + found = 1 + + while config_key in config: + platform_config = config[config_key] + + platform_type = platform_config.get(CONF_PLATFORM) + + # DEPRECATED, still supported for now. + if platform_type is None: + platform_type = platform_config.get(CONF_TYPE) + + if platform_type is not None: + logger.warning(( + 'Please update your config for {}.{} to use "platform" ' + 'instead of "type"').format(domain, platform_type)) + + if platform_type is None: + logger.warning('No platform specified for %s', config_key) + break + + yield platform_type, platform_config + + found += 1 + config_key = "{} {}".format(domain, found) + + +def platform_devices_from_config(config, domain, hass, logger): + """ Parses the config for specified domain. + Loads different platforms and retrieve domains. """ + devices = [] + + for p_type, p_config in config_per_platform(config, domain, logger): + platform = get_component('{}.{}'.format(domain, p_type)) + + if platform is None: + logger.error("Unknown %s type specified: %s", domain, p_type) + + else: + try: + p_devices = platform.get_devices(hass, p_config) + except AttributeError: + # DEPRECATED, still supported for now + logger.warning( + 'Platform %s should migrate to use the method get_devices', + p_type) + + if domain == 'light': + p_devices = platform.get_lights(hass, p_config) + elif domain == 'switch': + p_devices = platform.get_switches(hass, p_config) + else: + raise + + logger.info("Found %d %s %ss", len(p_devices), p_type, domain) + + devices.extend(p_devices) + + if len(devices) == 0: + logger.error("No devices found for %s", domain) + + return devices + + +class ToggleDevice(object): + """ ABC for devices that can be turned on and off. """ + # pylint: disable=no-self-use + + entity_id = None + + def get_name(self): + """ Returns the name of the device if any. """ + return None + + def turn_on(self, **kwargs): + """ Turn the device on. """ + pass + + def turn_off(self, **kwargs): + """ Turn the device off. """ + pass + + def is_on(self): + """ True if device is on. """ + return False + + def get_state_attributes(self): + """ Returns optional state attributes. """ + return {} + + def update(self): + """ Retrieve latest state from the real device. """ + pass + + def update_ha_state(self, hass, force_refresh=False): + """ + Updates Home Assistant with current state of device. + If force_refresh == True will update device before setting state. + """ + if self.entity_id is None: + raise NoEntitySpecifiedError( + "No entity specified for device {}".format(self.get_name())) + + if force_refresh: + self.update() + + state = STATE_ON if self.is_on() else STATE_OFF + + return hass.states.set(self.entity_id, state, + self.get_state_attributes()) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index bcfe6e68e65..869c690be6b 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -19,18 +19,10 @@ import requests import homeassistant as ha -SERVER_PORT = 8123 - -AUTH_HEADER = "HA-access" - -URL_API = "/api/" -URL_API_STATES = "/api/states" -URL_API_STATES_ENTITY = "/api/states/{}" -URL_API_EVENTS = "/api/events" -URL_API_EVENTS_EVENT = "/api/events/{}" -URL_API_SERVICES = "/api/services" -URL_API_SERVICES_SERVICE = "/api/services/{}/{}" -URL_API_EVENT_FORWARD = "/api/event_forwarding" +from homeassistant.const import ( + SERVER_PORT, AUTH_HEADER, URL_API, URL_API_STATES, URL_API_STATES_ENTITY, + URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, + URL_API_SERVICES_SERVICE, URL_API_EVENT_FORWARD) METHOD_GET = "get" METHOD_POST = "post" diff --git a/homeassistant/util.py b/homeassistant/util.py index d6bb9423a42..4598a230c65 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -127,6 +127,7 @@ def ensure_unique_string(preferred_string, current_strings): """ Returns a string that is not present in current_strings. If preferred string exists will append _2, _3, .. """ string = preferred_string + current_strings = list(current_strings) tries = 1 @@ -248,32 +249,6 @@ class OrderedSet(collections.MutableSet): return set(self) == set(other) -def validate_config(config, items, logger): - """ - Validates if all items are available in the configuration. - - config is the general dictionary with all the configurations. - items is a dict with per domain which attributes we require. - logger is the logger from the caller to log the errors to. - - Returns True if all required items were found. - """ - errors_found = False - for domain in items.keys(): - config.setdefault(domain, {}) - - errors = [item for item in items[domain] if item not in config[domain]] - - if errors: - logger.error( - "Missing required configuration items in {}: {}".format( - domain, ", ".join(errors))) - - errors_found = True - - return not errors_found - - class Throttle(object): """ A method decorator to add a cooldown to a method to prevent it from being From 68aa78d1fe106459b6a689615dc447edc17ba91a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Dec 2014 01:28:52 -0800 Subject: [PATCH 19/60] Fix some style warnings by PyCharm --- ha_test/test_util.py | 4 ++-- homeassistant/components/http/__init__.py | 4 ++-- homeassistant/components/light/__init__.py | 1 - homeassistant/components/tellstick_sensor.py | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ha_test/test_util.py b/ha_test/test_util.py index 8b986e76399..0f606fb45f2 100644 --- a/ha_test/test_util.py +++ b/ha_test/test_util.py @@ -176,10 +176,10 @@ class TestUtil(unittest.TestCase): self.assertEqual(set1, util.OrderedSet([2, 3])) self.assertNotEqual(set1, util.OrderedSet([3, 2])) self.assertEqual(set1, set([2, 3])) - self.assertEqual(set1, set([3, 2])) + self.assertEqual(set1, {3, 2}) self.assertEqual(set1, [2, 3]) self.assertEqual(set1, [3, 2]) - self.assertNotEqual(set1, set([2])) + self.assertNotEqual(set1, {2}) set3 = util.OrderedSet(set1) set3.update(set2) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0149130c0fc..815f732d8ab 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -157,9 +157,9 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True # pylint: disable=too-many-arguments - def __init__(self, server_address, RequestHandlerClass, + def __init__(self, server_address, request_handler_class, hass, api_password, development=False): - super().__init__(server_address, RequestHandlerClass) + super().__init__(server_address, request_handler_class) self.server_address = server_address self.hass = hass diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 55bbdb307f2..c398f9d8c82 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -235,7 +235,6 @@ def setup(hass, config): profile = profiles.get(dat.get(ATTR_PROFILE)) if profile: - # *color, bright = profile *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile if ATTR_BRIGHTNESS in dat: diff --git a/homeassistant/components/tellstick_sensor.py b/homeassistant/components/tellstick_sensor.py index e021632095b..79a91241316 100644 --- a/homeassistant/components/tellstick_sensor.py +++ b/homeassistant/components/tellstick_sensor.py @@ -87,7 +87,7 @@ def setup(hass, config): } def update_sensor_value_state(sensor_name, sensor_value): - "Update the state of a sensor value" + """ Update the state of a sensor value """ sensor_value_description = \ sensor_value_descriptions[sensor_value.datatype] sensor_value_name = '{} {}'.format( @@ -116,7 +116,7 @@ def setup(hass, config): ] def update_sensor_state(sensor): - "Updates all the sensor values from the sensor" + """ Updates all the sensor values from the sensor """ try: sensor_name = config[DOMAIN][str(sensor.id)] except KeyError: @@ -131,7 +131,7 @@ def setup(hass, config): # pylint: disable=unused-argument def update_sensors_state(time): - "Update the state of all sensors" + """ Update the state of all sensors """ for sensor in sensors: update_sensor_state(sensor) From 1f582cbeec12ef3270c112ba258981f1db4c7042 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Dec 2014 22:06:38 -0800 Subject: [PATCH 20/60] Fix: Read known devices file once on init device_tracker --- homeassistant/components/device_tracker/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index fa96a5b3c28..b3a2fdd37fd 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -105,11 +105,6 @@ class DeviceTracker(object): # Did we encounter an invalid known devices file self.invalid_known_devices_file = False - self._read_known_devices_file() - - if self.invalid_known_devices_file: - return - # Wrap it in a func instead of lambda so it can be identified in # the bus by its __name__ attribute. def update_device_state(now): @@ -130,14 +125,17 @@ class DeviceTracker(object): self.hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False) + reload_known_devices_service(None) + + if self.invalid_known_devices_file: + return + hass.track_time_change(update_device_state) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, reload_known_devices_service) - reload_known_devices_service(None) - @property def device_entity_ids(self): """ Returns a set containing all device entity ids From e7dff308ef3bef0d826638c6e2605b316cbdfdba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Dec 2014 22:06:57 -0800 Subject: [PATCH 21/60] Updated example component with more examples --- config/custom_components/example.py | 114 ++++++++++++++++++++++++---- homeassistant/__init__.py | 2 +- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index 51565d00d89..b47f2706f9e 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -1,32 +1,120 @@ """ custom_components.example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~ -Bare minimum what is needed for a component to be valid. +Example component to target an entity_id to: + - turn it on at 7AM in the morning + - turn it on if anyone comes home and it is off + - turn it off if all lights are turned off + - turn it off if all people leave the house + - offer a service to turn it on for 10 seconds """ +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 +import homeassistant.components as core # The domain of your component. Should be equal to the name of your component DOMAIN = "example" # List of component names (string) your component depends upon -# If you are setting up a group but not using a group for anything, -# don't depend on group -DEPENDENCIES = [] +# We depend on group because group will be loaded after all the components that +# initalize devices have been setup. +DEPENDENCIES = ['group'] + +# Configuration key for the entity id we are targetting +CONF_TARGET = 'target' + +# Name of the service that we expose +SERVICE_FLASH = 'flash' + +_LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup(hass, config): - """ Register services or listen for events that your component needs. """ + """ Setup christmas. """ - # Example of a service that prints the service call to the command-line. - hass.services.register(DOMAIN, "example_service_name", print) + # Validate that all required config options are given + if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): + return False - # This prints a time change event to the command-line twice a minute. - hass.track_time_change(print, second=[0, 30]) + target_id = config[DOMAIN][CONF_TARGET] - # See also (defined in homeassistant/__init__.py): - # hass.states.track_change - # hass.track_point_in_time + # 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) + + # 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 succesfully initialized return True diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 74f27b9f6f5..b7b0f8ef439 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -207,7 +207,7 @@ class HomeAssistant(object): def _process_match_param(parameter): """ Wraps parameter in a list if it is not one and returns it. """ - if not parameter or parameter == MATCH_ALL: + if parameter is None or parameter == MATCH_ALL: return MATCH_ALL elif isinstance(parameter, list): return parameter From 99447eaa175e6698d3dae6f70522be3d7dd0d4f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Dec 2014 23:02:38 -0800 Subject: [PATCH 22/60] Light component now supports sending flash command --- homeassistant/components/light/__init__.py | 58 ++++++++++++---------- homeassistant/components/light/hue.py | 7 ++- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c398f9d8c82..64f54c3d66a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -82,6 +82,12 @@ ATTR_BRIGHTNESS = "brightness" # String representing a profile (built-in ones or external defined) ATTR_PROFILE = "profile" +# If the light should flash, can be FLASH_SHORT or FLASH_LONG +ATTR_FLASH = "flash" +FLASH_SHORT = "short" +FLASH_LONG = "long" + + LIGHT_PROFILES_FILE = "light_profiles.csv" _LOGGER = logging.getLogger(__name__) @@ -96,40 +102,31 @@ def is_on(hass, entity_id=None): # pylint: disable=too-many-arguments def turn_on(hass, entity_id=None, transition=None, brightness=None, - rgb_color=None, xy_color=None, profile=None): + rgb_color=None, xy_color=None, profile=None, flash=None): """ Turns all or specified light on. """ - data = {} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if profile: - data[ATTR_PROFILE] = profile - - if transition is not None: - data[ATTR_TRANSITION] = transition - - if brightness is not None: - data[ATTR_BRIGHTNESS] = brightness - - if rgb_color: - data[ATTR_RGB_COLOR] = rgb_color - - if xy_color: - data[ATTR_XY_COLOR] = xy_color + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PROFILE, profile), + (ATTR_TRANSITION, transition), + (ATTR_BRIGHTNESS, brightness), + (ATTR_RGB_COLOR, rgb_color), + (ATTR_XY_COLOR, xy_color), + (ATTR_FLASH, flash), + ] if value is not None + } hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None, transition=None): """ Turns all or specified light off. """ - data = {} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if transition is not None: - data[ATTR_TRANSITION] = transition + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @@ -273,6 +270,13 @@ def setup(hass, config): # ValueError if not all values can be converted to int pass + if ATTR_FLASH in dat: + if dat[ATTR_FLASH] == FLASH_SHORT: + params[ATTR_FLASH] = FLASH_SHORT + + elif dat[ATTR_FLASH] == FLASH_LONG: + params[ATTR_FLASH] = FLASH_LONG + for light in lights: # pylint: disable=star-args light.turn_on(**params) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 19f4034eb39..2cede156381 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -7,7 +7,7 @@ import homeassistant.util as util from homeassistant.helpers import ToggleDevice from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION) + ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) @@ -95,6 +95,11 @@ class HueLight(ToggleDevice): if ATTR_XY_COLOR in kwargs: command['xy'] = kwargs[ATTR_XY_COLOR] + flash = kwargs.get(ATTR_FLASH) + + if flash is not None: + command['alert'] = 'lselect' if flash == FLASH_LONG else 'select' + self.bridge.set_light(self.light_id, command) def turn_off(self, **kwargs): From 6044742ceea22f0d212a5915665b38a7498971ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Dec 2014 23:41:52 -0800 Subject: [PATCH 23/60] Fix: Hue lights will not flash anymore on next turn on call after having flashed --- homeassistant/components/light/hue.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 2cede156381..bf8dae839c5 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -7,7 +7,8 @@ import homeassistant.util as util from homeassistant.helpers import ToggleDevice from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG) + ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, + ATTR_FLASH, FLASH_LONG, FLASH_SHORT) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) @@ -97,8 +98,12 @@ class HueLight(ToggleDevice): flash = kwargs.get(ATTR_FLASH) - if flash is not None: - command['alert'] = 'lselect' if flash == FLASH_LONG else 'select' + if flash == FLASH_LONG: + command['alert'] = 'lselect' + elif flash == FLASH_SHORT: + command['alert'] = 'select' + else: + command['alert'] = 'none' self.bridge.set_light(self.light_id, command) From dd55d6c7f957259759542c994b53b1531eb2c902 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Dec 2014 23:47:20 -0800 Subject: [PATCH 24/60] Added a simple_alarm component --- README.md | 9 +++ config/home-assistant.conf.example | 6 ++ homeassistant/components/simple_alarm.py | 95 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 homeassistant/components/simple_alarm.py diff --git a/README.md b/README.md index 08564802e77..01e1e1599f9 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,15 @@ Registers service `browser/browse_url` that opens `url` as specified in event_da **tellstick_sensor** Shows the values of that sensors that is connected to your Tellstick. +**simple_alarm** +Will provide simple alarm functionality. Will flash a light shortly if a known device comes home. Will flash the lights red if the lights turn on while no one is home. + +Depends on device_tracker, light. + +Config options: +known_light: entity id of the light/light group to target to flash when a known device comes home +unknown_light: entity if of the light/light group to target when a light is turned on while no one is at home. + ## Rest API diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example index 5a919ac38b0..e5f8db42a9b 100644 --- a/config/home-assistant.conf.example +++ b/config/home-assistant.conf.example @@ -53,6 +53,12 @@ xbmc=XBMC.App [example] +[simple_alarm] +# Which light/light group has to flash when a known device comes home +known_light=light.Bowl +# Which light/light group has to flash red when light turns on while no one home +unknown_light=group.living_room + [browser] [keyboard] diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py new file mode 100644 index 00000000000..515b5acda25 --- /dev/null +++ b/homeassistant/components/simple_alarm.py @@ -0,0 +1,95 @@ +""" +homeassistant.components.simple_alarm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a simple alarm feature: + - flash a light when a known device comes home + - flash a light red if a light turns on while there is no one home. +""" +import logging + +import homeassistant.loader as loader +from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME + +DOMAIN = "simple_alarm" + +DEPENDENCIES = ['group', 'device_tracker', 'light'] + +# Attribute to tell which light has to flash whem a known person comes home +# If ommitted will flash all. +CONF_KNOWN_LIGHT = "known_light" + +# Attribute to tell which light has to flash whem an unknown person comes home +# If ommitted will flash all. +CONF_UNKNOWN_LIGHT = "unknown_light" + +# Services to test the alarms +SERVICE_TEST_KNOWN_ALARM = "test_known" +SERVICE_TEST_UNKNOWN_ALARM = "test_unknown" + + +def setup(hass, config): + """ Sets up the simple alarms. """ + logger = logging.getLogger(__name__) + + device_tracker = loader.get_component('device_tracker') + light = loader.get_component('light') + + light_ids = [] + + for conf_key in (CONF_KNOWN_LIGHT, CONF_UNKNOWN_LIGHT): + light_id = config[DOMAIN].get(conf_key) or light.ENTITY_ID_ALL_LIGHTS + + if hass.states.get(light_id) is None: + logger.error( + 'Light id %s could not be found in state machine', light_id) + + return False + + else: + light_ids.append(light_id) + + # pylint: disable=unbalanced-tuple-unpacking + known_light_id, unknown_light_id = light_ids + + if hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES) is None: + logger.error('No devices are being tracked, cannot setup alarm') + + return False + + def known_alarm(): + """ Fire an alarm if a known person arrives home. """ + light.turn_on(hass, known_light_id, flash=light.FLASH_SHORT) + + def unknown_alarm(): + """ Fire an alarm if the light turns on while no one is home. """ + light.turn_on( + hass, unknown_light_id, + flash=light.FLASH_LONG, rgb_color=[255, 0, 0]) + + # Setup services to test the effect + hass.services.register( + DOMAIN, SERVICE_TEST_KNOWN_ALARM, lambda call: known_alarm()) + hass.services.register( + DOMAIN, SERVICE_TEST_UNKNOWN_ALARM, lambda call: unknown_alarm()) + + # pylint: disable=unused-argument + def unknown_alarm_if_lights_on(entity_id, old_state, new_state): + """ Called when a light has been turned on. """ + if not device_tracker.is_on(hass): + unknown_alarm() + + hass.states.track_change( + light.ENTITY_ID_ALL_LIGHTS, + unknown_alarm_if_lights_on, STATE_OFF, STATE_ON) + + # Not defined as a lambda so the __repr__ has a nice name. + # pylint: disable=unused-argument + def ring_known_alarm(entity_id, old_state, new_state): + """ Called when a known person comes home. """ + known_alarm() + + # Track home coming of each device + hass.states.track_change( + hass.states.entity_ids(device_tracker.DOMAIN), + ring_known_alarm, STATE_NOT_HOME, STATE_HOME) From 4e1b09444945e72f1923693119bbe36eab17241c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Dec 2014 00:06:55 -0800 Subject: [PATCH 25/60] Simple_alarm will only show known_alarm if that light is already on --- homeassistant/components/simple_alarm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py index 515b5acda25..5e033b5c514 100644 --- a/homeassistant/components/simple_alarm.py +++ b/homeassistant/components/simple_alarm.py @@ -83,11 +83,11 @@ def setup(hass, config): light.ENTITY_ID_ALL_LIGHTS, unknown_alarm_if_lights_on, STATE_OFF, STATE_ON) - # Not defined as a lambda so the __repr__ has a nice name. # pylint: disable=unused-argument def ring_known_alarm(entity_id, old_state, new_state): """ Called when a known person comes home. """ - known_alarm() + if light.is_on(hass, known_light_id): + known_alarm() # Track home coming of each device hass.states.track_change( From 00e1ecb5ade9a7dfb97972fb628d42c443580268 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Dec 2014 00:14:12 -0800 Subject: [PATCH 26/60] Fix: simple_alarm will now report if proper initialized --- homeassistant/components/simple_alarm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py index 5e033b5c514..5e3c125c317 100644 --- a/homeassistant/components/simple_alarm.py +++ b/homeassistant/components/simple_alarm.py @@ -93,3 +93,5 @@ def setup(hass, config): hass.states.track_change( hass.states.entity_ids(device_tracker.DOMAIN), ring_known_alarm, STATE_NOT_HOME, STATE_HOME) + + return True From 756425f7b41cb4da7e5eed3df52fd6af54e04e4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Dec 2014 00:29:07 -0800 Subject: [PATCH 27/60] Added frontend icon for simple_alarm --- homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 4 ++-- .../http/www_static/polymer/domain-icon.html | 21 ++++++++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index 0392f4b5c78..ed7199d1a93 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "12ba7bca8ad0c196cb04ada4fe85a76b" +VERSION = "f0d938f2a01a613e9fa0beb44940713d" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index 322cba2605f..d9e0e9f0e81 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -55,9 +55,9 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t - + diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index 1c7795b0249..a5b398338ef 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -69,10 +69,6 @@ events: [], stateUpdateTimeout: null, - computed: { - ha_headers: '{"HA-access": auth}' - }, - created: function() { this.api = this; @@ -285,7 +281,7 @@ call_api: function(method, path, parameters, onSuccess, onError) { var req = new XMLHttpRequest(); req.open(method, "/api/" + path, true) - req.setRequestHeader("HA-access", this.auth); + req.setRequestHeader("X-HA-access", this.auth); req.onreadystatechange = function() { diff --git a/homeassistant/const.py b/homeassistant/const.py index 8930ae5649c..0dc4e7881e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -66,7 +66,7 @@ SERVICE_MEDIA_PREV_TRACK = "media_prev_track" # #### API / REMOTE #### SERVER_PORT = 8123 -AUTH_HEADER = "HA-access" +AUTH_HEADER = "X-HA-access" URL_API = "/api/" URL_API_STATES = "/api/states" From df3521e70663030213a868e478f11646077941c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Dec 2014 17:31:01 -0800 Subject: [PATCH 29/60] Group component is more flexible when to setup a group --- ha_test/test_component_group.py | 70 ++++++++++++------------ homeassistant/components/group.py | 88 +++++++++++++++++-------------- 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/ha_test/test_component_group.py b/ha_test/test_component_group.py index d83596cee9d..204494b02e8 100644 --- a/ha_test/test_component_group.py +++ b/ha_test/test_component_group.py @@ -40,8 +40,41 @@ class TestComponentsGroup(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_and_monitor_group(self): + def test_setup_group(self): """ Test setup_group method. """ + # Try to setup a group with mixed groupable states + self.hass.states.set('device_tracker.Paulus', STATE_HOME) + self.assertTrue(group.setup_group( + self.hass, 'person_and_light', + ['light.Bowl', 'device_tracker.Paulus'])) + self.assertEqual( + STATE_ON, + self.hass.states.get( + group.ENTITY_ID_FORMAT.format('person_and_light')).state) + + # Try to setup a group with a non existing state + self.assertNotIn('non.existing', self.hass.states.entity_ids()) + self.assertTrue(group.setup_group( + self.hass, 'light_and_nothing', + ['light.Bowl', 'non.existing'])) + self.assertEqual( + STATE_ON, + self.hass.states.get( + group.ENTITY_ID_FORMAT.format('light_and_nothing')).state) + + # Try to setup a group with non groupable states + self.hass.states.set('cast.living_room', "Plex") + self.hass.states.set('cast.bedroom', "Netflix") + self.assertFalse( + group.setup_group( + self.hass, 'chromecasts', + ['cast.living_room', 'cast.bedroom'])) + + # Try to setup an empty group + self.assertFalse(group.setup_group(self.hass, 'nothing', [])) + + def test_monitor_group(self): + """ Test if the group keeps track of states. """ # Test if group setup in our init mode is ok self.assertIn(self.group_name, self.hass.states.entity_ids()) @@ -66,41 +99,6 @@ class TestComponentsGroup(unittest.TestCase): group_state = self.hass.states.get(self.group_name) self.assertEqual(STATE_ON, group_state.state) - # Try to setup a group with mixed groupable states - self.hass.states.set('device_tracker.Paulus', STATE_HOME) - self.assertFalse(group.setup_group( - self.hass, 'person_and_light', - ['light.Bowl', 'device_tracker.Paulus'])) - - # Try to setup a group with a non existing state - self.assertNotIn('non.existing', self.hass.states.entity_ids()) - self.assertFalse(group.setup_group( - self.hass, 'light_and_nothing', - ['light.Bowl', 'non.existing'])) - - # Try to setup a group with non groupable states - self.hass.states.set('cast.living_room', "Plex") - self.hass.states.set('cast.bedroom', "Netflix") - self.assertFalse( - group.setup_group( - self.hass, 'chromecasts', - ['cast.living_room', 'cast.bedroom'])) - - # Try to setup an empty group - self.assertFalse(group.setup_group(self.hass, 'nothing', [])) - - def test__get_group_type(self): - """ Test _get_group_type method. """ - self.assertEqual('on_off', group._get_group_type(STATE_ON)) - self.assertEqual('on_off', group._get_group_type(STATE_OFF)) - self.assertEqual('home_not_home', - group._get_group_type(STATE_HOME)) - self.assertEqual('home_not_home', - group._get_group_type(STATE_NOT_HOME)) - - # Unsupported state - self.assertIsNone(group._get_group_type('unsupported_state')) - def test_is_on(self): """ Test is_on method. """ self.assertTrue(group.is_on(self.hass, self.group_name)) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 59033b02b68..affdf012318 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -19,21 +19,19 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" ATTR_AUTO = "auto" -_GROUP_TYPES = { - "on_off": (STATE_ON, STATE_OFF), - "home_not_home": (STATE_HOME, STATE_NOT_HOME) -} +# List of ON/OFF state tuples for groupable states +_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)] _GROUPS = {} -def _get_group_type(state): - """ Determine the group type based on the given group type. """ - for group_type, states in _GROUP_TYPES.items(): +def _get_group_on_off(state): + """ Determine the group on/off states based on a state. """ + for states in _GROUP_TYPES: if state in states: - return group_type + return states - return None + return None, None def is_on(hass, entity_id): @@ -41,10 +39,10 @@ def is_on(hass, entity_id): state = hass.states.get(entity_id) if state: - group_type = _get_group_type(state.state) + group_on, _ = _get_group_on_off(state.state) # If we found a group_type, compare to ON-state - return group_type and state.state == _GROUP_TYPES[group_type][0] + return group_on is not None and state.state == group_on return False @@ -103,64 +101,74 @@ def setup(hass, config): return True -# pylint: disable=too-many-branches def setup_group(hass, name, entity_ids, user_defined=True): """ Sets up a group state that is the combined state of several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ + logger = logging.getLogger(__name__) + # In case an iterable is passed in entity_ids = list(entity_ids) + if not entity_ids: + logger.error( + 'Error setting up group %s: no entities passed in to track', name) + + return False + # Loop over the given entities to: # - determine which group type this is (on_off, device_home) - # - if all states exist and have valid states - # - retrieve the current state of the group - errors = [] - group_type, group_on, group_off, group_state = None, None, None, None + # - determine which states exist and have groupable states + # - determine the current state of the group + warnings = [] + group_ids = [] + group_on, group_off = None, None + group_state = False for entity_id in entity_ids: state = hass.states.get(entity_id) # Try to determine group type if we didn't yet - if not group_type and state: - group_type = _get_group_type(state.state) + if group_on is None and state: + group_on, group_off = _get_group_on_off(state.state) - if group_type: - group_on, group_off = _GROUP_TYPES[group_type] - group_state = group_off - - else: + if group_on is None: # We did not find a matching group_type - errors.append( + warnings.append( "Entity {} has ungroupable state '{}'".format( name, state.state)) - # Stop check all other entity IDs and report as error - break + continue # Check if entity exists if not state: - errors.append("Entity {} does not exist".format(entity_id)) + warnings.append("Entity {} does not exist".format(entity_id)) - # Check if entity is valid state + # Check if entity is invalid state elif state.state != group_off and state.state != group_on: - errors.append("State of {} is {} (expected: {} or {})".format( + warnings.append("State of {} is {} (expected: {} or {})".format( entity_id, state.state, group_off, group_on)) - # Keep track of the group state to init later on - elif state.state == group_on: - group_state = group_on + # We have a valid group state + else: + group_ids.append(entity_id) - if group_type is None and not errors: - errors.append('Unable to determine group type for {}'.format(name)) + # Keep track of the group state to init later on + group_state = group_state or state.state == group_on - if errors: - logging.getLogger(__name__).error( - "Error setting up group %s: %s", name, ", ".join(errors)) + # If none of the entities could be found during setup + if not group_ids: + logger.error('Unable to find any entities to track for group %s', name) return False + elif warnings: + logger.warning( + 'Warnings during setting up group %s: %s', + name, ", ".join(warnings)) + group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) + state = group_on if group_state else group_off state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined} # pylint: disable=unused-argument @@ -182,14 +190,14 @@ def setup_group(hass, name, entity_ids, user_defined=True): # Check if any of the other states is still on if not any([hass.states.is_state(ent_id, group_on) - for ent_id in entity_ids + for ent_id in group_ids if entity_id != ent_id]): hass.states.set(group_entity_id, group_off, state_attr) _GROUPS[group_entity_id] = hass.states.track_change( - entity_ids, update_group_state) + group_ids, update_group_state) - hass.states.set(group_entity_id, group_state, state_attr) + hass.states.set(group_entity_id, state, state_attr) return True From 36cb12cd157b22bdc1fa28b700ca0fb751cca7a4 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 12 Dec 2014 11:40:20 +0100 Subject: [PATCH 30/60] Fix typo in doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01e1e1599f9..aca434b022b 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Registers services `light/turn_on` and `light/turn_off` to turn a or all lights Optional service data: - `entity_id` - only act on specific light. Else targets all. - - `transition_seconds` - seconds to take to swithc to new state. + - `transition_seconds` - seconds to take to switch to new state. - `profile` - which light profile to use. - `xy_color` - two comma seperated floats that represent the color in XY - `rgb_color` - three comma seperated integers that represent the color in RGB From 81be3811dc78b6c1e98ae95f6374d0d6578d819e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Dec 2014 07:21:25 -0800 Subject: [PATCH 31/60] Only save the valid entities in the group entity_ids attribute --- homeassistant/components/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index affdf012318..eac63ee845b 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -169,7 +169,7 @@ def setup_group(hass, name, entity_ids, user_defined=True): group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) state = group_on if group_state else group_off - state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined} + state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined} # pylint: disable=unused-argument def update_group_state(entity_id, old_state, new_state): From 7c404a055188818d9903d39e04d2822b9bcfa477 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Dec 2014 07:32:50 -0800 Subject: [PATCH 32/60] device_tracker - No longer keep writing same new devices to known_devices.csv --- .../components/device_tracker/__init__.py | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b3a2fdd37fd..c478e118036 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -177,38 +177,41 @@ class DeviceTracker(object): # Did we find any devices that we didn't know about yet? new_devices = found_devices - self.untracked_devices - # Write new devices to known devices file - if not self.invalid_known_devices_file and new_devices: + if new_devices: + self.untracked_devices.update(new_devices) - known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) + # Write new devices to known devices file + if not self.invalid_known_devices_file: - try: - # If file does not exist we will write the header too - is_new_file = not os.path.isfile(known_dev_path) + known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE) - with open(known_dev_path, 'a') as outp: - _LOGGER.info( - "Found %d new devices, updating %s", - len(new_devices), known_dev_path) + try: + # If file does not exist we will write the header too + is_new_file = not os.path.isfile(known_dev_path) - writer = csv.writer(outp) + with open(known_dev_path, 'a') as outp: + _LOGGER.info( + "Found %d new devices, updating %s", + len(new_devices), known_dev_path) - if is_new_file: - writer.writerow(( - "device", "name", "track", "picture")) + writer = csv.writer(outp) - for device in new_devices: - # See if the device scanner knows the name - # else defaults to unknown device - name = (self.device_scanner.get_device_name(device) - or "unknown_device") + if is_new_file: + writer.writerow(( + "device", "name", "track", "picture")) - writer.writerow((device, name, 0, "")) + for device in new_devices: + # See if the device scanner knows the name + # else defaults to unknown device + name = (self.device_scanner.get_device_name(device) + or "unknown_device") - except IOError: - _LOGGER.exception( - "Error updating %s with %d new devices", - known_dev_path, len(new_devices)) + writer.writerow((device, name, 0, "")) + + except IOError: + _LOGGER.exception( + "Error updating %s with %d new devices", + known_dev_path, len(new_devices)) self.lock.release() From e2b434b24ec0c0db0b1aba9c0d6aab4d8779345a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Dec 2014 08:15:34 -0800 Subject: [PATCH 33/60] Log file now also contains warnings --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7675d3f12f3..112a8018d63 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -90,7 +90,7 @@ def from_config_file(config_path, hass=None, enable_logging=True): err_handler = logging.FileHandler( err_log_path, mode='w', delay=True) - err_handler.setLevel(logging.ERROR) + err_handler.setLevel(logging.WARNING) err_handler.setFormatter( logging.Formatter('%(asctime)s %(name)s: %(message)s', datefmt='%H:%M %d-%m-%y')) From f8223053bd640893fc8600d16ee16106bf7b05bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Dec 2014 22:29:05 -0800 Subject: [PATCH 34/60] Style fixes in home-assistant-api.html --- .../polymer/home-assistant-api.html | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index a5b398338ef..6bd7918fd4e 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -112,7 +112,7 @@ } else { return 0; } - }) + }); }, _pushNewState: function(new_state) { @@ -136,7 +136,7 @@ this._sortStates(this.states); } - this.fire('states-updated') + this.fire('states-updated'); }, // call api methods @@ -149,7 +149,7 @@ fetchState: function(entityId) { var successStateUpdate = function(new_state) { this._pushNewState(new_state); - } + }; this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this)); }, @@ -162,14 +162,14 @@ return new State(json, this); }.bind(this)); - this.fire('states-updated') + this.fire('states-updated'); this._laterFetchStates(); if(onSuccess) { onSuccess(this.states); } - } + }; this.call_api( "GET", "states", null, successStatesUpdate.bind(this), onError); @@ -179,12 +179,12 @@ var successEventsUpdated = function(events) { this.events = events; - this.fire('events-updated') + this.fire('events-updated'); if(onSuccess) { onSuccess(events); } - } + }; this.call_api( "GET", "events", null, successEventsUpdated.bind(this), onError); @@ -194,12 +194,12 @@ var successServicesUpdated = function(services) { this.services = services; - this.fire('services-updated') + this.fire('services-updated'); if(onSuccess) { onSuccess(this.services); } - } + }; this.call_api( "GET", "services", null, successServicesUpdated.bind(this), onError); @@ -210,11 +210,11 @@ }, turn_off: function(entity_id) { - this.call_service("homeassistant", "turn_off", {entity_id: entity_id}) + this.call_service("homeassistant", "turn_off", {entity_id: entity_id}); }, set_state: function(entity_id, state, attributes) { - var payload = {state: state} + var payload = {state: state}; if(attributes) { payload.attributes = attributes; @@ -223,7 +223,7 @@ var successToast = function(new_state) { this.showToast("State of "+entity_id+" set to "+state+"."); this._pushNewState(new_state); - } + }; this.call_api("POST", "states/" + entity_id, payload, successToast.bind(this)); @@ -249,19 +249,19 @@ if(typeof(parameters.entity_id === "string")) { // if it is a group, fetch all if(parameters.entity_id.slice(0,6) == "group.") { - update_func = this.fetchStates + update_func = this.fetchStates; } else { update_func = function() { this.fetchState(parameters.entity_id); - } + }; } } else { - update_func = this.fetchStates + update_func = this.fetchStates; } setTimeout(update_func.bind(this), 1000); } - } + }; this.call_api("POST", "services/" + domain + "/" + service, parameters, successToast.bind(this)); @@ -272,7 +272,7 @@ var successToast = function() { this.showToast("Event "+eventType+" fired."); - } + }; this.call_api("POST", "events/" + eventType, eventData, successToast.bind(this)); @@ -280,7 +280,7 @@ call_api: function(method, path, parameters, onSuccess, onError) { var req = new XMLHttpRequest(); - req.open(method, "/api/" + path, true) + req.open(method, "/api/" + path, true); req.setRequestHeader("X-HA-access", this.auth); req.onreadystatechange = function() { @@ -299,7 +299,7 @@ } - }.bind(this) + }.bind(this); if(parameters) { req.send(JSON.stringify(parameters)); @@ -312,7 +312,7 @@ showEditStateDialog: function(entityId) { var state = this.getState(entityId); - this.showSetStateDialog(entityId, state.state, state.attributes) + this.showSetStateDialog(entityId, state.state, state.attributes); }, showSetStateDialog: function(entityId, state, stateAttributes) { From 78d5625acef082c1262305828e42e49f93153ee3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Dec 2014 22:40:00 -0800 Subject: [PATCH 35/60] Calling a service can now block till execution is done --- homeassistant/__init__.py | 70 +++++++++++++++++-- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 2 +- .../polymer/home-assistant-api.html | 29 +++----- homeassistant/const.py | 9 ++- 6 files changed, 85 insertions(+), 29 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index b7b0f8ef439..a2be3780619 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -18,7 +18,8 @@ import functools as ft from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, - EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL) + EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, + EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID) import homeassistant.util as util DOMAIN = "homeassistant" @@ -26,6 +27,9 @@ DOMAIN = "homeassistant" # How often time_changed event should fire TIMER_INTERVAL = 10 # seconds +# How long we wait for the result of a service call +SERVICE_CALL_LIMIT = 10 # seconds + # Number of worker threads POOL_NUM_THREAD = 4 @@ -227,6 +231,7 @@ class JobPriority(util.OrderedEnum): """ Provides priorities for bus events. """ # pylint: disable=no-init,too-few-public-methods + EVENT_CALLBACK = 0 EVENT_SERVICE = 1 EVENT_STATE = 2 EVENT_TIME = 3 @@ -241,6 +246,8 @@ class JobPriority(util.OrderedEnum): return JobPriority.EVENT_STATE elif event_type == EVENT_CALL_SERVICE: return JobPriority.EVENT_SERVICE + elif event_type == EVENT_SERVICE_EXECUTED: + return JobPriority.EVENT_CALLBACK else: return JobPriority.EVENT_DEFAULT @@ -594,6 +601,7 @@ class ServiceRegistry(object): self._lock = threading.Lock() self._pool = pool or create_worker_pool() self._bus = bus + self._cur_id = 0 bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) @property @@ -615,9 +623,14 @@ class ServiceRegistry(object): else: self._services[domain] = {service: service_func} - def call(self, domain, service, service_data=None): + def call(self, domain, service, service_data=None, blocking=False): """ - Fires event to call specified service. + Calls specified service. + Specify blocking=True to wait till service is executed. + Waits a maximum of SERVICE_CALL_LIMIT. + + If blocking = True, will return boolean if service executed + succesfully within SERVICE_CALL_LIMIT. This method will fire an event to call the service. This event will be picked up by this ServiceRegistry and any @@ -626,12 +639,41 @@ class ServiceRegistry(object): Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. """ + call_id = self._generate_unique_id() event_data = service_data or {} event_data[ATTR_DOMAIN] = domain event_data[ATTR_SERVICE] = service + event_data[ATTR_SERVICE_CALL_ID] = call_id + + if blocking: + executed_event = threading.Event() + + def service_executed(call): + """ + Called when a service is executed. + Will set the event if matches our service call. + """ + if call.data[ATTR_SERVICE_CALL_ID] == call_id: + executed_event.set() + + self._bus.remove_listener( + EVENT_SERVICE_EXECUTED, service_executed) + + self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) self._bus.fire(EVENT_CALL_SERVICE, event_data) + if blocking: + # wait will return False if event not set after our limit has + # passed. If not set, clean up the listener + if not executed_event.wait(SERVICE_CALL_LIMIT): + self._bus.remove_listener( + EVENT_SERVICE_EXECUTED, service_executed) + + return False + + return True + def _event_to_service_call(self, event): """ Calls a service from an event. """ service_data = dict(event.data) @@ -642,9 +684,27 @@ class ServiceRegistry(object): if domain in self._services and service in self._services[domain]: service_call = ServiceCall(domain, service, service_data) + # Add a job to the pool that calls _execute_service self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._services[domain][service], - service_call)) + (self._execute_service, + (self._services[domain][service], + service_call))) + + def _execute_service(self, service_and_call): + """ Executes a service and fires a SERVICE_EXECUTED event. """ + service, call = service_and_call + + service(call) + + self._bus.fire( + EVENT_SERVICE_EXECUTED, { + ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID] + }) + + def _generate_unique_id(self): + """ Generates a unique service call id. """ + self._cur_id += 1 + return "{}-{}".format(id(self), self._cur_id) class Timer(threading.Thread): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 815f732d8ab..5bb139b2123 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -484,7 +484,7 @@ class RequestHandler(SimpleHTTPRequestHandler): domain = path_match.group('domain') service = path_match.group('service') - self.server.hass.services.call(domain, service, data) + self.server.hass.services.call(domain, service, data, True) self._json_message("Service {}/{} called.".format(domain, service)) diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index 5190b87f741..2feb9bc20fc 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "b32f2de4e3336946fe68cd1e5cd0fe6f" +VERSION = "5c3b2dd8a63197e380e798da8b057b0a" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index a49e7c83367..ab03282f023 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -66,6 +66,6 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t + diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index 6bd7918fd4e..6f10dbaa84c 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -206,11 +206,18 @@ }, turn_on: function(entity_id) { - this.call_service("homeassistant", "turn_on", {entity_id: entity_id}); + // we call the turn_on method on the domain of the entity_id + // because the call to homeassistant.turn_on does not wait + // till the call is done. + var parts = entity_id.split("."); + + this.call_service(parts[0], "turn_on", {entity_id: entity_id}); }, turn_off: function(entity_id) { - this.call_service("homeassistant", "turn_off", {entity_id: entity_id}); + var parts = entity_id.split("."); + + this.call_service(parts[0], "turn_off", {entity_id: entity_id}); }, set_state: function(entity_id, state, attributes) { @@ -243,23 +250,7 @@ // if we call a service on an entity_id, update the state if(parameters && parameters.entity_id) { - var update_func; - - // if entity_id is a string, update 1 state, else all. - if(typeof(parameters.entity_id === "string")) { - // if it is a group, fetch all - if(parameters.entity_id.slice(0,6) == "group.") { - update_func = this.fetchStates; - } else { - update_func = function() { - this.fetchState(parameters.entity_id); - }; - } - } else { - update_func = this.fetchStates; - } - - setTimeout(update_func.bind(this), 1000); + this.fetchStates(); } }; diff --git a/homeassistant/const.py b/homeassistant/const.py index 0dc4e7881e5..b8183edc2d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -20,7 +20,8 @@ EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_STATE_CHANGED = "state_changed" EVENT_TIME_CHANGED = "time_changed" -EVENT_CALL_SERVICE = "services.call" +EVENT_CALL_SERVICE = "call_service" +EVENT_SERVICE_EXECUTED = "service_executed" # #### STATES #### STATE_ON = 'on' @@ -28,7 +29,7 @@ STATE_OFF = 'off' STATE_HOME = 'home' STATE_NOT_HOME = 'not_home' -# #### STATE ATTRIBUTES #### +# #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event ATTR_NOW = "now" @@ -36,6 +37,10 @@ ATTR_NOW = "now" ATTR_DOMAIN = "domain" ATTR_SERVICE = "service" +# Data for a SERVICE_EXECUTED event +ATTR_SERVICE_CALL_ID = "service_call_id" +ATTR_RESULT = "result" + # Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID = 'entity_id' From dfa1e1c586c5db61cc3d91c0337aa90d0d7ca0a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Dec 2014 22:49:49 -0800 Subject: [PATCH 36/60] Fix Chromecast tests --- ha_test/test_component_chromecast.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ha_test/test_component_chromecast.py b/ha_test/test_component_chromecast.py index 3d9733648c9..c653c84b74f 100644 --- a/ha_test/test_component_chromecast.py +++ b/ha_test/test_component_chromecast.py @@ -67,19 +67,18 @@ class TestChromecast(unittest.TestCase): self.assertEqual(1, len(calls)) call = calls[-1] - self.assertEqual(call.domain, chromecast.DOMAIN) - self.assertEqual(call.service, service_name) - self.assertEqual(call.data, {}) + self.assertEqual(chromecast.DOMAIN, call.domain) + self.assertEqual(service_name, call.service) service_method(self.hass, self.test_entity) self.hass._pool.block_till_done() self.assertEqual(2, len(calls)) call = calls[-1] - self.assertEqual(call.domain, chromecast.DOMAIN) - self.assertEqual(call.service, service_name) - self.assertEqual(call.data, - {ATTR_ENTITY_ID: self.test_entity}) + self.assertEqual(chromecast.DOMAIN, call.domain) + self.assertEqual(service_name, call.service) + self.assertEqual(self.test_entity, + call.data.get(ATTR_ENTITY_ID)) def test_setup(self): """ From b091e9c31c1d83e24eee2435236979f4f492deb9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Dec 2014 00:32:20 -0800 Subject: [PATCH 37/60] Added TrackStates context manager --- homeassistant/__init__.py | 23 ++++++++++++++++------- homeassistant/const.py | 1 - homeassistant/helpers.py | 21 +++++++++++++++++++++ homeassistant/util.py | 10 +++++++++- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index a2be3780619..8bb4a0b8b1f 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -418,17 +418,13 @@ class State(object): self.entity_id = entity_id self.state = state self.attributes = attributes or {} - last_changed = last_changed or dt.datetime.now() # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) # This behavior occurs because to_dict uses datetime_to_str - # which strips microseconds - if last_changed.microsecond: - self.last_changed = last_changed - dt.timedelta( - microseconds=last_changed.microsecond) - else: - self.last_changed = last_changed + # which does not preserve microseconds + self.last_changed = util.strip_microseconds( + last_changed or dt.datetime.now()) def copy(self): """ Creates a copy of itself. """ @@ -504,6 +500,19 @@ class StateMachine(object): # Make a copy so people won't mutate the state return state.copy() if state else None + def get_since(self, point_in_time): + """ + Returns all states that have been changed since point_in_time. + + Note: States keep track of last_changed -without- microseconds. + Therefore your point_in_time will also be stripped of microseconds. + """ + point_in_time = util.strip_microseconds(point_in_time) + + with self._lock: + return [state for state in self._states.values() + if state.last_changed >= point_in_time] + def is_state(self, entity_id, state): """ Returns True if entity exists and is specified state. """ return (entity_id in self._states and diff --git a/homeassistant/const.py b/homeassistant/const.py index b8183edc2d0..49f39b11c94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -39,7 +39,6 @@ ATTR_SERVICE = "service" # Data for a SERVICE_EXECUTED event ATTR_SERVICE_CALL_ID = "service_call_id" -ATTR_RESULT = "result" # Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID = 'entity_id' diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py index f673cc5345f..cdf645cb75f 100644 --- a/homeassistant/helpers.py +++ b/homeassistant/helpers.py @@ -1,6 +1,8 @@ """ Helper methods for components within Home Assistant. """ +from datetime import datetime + from homeassistant import NoEntitySpecifiedError from homeassistant.loader import get_component @@ -33,6 +35,25 @@ def extract_entity_ids(hass, service): return entity_ids +# pylint: disable=too-few-public-methods, attribute-defined-outside-init +class TrackStates(object): + """ + Records the time when the with-block is entered. Will add all states + that have changed since the start time to the return list when with-block + is exited. + """ + def __init__(self, hass): + self.hass = hass + self.states = [] + + def __enter__(self): + self.now = datetime.now() + return self.states + + def __exit__(self, exc_type, exc_value, traceback): + self.states.extend(self.hass.states.get_since(self.now)) + + def validate_config(config, items, logger): """ Validates if all items are available in the configuration. diff --git a/homeassistant/util.py b/homeassistant/util.py index 4598a230c65..fbf686c39ab 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -8,7 +8,7 @@ import collections from itertools import chain import threading import queue -from datetime import datetime +from datetime import datetime, timedelta import re import enum import socket @@ -57,6 +57,14 @@ def str_to_datetime(dt_str): return None +def strip_microseconds(dattim): + """ Returns a copy of dattime object but with microsecond set to 0. """ + if dattim.microsecond: + return dattim - timedelta(microseconds=dattim.microsecond) + else: + return dattim + + def split_entity_id(entity_id): """ Splits a state entity_id into domain, object_id. """ return entity_id.split(".", 1) From 5e8673fc4a38dafd69b9e914a5516bd20571626c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Dec 2014 00:35:16 -0800 Subject: [PATCH 38/60] API Call Service returns states changed --- README.md | 24 +++++++++-- homeassistant/components/__init__.py | 2 +- homeassistant/components/http/__init__.py | 7 +-- homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 4 +- .../polymer/home-assistant-api.html | 43 +++++++++++-------- .../http/www_static/polymer/state-card.html | 33 +++++++------- 7 files changed, 72 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 26b9460e53a..4693ec92d6d 100644 --- a/README.md +++ b/README.md @@ -370,13 +370,29 @@ optional body: JSON encoded object that represents event_data ``` **/api/services/<domain>/<service>** - POST
-Calls a service within a specific domain.
+Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
optional body: JSON encoded object that represents service_data +Returns a list of states that have changed since the start of this service call. + ```json -{ - "message": "Service keyboard/volume_up called." -} +[ + { + "attributes": { + "next_rising": "07:04:15 29-10-2013", + "next_setting": "18:00:31 29-10-2013" + }, + "entity_id": "sun.sun", + "last_changed": "23:24:33 28-10-2013", + "state": "below_horizon" + }, + { + "attributes": {}, + "entity_id": "process.Dropbox", + "last_changed": "23:24:33 28-10-2013", + "state": "on" + } +] ``` **/api/event_forwarding** - POST
diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 0680b5c67af..6720ae2a2d9 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -96,7 +96,7 @@ def setup(hass, config): # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data) + hass.services.call(domain, service.service, data, True) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5bb139b2123..a50ed7c9845 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -86,7 +86,7 @@ import homeassistant as ha from homeassistant.const import ( SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) -from homeassistant.helpers import validate_config +from homeassistant.helpers import validate_config, TrackStates import homeassistant.remote as rem import homeassistant.util as util from . import frontend @@ -484,9 +484,10 @@ class RequestHandler(SimpleHTTPRequestHandler): domain = path_match.group('domain') service = path_match.group('service') - self.server.hass.services.call(domain, service, data, True) + with TrackStates(self.server.hass) as changed_states: + self.server.hass.services.call(domain, service, data, True) - self._json_message("Service {}/{} called.".format(domain, service)) + self._write_json(changed_states) # pylint: disable=invalid-name def _handle_post_api_event_forward(self, path_match, data): diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index 2feb9bc20fc..af8b97f120c 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "5c3b2dd8a63197e380e798da8b057b0a" +VERSION = "dc16db6d5f4ba9f6dbf9d88f65592184" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index ab03282f023..93dcefa6fb6 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -61,11 +61,11 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t {{stateObj.entityDisplay}}
{{lastChangedFromNow(stateObj.last_changed)}} -
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index 6f10dbaa84c..3bccf223179 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -139,6 +139,12 @@ this.fire('states-updated'); }, + _pushNewStates: function(new_states) { + new_states.map(function(state) { + this._pushNewState(state); + }.bind(this)); + }, + // call api methods fetchAll: function() { this.fetchStates(); @@ -205,19 +211,14 @@ "GET", "services", null, successServicesUpdated.bind(this), onError); }, - turn_on: function(entity_id) { - // we call the turn_on method on the domain of the entity_id - // because the call to homeassistant.turn_on does not wait - // till the call is done. - var parts = entity_id.split("."); - - this.call_service(parts[0], "turn_on", {entity_id: entity_id}); + turn_on: function(entity_id, options) { + this.call_service( + "homeassistant", "turn_on", {entity_id: entity_id}, options); }, - turn_off: function(entity_id) { - var parts = entity_id.split("."); - - this.call_service(parts[0], "turn_off", {entity_id: entity_id}); + turn_off: function(entity_id, options) { + this.call_service( + "homeassistant", "turn_off", {entity_id: entity_id}, options); }, set_state: function(entity_id, state, attributes) { @@ -236,10 +237,11 @@ payload, successToast.bind(this)); }, - call_service: function(domain, service, parameters) { + call_service: function(domain, service, parameters, options) { parameters = parameters || {}; + options = options || {}; - var successToast = function() { + var successHandler = function(changed_states) { if(service == "turn_on" && parameters.entity_id) { this.showToast("Turned on " + parameters.entity_id + '.'); } else if(service == "turn_off" && parameters.entity_id) { @@ -248,14 +250,21 @@ this.showToast("Service "+domain+"/"+service+" called."); } - // if we call a service on an entity_id, update the state - if(parameters && parameters.entity_id) { - this.fetchStates(); + this._pushNewStates(changed_states); + + if(options.success) { + options.success(); + } + }; + + var errorHandler = function(error_data) { + if(options.error) { + options.error(error_data); } }; this.call_api("POST", "services/" + domain + "/" + service, - parameters, successToast.bind(this)); + parameters, successHandler.bind(this), errorHandler); }, fire_event: function(eventType, eventData) { diff --git a/homeassistant/components/http/www_static/polymer/state-card.html b/homeassistant/components/http/www_static/polymer/state-card.html index c7656e3787a..8b0be4dc076 100755 --- a/homeassistant/components/http/www_static/polymer/state-card.html +++ b/homeassistant/components/http/www_static/polymer/state-card.html @@ -139,31 +139,34 @@ }, stateChanged: function(oldVal, newVal) { - this.stateUnknown = newVal === null; this.toggleChecked = newVal === "on"; }, turn_on: function() { + // We call stateChanged after a successful call to re-sync the toggle + // with the state. It will be out of sync if our service call did not + // result in the entity to be turned on. Since the state is not changing, + // the resync is not called automatic. if(this.cb_turn_on) { - this.cb_turn_on(this.stateObj.entity_id); - - // unset state while we wait for an update - var delayUnsetSate = function() { - this.stateObj.state = null; - }; - setTimeout(delayUnsetSate.bind(this), 500); + this.cb_turn_on(this.stateObj.entity_id, { + success: function() { + this.stateChanged(this.stateObj.state, this.stateObj.state); + }.bind(this) + }); } }, turn_off: function() { + // We call stateChanged after a successful call to re-sync the toggle + // with the state. It will be out of sync if our service call did not + // result in the entity to be turned on. Since the state is not changing, + // the resync is not called automatic. if(this.cb_turn_off) { - this.cb_turn_off(this.stateObj.entity_id); - - // unset state while we wait for an update - var delayUnsetSate = function() { - this.stateObj.state = null; - }; - setTimeout(delayUnsetSate.bind(this), 500); + this.cb_turn_off(this.stateObj.entity_id, { + success: function() { + this.stateChanged(this.stateObj.state, this.stateObj.state); + }.bind(this) + }); } }, From 4e4e6b1133eb05a7fbad3fc8e3b93fae0aa35a1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Dec 2014 00:45:29 -0800 Subject: [PATCH 39/60] Upgrade to Polymer 0.5.2 --- homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 23 ++++------ .../http/www_static/polymer/bower.json | 44 +++++++++---------- .../http/www_static/webcomponents.min.js | 10 ++--- 4 files changed, 36 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index af8b97f120c..9128460efc3 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "dc16db6d5f4ba9f6dbf9d88f65592184" +VERSION = "1b079af6522e827e395c4fbf0282222f" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index 93dcefa6fb6..3a345976578 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -2,9 +2,7 @@ +}},dispatchMethod:function(a,c,d){if(a){b.events&&console.group("[%s] dispatch [%s]",a.localName,c);var e="function"==typeof c?c:a[c];e&&e[d?"apply":"call"](a,d),b.events&&console.groupEnd(),Polymer.flush()}}};a.api.instance.events=d,a.addEventListener=function(a,b,c,d){PolymerGestures.addEventListener(wrap(a),b,c,d)},a.removeEventListener=function(a,b,c,d){PolymerGestures.removeEventListener(wrap(a),b,c,d)}}(Polymer),function(a){var b={copyInstanceAttributes:function(){var a=this._instanceAttributes;for(var b in a)this.hasAttribute(b)||this.setAttribute(b,a[b])},takeAttributes:function(){if(this._publishLC)for(var a,b=0,c=this.attributes,d=c.length;(a=c[b])&&d>b;b++)this.attributeToProperty(a.name,a.value)},attributeToProperty:function(b,c){var b=this.propertyForAttribute(b);if(b){if(c&&c.search(a.bindPattern)>=0)return;var d=this[b],c=this.deserializeValue(c,d);c!==d&&(this[b]=c)}},propertyForAttribute:function(a){var b=this._publishLC&&this._publishLC[a];return b},deserializeValue:function(b,c){return a.deserializeValue(b,c)},serializeValue:function(a,b){return"boolean"===b?a?"":void 0:"object"!==b&&"function"!==b&&void 0!==a?a:void 0},reflectPropertyToAttribute:function(a){var b=typeof this[a],c=this.serializeValue(this[a],b);void 0!==c?this.setAttribute(a,c):"boolean"===b&&this.removeAttribute(a)}};a.api.instance.attributes=b}(Polymer),function(a){function b(a,b){return a===b?0!==a||1/a===1/b:f(a)&&f(b)?!0:a!==a&&b!==b}function c(a,b){return void 0===b&&null===a?b:null===b||void 0===b?a:b}var d=window.WebComponents?WebComponents.flags.log:{},e={object:void 0,type:"update",name:void 0,oldValue:void 0},f=Number.isNaN||function(a){return"number"==typeof a&&isNaN(a)},g={createPropertyObserver:function(){var a=this._observeNames;if(a&&a.length){var b=this._propertyObserver=new CompoundObserver(!0);this.registerObserver(b);for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.addPath(this,c),this.observeArrayValue(c,this[c],null)}},openPropertyObserver:function(){this._propertyObserver&&this._propertyObserver.open(this.notifyPropertyChanges,this)},notifyPropertyChanges:function(a,b,c){var d,e,f={};for(var g in b)if(d=c[2*g+1],e=this.observe[d]){var h=b[g],i=a[g];this.observeArrayValue(d,i,h),f[e]||(void 0!==h&&null!==h||void 0!==i&&null!==i)&&(f[e]=!0,this.invokeMethod(e,[h,i,arguments]))}},invokeMethod:function(a,b){var c=this[a]||a;"function"==typeof c&&c.apply(this,b)},deliverChanges:function(){this._propertyObserver&&this._propertyObserver.deliver()},observeArrayValue:function(a,b,c){var e=this.observe[a];if(e&&(Array.isArray(c)&&(d.observe&&console.log("[%s] observeArrayValue: unregister observer [%s]",this.localName,a),this.closeNamedObserver(a+"__array")),Array.isArray(b))){d.observe&&console.log("[%s] observeArrayValue: register observer [%s]",this.localName,a,b);var f=new ArrayObserver(b);f.open(function(a){this.invokeMethod(e,[a])},this),this.registerNamedObserver(a+"__array",f)}},emitPropertyChangeRecord:function(a,c,d){if(!b(c,d)&&(this._propertyChanged(a,c,d),Observer.hasObjectObserve)){var f=this._objectNotifier;f||(f=this._objectNotifier=Object.getNotifier(this)),e.object=this,e.name=a,e.oldValue=d,f.notify(e)}},_propertyChanged:function(a){this.reflect[a]&&this.reflectPropertyToAttribute(a)},bindProperty:function(a,b,d){if(d)return void(this[a]=b);var e=this.element.prototype.computed;if(e&&e[a]){var f=a+"ComputedBoundObservable_";return void(this[f]=b)}return this.bindToAccessor(a,b,c)},bindToAccessor:function(a,c,d){function e(b,c){j[f]=b;var d=j[h];d&&"function"==typeof d.setValue&&d.setValue(b),j.emitPropertyChangeRecord(a,b,c)}var f=a+"_",g=a+"Observable_",h=a+"ComputedBoundObservable_";this[g]=c;var i=this[f],j=this,k=c.open(e);if(d&&!b(i,k)){var l=d(i,k);b(k,l)||(k=l,c.setValue&&c.setValue(k))}e(k,i);var m={close:function(){c.close(),j[g]=void 0,j[h]=void 0}};return this.registerObserver(m),m},createComputedProperties:function(){if(this._computedNames)for(var a=0;ae&&(c=d[e]);e++)b[c.id]=c},onMutation:function(a,b){var c=new MutationObserver(function(a){b.call(this,c,a),c.disconnect()}.bind(this));c.observe(a,{childList:!0,subtree:!0})}};c.prototype=d,d.constructor=c,a.Base=c,a.isBase=b,a.api.instance.base=d}(Polymer),function(a){function b(a){return a.__proto__}function c(a,b){var c="",d=!1;b&&(c=b.localName,d=b.hasAttribute("is"));var e=WebComponents.ShadowCSS.makeScopeSelector(c,d);return WebComponents.ShadowCSS.shimCssText(a,e)}var d=(window.WebComponents?WebComponents.flags.log:{},window.ShadowDOMPolyfill),e="element",f="controller",g={STYLE_SCOPE_ATTRIBUTE:e,installControllerStyles:function(){var a=this.findStyleScope();if(a&&!this.scopeHasNamedStyle(a,this.localName)){for(var c=b(this),d="";c&&c.element;)d+=c.element.cssTextForScope(f),c=b(c);d&&this.installScopeCssText(d,a)}},installScopeStyle:function(a,b,c){var c=c||this.findStyleScope(),b=b||"";if(c&&!this.scopeHasNamedStyle(c,this.localName+b)){var d="";if(a instanceof Array)for(var e,f=0,g=a.length;g>f&&(e=a[f]);f++)d+=e.textContent+"\n\n";else d=a.textContent;this.installScopeCssText(d,c,b)}},installScopeCssText:function(a,b,e){if(b=b||this.findStyleScope(),e=e||"",b){d&&(a=c(a,b.host));var g=this.element.cssTextToScopeStyle(a,f);Polymer.applyStyleToScope(g,b),this.styleCacheForScope(b)[this.localName+e]=!0}},findStyleScope:function(a){for(var b=a||this;b.parentNode;)b=b.parentNode;return b},scopeHasNamedStyle:function(a,b){var c=this.styleCacheForScope(a);return c[b]},styleCacheForScope:function(a){if(d){var b=a.host?a.host.localName:a.localName;return h[b]||(h[b]={})}return a._scopeStyles=a._scopeStyles||{}}},h={};a.api.instance.styles=g}(Polymer),function(a){function b(a,b){if("string"!=typeof a){var c=b||document._currentScript;if(b=a,a=c&&c.parentNode&&c.parentNode.getAttribute?c.parentNode.getAttribute("name"):"",!a)throw"Element name could not be inferred."}if(f(a))throw"Already registered (Polymer) prototype for element "+a;e(a,b),d(a)}function c(a,b){i[a]=b}function d(a){i[a]&&(i[a].registerWhenReady(),delete i[a])}function e(a,b){return j[a]=b||{}}function f(a){return j[a]}function g(a,b){if("string"!=typeof b)return!1;var c=HTMLElement.getPrototypeForTag(b),d=c&&c.constructor;return d?CustomElements.instanceof?CustomElements.instanceof(a,d):a instanceof d:!1}var h=a.extend,i=(a.api,{}),j={};a.getRegisteredPrototype=f,a.waitingForPrototype=c,a.instanceOfType=g,window.Polymer=b,h(Polymer,a),WebComponents.consumeDeclarations&&WebComponents.consumeDeclarations(function(a){if(a)for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.apply(null,c)})}(Polymer),function(a){var b={resolveElementPaths:function(a){Polymer.urlResolver.resolveDom(a)},addResolvePathApi:function(){var a=this.getAttribute("assetpath")||"",b=new URL(a,this.ownerDocument.baseURI);this.prototype.resolvePath=function(a,c){var d=new URL(a,c||b);return d.href}}};a.api.declaration.path=b}(Polymer),function(a){function b(a,b){var c=new URL(a.getAttribute("href"),b).href;return"@import '"+c+"';"}function c(a,b){if(a){b===document&&(b=document.head),i&&(b=document.head);var c=d(a.textContent),e=a.getAttribute(h);e&&c.setAttribute(h,e);var f=b.firstElementChild;if(b===document.head){var g="style["+h+"]",j=document.head.querySelectorAll(g);j.length&&(f=j[j.length-1].nextElementSibling)}b.insertBefore(c,f)}}function d(a,b){b=b||document,b=b.createElement?b:b.ownerDocument;var c=b.createElement("style");return c.textContent=a,c}function e(a){return a&&a.__resource||""}function f(a,b){return q?q.call(a,b):void 0}var g=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.styles),h=g.STYLE_SCOPE_ATTRIBUTE,i=window.ShadowDOMPolyfill,j="style",k="@import",l="link[rel=stylesheet]",m="global",n="polymer-scope",o={loadStyles:function(a){var b=this.fetchTemplate(),c=b&&this.templateContent();if(c){this.convertSheetsToStyles(c);var d=this.findLoadableStyles(c);if(d.length){var e=b.ownerDocument.baseURI;return Polymer.styleResolver.loadStyles(d,e,a)}}a&&a()},convertSheetsToStyles:function(a){for(var c,e,f=a.querySelectorAll(l),g=0,h=f.length;h>g&&(c=f[g]);g++)e=d(b(c,this.ownerDocument.baseURI),this.ownerDocument),this.copySheetAttributes(e,c),c.parentNode.replaceChild(e,c)},copySheetAttributes:function(a,b){for(var c,d=0,e=b.attributes,f=e.length;(c=e[d])&&f>d;d++)"rel"!==c.name&&"href"!==c.name&&a.setAttribute(c.name,c.value)},findLoadableStyles:function(a){var b=[];if(a)for(var c,d=a.querySelectorAll(j),e=0,f=d.length;f>e&&(c=d[e]);e++)c.textContent.match(k)&&b.push(c);return b},installSheets:function(){this.cacheSheets(),this.cacheStyles(),this.installLocalSheets(),this.installGlobalStyles()},cacheSheets:function(){this.sheets=this.findNodes(l),this.sheets.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},cacheStyles:function(){this.styles=this.findNodes(j+"["+n+"]"),this.styles.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},installLocalSheets:function(){var a=this.sheets.filter(function(a){return!a.hasAttribute(n)}),b=this.templateContent();if(b){var c="";if(a.forEach(function(a){c+=e(a)+"\n"}),c){var f=d(c,this.ownerDocument);b.insertBefore(f,b.firstChild)}}},findNodes:function(a,b){var c=this.querySelectorAll(a).array(),d=this.templateContent();if(d){var e=d.querySelectorAll(a).array();c=c.concat(e)}return b?c.filter(b):c},installGlobalStyles:function(){var a=this.styleForScope(m);c(a,document.head)},cssTextForScope:function(a){var b="",c="["+n+"="+a+"]",d=function(a){return f(a,c)},g=this.sheets.filter(d);g.forEach(function(a){b+=e(a)+"\n\n"});var h=this.styles.filter(d);return h.forEach(function(a){b+=a.textContent+"\n\n"}),b},styleForScope:function(a){var b=this.cssTextForScope(a);return this.cssTextToScopeStyle(b,a)},cssTextToScopeStyle:function(a,b){if(a){var c=d(a);return c.setAttribute(h,this.getAttribute("name")+"-"+b),c}}},p=HTMLElement.prototype,q=p.matches||p.matchesSelector||p.webkitMatchesSelector||p.mozMatchesSelector;a.api.declaration.styles=o,a.applyStyleToScope=c}(Polymer),function(a){var b=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.events),c=b.EVENT_PREFIX,d={};["webkitAnimationStart","webkitAnimationEnd","webkitTransitionEnd","DOMFocusOut","DOMFocusIn","DOMMouseScroll"].forEach(function(a){d[a.toLowerCase()]=a});var e={parseHostEvents:function(){var a=this.prototype.eventDelegates;this.addAttributeDelegates(a)},addAttributeDelegates:function(a){for(var b,c=0;b=this.attributes[c];c++)this.hasEventPrefix(b.name)&&(a[this.removeEventPrefix(b.name)]=b.value.replace("{{","").replace("}}","").trim())},hasEventPrefix:function(a){return a&&"o"===a[0]&&"n"===a[1]&&"-"===a[2]},removeEventPrefix:function(a){return a.slice(f)},findController:function(a){for(;a.parentNode;){if(a.eventController)return a.eventController;a=a.parentNode}return a.host},getEventHandler:function(a,b,c){var d=this;return function(e){a&&a.PolymerBase||(a=d.findController(b));var f=[e,e.detail,e.currentTarget];a.dispatchMethod(a,c,f)}},prepareEventBinding:function(a,b){if(this.hasEventPrefix(b)){var c=this.removeEventPrefix(b);c=d[c]||c;var e=this;return function(b,d,f){function g(){return"{{ "+a+" }}"}var h=e.getEventHandler(void 0,d,a);return PolymerGestures.addEventListener(d,c,h),f?void 0:{open:g,discardChanges:g,close:function(){PolymerGestures.removeEventListener(d,c,h)}}}}}},f=c.length;a.api.declaration.events=e}(Polymer),function(a){var b=["attribute"],c={inferObservers:function(a){var b,c=a.observe;for(var d in a)"Changed"===d.slice(-7)&&(b=d.slice(0,-7),this.canObserveProperty(b)&&(c||(c=a.observe={}),c[b]=c[b]||d))},canObserveProperty:function(a){return b.indexOf(a)<0},explodeObservers:function(a){var b=a.observe;if(b){var c={};for(var d in b)for(var e,f=d.split(" "),g=0;e=f[g];g++)c[e]=b[d];a.observe=c}},optimizePropertyMaps:function(a){if(a.observe){var b=a._observeNames=[];for(var c in a.observe)for(var d,e=c.split(" "),f=0;d=e[f];f++)b.push(d)}if(a.publish){var b=a._publishNames=[];for(var c in a.publish)b.push(c)}if(a.computed){var b=a._computedNames=[];for(var c in a.computed)b.push(c)}},publishProperties:function(a,b){var c=a.publish;c&&(this.requireProperties(c,a,b),this.filterInvalidAccessorNames(c),a._publishLC=this.lowerCaseMap(c));var d=a.computed;d&&this.filterInvalidAccessorNames(d)},filterInvalidAccessorNames:function(a){for(var b in a)this.propertyNameBlacklist[b]&&(console.warn('Cannot define property "'+b+'" for element "'+this.name+'" because it has the same name as an HTMLElement property, and not all browsers support overriding that. Consider giving it a different name.'),delete a[b])},requireProperties:function(a,b){b.reflect=b.reflect||{};for(var c in a){var d=a[c];d&&void 0!==d.reflect&&(b.reflect[c]=Boolean(d.reflect),d=d.value),void 0!==d&&(b[c]=d)}},lowerCaseMap:function(a){var b={};for(var c in a)b[c.toLowerCase()]=c;return b},createPropertyAccessor:function(a,b){var c=this.prototype,d=a+"_",e=a+"Observable_";c[d]=c[a],Object.defineProperty(c,a,{get:function(){var a=this[e];return a&&a.deliver(),this[d]},set:function(c){if(b)return this[d];var f=this[e];if(f)return void f.setValue(c);var g=this[d];return this[d]=c,this.emitPropertyChangeRecord(a,c,g),c},configurable:!0})},createPropertyAccessors:function(a){var b=a._computedNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)this.createPropertyAccessor(c,!0);var b=a._publishNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)a.computed&&a.computed[c]||this.createPropertyAccessor(c)},propertyNameBlacklist:{children:1,"class":1,id:1,hidden:1,style:1,title:1}};a.api.declaration.properties=c}(Polymer),function(a){var b="attributes",c=/\s|,/,d={inheritAttributesObjects:function(a){this.inheritObject(a,"publishLC"),this.inheritObject(a,"_instanceAttributes")},publishAttributes:function(a){var d=this.getAttribute(b);if(d)for(var e,f=a.publish||(a.publish={}),g=d.split(c),h=0,i=g.length;i>h;h++)e=g[h].trim(),e&&void 0===f[e]&&(f[e]=void 0)},accumulateInstanceAttributes:function(){for(var a,b=this.prototype._instanceAttributes,c=this.attributes,d=0,e=c.length;e>d&&(a=c[d]);d++)this.isInstanceAttribute(a.name)&&(b[a.name]=a.value)},isInstanceAttribute:function(a){return!this.blackList[a]&&"on-"!==a.slice(0,3)},blackList:{name:1,"extends":1,constructor:1,noscript:1,assetpath:1,"cache-csstext":1}};d.blackList[b]=1,a.api.declaration.attributes=d}(Polymer),function(a){var b=a.api.declaration.events,c=new PolymerExpressions,d=c.prepareBinding;c.prepareBinding=function(a,e,f){return b.prepareEventBinding(a,e,f)||d.call(c,a,e,f)};var e={syntax:c,fetchTemplate:function(){return this.querySelector("template")},templateContent:function(){var a=this.fetchTemplate();return a&&a.content},installBindingDelegate:function(a){a&&(a.bindingDelegate=this.syntax)}};a.api.declaration.mdv=e}(Polymer),function(a){function b(a){if(!Object.__proto__){var b=Object.getPrototypeOf(a);a.__proto__=b,d(b)&&(b.__proto__=Object.getPrototypeOf(b))}}var c=a.api,d=a.isBase,e=a.extend,f=window.ShadowDOMPolyfill,g={register:function(a,b){this.buildPrototype(a,b),this.registerPrototype(a,b),this.publishConstructor()},buildPrototype:function(b,c){var d=a.getRegisteredPrototype(b),e=this.generateBasePrototype(c);this.desugarBeforeChaining(d,e),this.prototype=this.chainPrototypes(d,e),this.desugarAfterChaining(b,c)},desugarBeforeChaining:function(a,b){a.element=this,this.publishAttributes(a,b),this.publishProperties(a,b),this.inferObservers(a),this.explodeObservers(a)},chainPrototypes:function(a,c){this.inheritMetaData(a,c);var d=this.chainObject(a,c);return b(d),d},inheritMetaData:function(a,b){this.inheritObject("observe",a,b),this.inheritObject("publish",a,b),this.inheritObject("reflect",a,b),this.inheritObject("_publishLC",a,b),this.inheritObject("_instanceAttributes",a,b),this.inheritObject("eventDelegates",a,b)},desugarAfterChaining:function(a,b){this.optimizePropertyMaps(this.prototype),this.createPropertyAccessors(this.prototype),this.installBindingDelegate(this.fetchTemplate()),this.installSheets(),this.resolveElementPaths(this),this.accumulateInstanceAttributes(),this.parseHostEvents(),this.addResolvePathApi(),f&&WebComponents.ShadowCSS.shimStyling(this.templateContent(),a,b),this.prototype.registerCallback&&this.prototype.registerCallback(this)},publishConstructor:function(){var a=this.getAttribute("constructor");a&&(window[a]=this.ctor)},generateBasePrototype:function(a){var b=this.findBasePrototype(a);if(!b){var b=HTMLElement.getPrototypeForTag(a);b=this.ensureBaseApi(b),h[a]=b}return b},findBasePrototype:function(a){return h[a]},ensureBaseApi:function(a){if(a.PolymerBase)return a;var b=Object.create(a);return c.publish(c.instance,b),this.mixinMethod(b,a,c.instance.mdv,"bind"),b},mixinMethod:function(a,b,c,d){var e=function(a){return b[d].apply(this,a)};a[d]=function(){return this.mixinSuper=e,c[d].apply(this,arguments)}},inheritObject:function(a,b,c){var d=b[a]||{};b[a]=this.chainObject(d,c[a])},registerPrototype:function(a,b){var c={prototype:this.prototype},d=this.findTypeExtension(b);d&&(c.extends=d),HTMLElement.register(a,this.prototype),this.ctor=document.registerElement(a,c)},findTypeExtension:function(a){if(a&&a.indexOf("-")<0)return a;var b=this.findBasePrototype(a);return b.element?this.findTypeExtension(b.element.extends):void 0}},h={};g.chainObject=Object.__proto__?function(a,b){return a&&b&&a!==b&&(a.__proto__=b),a}:function(a,b){if(a&&b&&a!==b){var c=Object.create(b);a=e(c,a)}return a},c.declaration.prototype=g}(Polymer),function(a){function b(a){return document.contains(a)?j:i}function c(){return i.length?i[0]:j[0]}function d(a){f.waitToReady=!0,Polymer.endOfMicrotask(function(){HTMLImports.whenReady(function(){f.addReadyCallback(a),f.waitToReady=!1,f.check()})})}function e(a){if(void 0===a)return void f.ready();var b=setTimeout(function(){f.ready()},a);Polymer.whenReady(function(){clearTimeout(b)})}var f={wait:function(a){a.__queue||(a.__queue={},g.push(a))},enqueue:function(a,c,d){var e=a.__queue&&!a.__queue.check;return e&&(b(a).push(a),a.__queue.check=c,a.__queue.go=d),0!==this.indexOf(a)},indexOf:function(a){var c=b(a).indexOf(a);return c>=0&&document.contains(a)&&(c+=HTMLImports.useNative||HTMLImports.ready?i.length:1e9),c},go:function(a){var b=this.remove(a);b&&(a.__queue.flushable=!0,this.addToFlushQueue(b),this.check())},remove:function(a){var c=this.indexOf(a);if(0===c)return b(a).shift()},check:function(){var a=this.nextElement();return a&&a.__queue.check.call(a),this.canReady()?(this.ready(),!0):void 0},nextElement:function(){return c()},canReady:function(){return!this.waitToReady&&this.isEmpty()},isEmpty:function(){for(var a,b=0,c=g.length;c>b&&(a=g[b]);b++)if(a.__queue&&!a.__queue.flushable)return;return!0},addToFlushQueue:function(a){h.push(a)},flush:function(){if(!this.flushing){this.flushing=!0;for(var a;h.length;)a=h.shift(),a.__queue.go.call(a),a.__queue=null;this.flushing=!1}},ready:function(){var a=CustomElements.ready;CustomElements.ready=!1,this.flush(),CustomElements.useNative||CustomElements.upgradeDocumentTree(document),CustomElements.ready=a,Polymer.flush(),requestAnimationFrame(this.flushReadyCallbacks)},addReadyCallback:function(a){a&&k.push(a)},flushReadyCallbacks:function(){if(k)for(var a;k.length;)(a=k.shift())()},waitingFor:function(){for(var a,b=[],c=0,d=g.length;d>c&&(a=g[c]);c++)a.__queue&&!a.__queue.flushable&&b.push(a);return b},waitToReady:!0},g=[],h=[],i=[],j=[],k=[];a.elements=g,a.waitingFor=f.waitingFor.bind(f),a.forceReady=e,a.queue=f,a.whenReady=a.whenPolymerReady=d}(Polymer),function(a){function b(a){return Boolean(HTMLElement.getPrototypeForTag(a))}function c(a){return a&&a.indexOf("-")>=0}var d=a.extend,e=a.api,f=a.queue,g=a.whenReady,h=a.getRegisteredPrototype,i=a.waitingForPrototype,j=d(Object.create(HTMLElement.prototype),{createdCallback:function(){this.getAttribute("name")&&this.init()},init:function(){this.name=this.getAttribute("name"),this.extends=this.getAttribute("extends"),f.wait(this),this.loadResources(),this.registerWhenReady()},registerWhenReady:function(){this.registered||this.waitingForPrototype(this.name)||this.waitingForQueue()||this.waitingForResources()||f.go(this)},_register:function(){c(this.extends)&&!b(this.extends)&&console.warn("%s is attempting to extend %s, an unregistered element or one that was not registered with Polymer.",this.name,this.extends),this.register(this.name,this.extends),this.registered=!0},waitingForPrototype:function(a){return h(a)?void 0:(i(a,this),this.handleNoScript(a),!0)},handleNoScript:function(a){this.hasAttribute("noscript")&&!this.noscript&&(this.noscript=!0,Polymer(a))},waitingForResources:function(){return this._needsResources},waitingForQueue:function(){return f.enqueue(this,this.registerWhenReady,this._register)},loadResources:function(){this._needsResources=!0,this.loadStyles(function(){this._needsResources=!1,this.registerWhenReady()}.bind(this))}});e.publish(e.declaration,j),g(function(){document.body.removeAttribute("unresolved"),document.dispatchEvent(new CustomEvent("polymer-ready",{bubbles:!0}))}),document.registerElement("polymer-element",{prototype:j})}(Polymer),function(a){function b(a,b){a?(document.head.appendChild(a),d(b)):b&&b()}function c(a,c){if(a&&a.length){for(var d,e,f=document.createDocumentFragment(),g=0,h=a.length;h>g&&(d=a[g]);g++)e=document.createElement("link"),e.rel="import",e.href=d,f.appendChild(e);b(f,c)}else c&&c()}var d=a.whenReady;a.import=c,a.importElements=b}(Polymer),function(){var a=document.createElement("polymer-element");a.setAttribute("name","auto-binding"),a.setAttribute("extends","template"),a.init(),Polymer("auto-binding",{createdCallback:function(){this.syntax=this.bindingDelegate=this.makeSyntax(),Polymer.whenPolymerReady(function(){this.model=this,this.setAttribute("bind",""),this.async(function(){this.marshalNodeReferences(this.parentNode),this.fire("template-bound")})}.bind(this))},makeSyntax:function(){var a=Object.create(Polymer.api.declaration.events),b=this;a.findController=function(){return b.model};var c=new PolymerExpressions,d=c.prepareBinding;return c.prepareBinding=function(b,e,f){return a.prepareEventBinding(b,e,f)||d.call(c,b,e,f)},c}})}(); + - + - diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index 3bccf223179..5e9ee3d0f23 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -279,8 +279,18 @@ }, call_api: function(method, path, parameters, onSuccess, onError) { + var url = "/api/" + path; + + // set to true to generate a frontend to be used as demo on the website + if (false) { + if (path === "states" || path === "services" || path === "events") { + url = "/demo/" + path + ".json"; + } else { + return; + } + } var req = new XMLHttpRequest(); - req.open(method, "/api/" + path, true); + req.open(method, url, true); req.setRequestHeader("X-HA-access", this.auth); req.onreadystatechange = function() { From 0cfae7245bf17f406d2d8e075425846b81a335ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Dec 2014 22:20:17 -0800 Subject: [PATCH 55/60] Add link to demo in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5803b165d95..8f961c1321e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) -This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). +This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a fully functioning frontend of Home Assistant, [click here](https://home-assistant.io/demo/). Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. @@ -25,7 +25,7 @@ Home Assistant also includes functionality for controlling HTPCs: * Download files * Open URLs in the default browser -![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png) +[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/) The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](#architecture) and the [section on customizing](#customizing). From da4ab542a7233479a10f007b7af989717cb8b3d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Dec 2014 22:21:32 -0800 Subject: [PATCH 56/60] Update documentation for example component --- config/custom_components/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index b47f2706f9e..ee422174377 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup(hass, config): - """ Setup christmas. """ + """ Setup example component. """ # Validate that all required config options are given if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): From 5f7a9d9918d46e28d145f417067e4c6a858011b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Dec 2014 23:42:21 -0800 Subject: [PATCH 57/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f961c1321e..62d20f7df4c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) -This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a fully functioning frontend of Home Assistant, [click here](https://home-assistant.io/demo/). +This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/). Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. From 029c38874b59dea2c0f49f1d5cc432eec84c08ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Dec 2014 21:12:42 -0800 Subject: [PATCH 58/60] State card rendering now way more flexible Decoupled state card into reusable components. Now able to have different states be rendered with different state card components. --- homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 28 ++++-- .../http/www_static/polymer/bower.json | 3 +- .../polymer/home-assistant-api.html | 17 +++- .../polymer/state-card-display.html | 30 +++++++ ...state-card.html => state-card-toggle.html} | 86 +++---------------- .../http/www_static/polymer/state-info.html | 74 ++++++++++++++++ .../http/www_static/polymer/states-cards.html | 51 ++++++++--- 8 files changed, 191 insertions(+), 100 deletions(-) create mode 100755 homeassistant/components/http/www_static/polymer/state-card-display.html rename homeassistant/components/http/www_static/polymer/{state-card.html => state-card-toggle.html} (57%) create mode 100755 homeassistant/components/http/www_static/polymer/state-info.html diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index cc7632f5523..2ec7cc69630 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "43a1e2517105b676c05bb1b8dce357b3" +VERSION = "78343829ea70bf07a9e939b321587122" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index d1b3c2e9de5..35d51134f0c 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -49,16 +49,30 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t diff --git a/homeassistant/components/http/www_static/polymer/bower.json b/homeassistant/components/http/www_static/polymer/bower.json index aca60a6912f..24f472ca259 100644 --- a/homeassistant/components/http/www_static/polymer/bower.json +++ b/homeassistant/components/http/www_static/polymer/bower.json @@ -32,6 +32,7 @@ "paper-menu-button": "polymer/paper-menu-button#~0.5.2", "paper-dropdown": "polymer/paper-dropdown#~0.5.2", "paper-item": "polymer/paper-item#~0.5.2", - "moment": "~2.8.4" + "moment": "~2.8.4", + "core-style": "polymer/core-style#~0.5.2" } } diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html index 5e9ee3d0f23..4e9c5729dba 100644 --- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html +++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html @@ -35,7 +35,7 @@ }; Object.defineProperties(State.prototype, { - "stateDisplay": { + stateDisplay: { get: function() { var state = this.state.replace(/_/g, " "); if(this.attributes.unit_of_measurement) { @@ -46,19 +46,30 @@ } }, - "isCustomGroup": { + isCustomGroup: { get: function() { return this.domain == "group" && !this.attributes.auto; } }, - "canToggle": { + canToggle: { get: function() { // groups that have the on/off state or if there is a turn_on service return ((this.domain == 'group' && (this.state == 'on' || this.state == 'off')) || this.api.hasService(this.domain, 'turn_on')); } + }, + + // how to render the card for this state + cardType: { + get: function() { + if(this.canToggle) { + return "toggle"; + } else { + return "display"; + } + } } }); diff --git a/homeassistant/components/http/www_static/polymer/state-card-display.html b/homeassistant/components/http/www_static/polymer/state-card-display.html new file mode 100755 index 00000000000..d54800c0af5 --- /dev/null +++ b/homeassistant/components/http/www_static/polymer/state-card-display.html @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/homeassistant/components/http/www_static/polymer/state-card.html b/homeassistant/components/http/www_static/polymer/state-card-toggle.html similarity index 57% rename from homeassistant/components/http/www_static/polymer/state-card.html rename to homeassistant/components/http/www_static/polymer/state-card-toggle.html index 8b0be4dc076..7c7b050d517 100755 --- a/homeassistant/components/http/www_static/polymer/state-card.html +++ b/homeassistant/components/http/www_static/polymer/state-card-toggle.html @@ -1,55 +1,19 @@ - - + - + + + diff --git a/homeassistant/components/http/www_static/polymer/states-cards.html b/homeassistant/components/http/www_static/polymer/states-cards.html index 4727852b113..1710c52e5fd 100755 --- a/homeassistant/components/http/www_static/polymer/states-cards.html +++ b/homeassistant/components/http/www_static/polymer/states-cards.html @@ -1,5 +1,7 @@ - + + +