From b67964274b4cdc160eac32d820cf11e669cd7ffd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2016 11:29:42 -0800 Subject: [PATCH 01/76] Version bump to 0.16.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 52d0f4843f2..559dda36b71 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.15.0" +__version__ = "0.16.0.dev0" REQUIRED_PYTHON_VER = (3, 4) # Can be used to specify a catch all when registering state or event listeners. From 13d7f742a71fadb2348026803785d6c5878ebd9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2016 16:43:33 -0800 Subject: [PATCH 02/76] Add thread names --- homeassistant/components/http.py | 3 ++- homeassistant/core.py | 2 +- homeassistant/util/__init__.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 90d43d38818..88ac58cc879 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -74,7 +74,8 @@ def setup(hass, config): hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_START, lambda event: - threading.Thread(target=server.start, daemon=True).start()) + threading.Thread(target=server.start, daemon=True, + name='HTTP-server').start()) hass.http = server hass.config.api = rem.API(util.get_local_ip(), api_password, server_port, diff --git a/homeassistant/core.py b/homeassistant/core.py index a0d63d16018..f9333594c0e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -775,7 +775,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): def start_timer(event): """Start the timer.""" - thread = threading.Thread(target=timer) + thread = threading.Thread(target=timer, name='Timer') thread.daemon = True thread.start() diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 975dce4c9df..190f6ad340a 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -331,7 +331,9 @@ class ThreadPool(object): if not self.running: raise RuntimeError("ThreadPool not running") - worker = threading.Thread(target=self._worker) + worker = threading.Thread( + target=self._worker, + name='ThreadPool Worker {}'.format(self.worker_count)) worker.daemon = True worker.start() From 8386bda4e49a14661d8a9f9020002d1434f83372 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2016 16:45:37 -0800 Subject: [PATCH 03/76] MQTT: Start embedded server if no config given --- homeassistant/components/mqtt/__init__.py | 52 ++++++-- homeassistant/components/mqtt/server.py | 114 ++++++++++++++++++ requirements_all.txt | 3 + .../{test_mqtt.py => mqtt/test_init.py} | 4 - tests/components/mqtt/test_server.py | 55 +++++++++ 5 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/mqtt/server.py rename tests/components/{test_mqtt.py => mqtt/test_init.py} (98%) create mode 100644 tests/components/mqtt/test_server.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 44b11b3df1b..0d25e2dffe9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -10,11 +10,11 @@ import socket import time +from homeassistant.bootstrap import prepare_setup_platform from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError import homeassistant.util as util from homeassistant.helpers import template -from homeassistant.helpers import validate_config from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -29,6 +29,7 @@ EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' REQUIREMENTS = ['paho-mqtt==1.1'] +CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' CONF_PORT = 'port' CONF_CLIENT_ID = 'client_id' @@ -92,21 +93,50 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS): MQTT_CLIENT.subscribe(topic, qos) +def _setup_server(hass, config): + """Try to start embedded MQTT broker.""" + conf = config.get(DOMAIN, {}) + + # Only setup if embedded config passed in or no broker specified + if CONF_EMBEDDED not in conf and CONF_BROKER in conf: + return None + + server = prepare_setup_platform(hass, config, DOMAIN, 'server') + + if server is None: + _LOGGER.error('Unable to load embedded server.') + return None + + success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED)) + + return success and broker_config + + def setup(hass, config): """Start the MQTT protocol service.""" - if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER): - return False + # pylint: disable=too-many-locals + conf = config.get(DOMAIN, {}) - conf = config[DOMAIN] - - broker = conf[CONF_BROKER] - port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) client_id = util.convert(conf.get(CONF_CLIENT_ID), str) keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) - username = util.convert(conf.get(CONF_USERNAME), str) - password = util.convert(conf.get(CONF_PASSWORD), str) - certificate = util.convert(conf.get(CONF_CERTIFICATE), str) - protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) + + broker_config = _setup_server(hass, config) + + # Only auto config if no server config was passed in + if broker_config and CONF_EMBEDDED not in conf: + broker, port, username, password, certificate, protocol = broker_config + elif not broker_config and (CONF_EMBEDDED in conf or + CONF_BROKER not in conf): + _LOGGER.error('Unable to start broker and auto-configure MQTT.') + return False + + if CONF_BROKER in conf: + broker = conf[CONF_BROKER] + port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) + username = util.convert(conf.get(CONF_USERNAME), str) + password = util.convert(conf.get(CONF_PASSWORD), str) + certificate = util.convert(conf.get(CONF_CERTIFICATE), str) + protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) if protocol not in (PROTOCOL_31, PROTOCOL_311): _LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s', diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py new file mode 100644 index 00000000000..eba8ce37b3c --- /dev/null +++ b/homeassistant/components/mqtt/server.py @@ -0,0 +1,114 @@ +"""MQTT server.""" +import asyncio +import logging +import tempfile +import threading + +from homeassistant.components.mqtt import PROTOCOL_311 +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['hbmqtt==0.6.3'] +DEPENDENCIES = ['http'] + + +@asyncio.coroutine +def broker_coro(loop, config): + """Start broker coroutine.""" + from hbmqtt.broker import Broker + broker = Broker(config, loop) + yield from broker.start() + return broker + + +def loop_run(loop, broker, shutdown_complete): + """Run broker and clean up when done.""" + loop.run_forever() + # run_forever ends when stop is called because we're shutting down + loop.run_until_complete(broker.shutdown()) + loop.close() + shutdown_complete.set() + + +def start(hass, server_config): + """Initialize MQTT Server.""" + from hbmqtt.broker import BrokerException + + loop = asyncio.new_event_loop() + + try: + passwd = tempfile.NamedTemporaryFile() + + if server_config is None: + server_config, client_config = generate_config(hass, passwd) + else: + client_config = None + + start_server = asyncio.gather(broker_coro(loop, server_config), + loop=loop) + loop.run_until_complete(start_server) + # Result raises exception if one was raised during startup + broker = start_server.result()[0] + except BrokerException: + logging.getLogger(__name__).exception('Error initializing MQTT server') + loop.close() + return False, None + finally: + passwd.close() + + shutdown_complete = threading.Event() + + def shutdown(event): + """Gracefully shutdown MQTT broker.""" + loop.call_soon_threadsafe(loop.stop) + shutdown_complete.wait() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete), + name="MQTT-server").start() + + return True, client_config + + +def generate_config(hass, passwd): + """Generate a configuration based on current Home Assistant instance.""" + config = { + 'listeners': { + 'default': { + 'max-connections': 50000, + 'bind': '0.0.0.0:1883', + 'type': 'tcp', + }, + 'ws-1': { + 'bind': '0.0.0.0:8080', + 'type': 'ws', + }, + }, + 'auth': { + 'allow-anonymous': hass.config.api.api_password is None + }, + 'plugins': ['auth_anonymous'], + } + + if hass.config.api.api_password: + username = 'homeassistant' + password = hass.config.api.api_password + + # Encrypt with what hbmqtt uses to verify + from passlib.apps import custom_app_context + + passwd.write( + 'homeassistant:{}\n'.format( + custom_app_context.encrypt( + hass.config.api.api_password)).encode('utf-8')) + passwd.flush() + + config['auth']['password-file'] = passwd.name + config['plugins'].append('auth_file') + else: + username = None + password = None + + client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) + + return config, client_config diff --git a/requirements_all.txt b/requirements_all.txt index 56043896810..0e8650034a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,6 +54,9 @@ freesms==0.1.0 # homeassistant.components.conversation fuzzywuzzy==0.8.0 +# homeassistant.components.mqtt.server +hbmqtt==0.6.3 + # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 diff --git a/tests/components/test_mqtt.py b/tests/components/mqtt/test_init.py similarity index 98% rename from tests/components/test_mqtt.py rename to tests/components/mqtt/test_init.py index 4e5c59f73ef..946a297e3a8 100644 --- a/tests/components/test_mqtt.py +++ b/tests/components/mqtt/test_init.py @@ -44,10 +44,6 @@ class TestMQTT(unittest.TestCase): self.hass.pool.block_till_done() self.assertTrue(mqtt.MQTT_CLIENT.stop.called) - def test_setup_fails_if_no_broker_config(self): - """Test for setup failure if broker configuration is missing.""" - self.assertFalse(mqtt.setup(self.hass, {mqtt.DOMAIN: {}})) - def test_setup_fails_if_no_connect_broker(self): """Test for setup failure if connection to broker is missing.""" with mock.patch('homeassistant.components.mqtt.MQTT', diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py new file mode 100644 index 00000000000..d8710035916 --- /dev/null +++ b/tests/components/mqtt/test_server.py @@ -0,0 +1,55 @@ +"""The tests for the MQTT component embedded server.""" +from unittest.mock import MagicMock, patch + +import homeassistant.components.mqtt as mqtt + +from tests.common import get_test_home_assistant + + +class TestMQTT: + """Test the MQTT component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @patch('homeassistant.components.mqtt.MQTT') + @patch('asyncio.gather') + @patch('asyncio.new_event_loop') + def test_creating_config_with_http_pass(self, mock_new_loop, mock_gather, + mock_mqtt): + """Test if the MQTT server gets started and subscribe/publish msg.""" + self.hass.config.components.append('http') + password = 'super_secret' + + self.hass.config.api = MagicMock(api_password=password) + assert mqtt.setup(self.hass, {}) + assert mock_mqtt.called + assert mock_mqtt.mock_calls[0][1][5] == 'homeassistant' + assert mock_mqtt.mock_calls[0][1][6] == password + + mock_mqtt.reset_mock() + + self.hass.config.api = MagicMock(api_password=None) + assert mqtt.setup(self.hass, {}) + assert mock_mqtt.called + assert mock_mqtt.mock_calls[0][1][5] is None + assert mock_mqtt.mock_calls[0][1][6] is None + + @patch('asyncio.gather') + @patch('asyncio.new_event_loop') + def test_broker_config_fails(self, mock_new_loop, mock_gather): + """Test if the MQTT component fails if server fails.""" + self.hass.config.components.append('http') + from hbmqtt.broker import BrokerException + + mock_gather.side_effect = BrokerException + + self.hass.config.api = MagicMock(api_password=None) + assert not mqtt.setup(self.hass, { + 'mqtt': {'embedded': {}} + }) From f8d2da2ace6d048425c6a8d1729f77293dc4d2b1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2016 23:41:00 -0800 Subject: [PATCH 04/76] Add content-length header to http resonses --- homeassistant/components/http.py | 11 +++++++---- tests/components/test_api.py | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 88ac58cc879..a1bfd2dc53e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -278,8 +278,11 @@ class RequestHandler(SimpleHTTPRequestHandler): def write_json(self, data=None, status_code=HTTP_OK, location=None): """Helper method to return JSON to the caller.""" + json_data = json.dumps(data, indent=4, sort_keys=True, + cls=rem.JSONEncoder).encode('UTF-8') self.send_response(status_code) self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(json_data))) if location: self.send_header('Location', location) @@ -289,20 +292,20 @@ class RequestHandler(SimpleHTTPRequestHandler): self.end_headers() if data is not None: - self.wfile.write( - json.dumps(data, indent=4, sort_keys=True, - cls=rem.JSONEncoder).encode("UTF-8")) + self.wfile.write(json_data) def write_text(self, message, status_code=HTTP_OK): """Helper method to return a text message to the caller.""" + msg_data = message.encode('UTF-8') self.send_response(status_code) self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN) + self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(msg_data))) self.set_session_cookie_header() self.end_headers() - self.wfile.write(message.encode("UTF-8")) + self.wfile.write(msg_data) def write_file(self, path, cache_headers=True): """Return a file to the user.""" diff --git a/tests/components/test_api.py b/tests/components/test_api.py index fb571fe5811..6acb1c2a569 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -104,6 +104,8 @@ class TestAPI(unittest.TestCase): _url(const.URL_API_STATES_ENTITY.format("test.test")), headers=HA_HEADERS) + self.assertEqual(req.headers['content-length'], str(len(req.content))) + data = ha.State.from_dict(req.json()) state = hass.states.get("test.test") From 956f6056f9a22ea900f8e4e5f4d84756634842b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2016 23:53:58 -0800 Subject: [PATCH 05/76] Remove support for old deprecated automation config --- .../components/automation/__init__.py | 68 +----- tests/components/automation/test_event.py | 44 ---- tests/components/automation/test_init.py | 65 ------ tests/components/automation/test_mqtt.py | 44 ---- tests/components/automation/test_state.py | 137 ------------ tests/components/automation/test_time.py | 203 ------------------ 6 files changed, 11 insertions(+), 550 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 31cac543b6c..7a3f635bc49 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,8 +9,9 @@ import logging from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook -from homeassistant.helpers.service import call_from_config -from homeassistant.helpers.service import validate_service_call +from homeassistant.helpers import extract_domain_configs +from homeassistant.helpers.service import (call_from_config, + validate_service_call) DOMAIN = 'automation' @@ -35,30 +36,17 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """Setup the automation.""" - config_key = DOMAIN - found = 1 + for config_key in extract_domain_configs(config, DOMAIN): + conf = config[config_key] - while config_key in config: - # Check for one block syntax - if isinstance(config[config_key], dict): - config_block = _migrate_old_config(config[config_key]) - name = config_block.get(CONF_ALIAS, config_key) + if not isinstance(conf, list): + conf = [conf] + + for list_no, config_block in enumerate(conf): + name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, + list_no)) _setup_automation(hass, config_block, name, config) - # Check for multiple block syntax - elif isinstance(config[config_key], list): - for list_no, config_block in enumerate(config[config_key]): - name = config_block.get(CONF_ALIAS, - "{}, {}".format(config_key, list_no)) - _setup_automation(hass, config_block, name, config) - - # Any scalar value is incorrect - else: - _LOGGER.error('Error in config in section %s.', config_key) - - found += 1 - config_key = "{} {}".format(DOMAIN, found) - return True @@ -97,40 +85,6 @@ def _get_action(hass, config, name): return action -def _migrate_old_config(config): - """Migrate old configuration to new.""" - if CONF_PLATFORM not in config: - return config - - _LOGGER.warning( - 'You are using an old configuration format. Please upgrade: ' - 'https://home-assistant.io/components/automation/') - - new_conf = { - CONF_TRIGGER: dict(config), - CONF_CONDITION: config.get('if', []), - CONF_ACTION: dict(config), - } - - for cat, key, new_key in (('trigger', 'mqtt_topic', 'topic'), - ('trigger', 'mqtt_payload', 'payload'), - ('trigger', 'state_entity_id', 'entity_id'), - ('trigger', 'state_before', 'before'), - ('trigger', 'state_after', 'after'), - ('trigger', 'state_to', 'to'), - ('trigger', 'state_from', 'from'), - ('trigger', 'state_hours', 'hours'), - ('trigger', 'state_minutes', 'minutes'), - ('trigger', 'state_seconds', 'seconds'), - ('action', 'execute_service', 'service'), - ('action', 'service_entity_id', 'entity_id'), - ('action', 'service_data', 'data')): - if key in new_conf[cat]: - new_conf[cat][new_key] = new_conf[cat].pop(key) - - return new_conf - - def _process_if(hass, config, p_config, action): """Process if checks.""" cond_type = p_config.get(CONF_CONDITION_TYPE, diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index e67402820a6..7d8c6a64ab1 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -24,50 +24,6 @@ class TestAutomationEvent(unittest.TestCase): """"Stop everything that was started.""" self.hass.stop() - def test_old_config_if_fires_on_event(self): - """.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation' - } - })) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_on_event_with_data(self): - """Test old configuration .""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'event_data': {'some_attr': 'some_value'}, - 'execute_service': 'test.automation' - } - })) - - self.hass.bus.fire('test_event', {'some_attr': 'some_value'}) - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_not_fires_if_event_data_not_matches(self): - """test old configuration.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'event_data': {'some_attr': 'some_value'}, - 'execute_service': 'test.automation' - } - })) - - self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'}) - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - def test_if_fires_on_event(self): """Test the firing of events.""" self.assertTrue(automation.setup(self.hass, { diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 14cbf025a56..cb996d5a227 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -24,71 +24,6 @@ class TestAutomation(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_old_config_service_data_not_a_dict(self): - """Test old configuration service data.""" - automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation', - 'service_data': 100 - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_service_specify_data(self): - """Test old configuration service data.""" - automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation', - 'service_data': {'some': 'data'} - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - self.assertEqual('data', self.calls[0].data['some']) - - def test_old_config_service_specify_entity_id(self): - """Test old configuration service data.""" - automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation', - 'service_entity_id': 'hello.world' - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - self.assertEqual(['hello.world'], - self.calls[0].data.get(ATTR_ENTITY_ID)) - - def test_old_config_service_specify_entity_id_list(self): - """Test old configuration service data.""" - automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation', - 'service_entity_id': ['hello.world', 'hello.world2'] - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - self.assertEqual(['hello.world', 'hello.world2'], - self.calls[0].data.get(ATTR_ENTITY_ID)) - def test_service_data_not_a_dict(self): """Test service data not dict.""" automation.setup(self.hass, { diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index bb90d66a61f..482becab937 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -24,50 +24,6 @@ class TestAutomationMQTT(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_old_config_if_fires_on_topic_match(self): - """Test if message is fired on topic match.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'mqtt', - 'mqtt_topic': 'test-topic', - 'execute_service': 'test.automation' - } - })) - - fire_mqtt_message(self.hass, 'test-topic', '') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_on_topic_and_payload_match(self): - """Test if message is fired on topic and payload match.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'mqtt', - 'mqtt_topic': 'test-topic', - 'mqtt_payload': 'hello', - 'execute_service': 'test.automation' - } - })) - - fire_mqtt_message(self.hass, 'test-topic', 'hello') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_not_fires_on_topic_but_no_payload_match(self): - """Test if message is not fired on topic but no payload.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'mqtt', - 'mqtt_topic': 'test-topic', - 'mqtt_payload': 'hello', - 'execute_service': 'test.automation' - } - })) - - fire_mqtt_message(self.hass, 'test-topic', 'no-hello') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - def test_if_fires_on_topic_match(self): """Test if message is fired on topic match.""" self.assertTrue(automation.setup(self.hass, { diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 17b5f52ebfa..3ccfcaeaeef 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -28,143 +28,6 @@ class TestAutomationState(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_old_config_if_fires_on_entity_change(self): - """Test for firing if entity change .""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_on_entity_change_with_from_filter(self): - """Test for firing on entity change with filter.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'state_from': 'hello', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_on_entity_change_with_to_filter(self): - """Test for firing on entity change no filter.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'state_to': 'world', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_on_entity_change_with_both_filters(self): - """Test for firing on entity change with both filters.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'state_from': 'hello', - 'state_to': 'world', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_not_fires_if_to_filter_not_match(self): - """Test for not firing if no match.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'state_from': 'hello', - 'state_to': 'world', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'moon') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_old_config_if_not_fires_if_from_filter_not_match(self): - """Test for no firing if no match.""" - self.hass.states.set('test.entity', 'bye') - - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.entity', - 'state_from': 'hello', - 'state_to': 'world', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_old_config_if_not_fires_if_entity_not_match(self): - """Test for not firing if no match.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'state_entity_id': 'test.another_entity', - 'execute_service': 'test.automation' - } - })) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_old_config_if_action(self): - """Test for if action.""" - entity_id = 'domain.test_entity' - test_state = 'new_state' - automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'event', - 'event_type': 'test_event', - 'execute_service': 'test.automation', - 'if': [{ - 'platform': 'state', - 'entity_id': entity_id, - 'state': test_state, - }] - } - }) - - self.hass.states.set(entity_id, test_state) - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - self.hass.states.set(entity_id, test_state + 'something') - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - def test_if_fires_on_entity_change(self): """Test for firing on entity change.""" self.assertTrue(automation.setup(self.hass, { diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 824d611f4fb..ee5ed04340a 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -5,8 +5,6 @@ from unittest.mock import patch import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation -from homeassistant.components.automation import time, event -from homeassistant.const import CONF_PLATFORM from tests.common import fire_time_changed, get_test_home_assistant @@ -28,207 +26,6 @@ class TestAutomationTime(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_old_config_if_fires_when_hour_matches(self): - """Test for firing if hours are matching.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'time', - time.CONF_HOURS: 0, - 'execute_service': 'test.automation' - } - })) - - fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_when_minute_matches(self): - """Test for firing if minutes are matching.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'time', - time.CONF_MINUTES: 0, - 'execute_service': 'test.automation' - } - })) - - fire_time_changed(self.hass, dt_util.utcnow().replace(minute=0)) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_when_second_matches(self): - """Test for firing if seconds are matching.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'time', - time.CONF_SECONDS: 0, - 'execute_service': 'test.automation' - } - })) - - fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_fires_when_all_matches(self): - """Test for firing if everything matches.""" - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'time', - time.CONF_HOURS: 0, - time.CONF_MINUTES: 0, - time.CONF_SECONDS: 0, - 'execute_service': 'test.automation' - } - })) - - fire_time_changed(self.hass, dt_util.utcnow().replace( - hour=0, minute=0, second=0)) - - self.hass.states.set('test.entity', 'world') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_action_before(self): - """Test for action before.""" - automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - 'execute_service': 'test.automation', - 'if': { - CONF_PLATFORM: 'time', - time.CONF_BEFORE: '10:00' - } - } - }) - - before_10 = dt_util.now().replace(hour=8) - after_10 = dt_util.now().replace(hour=14) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=before_10): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=after_10): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_action_after(self): - """Test for action after.""" - automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - 'execute_service': 'test.automation', - 'if': { - CONF_PLATFORM: 'time', - time.CONF_AFTER: '10:00' - } - } - }) - - before_10 = dt_util.now().replace(hour=8) - after_10 = dt_util.now().replace(hour=14) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=before_10): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(0, len(self.calls)) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=after_10): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_action_one_weekday(self): - """Test for action with one weekday.""" - automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - 'execute_service': 'test.automation', - 'if': { - CONF_PLATFORM: 'time', - time.CONF_WEEKDAY: 'mon', - } - } - }) - - days_past_monday = dt_util.now().weekday() - monday = dt_util.now() - timedelta(days=days_past_monday) - tuesday = monday + timedelta(days=1) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=monday): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=tuesday): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - def test_old_config_if_action_list_weekday(self): - """Test for action with a list of weekdays.""" - automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - 'execute_service': 'test.automation', - 'if': { - CONF_PLATFORM: 'time', - time.CONF_WEEKDAY: ['mon', 'tue'], - } - } - }) - - days_past_monday = dt_util.now().weekday() - monday = dt_util.now() - timedelta(days=days_past_monday) - tuesday = monday + timedelta(days=1) - wednesday = tuesday + timedelta(days=1) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=monday): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(self.calls)) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=tuesday): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(2, len(self.calls)) - - with patch('homeassistant.components.automation.time.dt_util.now', - return_value=wednesday): - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - - self.assertEqual(2, len(self.calls)) - def test_if_fires_when_hour_matches(self): """Test for firing if hour is matching.""" self.assertTrue(automation.setup(self.hass, { From fc455a1047593f240b827b07c91a5211955c706b Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 13 Mar 2016 18:01:29 +0000 Subject: [PATCH 06/76] Allow entry into passive zones. --- .../components/device_tracker/owntracks.py | 12 ++--- .../device_tracker/test_owntracks.py | 46 ------------------- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b8d202907b0..709312acc0b 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -99,14 +99,11 @@ def setup_scanner(hass, config, see): _LOGGER.info("Added beacon %s", location) else: # Normal region - if not zone.attributes.get('passive'): - kwargs['location_name'] = location - regions = REGIONS_ENTERED[dev_id] if location not in regions: regions.append(location) _LOGGER.info("Enter region %s", location) - _set_gps_from_zone(kwargs, zone) + _set_gps_from_zone(kwargs, location, zone) see(**kwargs) see_beacons(dev_id, kwargs) @@ -121,9 +118,7 @@ def setup_scanner(hass, config, see): if new_region: # Exit to previous region zone = hass.states.get("zone.{}".format(new_region)) - if not zone.attributes.get('passive'): - kwargs['location_name'] = new_region - _set_gps_from_zone(kwargs, zone) + _set_gps_from_zone(kwargs, new_region, zone) _LOGGER.info("Exit to %s", new_region) see(**kwargs) see_beacons(dev_id, kwargs) @@ -184,11 +179,12 @@ def _parse_see_args(topic, data): return dev_id, kwargs -def _set_gps_from_zone(kwargs, zone): +def _set_gps_from_zone(kwargs, location, zone): """Set the see parameters from the zone parameters.""" if zone is not None: kwargs['gps'] = ( zone.attributes['latitude'], zone.attributes['longitude']) kwargs['gps_accuracy'] = zone.attributes['radius'] + kwargs['location_name'] = location return kwargs diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 98e9349ca00..7fa290e4005 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -133,15 +133,6 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): 'radius': 100000 }) - self.hass.states.set( - 'zone.passive', 'zoning', - { - 'name': 'zone', - 'latitude': 3.0, - 'longitude': 1.0, - 'radius': 10, - 'passive': True - }) # Clear state between teste self.hass.states.set(DEVICE_TRACKER_STATE, None) owntracks.REGIONS_ENTERED = defaultdict(list) @@ -325,43 +316,6 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): self.send_message(EVENT_TOPIC, message) self.assert_location_state('outer') - def test_event_entry_exit_passive_zone(self): - """Test the event for passive zone exits.""" - # Enter passive zone - message = REGION_ENTER_MESSAGE.copy() - message['desc'] = "passive" - self.send_message(EVENT_TOPIC, message) - - # Should pick up gps put not zone - self.assert_location_state('not_home') - self.assert_location_latitude(3.0) - self.assert_location_accuracy(10.0) - - # Enter inner2 zone - message = REGION_ENTER_MESSAGE.copy() - message['desc'] = "inner_2" - self.send_message(EVENT_TOPIC, message) - self.assert_location_state('inner_2') - self.assert_location_latitude(2.1) - self.assert_location_accuracy(10.0) - - # Exit inner_2 - should be in 'passive' - # ie gps co-ords - but not zone - message = REGION_LEAVE_MESSAGE.copy() - message['desc'] = "inner_2" - self.send_message(EVENT_TOPIC, message) - self.assert_location_state('not_home') - self.assert_location_latitude(3.0) - self.assert_location_accuracy(10.0) - - # Exit passive - should be in 'outer' - message = REGION_LEAVE_MESSAGE.copy() - message['desc'] = "passive" - self.send_message(EVENT_TOPIC, message) - self.assert_location_state('outer') - self.assert_location_latitude(2.0) - self.assert_location_accuracy(60.0) - def test_event_entry_unknown_zone(self): """Test the event for unknown zone.""" # Just treat as location update From eb9ed5ccfec4761ff160ff7843962b33d08663b0 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Fri, 11 Mar 2016 21:54:43 +0100 Subject: [PATCH 07/76] Rewrite of the tellstick module. It now uses a common base for all shared functionality. The rewrite addresses a problem with the tellstick hardware dropping commands when too many simultaneous calls is being made from HA. Also fixes a bug when the dim level was changed externally. This breaks previous configurations. The new config for tellstick is ```yaml tellstick: signal_repetitions: X ``` Lights and Switches are detected automatically. Sensors work like before because they do not share any functionality with the other devices and they also needs a complete other configuration. --- .coveragerc | 1 + homeassistant/components/light/__init__.py | 4 +- homeassistant/components/light/tellstick.py | 139 ++++-------- homeassistant/components/switch/__init__.py | 4 +- homeassistant/components/switch/tellstick.py | 104 +++------ homeassistant/components/tellstick.py | 217 +++++++++++++++++++ requirements_all.txt | 3 +- 7 files changed, 302 insertions(+), 170 deletions(-) create mode 100644 homeassistant/components/tellstick.py diff --git a/.coveragerc b/.coveragerc index c37e4f47291..63eb4ea9be1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -26,6 +26,7 @@ omit = homeassistant/components/modbus.py homeassistant/components/*/modbus.py + homeassistant/components/tellstick.py homeassistant/components/*/tellstick.py homeassistant/components/tellduslive.py diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 357a6156c3a..159cd19a96a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -9,7 +9,8 @@ import os import csv from homeassistant.components import ( - group, discovery, wemo, wink, isy994, zwave, insteon_hub, mysensors) + group, discovery, wemo, wink, isy994, + zwave, insteon_hub, mysensors, tellstick) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, @@ -64,6 +65,7 @@ DISCOVERY_PLATFORMS = { discovery.SERVICE_HUE: 'hue', zwave.DISCOVER_LIGHTS: 'zwave', mysensors.DISCOVER_LIGHTS: 'mysensors', + tellstick.DISCOVER_LIGHTS: 'tellstick', } PROP_TO_ATTR = { diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 8d54ddb1604..3571bd9737d 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -4,127 +4,80 @@ Support for Tellstick lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tellstick/ """ +from homeassistant.components import tellstick from homeassistant.components.light import ATTR_BRIGHTNESS, Light -from homeassistant.const import EVENT_HOMEASSISTANT_STOP - -REQUIREMENTS = ['tellcore-py==1.1.2'] -SIGNAL_REPETITIONS = 1 +from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVER_CONFIG) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick lights.""" - import tellcore.telldus as telldus - from tellcore.library import DirectCallbackDispatcher - import tellcore.constants as tellcore_constants + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None or + tellstick.TELLCORE_REGISTRY is None): + return - core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) - signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS) + signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, + DEFAULT_SIGNAL_REPETITIONS) - switches_and_lights = core.devices() - lights = [] - - for switch in switches_and_lights: - if switch.methods(tellcore_constants.TELLSTICK_DIM): - lights.append(TellstickLight(switch, signal_repetitions)) - - def _device_event_callback(id_, method, data, cid): - """Called from the TelldusCore library to update one device.""" - for light_device in lights: - if light_device.tellstick_device.id == id_: - # Execute the update in another thread - light_device.update_ha_state(True) - break - - callback_id = core.register_device_event(_device_event_callback) - - def unload_telldus_lib(event): - """Un-register the callback bindings.""" - if callback_id is not None: - core.unregister_callback(callback_id) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) - - add_devices_callback(lights) + add_devices(TellstickLight( + tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) + for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickLight(Light): +class TellstickLight(tellstick.TellstickDevice, Light): """Representation of a Tellstick light.""" def __init__(self, tellstick_device, signal_repetitions): """Initialize the light.""" - import tellcore.constants as tellcore_constants - - self.tellstick_device = tellstick_device - self.signal_repetitions = signal_repetitions - self._brightness = 0 - - self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF | - tellcore_constants.TELLSTICK_DIM | - tellcore_constants.TELLSTICK_UP | - tellcore_constants.TELLSTICK_DOWN) - self.update() - - @property - def name(self): - """Return the name of the switch if any.""" - return self.tellstick_device.name + self._brightness = 255 + tellstick.TellstickDevice.__init__(self, + tellstick_device, + signal_repetitions) @property def is_on(self): """Return true if switch is on.""" - return self._brightness > 0 + return self._state @property def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness - def turn_off(self, **kwargs): - """Turn the switch off.""" - for _ in range(self.signal_repetitions): + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Update the internal representation of the switch.""" + from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM + if last_command_sent == TELLSTICK_DIM: + if last_data_sent is not None: + self._brightness = int(last_data_sent) + self._state = self._brightness > 0 + else: + self._state = last_command_sent == TELLSTICK_TURNON + + def _send_tellstick_command(self, command, data): + """Handle the turn_on / turn_off commands.""" + from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM) + if command == TELLSTICK_TURNOFF: self.tellstick_device.turn_off() - self._brightness = 0 - self.update_ha_state() + elif command == TELLSTICK_DIM: + self.tellstick_device.dim(self._brightness) + else: + raise NotImplementedError( + "Command not implemented: {}".format(command)) def turn_on(self, **kwargs): """Turn the switch on.""" + from tellcore.constants import TELLSTICK_DIM brightness = kwargs.get(ATTR_BRIGHTNESS) - - if brightness is None: - self._brightness = 255 - else: + if brightness is not None: self._brightness = brightness - for _ in range(self.signal_repetitions): - self.tellstick_device.dim(self._brightness) - self.update_ha_state() + self.call_tellstick(TELLSTICK_DIM, self._brightness) - def update(self): - """Update state of the light.""" - import tellcore.constants as tellcore_constants - - last_command = self.tellstick_device.last_sent_command( - self.last_sent_command_mask) - - if last_command == tellcore_constants.TELLSTICK_TURNON: - self._brightness = 255 - elif last_command == tellcore_constants.TELLSTICK_TURNOFF: - self._brightness = 0 - elif (last_command == tellcore_constants.TELLSTICK_DIM or - last_command == tellcore_constants.TELLSTICK_UP or - last_command == tellcore_constants.TELLSTICK_DOWN): - last_sent_value = self.tellstick_device.last_sent_value() - if last_sent_value is not None: - self._brightness = last_sent_value - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """Tellstick devices are always assumed state.""" - return True + def turn_off(self, **kwargs): + """Turn the switch off.""" + from tellcore.constants import TELLSTICK_TURNOFF + self.call_tellstick(TELLSTICK_TURNOFF) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 386fb34fa17..1797fc2af8a 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -16,7 +16,8 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.components import ( - group, wemo, wink, isy994, verisure, zwave, tellduslive, mysensors) + group, wemo, wink, isy994, verisure, + zwave, tellduslive, tellstick, mysensors) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +41,7 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_SWITCHES: 'zwave', tellduslive.DISCOVER_SWITCHES: 'tellduslive', mysensors.DISCOVER_SWITCHES: 'mysensors', + tellstick.DISCOVER_SWITCHES: 'tellstick', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index b55232e14fc..8014244e828 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -4,98 +4,56 @@ Support for Tellstick switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.tellstick/ """ -import logging - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.components import tellstick +from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES, + ATTR_DISCOVER_CONFIG) from homeassistant.helpers.entity import ToggleEntity -SIGNAL_REPETITIONS = 1 -REQUIREMENTS = ['tellcore-py==1.1.2'] -_LOGGER = logging.getLogger(__name__) - # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick switches.""" - import tellcore.telldus as telldus - import tellcore.constants as tellcore_constants - from tellcore.library import DirectCallbackDispatcher + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None or + tellstick.TELLCORE_REGISTRY is None): + return - core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) - signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS) - switches_and_lights = core.devices() + # Allow platform level override, fallback to module config + signal_repetitions = discovery_info.get( + ATTR_DISCOVER_CONFIG, tellstick.DEFAULT_SIGNAL_REPETITIONS) - switches = [] - for switch in switches_and_lights: - if not switch.methods(tellcore_constants.TELLSTICK_DIM): - switches.append( - TellstickSwitchDevice(switch, signal_repetitions)) - - def _device_event_callback(id_, method, data, cid): - """Called from the TelldusCore library to update one device.""" - for switch_device in switches: - if switch_device.tellstick_device.id == id_: - switch_device.update_ha_state() - break - - callback_id = core.register_device_event(_device_event_callback) - - def unload_telldus_lib(event): - """Un-register the callback bindings.""" - if callback_id is not None: - core.unregister_callback(callback_id) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) - - add_devices_callback(switches) + add_devices(TellstickSwitchDevice( + tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) + for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickSwitchDevice(ToggleEntity): +class TellstickSwitchDevice(tellstick.TellstickDevice, ToggleEntity): """Representation of a Tellstick switch.""" - def __init__(self, tellstick_device, signal_repetitions): - """Initialize the Tellstick switch.""" - import tellcore.constants as tellcore_constants - - self.tellstick_device = tellstick_device - self.signal_repetitions = signal_repetitions - - self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """The Tellstick devices are always assumed state.""" - return True - - @property - def name(self): - """Return the name of the switch if any.""" - return self.tellstick_device.name - @property def is_on(self): """Return true if switch is on.""" - import tellcore.constants as tellcore_constants + return self._state - last_command = self.tellstick_device.last_sent_command( - self.last_sent_command_mask) + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Update the internal representation of the switch.""" + from tellcore.constants import TELLSTICK_TURNON + self._state = last_command_sent == TELLSTICK_TURNON - return last_command == tellcore_constants.TELLSTICK_TURNON + def _send_tellstick_command(self, command, data): + """Handle the turn_on / turn_off commands.""" + from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_TURNOFF + if command == TELLSTICK_TURNON: + self.tellstick_device.turn_on() + elif command == TELLSTICK_TURNOFF: + self.tellstick_device.turn_off() def turn_on(self, **kwargs): """Turn the switch on.""" - for _ in range(self.signal_repetitions): - self.tellstick_device.turn_on() - self.update_ha_state() + from tellcore.constants import TELLSTICK_TURNON + self.call_tellstick(TELLSTICK_TURNON) def turn_off(self, **kwargs): """Turn the switch off.""" - for _ in range(self.signal_repetitions): - self.tellstick_device.turn_off() - self.update_ha_state() + from tellcore.constants import TELLSTICK_TURNOFF + self.call_tellstick(TELLSTICK_TURNOFF) diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py new file mode 100644 index 00000000000..04927d9e652 --- /dev/null +++ b/homeassistant/components/tellstick.py @@ -0,0 +1,217 @@ +""" +Tellstick Component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/Tellstick/ +""" +import logging +import threading + +from homeassistant import bootstrap +from homeassistant.const import ( + ATTR_DISCOVERED, ATTR_SERVICE, + EVENT_PLATFORM_DISCOVERED, EVENT_HOMEASSISTANT_STOP) +from homeassistant.loader import get_component +from homeassistant.helpers.entity import Entity + +DOMAIN = "tellstick" + +REQUIREMENTS = ['tellcore-py==1.1.2'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_SIGNAL_REPETITIONS = "signal_repetitions" +DEFAULT_SIGNAL_REPETITIONS = 1 + +DISCOVER_SWITCHES = "tellstick.switches" +DISCOVER_LIGHTS = "tellstick.lights" +DISCOVERY_TYPES = {"switch": DISCOVER_SWITCHES, + "light": DISCOVER_LIGHTS} + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_DISCOVER_CONFIG = "config" + +# Use a global tellstick domain lock to handle +# tellcore errors then calling to concurrently +TELLSTICK_LOCK = threading.Lock() + +# Keep a reference the the callback registry +# Used from entities that register callback listeners +TELLCORE_REGISTRY = None + + +def _discover(hass, config, found_devices, component_name): + """Setup and send the discovery event.""" + if not len(found_devices): + return + + _LOGGER.info("discovered %d new %s devices", + len(found_devices), component_name) + + component = get_component(component_name) + bootstrap.setup_component(hass, component.DOMAIN, + config) + + signal_repetitions = config[DOMAIN].get( + ATTR_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS) + + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: DISCOVERY_TYPES[component_name], + ATTR_DISCOVERED: {ATTR_DISCOVER_DEVICES: found_devices, + ATTR_DISCOVER_CONFIG: + signal_repetitions}}) + + +def setup(hass, config): + """Setup the Tellstick component.""" + # pylint: disable=global-statement, import-error + global TELLCORE_REGISTRY + + import tellcore.telldus as telldus + import tellcore.constants as tellcore_constants + from tellcore.library import DirectCallbackDispatcher + + core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) + + TELLCORE_REGISTRY = TellstickRegistry(hass, core) + + devices = core.devices() + + # Register devices + TELLCORE_REGISTRY.register_devices(devices) + + # Discover the switches + _discover(hass, config, [switch.id for switch in + devices if not switch.methods( + tellcore_constants.TELLSTICK_DIM)], + "switch") + + # Discover the lights + _discover(hass, config, [light.id for light in + devices if light.methods( + tellcore_constants.TELLSTICK_DIM)], + "light") + + return True + + +class TellstickRegistry: + """Handle everything around tellstick callbacks. + + Keeps a map device ids to home-assistant entities. + Also responsible for registering / cleanup of callbacks. + + All device specific logic should be elsewhere (Entities). + + """ + + def __init__(self, hass, tellcore_lib): + """Init the tellstick mappings and callbacks.""" + self._core_lib = tellcore_lib + # used when map callback device id to ha entities. + self._id_to_entity_map = {} + self._id_to_device_map = {} + self._setup_device_callback(hass, tellcore_lib) + + def _device_callback(self, tellstick_id, method, data, cid): + """Handle the actual callback from tellcore.""" + entity = self._id_to_entity_map.get(tellstick_id, None) + if entity is not None: + entity.set_tellstick_state(method, data) + entity.update_ha_state() + + def _setup_device_callback(self, hass, tellcore_lib): + """Register the callback handler.""" + callback_id = tellcore_lib.register_device_event( + self._device_callback) + + def clean_up_callback(event): + """Unregister the callback bindings.""" + if callback_id is not None: + tellcore_lib.unregister_callback(callback_id) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, clean_up_callback) + + def register_entity(self, tellcore_id, entity): + """Register a new entity to receive callback updates.""" + self._id_to_entity_map[tellcore_id] = entity + + def register_devices(self, devices): + """Register a list of devices.""" + self._id_to_device_map.update({device.id: + device for device in devices}) + + def get_device(self, tellcore_id): + """Return a device by tellcore_id.""" + return self._id_to_device_map.get(tellcore_id, None) + + +class TellstickDevice(Entity): + """Represents a Tellstick device. + + Contains the common logic for all Tellstick devices. + + """ + + def __init__(self, tellstick_device, signal_repetitions): + """Init the tellstick device.""" + self.signal_repetitions = signal_repetitions + self._state = None + self.tellstick_device = tellstick_device + # add to id to entity mapping + TELLCORE_REGISTRY.register_entity(tellstick_device.id, self) + # Query tellcore for the current state + self.update() + + @property + def should_poll(self): + """Tell Home Assistant not to poll this entity.""" + return False + + @property + def assumed_state(self): + """Tellstick devices are always assumed state.""" + return True + + @property + def name(self): + """Return the name of the switch if any.""" + return self.tellstick_device.name + + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Set the private switch state.""" + raise NotImplementedError( + "set_tellstick_state needs to be implemented.") + + def _send_tellstick_command(self, command, data): + """Do the actual call to the tellstick device.""" + raise NotImplementedError( + "_call_tellstick needs to be implemented.") + + def call_tellstick(self, command, data=None): + """Send a command to the device.""" + from tellcore.library import TelldusError + with TELLSTICK_LOCK: + try: + for _ in range(self.signal_repetitions): + self._send_tellstick_command(command, data) + # Update the internal state + self.set_tellstick_state(command, data) + self.update_ha_state() + except TelldusError: + _LOGGER.error(TelldusError) + + def update(self): + """Poll the current state of the device.""" + import tellcore.constants as tellcore_constants + from tellcore.library import TelldusError + try: + last_command = self.tellstick_device.last_sent_command( + tellcore_constants.TELLSTICK_TURNON | + tellcore_constants.TELLSTICK_TURNOFF | + tellcore_constants.TELLSTICK_DIM + ) + last_value = self.tellstick_device.last_sent_value() + self.set_tellstick_state(last_command, last_value) + except TelldusError: + _LOGGER.error(TelldusError) diff --git a/requirements_all.txt b/requirements_all.txt index 56043896810..4c3226938a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -256,9 +256,8 @@ speedtest-cli==0.3.4 # homeassistant.components.sensor.steam_online steamodd==4.21 -# homeassistant.components.light.tellstick +# homeassistant.components.tellstick # homeassistant.components.sensor.tellstick -# homeassistant.components.switch.tellstick tellcore-py==1.1.2 # homeassistant.components.tellduslive From b7044a79b239f6d8942677739d522bb197946ce4 Mon Sep 17 00:00:00 2001 From: Charles Spirakis Date: Sun, 13 Mar 2016 10:51:09 -0700 Subject: [PATCH 08/76] Enable openzwave's network heal and soft reset. Makes Zwave's network heal and the controller's soft reset commands available as service calls. They can be used in automation to help keep the zwave netwrok healthy. For example: automation: alias: At 2:35am, heal problematic zwave network routes trigger: platform: time after: '2:35:00' action: service: zwave.heal --- homeassistant/components/zwave.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index f920b81e8db..2e36352c2eb 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -28,6 +28,8 @@ DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share', SERVICE_ADD_NODE = "add_node" SERVICE_REMOVE_NODE = "remove_node" +SERVICE_HEAL_NETWORK = "heal_network" +SERVICE_SOFT_RESET = "soft_reset" DISCOVER_SENSORS = "zwave.sensors" DISCOVER_SWITCHES = "zwave.switch" @@ -149,6 +151,7 @@ def get_config_value(node, value_index): return get_config_value(node, value_index) +# pylint: disable=R0914 def setup(hass, config): """Setup Z-Wave. @@ -249,6 +252,14 @@ def setup(hass, config): """Switch into exclusion mode.""" NETWORK.controller.begin_command_remove_device() + def heal_network(event): + """Heal the network.""" + NETWORK.heal() + + def soft_reset(event): + """Soft reset the controller.""" + NETWORK.controller.soft_reset() + def stop_zwave(event): """Stop Z-Wave.""" NETWORK.stop() @@ -268,6 +279,8 @@ def setup(hass, config): # hardware inclusion button hass.services.register(DOMAIN, SERVICE_ADD_NODE, add_node) hass.services.register(DOMAIN, SERVICE_REMOVE_NODE, remove_node) + hass.services.register(DOMAIN, SERVICE_HEAL_NETWORK, heal_network) + hass.services.register(DOMAIN, SERVICE_SOFT_RESET, soft_reset) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) From 61b387cd0b604179ac18b40f059dafccabc138d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Mar 2016 22:29:36 -0700 Subject: [PATCH 09/76] Script: fix template service calls and remove old config support --- homeassistant/components/script.py | 16 ++----- tests/components/test_script.py | 71 +++++++++++++++++------------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 071921426bb..cb5236ef1ec 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -19,7 +19,8 @@ from homeassistant.const import ( from homeassistant.helpers.entity import ToggleEntity, split_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.helpers.service import call_from_config +from homeassistant.helpers.service import (call_from_config, + validate_service_call) from homeassistant.util import slugify DOMAIN = "script" @@ -30,9 +31,7 @@ STATE_NOT_RUNNING = 'Not Running' CONF_ALIAS = "alias" CONF_SERVICE = "service" -CONF_SERVICE_OLD = "execute_service" CONF_SERVICE_DATA = "data" -CONF_SERVICE_DATA_OLD = "service_data" CONF_SEQUENCE = "sequence" CONF_EVENT = "event" CONF_EVENT_DATA = "event_data" @@ -174,7 +173,7 @@ class Script(ToggleEntity): for cur, action in islice(enumerate(self.sequence), self._cur, None): - if CONF_SERVICE in action or CONF_SERVICE_OLD in action: + if validate_service_call(action) is None: self._call_service(action) elif CONF_EVENT in action: @@ -211,14 +210,7 @@ class Script(ToggleEntity): def _call_service(self, action): """Call the service specified in the action.""" - # Backwards compatibility - if CONF_SERVICE not in action and CONF_SERVICE_OLD in action: - action[CONF_SERVICE] = action[CONF_SERVICE_OLD] - - if CONF_SERVICE_DATA not in action and CONF_SERVICE_DATA_OLD in action: - action[CONF_SERVICE_DATA] = action[CONF_SERVICE_DATA_OLD] - - self._last_action = action.get(CONF_ALIAS, action[CONF_SERVICE]) + self._last_action = action.get(CONF_ALIAS, 'call service') _LOGGER.info("Executing script %s step %s", self._name, self._last_action) call_from_config(self.hass, action, True) diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 450c75740b5..2236128227c 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -92,35 +92,6 @@ class TestScript(unittest.TestCase): self.assertIsNone( self.hass.states.get(ENTITY_ID).attributes.get('can_cancel')) - def test_calling_service_old(self): - """Test the calling of an old service.""" - calls = [] - - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - self.hass.services.register('test', 'script', record_call) - - self.assertTrue(script.setup(self.hass, { - 'script': { - 'test': { - 'sequence': [{ - 'execute_service': 'test.script', - 'service_data': { - 'hello': 'world' - } - }] - } - } - })) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertEqual(1, len(calls)) - self.assertEqual('world', calls[0].data.get('hello')) - def test_calling_service(self): """Test the calling of a service.""" calls = [] @@ -136,7 +107,7 @@ class TestScript(unittest.TestCase): 'test': { 'sequence': [{ 'service': 'test.script', - 'service_data': { + 'data': { 'hello': 'world' } }] @@ -150,6 +121,46 @@ class TestScript(unittest.TestCase): self.assertEqual(1, len(calls)) self.assertEqual('world', calls[0].data.get('hello')) + def test_calling_service_template(self): + """Test the calling of a service.""" + calls = [] + + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + self.hass.services.register('test', 'script', record_call) + + self.assertTrue(script.setup(self.hass, { + 'script': { + 'test': { + 'sequence': [{ + 'service_template': """ + {% if True %} + test.script + {% else %} + test.not_script + {% endif %}""", + 'data_template': { + 'hello': """ + {% if True %} + world + {% else %} + Not world + {% endif %} + """ + } + }] + } + } + })) + + script.turn_on(self.hass, ENTITY_ID) + self.hass.pool.block_till_done() + + self.assertEqual(1, len(calls)) + self.assertEqual('world', calls[0].data.get('hello')) + def test_delay(self): """Test the delay.""" event = 'test_event' From d6344d649292c7cde93549acbc7d6d76f7db22d2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 14 Mar 2016 08:25:04 +0100 Subject: [PATCH 10/76] Fixed close connection issue with rfxtrx device and update rfxtrx lib --- .coveragerc | 3 --- homeassistant/components/rfxtrx.py | 8 ++++++-- requirements_all.txt | 6 +++--- tests/components/light/test_rfxtrx.py | 5 ++--- tests/components/switch/test_rfxtrx.py | 5 ++--- tests/components/test_rfxtrx.py | 6 ++---- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index 63eb4ea9be1..a885c5a28a6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,9 +58,6 @@ omit = homeassistant/components/nest.py homeassistant/components/*/nest.py - homeassistant/components/rfxtrx.py - homeassistant/components/*/rfxtrx.py - homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index cfc6f1bab5a..f8adaa43223 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -7,9 +7,9 @@ https://home-assistant.io/components/rfxtrx/ import logging from homeassistant.util import slugify +from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/' + - 'archive/0.5.zip#pyRFXtrx==0.5'] +REQUIREMENTS = ['pyRFXtrx==0.6.5'] DOMAIN = "rfxtrx" @@ -72,6 +72,10 @@ def setup(hass, config): else: RFXOBJECT = rfxtrxmod.Core(device, handle_receive, debug=debug) + def _shutdown_rfxtrx(event): + RFXOBJECT.close_connection() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) + return True diff --git a/requirements_all.txt b/requirements_all.txt index a3f6a33bc27..0bf68dd8a8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,9 +66,6 @@ hikvision==0.4 # homeassistant.components.sensor.dht # http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 -# homeassistant.components.rfxtrx -https://github.com/Danielhiversen/pyRFXtrx/archive/0.5.zip#pyRFXtrx==0.5 - # homeassistant.components.sensor.netatmo https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0 @@ -157,6 +154,9 @@ pushetta==1.0.15 # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 +# homeassistant.components.rfxtrx +pyRFXtrx==0.6.5 + # homeassistant.components.media_player.cast pychromecast==0.7.2 diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index 4cf9a048863..1d55e6e732a 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -5,12 +5,9 @@ from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.components.light import rfxtrx from unittest.mock import patch -import pytest - from tests.common import get_test_home_assistant -@pytest.mark.skipif(True, reason='Does not clean up properly, takes 100% CPU') class TestLightRfxtrx(unittest.TestCase): """Test the Rfxtrx light platform.""" @@ -22,6 +19,8 @@ class TestLightRfxtrx(unittest.TestCase): """Stop everything that was started.""" rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] rfxtrx_core.RFX_DEVICES = {} + if rfxtrx_core.RFXOBJECT: + rfxtrx_core.RFXOBJECT.close_connection() self.hass.stop() def test_default_config(self): diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/switch/test_rfxtrx.py index f9fe805fa46..cf60ae108bb 100644 --- a/tests/components/switch/test_rfxtrx.py +++ b/tests/components/switch/test_rfxtrx.py @@ -5,12 +5,9 @@ from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.components.switch import rfxtrx from unittest.mock import patch -import pytest - from tests.common import get_test_home_assistant -@pytest.mark.skipif(True, reason='Does not clean up properly, takes 100% CPU') class TestSwitchRfxtrx(unittest.TestCase): """Test the Rfxtrx switch platform.""" @@ -22,6 +19,8 @@ class TestSwitchRfxtrx(unittest.TestCase): """Stop everything that was started.""" rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] rfxtrx_core.RFX_DEVICES = {} + if rfxtrx_core.RFXOBJECT: + rfxtrx_core.RFXOBJECT.close_connection() self.hass.stop() def test_default_config(self): diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index 7c2ef788a44..27ac0bfac5d 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -6,12 +6,9 @@ import time from homeassistant.components import rfxtrx as rfxtrx from homeassistant.components.sensor import rfxtrx as rfxtrx_sensor -import pytest - from tests.common import get_test_home_assistant -@pytest.mark.skipif(True, reason='Does not clean up properly, takes 100% CPU') class TestRFXTRX(unittest.TestCase): """Test the Rfxtrx component.""" @@ -23,7 +20,8 @@ class TestRFXTRX(unittest.TestCase): """Stop everything that was started.""" rfxtrx.RECEIVED_EVT_SUBSCRIBERS = [] rfxtrx.RFX_DEVICES = {} - rfxtrx.RFXOBJECT = None + if rfxtrx.RFXOBJECT: + rfxtrx.RFXOBJECT.close_connection() self.hass.stop() def test_default_config(self): From e5c8dd03e1f1076958e2fb31f23bb9ac4af28741 Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 14 Mar 2016 10:10:38 +0000 Subject: [PATCH 11/76] Catch exception common during startup. --- homeassistant/components/automation/template.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index aae892ea80d..1d17246c012 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -55,8 +55,13 @@ def _check_template(hass, value_template): """Check if result of template is true.""" try: value = template.render(hass, value_template, {}) - except TemplateError: - _LOGGER.exception('Error parsing template') + except TemplateError as ex: + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning(ex) + else: + _LOGGER.error(ex) return False return value.lower() == 'true' From fe2adff017f85d5de097a53892d4c0e81de18204 Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 14 Mar 2016 10:29:12 +0000 Subject: [PATCH 12/76] Handle startup race condition. --- homeassistant/components/binary_sensor/wemo.py | 3 +++ homeassistant/components/switch/wemo.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index f3cd91baa04..0e3259a3a96 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -45,6 +45,9 @@ class WemoBinarySensor(BinarySensorDevice): _LOGGER.info( 'Subscription update for %s', _device) + if not hasattr(self, 'hass'): + self.update() + return self.update_ha_state(True) @property diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 90d0f46ba68..63b2665449e 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -63,6 +63,9 @@ class WemoSwitch(SwitchDevice): _LOGGER.info( 'Subscription update for %s', _device) + if not hasattr(self, 'hass'): + self.update() + return self.update_ha_state(True) @property From 2bdad928d0ea7e8f85636dcba485e394e234b6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Mon, 14 Mar 2016 22:03:30 +0100 Subject: [PATCH 13/76] input slider --- homeassistant/components/input_slider.py | 145 +++++++++++++++++++++++ tests/components/test_input_slider.py | 64 ++++++++++ 2 files changed, 209 insertions(+) create mode 100644 homeassistant/components/input_slider.py create mode 100644 tests/components/test_input_slider.py diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py new file mode 100644 index 00000000000..bef270f1387 --- /dev/null +++ b/homeassistant/components/input_slider.py @@ -0,0 +1,145 @@ +""" +Component to offer a way to select a value from a slider. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/input_slider/ +""" +import logging + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util import slugify + +DOMAIN = 'input_slider' +ENTITY_ID_FORMAT = DOMAIN + '.{}' +_LOGGER = logging.getLogger(__name__) + +CONF_NAME = 'name' +CONF_INITIAL = 'initial' +CONF_MIN = 'min' +CONF_MAX = 'max' +CONF_ICON = 'icon' +CONF_STEP = 'step' + +ATTR_VALUE = 'value' +ATTR_MIN = 'min' +ATTR_MAX = 'max' +ATTR_STEP = 'step' + +SERVICE_SELECT_VALUE = 'select_value' + + +def select_value(hass, entity_id, value): + """Set input_slider to value.""" + hass.services.call(DOMAIN, SERVICE_SELECT_VALUE, { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }) + + +def setup(hass, config): + """Set up input slider.""" + if not isinstance(config.get(DOMAIN), dict): + _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) + return False + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if object_id != slugify(object_id): + _LOGGER.warning("Found invalid key for boolean input: %s. " + "Use %s instead", object_id, slugify(object_id)) + continue + if not cfg: + _LOGGER.warning("No configuration specified for %s", object_id) + continue + + name = cfg.get(CONF_NAME) + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + state = cfg.get(CONF_INITIAL) + step = cfg.get(CONF_STEP) + icon = cfg.get(CONF_ICON) + + if state < minimum: + state = minimum + if state > maximum: + state = maximum + + entities.append( + InputSlider(object_id, name, state, minimum, maximum, step, icon) + ) + + if not entities: + return False + + def select_value_service(call): + """Handle a calls to the input slider services.""" + target_inputs = component.extract_from_service(call) + + for input_slider in target_inputs: + input_slider.select_value(call.data.get(ATTR_VALUE)) + + hass.services.register(DOMAIN, SERVICE_SELECT_VALUE, + select_value_service) + + component.add_entities(entities) + + return True + + +class InputSlider(Entity): + """Represent an slider.""" + + # pylint: disable=too-many-arguments + def __init__(self, object_id, name, state, minimum, maximum, step, icon): + """Initialize a select input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._current_value = state + self._minimum = minimum + self._maximum = maximum + self._step = step + self._icon = icon + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Name of the select input.""" + return self._name + + @property + def icon(self): + """Icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """State of the component.""" + return self._current_value + + @property + def state_attributes(self): + """State attributes.""" + return { + ATTR_MIN: self._minimum, + ATTR_MAX: self._maximum, + ATTR_STEP: self._step + } + + def select_value(self, value): + """Select new value.""" + num_value = int(value) + if num_value < self._minimum or num_value > self._maximum: + _LOGGER.warning('Invalid value: %s (range %s - %s)', + num_value, self._minimum, self._maximum) + return + self._current_value = num_value + self.update_ha_state() diff --git a/tests/components/test_input_slider.py b/tests/components/test_input_slider.py new file mode 100644 index 00000000000..f69553a21f7 --- /dev/null +++ b/tests/components/test_input_slider.py @@ -0,0 +1,64 @@ +"""The tests for the Input slider component.""" +# pylint: disable=too-many-public-methods,protected-access +import unittest + +from homeassistant.components import input_slider + +from tests.common import get_test_home_assistant + + +class TestInputSlider(unittest.TestCase): + """Test the input slider component.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_config(self): + """Test config.""" + self.assertFalse(input_slider.setup(self.hass, { + 'input_slider': None + })) + + self.assertFalse(input_slider.setup(self.hass, { + 'input_slider': { + } + })) + + self.assertFalse(input_slider.setup(self.hass, { + 'input_slider': { + 'name with space': None + } + })) + + def test_select_value(self): + """Test select_value method.""" + self.assertTrue(input_slider.setup(self.hass, { + 'input_slider': { + 'test_1': { + 'initial': 50, + 'min': 0, + 'max': 100, + }, + } + })) + entity_id = 'input_slider.test_1' + + state = self.hass.states.get(entity_id) + self.assertEqual('50', state.state) + + input_slider.select_value(self.hass, entity_id, '70') + self.hass.pool.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual('70', state.state) + + input_slider.select_value(self.hass, entity_id, '110') + self.hass.pool.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual('70', state.state) From 8a6cc49438789467f179027275716d5c08db31a2 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 14 Mar 2016 19:08:44 -0600 Subject: [PATCH 14/76] Upgrading python-wink to 0.6.3 This corrects a bug where multi-sensors' internal states were rendered null when downloading state updates from the Wink API. --- homeassistant/components/binary_sensor/wink.py | 2 +- homeassistant/components/garage_door/wink.py | 2 +- homeassistant/components/light/wink.py | 2 +- homeassistant/components/lock/wink.py | 2 +- homeassistant/components/sensor/wink.py | 2 +- homeassistant/components/switch/wink.py | 2 +- homeassistant/components/wink.py | 2 +- requirements_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 3fe092f6cc8..47f6e3fff90 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index c721033c696..c1b662f2af2 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.garage_door import GarageDoorDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index f268864f350..920b646c934 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.light import ATTR_BRIGHTNESS, Light from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index a78713afce1..9e8b4e29c10 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index c6bc35d516b..a4367068c6a 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -10,7 +10,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, STATE_OPEN, TEMP_CELCIUS) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] SENSOR_TYPES = ['temperature', 'humidity'] diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 3bf6768919c..63ce576d9bf 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index b86b4b5338f..de4c1b3c9ce 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.loader import get_component DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.6.2'] +REQUIREMENTS = ['python-wink==0.6.3'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" diff --git a/requirements_all.txt b/requirements_all.txt index a3f6a33bc27..1a4d080fd88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -213,7 +213,7 @@ python-twitch==1.2.0 # homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.6.2 +python-wink==0.6.3 # homeassistant.components.keyboard pyuserinput==0.1.9 From cde05b91ce843784a018b47f42d2fa19b54c1d0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2016 21:12:42 -0700 Subject: [PATCH 15/76] Better netgear logging --- homeassistant/components/device_tracker/netgear.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index f1225d2fb73..6b0cbc5f465 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -89,4 +89,9 @@ class NetgearDeviceScanner(object): with self.lock: _LOGGER.info("Scanning") - self.last_results = self._api.get_attached_devices() or [] + results = self._api.get_attached_devices() + + if results is None: + _LOGGER.warning('Error scanning devices') + + self.last_results = results or [] From d28116f2bf25cacc70276bc2067de704ef0ea6cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2016 23:55:52 -0700 Subject: [PATCH 16/76] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 412 +++++++++++------- .../www_static/home-assistant-polymer | 2 +- .../www_static/webcomponents-lite.min.js | 6 +- 4 files changed, 271 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index d1c34c7e7ab..c389075f5d1 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """DO NOT MODIFY. Auto-generated by build_frontend script.""" -VERSION = "30bcc0eacc13a2317000824741dc9ac0" +VERSION = "fa0e5d3ffc68244b9ab5b1f0038f06cd" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index f7e6a8da0bf..4be85a39c64 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,7 +1,7 @@ -