From cbbda2db55cfd87700c5436d7cc621cd671a7172 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Jan 2015 07:49:49 -0800 Subject: [PATCH 01/16] Pull in PyWemo bugfixes --- homeassistant/external/pywemo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index 687fc493096..81735d47c33 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit 687fc4930967da6b2aa258a0e6bb0c4026a1907c +Subproject commit 81735d47c33a5dfa463d66ae315619fae84a6164 From fc33273464fe9b43af33d93ddfa46cc8157b1e3d Mon Sep 17 00:00:00 2001 From: Karsten Nerdinger Date: Wed, 7 Jan 2015 03:36:39 +0100 Subject: [PATCH 02/16] Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18. --- homeassistant/components/device_tracker/luci.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 9ed73f21375..f3567ab17f3 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -101,7 +101,10 @@ class LuciDeviceScanner(object): result = _req_json_rpc(url, 'net.arptable', params={'auth': self.token}) if result: - self.last_results = [x['HW address'] for x in result] + self.last_results = [] + for x in result: + # Check if the Flags for each device contain NUD_REACHABLE and if so, add it to last_results + if int(x['Flags'], 16) & 0x2: self.last_results.append(x['HW address']) return True From 2c5886f6d4f06be1269128f88b5d7c8104a305db Mon Sep 17 00:00:00 2001 From: Karsten Nerdinger Date: Wed, 7 Jan 2015 03:57:06 +0100 Subject: [PATCH 03/16] Fix warnings from flake8 and pylint --- homeassistant/components/device_tracker/luci.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index f3567ab17f3..9dbf503fc94 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -102,9 +102,11 @@ class LuciDeviceScanner(object): params={'auth': self.token}) if result: self.last_results = [] - for x in result: - # Check if the Flags for each device contain NUD_REACHABLE and if so, add it to last_results - if int(x['Flags'], 16) & 0x2: self.last_results.append(x['HW address']) + for device_entry in result: + # Check if the Flags for each device contain + # NUD_REACHABLE and if so, add it to last_results + if int(device_entry['Flags'], 16) & 0x2: + self.last_results.append(device_entry['HW address']) return True From efe820628ce08c91488d8c9a5e8cb5086c4ef6d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Jan 2015 20:52:26 -0800 Subject: [PATCH 04/16] Updated PyWemo to latest version --- homeassistant/external/pywemo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index 81735d47c33..6355e04357c 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit 81735d47c33a5dfa463d66ae315619fae84a6164 +Subproject commit 6355e04357cf78b38d293fae7bd418cf9f8d1ca0 From f5683797aa6749f966f6cc10553d8e055a0bac72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Jan 2015 18:45:27 -0800 Subject: [PATCH 05/16] Reorganized the main to be more modular --- homeassistant/__main__.py | 45 +++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index c8d50151cea..edda63b72c0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -18,19 +18,8 @@ except ImportError: from homeassistant import bootstrap -def main(): - """ Starts Home Assistant. Will create demo config if no config found. """ - - parser = argparse.ArgumentParser() - parser.add_argument( - '-c', '--config', - metavar='path_to_config_dir', - default="config", - help="Directory that contains the Home Assistant configuration") - - args = parser.parse_args() - - # Validate that all core dependencies are installed +def validate_dependencies(): + """ Validate all dependencies that HA uses. """ import_fail = False for module in ['requests']: @@ -44,11 +33,14 @@ def main(): if import_fail: print(("Install dependencies by running: " "pip3 install -r requirements.txt")) - exit() + sys.exit() + + +def ensure_config_path(config_dir): + """ Gets the path to the configuration file. + Creates one if it not exists. """ # Test if configuration directory exists - config_dir = os.path.join(os.getcwd(), args.config) - if not os.path.isdir(config_dir): print(('Fatal Error: Unable to find specified configuration ' 'directory {} ').format(config_dir)) @@ -68,6 +60,27 @@ def main(): 'to write a default one to {}').format(config_path)) sys.exit() + return config_path + + +def main(): + """ Starts Home Assistant. Will create demo config if no config found. """ + + parser = argparse.ArgumentParser() + parser.add_argument( + '-c', '--config', + metavar='path_to_config_dir', + default="config", + help="Directory that contains the Home Assistant configuration") + + args = parser.parse_args() + + validate_dependencies() + + config_dir = os.path.join(os.getcwd(), args.config) + + config_path = ensure_config_path(config_dir) + hass = bootstrap.from_config_file(config_path) hass.start() hass.block_till_stopped() From e0b424c88f4c258ff763cb5db773fedfcfe95934 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Jan 2015 20:02:34 -0800 Subject: [PATCH 06/16] Make group component more flexible --- ha_test/test_component_group.py | 45 ++--- .../components/device_tracker/__init__.py | 9 +- homeassistant/components/group.py | 160 +++++++++--------- homeassistant/components/light/__init__.py | 3 +- homeassistant/components/switch/__init__.py | 3 +- homeassistant/const.py | 1 + 6 files changed, 106 insertions(+), 115 deletions(-) diff --git a/ha_test/test_component_group.py b/ha_test/test_component_group.py index 4e6307aa2b5..46ae8942602 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 -from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME +from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN import homeassistant.components.group as group @@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_group(self): - """ Test setup_group method. """ - # Try to setup a group with mixed groupable states + def test_setup_group_with_mixed_groupable_states(self): + """ Try to setup a group with mixed groupable states """ self.hass.states.set('device_tracker.Paulus', STATE_HOME) - self.assertTrue(group.setup_group( + group.setup_group( self.hass, 'person_and_light', - ['light.Bowl', 'device_tracker.Paulus'])) + ['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( + def test_setup_group_with_a_non_existing_state(self): + """ Try to setup a group with a non existing state """ + grp = 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) + ['light.Bowl', 'non.existing']) - # Try to setup a group with non groupable states + self.assertEqual(STATE_ON, grp.state.state) + + def test_setup_group_with_non_groupable_states(self): 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', [])) + grp = group.setup_group( + self.hass, 'chromecasts', + ['cast.living_room', 'cast.bedroom']) + + self.assertEqual(STATE_UNKNOWN, grp.state.state) + + def test_setup_empty_group(self): + """ Try to setup an empty group. """ + grp = group.setup_group(self.hass, 'nothing', []) + + self.assertEqual(STATE_UNKNOWN, grp.state.state) def test_monitor_group(self): """ Test if the group keeps track of states. """ diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c478e118036..3da9054be8d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -111,19 +111,16 @@ class DeviceTracker(object): """ Triggers update of the device states. """ self.update_devices(now) + dev_group = group.Group(hass, GROUP_NAME_ALL_DEVICES) + # 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) + dev_group.update_tracked_entity_ids(self.device_entity_ids) reload_known_devices_service(None) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index eac63ee845b..41d8e0ae056 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -5,12 +5,11 @@ homeassistant.components.groups 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.const import ( - ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, + STATE_UNKNOWN) DOMAIN = "group" DEPENDENCIES = [] @@ -22,8 +21,6 @@ ATTR_AUTO = "auto" # List of ON/OFF state tuples for groupable states _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)] -_GROUPS = {} - def _get_group_on_off(state): """ Determine the group on/off states based on a state. """ @@ -101,114 +98,109 @@ def setup(hass, config): return True -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__) +class Group(object): + """ Tracks a group of entity ids. """ + def __init__(self, hass, name, entity_ids=None, user_defined=True): + self.hass = hass + self.name = name + self.user_defined = user_defined + self.entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) - # In case an iterable is passed in - entity_ids = list(entity_ids) + self.tracking = [] + self.group_on, self.group_off = None, None - if not entity_ids: - logger.error( - 'Error setting up group %s: no entities passed in to track', name) + if entity_ids is not None: + self.update_tracked_entity_ids(entity_ids) - return False + @property + def state(self): + """ Return the current state from the group. """ + return self.hass.states.get(self.entity_id) - # Loop over the given entities to: - # - determine which group type this is (on_off, device_home) - # - 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 + @property + def state_attr(self): + """ State attributes of this group. """ + return { + ATTR_ENTITY_ID: self.tracking, + ATTR_AUTO: not self.user_defined + } - for entity_id in entity_ids: - state = hass.states.get(entity_id) + def update_tracked_entity_ids(self, entity_ids): + """ Update the tracked entity IDs. """ + self.stop() - # Try to determine group type if we didn't yet - if group_on is None and state: - group_on, group_off = _get_group_on_off(state.state) + self.tracking = list(entity_ids) + self.group_on, self.group_off = None, None - if group_on is None: - # We did not find a matching group_type - warnings.append( - "Entity {} has ungroupable state '{}'".format( - name, state.state)) + self.force_update() - continue + self.start() - # Check if entity exists - if not state: - warnings.append("Entity {} does not exist".format(entity_id)) + def force_update(self): + """ Query all the tracked states and update group state. """ + for entity_id in self.tracking: + state = self.hass.states.get(entity_id) - # Check if entity is invalid state - elif state.state != group_off and state.state != group_on: + if state is not None: + self._update_group_state(state.entity_id, None, state) - warnings.append("State of {} is {} (expected: {} or {})".format( - entity_id, state.state, group_off, group_on)) + # If parsing the entitys did not result in a state, set UNKNOWN + if self.state is None: + self.hass.states.set(self.entity_id, STATE_UNKNOWN) - # We have a valid group state - else: - group_ids.append(entity_id) + def start(self): + """ Starts the tracking. """ + self.hass.states.track_change(self.tracking, self._update_group_state) - # Keep track of the group state to init later on - group_state = group_state or state.state == group_on + def stop(self): + """ Unregisters the group from Home Assistant. """ + self.hass.states.remove(self.entity_id) - # 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: group_ids, ATTR_AUTO: not user_defined} + self.hass.bus.remove_listener( + ha.EVENT_STATE_CHANGED, self._update_group_state) # pylint: disable=unused-argument - def update_group_state(entity_id, old_state, new_state): + def _update_group_state(self, 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 + # We have not determined type of group yet + if self.group_on is None: + self.group_on, self.group_off = _get_group_on_off(new_state.state) + + if self.group_on is not None: + # New state of the group is going to be based on the first + # state that we can recognize + self.hass.states.set( + self.entity_id, new_state.state, self.state_attr) + + return + + # There is already a group state + cur_gr_state = self.hass.states.get(self.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 == group_off and new_state.state == group_on: + if cur_gr_state == self.group_off and new_state.state == self.group_on: - hass.states.set(group_entity_id, group_on, state_attr) + self.hass.states.set( + self.entity_id, self.group_on, self.state_attr) - elif cur_gr_state == group_on and new_state.state == group_off: + elif (cur_gr_state == self.group_on and + new_state.state == self.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 group_ids + if not any([self.hass.states.is_state(ent_id, self.group_on) + for ent_id in self.tracking if entity_id != ent_id]): - hass.states.set(group_entity_id, group_off, state_attr) - - _GROUPS[group_entity_id] = hass.states.track_change( - group_ids, update_group_state) - - hass.states.set(group_entity_id, state, state_attr) - - return True + self.hass.states.set( + self.entity_id, self.group_off, self.state_attr) -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)) +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. """ - 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)) + return Group(hass, name, entity_ids, user_defined) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 352cc500a9b..6a8b97f363e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -178,8 +178,7 @@ def setup(hass, config): update_lights_state(None) # Track all lights in a group - group.setup_group( - hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False) + group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False) def handle_light_service(service): """ Hande a turn light on or off service call. """ diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 4c687b31ef6..a8e626fbab0 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -91,8 +91,7 @@ def setup(hass, config): switch.update_ha_state(hass) # Track all switches in a group - group.setup_group(hass, GROUP_NAME_ALL_SWITCHES, - switches.keys(), False) + group.Group(hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False) # Update state every 30 seconds hass.track_time_change(update_states, second=[0, 30]) diff --git a/homeassistant/const.py b/homeassistant/const.py index 061dcbbd571..9711b28465c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -29,6 +29,7 @@ STATE_ON = 'on' STATE_OFF = 'off' STATE_HOME = 'home' STATE_NOT_HOME = 'not_home' +STATE_UNKNOWN = "unknown" # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event From d4cad0b26705ff10555c2bbe7da80e37cceee0a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Jan 2015 20:05:12 -0800 Subject: [PATCH 07/16] Rename ha_test folder to tests --- .travis.yml | 2 +- run_tests.sh | 2 +- .../config/custom_components/device_tracker/test.py | 0 {ha_test => tests}/config/custom_components/light/test.py | 2 +- {ha_test => tests}/config/custom_components/switch/test.py | 2 +- {ha_test => tests}/helpers.py | 2 +- {ha_test => tests}/test_component_chromecast.py | 2 +- {ha_test => tests}/test_component_core.py | 2 +- {ha_test => tests}/test_component_demo.py | 2 +- {ha_test => tests}/test_component_device_scanner.py | 5 ++--- {ha_test => tests}/test_component_group.py | 2 +- {ha_test => tests}/test_component_http.py | 2 +- {ha_test => tests}/test_component_light.py | 2 +- {ha_test => tests}/test_component_sun.py | 2 +- {ha_test => tests}/test_component_switch.py | 2 +- {ha_test => tests}/test_core.py | 2 +- {ha_test => tests}/test_helpers.py | 2 +- {ha_test => tests}/test_loader.py | 2 +- {ha_test => tests}/test_remote.py | 2 +- {ha_test => tests}/test_util.py | 2 +- 20 files changed, 20 insertions(+), 21 deletions(-) rename {ha_test => tests}/config/custom_components/device_tracker/test.py (100%) rename {ha_test => tests}/config/custom_components/light/test.py (92%) rename {ha_test => tests}/config/custom_components/switch/test.py (92%) rename {ha_test => tests}/helpers.py (99%) rename {ha_test => tests}/test_component_chromecast.py (98%) rename {ha_test => tests}/test_component_core.py (98%) rename {ha_test => tests}/test_component_demo.py (98%) rename {ha_test => tests}/test_component_device_scanner.py (98%) rename {ha_test => tests}/test_component_group.py (99%) rename {ha_test => tests}/test_component_http.py (99%) rename {ha_test => tests}/test_component_light.py (99%) rename {ha_test => tests}/test_component_sun.py (99%) rename {ha_test => tests}/test_component_switch.py (99%) rename {ha_test => tests}/test_core.py (99%) rename {ha_test => tests}/test_helpers.py (98%) rename {ha_test => tests}/test_loader.py (99%) rename {ha_test => tests}/test_remote.py (99%) rename {ha_test => tests}/test_util.py (99%) diff --git a/.travis.yml b/.travis.yml index 61ed87bf6b5..e1cfad31623 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 ha_test + - coverage run --source=homeassistant -m unittest discover tests after_success: - coveralls diff --git a/run_tests.sh b/run_tests.sh index c5b93d4440d..899c7f8cba3 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 ha_test +python3 -m unittest discover tests diff --git a/ha_test/config/custom_components/device_tracker/test.py b/tests/config/custom_components/device_tracker/test.py similarity index 100% rename from ha_test/config/custom_components/device_tracker/test.py rename to tests/config/custom_components/device_tracker/test.py diff --git a/ha_test/config/custom_components/light/test.py b/tests/config/custom_components/light/test.py similarity index 92% rename from ha_test/config/custom_components/light/test.py rename to tests/config/custom_components/light/test.py index 0ed04a21717..9b4ebcac2b1 100644 --- a/ha_test/config/custom_components/light/test.py +++ b/tests/config/custom_components/light/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from ha_test.helpers import MockToggleDevice +from tests.helpers import MockToggleDevice DEVICES = [] diff --git a/ha_test/config/custom_components/switch/test.py b/tests/config/custom_components/switch/test.py similarity index 92% rename from ha_test/config/custom_components/switch/test.py rename to tests/config/custom_components/switch/test.py index 682c27f695f..35e544fa0cb 100644 --- a/ha_test/config/custom_components/switch/test.py +++ b/tests/config/custom_components/switch/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from ha_test.helpers import MockToggleDevice +from tests.helpers import MockToggleDevice DEVICES = [] diff --git a/ha_test/helpers.py b/tests/helpers.py similarity index 99% rename from ha_test/helpers.py rename to tests/helpers.py index f04dac72553..47026209c7c 100644 --- a/ha_test/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,5 @@ """ -ha_test.helper +tests.helper ~~~~~~~~~~~~~ Helper method for writing tests. diff --git a/ha_test/test_component_chromecast.py b/tests/test_component_chromecast.py similarity index 98% rename from ha_test/test_component_chromecast.py rename to tests/test_component_chromecast.py index 75ac9765c63..0067a9456a5 100644 --- a/ha_test/test_component_chromecast.py +++ b/tests/test_component_chromecast.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_chromecast +tests.test_component_chromecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Chromecast component. diff --git a/ha_test/test_component_core.py b/tests/test_component_core.py similarity index 98% rename from ha_test/test_component_core.py rename to tests/test_component_core.py index 2c53d578277..8c00616bbb4 100644 --- a/ha_test/test_component_core.py +++ b/tests/test_component_core.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_core +tests.test_component_core ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests core compoments. diff --git a/ha_test/test_component_demo.py b/tests/test_component_demo.py similarity index 98% rename from ha_test/test_component_demo.py rename to tests/test_component_demo.py index b687653a0bc..d92b292f312 100644 --- a/ha_test/test_component_demo.py +++ b/tests/test_component_demo.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_demo +tests.test_component_demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests demo component. diff --git a/ha_test/test_component_device_scanner.py b/tests/test_component_device_scanner.py similarity index 98% rename from ha_test/test_component_device_scanner.py rename to tests/test_component_device_scanner.py index 3c6385bc42f..1accc1c5ff8 100644 --- a/ha_test/test_component_device_scanner.py +++ b/tests/test_component_device_scanner.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_group +tests.test_component_group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests the group compoments. @@ -75,7 +75,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} })) - def test_device_tracker(self): + def test_writing_known_devices_file(self): """ Test the device tracker class. """ scanner = loader.get_component( 'device_tracker.test').get_scanner(None, None) @@ -117,7 +117,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): 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) diff --git a/ha_test/test_component_group.py b/tests/test_component_group.py similarity index 99% rename from ha_test/test_component_group.py rename to tests/test_component_group.py index 46ae8942602..47bdb68cc50 100644 --- a/ha_test/test_component_group.py +++ b/tests/test_component_group.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_group +tests.test_component_group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests the group compoments. diff --git a/ha_test/test_component_http.py b/tests/test_component_http.py similarity index 99% rename from ha_test/test_component_http.py rename to tests/test_component_http.py index 98b976cf099..ba547e2bbe4 100644 --- a/ha_test/test_component_http.py +++ b/tests/test_component_http.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_http +tests.test_component_http ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Home Assistant HTTP component does what it should do. diff --git a/ha_test/test_component_light.py b/tests/test_component_light.py similarity index 99% rename from ha_test/test_component_light.py rename to tests/test_component_light.py index e9cb219d07b..84fb07d6427 100644 --- a/ha_test/test_component_light.py +++ b/tests/test_component_light.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_switch +tests.test_component_switch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests switch component. diff --git a/ha_test/test_component_sun.py b/tests/test_component_sun.py similarity index 99% rename from ha_test/test_component_sun.py rename to tests/test_component_sun.py index a587f60bff5..33570cdcc4e 100644 --- a/ha_test/test_component_sun.py +++ b/tests/test_component_sun.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_sun +tests.test_component_sun ~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests Sun component. diff --git a/ha_test/test_component_switch.py b/tests/test_component_switch.py similarity index 99% rename from ha_test/test_component_switch.py rename to tests/test_component_switch.py index 687df62ed5f..02ee98bc4c0 100644 --- a/ha_test/test_component_switch.py +++ b/tests/test_component_switch.py @@ -1,5 +1,5 @@ """ -ha_test.test_component_switch +tests.test_component_switch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests switch component. diff --git a/ha_test/test_core.py b/tests/test_core.py similarity index 99% rename from ha_test/test_core.py rename to tests/test_core.py index 2c86cbab1b7..d3d97e0b8d4 100644 --- a/ha_test/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,5 @@ """ -ha_test.test_core +tests.test_core ~~~~~~~~~~~~~~~~~ Provides tests to verify that Home Assistant core works. diff --git a/ha_test/test_helpers.py b/tests/test_helpers.py similarity index 98% rename from ha_test/test_helpers.py rename to tests/test_helpers.py index f61204c837f..48e73536c03 100644 --- a/ha_test/test_helpers.py +++ b/tests/test_helpers.py @@ -1,5 +1,5 @@ """ -ha_test.test_helpers +tests.test_helpers ~~~~~~~~~~~~~~~~~~~~ Tests component helpers. diff --git a/ha_test/test_loader.py b/tests/test_loader.py similarity index 99% rename from ha_test/test_loader.py rename to tests/test_loader.py index b7ae75c0e2a..dd80587b247 100644 --- a/ha_test/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,5 @@ """ -ha_ha_test.test_loader +ha_tests.test_loader ~~~~~~~~~~~~~~~~~~~~~~ Provides tests to verify that we can load components. diff --git a/ha_test/test_remote.py b/tests/test_remote.py similarity index 99% rename from ha_test/test_remote.py rename to tests/test_remote.py index f6de538e54a..e22eca3e49f 100644 --- a/ha_test/test_remote.py +++ b/tests/test_remote.py @@ -1,5 +1,5 @@ """ -ha_test.remote +tests.remote ~~~~~~~~~~~~~~ Tests Home Assistant remote methods and classes. diff --git a/ha_test/test_util.py b/tests/test_util.py similarity index 99% rename from ha_test/test_util.py rename to tests/test_util.py index 0f606fb45f2..038db227e1a 100644 --- a/ha_test/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ """ -ha_test.test_util +tests.test_util ~~~~~~~~~~~~~~~~~ Tests Home Assistant util methods. From f1209a42a9123467d85d1da3c69ad9b262200851 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Jan 2015 20:17:05 -0800 Subject: [PATCH 08/16] Ensure groups always have unique entity id --- homeassistant/components/group.py | 18 +++++++++++------- tests/test_component_group.py | 7 +++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 41d8e0ae056..f717ffc0b34 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -8,8 +8,8 @@ Provides functionality to group devices that can be turned on or off. import homeassistant as ha import homeassistant.util as util from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, - STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, + STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN) DOMAIN = "group" DEPENDENCIES = [] @@ -91,9 +91,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): def setup(hass, config): """ Sets up all groups found definded in the configuration. """ for name, entity_ids in config.get(DOMAIN, {}).items(): - entity_ids = entity_ids.split(",") - - setup_group(hass, name, entity_ids) + setup_group(hass, name, entity_ids.split(",")) return True @@ -104,13 +102,18 @@ class Group(object): self.hass = hass self.name = name self.user_defined = user_defined - self.entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) + + self.entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(name)), + hass.states.entity_ids(DOMAIN)) self.tracking = [] self.group_on, self.group_off = None, None if entity_ids is not None: self.update_tracked_entity_ids(entity_ids) + else: + self.force_update() @property def state(self): @@ -122,7 +125,8 @@ class Group(object): """ State attributes of this group. """ return { ATTR_ENTITY_ID: self.tracking, - ATTR_AUTO: not self.user_defined + ATTR_AUTO: not self.user_defined, + ATTR_FRIENDLY_NAME: self.name } def update_tracked_entity_ids(self, entity_ids): diff --git a/tests/test_component_group.py b/tests/test_component_group.py index 47bdb68cc50..2be58b5acc3 100644 --- a/tests/test_component_group.py +++ b/tests/test_component_group.py @@ -162,3 +162,10 @@ class TestComponentsGroup(unittest.TestCase): self.assertEqual(STATE_ON, group_state.state) self.assertFalse(group_state.attributes[group.ATTR_AUTO]) + + def test_groups_get_unique_names(self): + """ Two groups with same name should both have a unique entity id. """ + grp1 = group.Group(self.hass, 'Je suis Charlie') + grp2 = group.Group(self.hass, 'Je suis Charlie') + + self.assertNotEqual(grp1.entity_id, grp2.entity_id) From ba179bc63883eb37278de2229c9a5566b6aa5be6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Jan 2015 00:07:58 -0800 Subject: [PATCH 09/16] Automatic discovery and setting up of devices --- .gitmodules | 3 + homeassistant/__init__.py | 1 + homeassistant/bootstrap.py | 56 ++++++++----- homeassistant/components/chromecast.py | 70 +++++++++------- homeassistant/components/discovery.py | 88 +++++++++++++++++++++ homeassistant/components/group.py | 22 +++--- homeassistant/components/switch/__init__.py | 45 ++++++++--- homeassistant/components/switch/wemo.py | 41 +++++++--- homeassistant/external/netdisco | 1 + homeassistant/external/pywemo | 2 +- homeassistant/helpers.py | 2 +- tests/test_component_chromecast.py | 9 --- tests/test_component_switch.py | 22 +----- 13 files changed, 252 insertions(+), 110 deletions(-) create mode 100644 homeassistant/components/discovery.py create mode 160000 homeassistant/external/netdisco diff --git a/.gitmodules b/.gitmodules index 8ea8376a6a4..5cfe7de0098 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "homeassistant/external/pywemo"] path = homeassistant/external/pywemo url = https://github.com/balloob/pywemo.git +[submodule "homeassistant/external/netdisco"] + path = homeassistant/external/netdisco + url = https://github.com/balloob/netdisco.git diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 777164b3283..34e8b393bcc 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -51,6 +51,7 @@ class HomeAssistant(object): self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) + self.components = [] self.config_dir = os.path.join(os.getcwd(), 'config') diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 1b2a8ee7312..07dc46692d1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -19,6 +19,33 @@ import homeassistant.loader as loader import homeassistant.components as core_components +_LOGGER = logging.getLogger(__name__) + + +def setup_component(hass, domain, config=None): + """ Setup a component for Home Assistant. """ + if config is None: + config = defaultdict(dict) + + component = loader.get_component(domain) + + try: + if component.setup(hass, config): + hass.components.append(component.DOMAIN) + + _LOGGER.info("component %s initialized", domain) + + return True + + else: + _LOGGER.error("component %s failed to initialize", domain) + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during setup of component %s", domain) + + return False + + # pylint: disable=too-many-branches, too-many-statements def from_config_dict(config, hass=None): """ @@ -29,8 +56,6 @@ def from_config_dict(config, hass=None): if hass is None: hass = homeassistant.HomeAssistant() - logger = logging.getLogger(__name__) - loader.prepare(hass) # Make a copy because we are mutating it. @@ -42,12 +67,12 @@ def from_config_dict(config, hass=None): if ' ' not in key and key != homeassistant.DOMAIN) if not core_components.setup(hass, config): - logger.error(("Home Assistant core failed to initialize. " - "Further initialization aborted.")) + _LOGGER.error("Home Assistant core failed to initialize. " + "Further initialization aborted.") return hass - logger.info("Home Assistant core initialized") + _LOGGER.info("Home Assistant core initialized") # Setup the components @@ -57,22 +82,11 @@ def from_config_dict(config, hass=None): add_worker = True for domain in loader.load_order_components(components): - component = loader.get_component(domain) + if setup_component(hass, domain, config): + add_worker = add_worker and domain != "group" - try: - if component.setup(hass, config): - logger.info("component %s initialized", domain) - - add_worker = add_worker and domain != "group" - - if add_worker: - hass.pool.add_worker() - - else: - logger.error("component %s failed to initialize", domain) - - except Exception: # pylint: disable=broad-except - logger.exception("Error during setup of component %s", domain) + if add_worker: + hass.pool.add_worker() return hass @@ -112,7 +126,7 @@ def from_config_file(config_path, hass=None, enable_logging=True): logging.getLogger('').addHandler(err_handler) else: - logging.getLogger(__name__).error( + _LOGGER.error( "Unable to setup error log %s (access denied)", err_log_path) # Read config diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index fc5f7e73dc3..b15bd8b08a7 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts. """ import logging +try: + import pychromecast +except ImportError: + # Ignore, we will raise appropriate error later + pass + +from homeassistant.loader import get_component import homeassistant.util as util 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) + SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK) DOMAIN = 'chromecast' @@ -105,12 +111,30 @@ def media_prev_track(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) -# pylint: disable=too-many-locals, too-many-branches +def setup_chromecast(casts, host): + """ Tries to convert host to Chromecast object and set it up. """ + try: + cast = pychromecast.PyChromecast(host) + + entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format( + util.slugify(cast.device.friendly_name)), + casts.keys()) + + casts[entity_id] = cast + + except pychromecast.ChromecastConnectionError: + pass + + def setup(hass, config): + # pylint: disable=unused-argument,too-many-locals """ Listen for chromecast events. """ logger = logging.getLogger(__name__) + discovery = get_component('discovery') try: + # pylint: disable=redefined-outer-name import pychromecast except ImportError: logger.exception(("Failed to import pychromecast. " @@ -119,33 +143,24 @@ def setup(hass, config): return False - if CONF_HOSTS in config[DOMAIN]: - hosts = config[DOMAIN][CONF_HOSTS].split(",") + casts = {} - # If no hosts given, scan for chromecasts - else: + # If discovery component not loaded, scan ourselves + if discovery.DOMAIN not in hass.components: logger.info("Scanning for Chromecasts") hosts = pychromecast.discover_chromecasts() - casts = {} + for host in hosts: + setup_chromecast(casts, host) - for host in hosts: - try: - cast = pychromecast.PyChromecast(host) + # pylint: disable=unused-argument + def chromecast_discovered(service, info): + """ Called when a Chromecast has been discovered. """ + logger.info("New Chromecast discovered: %s", info[0]) + setup_chromecast(casts, info[0]) - entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format( - util.slugify(cast.device.friendly_name)), - casts.keys()) - - casts[entity_id] = cast - - except pychromecast.ChromecastConnectionError: - pass - - if not casts: - logger.error("Could not find Chromecasts") - return False + discovery.listen( + hass, discovery.services.GOOGLE_CAST, chromecast_discovered) def update_chromecast_state(entity_id, chromecast): """ Retrieve state of Chromecast and update statemachine. """ @@ -194,10 +209,11 @@ def setup(hass, config): def update_chromecast_states(time): # pylint: disable=unused-argument """ Updates all chromecast states. """ - logger.info("Updating Chromecast status") + if casts: + logger.info("Updating Chromecast status") - for entity_id, cast in casts.items(): - update_chromecast_state(entity_id, cast) + for entity_id, cast in casts.items(): + update_chromecast_state(entity_id, cast) def _service_to_entities(service): """ Helper method to get entities from service. """ diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py new file mode 100644 index 00000000000..99c13c63794 --- /dev/null +++ b/homeassistant/components/discovery.py @@ -0,0 +1,88 @@ +""" +Starts a service to scan in intervals for new devices. + +Will emit EVENT_SERVICE_DISCOVERED whenever a new service has been discovered. + +Knows which components handle certain types, will make sure they are +loaded before the EVENT_SERVICE_DISCOVERED is fired. + +""" +import logging +import threading + +# pylint: disable=no-name-in-module, import-error +from homeassistant.external.netdisco.netdisco import DiscoveryService +import homeassistant.external.netdisco.netdisco.const as services + +from homeassistant import bootstrap +from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_SERVICE + +DOMAIN = "discovery" +DEPENDENCIES = [] + +EVENT_SERVICE_DISCOVERED = "service_discovered" + +ATTR_DISCOVERED = "discovered" + +SCAN_INTERVAL = 300 # seconds + +SERVICE_HANDLERS = { + services.BELKIN_WEMO: "switch", + services.GOOGLE_CAST: "chromecast", + services.PHILIPS_HUE: "light", +} + + +def listen(hass, service, callback): + """ + Setup listener for discovery of specific service. + Service can be a string or a list/tuple. + """ + + if not isinstance(service, str): + service = (service,) + + def discovery_event_listener(event): + """ Listens for discovery events. """ + if event.data[ATTR_SERVICE] in service: + callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED]) + + hass.bus.listen(EVENT_SERVICE_DISCOVERED, discovery_event_listener) + + +def setup(hass, config): + """ Starts a discovery service. """ + + # Disable zeroconf logging, it spams + logging.getLogger('zeroconf').setLevel(logging.CRITICAL) + + logger = logging.getLogger(__name__) + + lock = threading.Lock() + + def new_service_listener(service, info): + """ Called when a new service is found. """ + with lock: + component = SERVICE_HANDLERS.get(service) + + logger.info("Found new service: %s %s", service, info) + + if component and component not in hass.components: + if bootstrap.setup_component(hass, component, config): + hass.pool.add_worker() + + hass.bus.fire(EVENT_SERVICE_DISCOVERED, { + ATTR_SERVICE: service, + ATTR_DISCOVERED: info + }) + + # pylint: disable=unused-argument + def start_discovery(event): + """ Start discovering. """ + netdisco = DiscoveryService(SCAN_INTERVAL) + netdisco.add_listener(new_service_listener) + netdisco.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery) + + return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index f717ffc0b34..05c69b6e230 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -132,8 +132,7 @@ class Group(object): def update_tracked_entity_ids(self, entity_ids): """ Update the tracked entity IDs. """ self.stop() - - self.tracking = list(entity_ids) + self.tracking = tuple(entity_ids) self.group_on, self.group_off = None, None self.force_update() @@ -150,7 +149,8 @@ class Group(object): # If parsing the entitys did not result in a state, set UNKNOWN if self.state is None: - self.hass.states.set(self.entity_id, STATE_UNKNOWN) + self.hass.states.set( + self.entity_id, STATE_UNKNOWN, self.state_attr) def start(self): """ Starts the tracking. """ @@ -182,25 +182,25 @@ class Group(object): # There is already a group state cur_gr_state = self.hass.states.get(self.entity_id).state + group_on, group_off = self.group_on, self.group_off # 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 == self.group_off and new_state.state == self.group_on: + if cur_gr_state == group_off and new_state.state == group_on: self.hass.states.set( - self.entity_id, self.group_on, self.state_attr) + self.entity_id, group_on, self.state_attr) - elif (cur_gr_state == self.group_on and - new_state.state == self.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([self.hass.states.is_state(ent_id, self.group_on) - for ent_id in self.tracking - if entity_id != ent_id]): + if not any(self.hass.states.is_state(ent_id, group_on) + for ent_id in self.tracking if entity_id != ent_id): self.hass.states.set( - self.entity_id, self.group_off, self.state_attr) + self.entity_id, group_off, self.state_attr) def setup_group(hass, name, entity_ids, user_defined=True): diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a8e626fbab0..3f5e0468040 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -6,12 +6,13 @@ Component to interface with various switches that can be controlled remotely. import logging from datetime import timedelta +from homeassistant.loader import get_component import homeassistant.util as util 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 +from homeassistant.components import group, discovery DOMAIN = 'switch' DEPENDENCIES = [] @@ -27,6 +28,11 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +# Maps discovered services to their platforms +DISCOVERY = { + discovery.services.BELKIN_WEMO: 'wemo' +} + _LOGGER = logging.getLogger(__name__) @@ -58,21 +64,41 @@ def setup(hass, config): switches = platform_devices_from_config( config, DOMAIN, hass, ENTITY_ID_FORMAT, logger) - if not switches: - return False - # pylint: disable=unused-argument @util.Throttle(MIN_TIME_BETWEEN_SCANS) def update_states(now): """ Update states of all switches. """ + if switches: + logger.info("Updating switch states") - logger.info("Updating switch states") - - for switch in switches.values(): - switch.update_ha_state(hass) + for switch in switches.values(): + switch.update_ha_state(hass) update_states(None) + # Track all switches in a group + switch_group = group.Group( + hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False) + + def switch_discovered(service, info): + """ Called when a switch is discovered. """ + platform = get_component("{}.{}".format(DOMAIN, DISCOVERY[service])) + + switch = platform.device_discovered(hass, config, info) + + if switch is not None: + switch.entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(switch.get_name())), + switches.keys()) + + switches[switch.entity_id] = switch + + switch.update_ha_state(hass) + + switch_group.update_tracked_entity_ids(switches.keys()) + + discovery.listen(hass, discovery.services.BELKIN_WEMO, switch_discovered) + def handle_switch_service(service): """ Handles calls to the switch services. """ target_switches = [switches[entity_id] for entity_id @@ -90,9 +116,6 @@ def setup(hass, config): switch.update_ha_state(hass) - # Track all switches in a group - group.Group(hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False) - # Update state every 30 seconds hass.track_time_change(update_states, second=[0, 30]) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index e75d1832ea1..98bbfcefa0c 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -11,16 +11,9 @@ from homeassistant.components.switch import ( def get_devices(hass, config): """ Find and return WeMo switches. """ - try: - # Pylint does not play nice if not every folders has an __init__.py - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.pywemo.pywemo as pywemo - except ImportError: - logging.getLogger(__name__).exception(( - "Failed to import pywemo. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) + pywemo, _ = get_pywemo() + if pywemo is None: return [] logging.getLogger(__name__).info("Scanning for WeMo devices") @@ -31,6 +24,36 @@ def get_devices(hass, config): if isinstance(switch, pywemo.Switch)] +def device_discovered(hass, config, info): + """ Called when a device is discovered. """ + _, discovery = get_pywemo() + + if discovery is None: + return + + device = discovery.device_from_description(info) + + return None if device is None else WemoSwitch(device) + + +def get_pywemo(): + """ Tries to import PyWemo. """ + try: + # pylint: disable=no-name-in-module, import-error + import homeassistant.external.pywemo.pywemo as pywemo + import homeassistant.external.pywemo.pywemo.discovery as discovery + + return pywemo, discovery + + except ImportError: + logging.getLogger(__name__).exception(( + "Failed to import pywemo. " + "Did you maybe not run `git submodule init` " + "and `git submodule update`?")) + + return None, None + + class WemoSwitch(ToggleDevice): """ represents a WeMo switch within home assistant. """ def __init__(self, wemo): diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco new file mode 160000 index 00000000000..20cb8863fce --- /dev/null +++ b/homeassistant/external/netdisco @@ -0,0 +1 @@ +Subproject commit 20cb8863fce3ce7d771ae077ce29ecafe98f8960 diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index 6355e04357c..7f6c383ded7 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit 6355e04357cf78b38d293fae7bd418cf9f8d1ca0 +Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4 diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py index 8bd69d0b1a0..4e479de1472 100644 --- a/homeassistant/helpers.py +++ b/homeassistant/helpers.py @@ -179,7 +179,7 @@ class Device(object): def get_name(self): """ Returns the name of the device if any. """ - return None + return "No Name" def get_state(self): """ Returns state of the device. """ diff --git a/tests/test_component_chromecast.py b/tests/test_component_chromecast.py index 0067a9456a5..962afcf982a 100644 --- a/tests/test_component_chromecast.py +++ b/tests/test_component_chromecast.py @@ -79,12 +79,3 @@ class TestChromecast(unittest.TestCase): self.assertEqual(service_name, call.service) self.assertEqual(self.test_entity, call.data.get(ATTR_ENTITY_ID)) - - def test_setup(self): - """ - Test Chromecast setup. - We do not have access to a Chromecast while testing so test errors. - In an ideal world we would create a mock pychromecast API.. - """ - self.assertFalse(chromecast.setup( - self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}})) diff --git a/tests/test_component_switch.py b/tests/test_component_switch.py index 02ee98bc4c0..9c2624e0ce6 100644 --- a/tests/test_component_switch.py +++ b/tests/test_component_switch.py @@ -7,7 +7,6 @@ Tests switch component. # pylint: disable=too-many-public-methods,protected-access import unittest -import homeassistant as ha import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM import homeassistant.components.switch as switch @@ -82,29 +81,12 @@ class TestSwitch(unittest.TestCase): self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id)) self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id)) - def test_setup(self): - # Bogus config - self.assertFalse(switch.setup(self.hass, {})) - - self.assertFalse(switch.setup(self.hass, {switch.DOMAIN: {}})) - - # Test with non-existing component - self.assertFalse(switch.setup( - self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}} - )) - + def test_setup_two_platforms(self): + """ Test with bad config. """ # Test if switch component returns 0 switches test_platform = loader.get_component('switch.test') test_platform.init(True) - self.assertEqual( - [], test_platform.get_switches(None, None)) - - self.assertFalse(switch.setup( - 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) From e01fee918926a516e61d44464963843be0add966 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Jan 2015 00:13:24 -0800 Subject: [PATCH 10/16] Updated requirements.txt for the discovery component --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index f0fb8e89f12..0f90b83ec0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,9 @@ requests>=2.0 # optional, needed for specific components +# discovery +zeroconf>=0.16.0 + # sun pyephem>=3.7 From ebc67f460edb914998cf7d820ec3096980f2a45e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Jan 2015 22:22:19 -0800 Subject: [PATCH 11/16] Update netdisco to latest version --- homeassistant/external/netdisco | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco index 20cb8863fce..012e248bcb7 160000 --- a/homeassistant/external/netdisco +++ b/homeassistant/external/netdisco @@ -1 +1 @@ -Subproject commit 20cb8863fce3ce7d771ae077ce29ecafe98f8960 +Subproject commit 012e248bcb77b2a5fc8bf38728f7030feae44f35 From 9db1a58cb180783c1fa6f081cdfe16e3b9e6ff38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Jan 2015 08:49:52 -0800 Subject: [PATCH 12/16] Update netdisco to latest version --- homeassistant/external/netdisco | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco index 012e248bcb7..27026d1f4a1 160000 --- a/homeassistant/external/netdisco +++ b/homeassistant/external/netdisco @@ -1 +1 @@ -Subproject commit 012e248bcb77b2a5fc8bf38728f7030feae44f35 +Subproject commit 27026d1f4a13afceb794a176f01cad9c1b37dc3b From c2b8f8d34e18a9549dd28624f6c9f868f8781173 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Jan 2015 10:34:56 -0800 Subject: [PATCH 13/16] Prevent devices from being discovered twice --- homeassistant/components/chromecast.py | 5 +++++ homeassistant/components/light/hue.py | 6 ++++++ homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/switch/wemo.py | 5 +++++ homeassistant/helpers.py | 12 +++++++++--- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index b15bd8b08a7..1736f0d2444 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -113,6 +113,11 @@ def media_prev_track(hass, entity_id=None): def setup_chromecast(casts, host): """ Tries to convert host to Chromecast object and set it up. """ + + # Check if already setup + if any(cast.host == host for cast in casts.values()): + return + try: cast = pychromecast.PyChromecast(host) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index bf8dae839c5..d8e7d2705b0 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -81,6 +81,12 @@ class HueLight(ToggleDevice): """ Get the mame of the Hue light. """ return self.info['name'] + @property + def unique_id(self): + """ Returns the id of this Hue light """ + return "{}.{}".format( + self.__class__, self.info.get('uniqueid', self.get_name())) + def turn_on(self, **kwargs): """ Turn the specified or all lights on. """ command = {'on': True} diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 3f5e0468040..caf6355d179 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -86,7 +86,7 @@ def setup(hass, config): switch = platform.device_discovered(hass, config, info) - if switch is not None: + if switch is not None and switch not in switches.values(): switch.entity_id = util.ensure_unique_string( ENTITY_ID_FORMAT.format(util.slugify(switch.get_name())), switches.keys()) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 98bbfcefa0c..63c6fe3b815 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -59,6 +59,11 @@ class WemoSwitch(ToggleDevice): def __init__(self, wemo): self.wemo = wemo + @property + def unique_id(self): + """ Returns the id of this WeMo switch """ + return "{}.{}".format(self.__class__, self.wemo.serialnumber) + def get_name(self): """ Returns the name of the switch if any. """ return self.wemo.name diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py index 4e479de1472..a28befea07e 100644 --- a/homeassistant/helpers.py +++ b/homeassistant/helpers.py @@ -146,9 +146,6 @@ def platform_devices_from_config(config, domain, hass, devices.extend(p_devices) - if len(devices) == 0: - logger.error("No devices found for %s", domain) - # Setup entity IDs for each device no_name_count = 1 @@ -177,6 +174,11 @@ class Device(object): entity_id = None + @property + def unique_id(self): + """ Returns a unique id. """ + return "{}.{}".format(self.__class__, id(self)) + def get_name(self): """ Returns the name of the device if any. """ return "No Name" @@ -208,6 +210,10 @@ class Device(object): return hass.states.set(self.entity_id, self.get_state(), self.get_state_attributes()) + def __eq__(self, other): + return (isinstance(other, Device) and + other.unique_id == self.unique_id) + class ToggleDevice(Device): """ ABC for devices that can be turned on and off. """ From 035e3e686edf48462a9509f2e40a2fe356ad7170 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Jan 2015 08:09:25 -0800 Subject: [PATCH 14/16] Upgrade pushbullet.py to 0.7.1 --- homeassistant/components/notify/__init__.py | 2 +- homeassistant/components/notify/pushbullet.py | 12 +++++++++--- requirements.txt | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index d0c73045f63..0728a979588 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -50,7 +50,7 @@ def setup(hass, config): notify_service = notify_implementation.get_service(hass, config) if notify_service is None: - _LOGGER.error("Failed to initialize notificatino service %s", + _LOGGER.error("Failed to initialize notification service %s", platform) return False diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 0f22d7d2970..953fa874458 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -22,16 +22,22 @@ def get_service(hass, config): try: # pylint: disable=unused-variable - from pushbullet import PushBullet # noqa + from pushbullet import PushBullet, InvalidKeyError # noqa except ImportError: _LOGGER.exception( "Unable to import pushbullet. " - "Did you maybe not install the 'pushbullet' package?") + "Did you maybe not install the 'pushbullet.py' package?") return None - return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY]) + try: + return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY]) + + except InvalidKeyError: + _LOGGER.error( + "Wrong API key supplied. " + "Get it at https://www.pushbullet.com/account") # pylint: disable=too-few-public-methods diff --git a/requirements.txt b/requirements.txt index 0f90b83ec0b..e0e003dd5eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,8 +21,8 @@ pyuserinput>=0.1.9 # switch.tellstick, tellstick_sensor tellcore-py>=1.0.4 -# namp_tracker plugin +# device_tracker.nmap python-libnmap -# pushbullet -pushbullet.py>=0.5.0 +# notify.pushbullet +pushbullet.py>=0.7.1 From 283b187501e84e0626594ee4a8308502e0ace747 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Jan 2015 09:20:41 -0800 Subject: [PATCH 15/16] Use properties instead of getters for Device class --- homeassistant/components/light/hue.py | 49 +++++++++-------- homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/switch/tellstick.py | 27 +++++----- homeassistant/components/switch/wemo.py | 31 ++++++----- homeassistant/const.py | 3 ++ homeassistant/helpers.py | 57 ++++++++++++++------ tests/helpers.py | 34 +++++++----- 7 files changed, 123 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index d8e7d2705b0..04291217eff 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -77,15 +77,36 @@ class HueLight(ToggleDevice): self.bridge = bridge self.update_lights = update_lights - def get_name(self): - """ Get the mame of the Hue light. """ - return self.info['name'] - @property def unique_id(self): """ Returns the id of this Hue light """ return "{}.{}".format( - self.__class__, self.info.get('uniqueid', self.get_name())) + self.__class__, self.info.get('uniqueid', self.name)) + + @property + def name(self): + """ Get the mame of the Hue light. """ + return self.info.get('name', 'No name') + + @property + def state_attributes(self): + """ Returns optional state attributes. """ + attr = { + ATTR_FRIENDLY_NAME: self.name + } + + if self.is_on: + attr[ATTR_BRIGHTNESS] = self.info['state']['bri'] + attr[ATTR_XY_COLOR] = self.info['state']['xy'] + + return attr + + @property + def is_on(self): + """ True if device is on. """ + self.update_lights() + + return self.info['state']['reachable'] and self.info['state']['on'] def turn_on(self, **kwargs): """ Turn the specified or all lights on. """ @@ -124,24 +145,6 @@ class HueLight(ToggleDevice): self.bridge.set_light(self.light_id, command) - def is_on(self): - """ True if device is on. """ - self.update_lights() - - return self.info['state']['reachable'] and self.info['state']['on'] - - def get_state_attributes(self): - """ Returns optional state attributes. """ - attr = { - ATTR_FRIENDLY_NAME: self.get_name() - } - - if self.is_on(): - attr[ATTR_BRIGHTNESS] = self.info['state']['bri'] - attr[ATTR_XY_COLOR] = self.info['state']['xy'] - - return attr - def update(self): """ Synchronize state with bridge. """ self.update_lights(no_throttle=True) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index caf6355d179..c601fad92f7 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -88,7 +88,7 @@ def setup(hass, config): if switch is not None and switch not in switches.values(): switch.entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(switch.get_name())), + ENTITY_ID_FORMAT.format(util.slugify(switch.name)), switches.keys()) switches[switch.entity_id] = switch diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 54b85b3ecb4..67336481c0a 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -36,10 +36,24 @@ class TellstickSwitch(ToggleDevice): self.tellstick = tellstick self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} - def get_name(self): + @property + def name(self): """ Returns the name of the switch if any. """ return self.tellstick.name + @property + def state_attributes(self): + """ Returns optional state attributes. """ + return self.state_attr + + @property + def is_on(self): + """ True if switch is on. """ + last_command = self.tellstick.last_sent_command( + self.last_sent_command_mask) + + return last_command == tc_constants.TELLSTICK_TURNON + # pylint: disable=unused-argument def turn_on(self, **kwargs): """ Turns the switch on. """ @@ -49,14 +63,3 @@ class TellstickSwitch(ToggleDevice): def turn_off(self, **kwargs): """ Turns the switch off. """ self.tellstick.turn_off() - - def is_on(self): - """ True if switch is on. """ - last_command = self.tellstick.last_sent_command( - self.last_sent_command_mask) - - return last_command == tc_constants.TELLSTICK_TURNON - - def get_state_attributes(self): - """ Returns optional state attributes. """ - return self.state_attr diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 63c6fe3b815..3c3db895713 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -64,23 +64,13 @@ class WemoSwitch(ToggleDevice): """ Returns the id of this WeMo switch """ return "{}.{}".format(self.__class__, self.wemo.serialnumber) - def get_name(self): + @property + def name(self): """ Returns the name of the switch if any. """ return self.wemo.name - def turn_on(self, **kwargs): - """ Turns the switch on. """ - self.wemo.on() - - def turn_off(self): - """ Turns the switch off. """ - self.wemo.off() - - def is_on(self): - """ True if switch is on. """ - return self.wemo.get_state(True) - - def get_state_attributes(self): + @property + def state_attributes(self): """ Returns optional state attributes. """ if self.wemo.model.startswith('Belkin Insight'): cur_info = self.wemo.insight_params @@ -92,3 +82,16 @@ class WemoSwitch(ToggleDevice): } else: return {ATTR_FRIENDLY_NAME: self.wemo.name} + + @property + def is_on(self): + """ True if switch is on. """ + return self.wemo.get_state(True) + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + self.wemo.on() + + def turn_off(self): + """ Turns the switch off. """ + self.wemo.off() diff --git a/homeassistant/const.py b/homeassistant/const.py index 9711b28465c..e8772bcd869 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,6 +2,9 @@ # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' +# If no name is specified +DEVICE_DEFAULT_NAME = "Unnamed Device" + # #### CONFIG #### CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py index a28befea07e..70a085056a2 100644 --- a/homeassistant/helpers.py +++ b/homeassistant/helpers.py @@ -7,7 +7,8 @@ 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) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE, + DEVICE_DEFAULT_NAME) from homeassistant.util import ensure_unique_string, slugify @@ -147,16 +148,16 @@ def platform_devices_from_config(config, domain, hass, devices.extend(p_devices) # Setup entity IDs for each device - no_name_count = 1 - device_dict = {} - for device in devices: - name = device.get_name() + no_name_count = 0 - if name is None: - name = "{} #{}".format(domain, no_name_count) + for device in devices: + name = device.name + + if name == DEVICE_DEFAULT_NAME: no_name_count += 1 + name = "{} #{}".format(domain, no_name_count) entity_id = ensure_unique_string( entity_id_format.format(slugify(name)), @@ -179,9 +180,29 @@ class Device(object): """ Returns a unique id. """ return "{}.{}".format(self.__class__, id(self)) + @property + def name(self): + """ Returns the name of the device. """ + return self.get_name() + + @property + def state(self): + """ Returns the state of the device. """ + return self.get_state() + + @property + def state_attributes(self): + """ Returns the state attributes. """ + return {} + + # DEPRECATION NOTICE: + # Device is moving from getters to properties. + # For now the new properties will call the old functions + # This will be removed in the future. + def get_name(self): """ Returns the name of the device if any. """ - return "No Name" + return DEVICE_DEFAULT_NAME def get_state(self): """ Returns state of the device. """ @@ -202,13 +223,13 @@ class Device(object): """ if self.entity_id is None: raise NoEntitySpecifiedError( - "No entity specified for device {}".format(self.get_name())) + "No entity specified for device {}".format(self.name)) if force_refresh: self.update() - return hass.states.set(self.entity_id, self.get_state(), - self.get_state_attributes()) + return hass.states.set(self.entity_id, self.state, + self.state_attributes) def __eq__(self, other): return (isinstance(other, Device) and @@ -219,9 +240,15 @@ class ToggleDevice(Device): """ ABC for devices that can be turned on and off. """ # pylint: disable=no-self-use - def get_state(self): + @property + def state(self): """ Returns the state. """ - return STATE_ON if self.is_on() else STATE_OFF + return STATE_ON if self.is_on else STATE_OFF + + @property + def is_on(self): + """ True if device is on. """ + return False def turn_on(self, **kwargs): """ Turn the device on. """ @@ -230,7 +257,3 @@ class ToggleDevice(Device): def turn_off(self, **kwargs): """ Turn the device off. """ pass - - def is_on(self): - """ True if device is on. """ - return False diff --git a/tests/helpers.py b/tests/helpers.py index 47026209c7c..48808361410 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -8,7 +8,7 @@ import os import homeassistant as ha from homeassistant.helpers import ToggleDevice -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME def get_test_home_assistant(): @@ -45,29 +45,37 @@ class MockModule(object): class MockToggleDevice(ToggleDevice): """ Provides a mock toggle device. """ def __init__(self, name, state): - self.name = name - self.state = state + self._name = name or DEVICE_DEFAULT_NAME + self._state = state self.calls = [] - def get_name(self): + @property + def name(self): """ Returns the name of the device if any. """ - self.calls.append(('get_name', {})) - return self.name + self.calls.append(('name', {})) + return self._name + + @property + def state(self): + """ Returns the name of the device if any. """ + self.calls.append(('state', {})) + return self._state + + @property + def is_on(self): + """ True if device is on. """ + self.calls.append(('is_on', {})) + return self._state == STATE_ON def turn_on(self, **kwargs): """ Turn the device on. """ self.calls.append(('turn_on', kwargs)) - self.state = STATE_ON + self._state = STATE_ON def turn_off(self, **kwargs): """ Turn the device off. """ self.calls.append(('turn_off', kwargs)) - self.state = STATE_OFF - - def is_on(self): - """ True if device is on. """ - self.calls.append(('is_on', {})) - return self.state == STATE_ON + self._state = STATE_OFF def last_call(self, method=None): if method is None: From ca49a2aa68ca18324389c2367964c8d0bdd71bfa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Jan 2015 09:55:45 -0800 Subject: [PATCH 16/16] Use tuples instead of lists internally --- homeassistant/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 34e8b393bcc..e889282fae4 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -223,7 +223,7 @@ def _process_match_param(parameter): elif isinstance(parameter, list): return parameter else: - return [parameter] + return (parameter,) def _matcher(subject, pattern): @@ -589,7 +589,7 @@ class StateMachine(object): # Ensure it is a lowercase list with entity ids we want to match on if isinstance(entity_ids, str): - entity_ids = [entity_ids.lower()] + entity_ids = (entity_ids.lower(),) else: entity_ids = [entity_id.lower() for entity_id in entity_ids]