From 3333dcc6c2e921a052d17f9238085813cb3b13e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Feb 2018 14:55:20 -0800 Subject: [PATCH 1/4] Version bump to 0.63 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 915ee5ac216..1c923a35936 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 63 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From a9412d27aab92a694127949f4a7b1385322d08a1 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 10 Feb 2018 00:22:50 +0100 Subject: [PATCH 2/4] allow wildcards in subscription (#12247) * allow wildcards in subscription * remove whitespaces * make function public * also implement for mqtt_json * avoid mqtt-outside topic matching * add wildcard tests * add not matching wildcard tests * fix not-matching tests --- .../components/device_tracker/mqtt.py | 17 ++--- .../components/device_tracker/mqtt_json.py | 42 +++++----- tests/components/device_tracker/test_mqtt.py | 76 +++++++++++++++++++ .../device_tracker/test_mqtt_json.py | 74 ++++++++++++++++++ 4 files changed, 175 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index aab5b43acea..2e2d9b10d98 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - hass.async_add_job( - async_see(dev_id=dev_id_lookup[topic], location_name=payload)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + hass.async_add_job( + async_see(dev_id=dev_id, location_name=payload)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 0ef4f1835b6..7bcad60236a 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -41,32 +41,26 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): devices = config[CONF_DEVICES] qos = config[CONF_QOS] - dev_id_lookup = {} - - @callback - def async_tracker_message_received(topic, payload, qos): - """Handle received MQTT message.""" - dev_id = dev_id_lookup[topic] - - try: - data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) - except vol.MultipleInvalid: - _LOGGER.error("Skipping update for following data " - "because of missing or malformatted data: %s", - payload) - return - except ValueError: - _LOGGER.error("Error parsing JSON payload: %s", payload) - return - - kwargs = _parse_see_args(dev_id, data) - hass.async_add_job( - async_see(**kwargs)) - for dev_id, topic in devices.items(): - dev_id_lookup[topic] = dev_id + @callback + def async_message_received(topic, payload, qos, dev_id=dev_id): + """Handle received MQTT message.""" + try: + data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) + except vol.MultipleInvalid: + _LOGGER.error("Skipping update for following data " + "because of missing or malformatted data: %s", + payload) + return + except ValueError: + _LOGGER.error("Error parsing JSON payload: %s", payload) + return + + kwargs = _parse_see_args(dev_id, data) + hass.async_add_job(async_see(**kwargs)) + yield from mqtt.async_subscribe( - hass, topic, async_tracker_message_received, qos) + hass, topic, async_message_received, qos) return True diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index 4905ab4d029..78750e91f83 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -70,3 +70,79 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): fire_mqtt_message(self.hass, topic, location) self.hass.block_till_done() self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/location/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertEqual(location, self.hass.states.get(entity_id).state) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/+/paulus' + topic = '/location/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'paulus' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = '/location/#' + topic = '/somewhere/room/paulus' + location = 'work' + + self.hass.config.components = set(['mqtt', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) diff --git a/tests/components/device_tracker/test_mqtt_json.py b/tests/components/device_tracker/test_mqtt_json.py index 1755f424d29..43f4fc3bbf3 100644 --- a/tests/components/device_tracker/test_mqtt_json.py +++ b/tests/components/device_tracker/test_mqtt_json.py @@ -123,3 +123,77 @@ class TestComponentsDeviceTrackerJSONMQTT(unittest.TestCase): "Skipping update for following data because of missing " "or malformatted data: {\"longitude\": 2.0}", test_handle.output[0]) + + def test_single_level_wildcard_topic(self): + """Test single level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/+/zanzito' + topic = 'location/room/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_multi_level_wildcard_topic(self): + """Test multi level wildcard topic.""" + dev_id = 'zanzito' + subscription = 'location/#' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_single_level_wildcard_topic_not_matching(self): + """Test not matching single level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/+/zanzito' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) + + def test_multi_level_wildcard_topic_not_matching(self): + """Test not matching multi level wildcard topic.""" + dev_id = 'zanzito' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + subscription = 'location/#' + topic = 'somewhere/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: subscription} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIsNone(self.hass.states.get(entity_id)) From aad26599ae08cd21cff8c1c3fd0b9440980506e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Feb 2018 02:40:24 -0800 Subject: [PATCH 3/4] Retry keyset cloud (#12270) * Use less threads in helpers.event tests * Add helpers.event.async_call_later * Cloud: retry fetching keyset --- homeassistant/components/cloud/__init__.py | 56 ++++++++++------------ homeassistant/helpers/event.py | 9 ++++ tests/components/cloud/test_http_api.py | 4 +- tests/components/cloud/test_init.py | 2 +- tests/components/cloud/test_iot.py | 8 ++-- tests/helpers/test_event.py | 27 +++++++++-- 6 files changed, 66 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index a5bbf805d42..e17c9ee1b1e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -16,8 +16,7 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE) -from homeassistant.helpers import entityfilter -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import entityfilter, config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh @@ -105,12 +104,7 @@ def async_setup(hass, config): ) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) - - success = yield from cloud.initialize() - - if not success: - return False - + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) yield from http_api.async_setup(hass) return True @@ -192,19 +186,6 @@ class Cloud: return self._gactions_config - @asyncio.coroutine - def initialize(self): - """Initialize and load cloud info.""" - jwt_success = yield from self._fetch_jwt_keyset() - - if not jwt_success: - return False - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._start_cloud) - - return True - def path(self, *parts): """Get config path inside cloud dir. @@ -234,19 +215,34 @@ class Cloud: 'refresh_token': self.refresh_token, }, indent=4)) - def _start_cloud(self, event): + @asyncio.coroutine + def async_start(self, _): """Start the cloud component.""" - # Ensure config dir exists - path = self.hass.config.path(CONFIG_DIR) - if not os.path.isdir(path): - os.mkdir(path) + success = yield from self._fetch_jwt_keyset() - user_info = self.user_info_path - if not os.path.isfile(user_info): + # Fetching keyset can fail if internet is not up yet. + if not success: + self.hass.helpers.async_call_later(5, self.async_start) return - with open(user_info, 'rt') as file: - info = json.loads(file.read()) + def load_config(): + """Load config.""" + # Ensure config dir exists + path = self.hass.config.path(CONFIG_DIR) + if not os.path.isdir(path): + os.mkdir(path) + + user_info = self.user_info_path + if not os.path.isfile(user_info): + return None + + with open(user_info, 'rt') as file: + return json.loads(file.read()) + + info = yield from self.hass.async_add_job(load_config) + + if info is None: + return # Validate tokens try: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f11b2eacf3a..eab2d583f45 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,5 @@ """Helpers for listening to events.""" +from datetime import timedelta import functools as ft from homeassistant.loader import bind_hass @@ -219,6 +220,14 @@ track_point_in_utc_time = threaded_listener_factory( async_track_point_in_utc_time) +@callback +@bind_hass +def async_call_later(hass, delay, action): + """Add a listener that is called in .""" + return async_track_point_in_utc_time( + hass, action, dt_util.utcnow() + timedelta(seconds=delay)) + + @callback @bind_hass def async_track_time_interval(hass, action, interval): diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 7623b25d401..69cd540e7d5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,8 +14,8 @@ from tests.common import mock_coro @pytest.fixture def cloud_client(hass, test_client): """Fixture that can fetch from the cloud client.""" - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): hass.loop.run_until_complete(async_setup_component(hass, 'cloud', { 'cloud': { 'mode': 'development', diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 7d23d9faad4..70990519a0b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -87,7 +87,7 @@ def test_initialize_loads_info(mock_os, hass): with patch('homeassistant.components.cloud.open', mopen, create=True), \ patch('homeassistant.components.cloud.Cloud._decode_claims'): - cl._start_cloud(None) + yield from cl.async_start(None) assert cl.id_token == 'test-id-token' assert cl.access_token == 'test-access-token' diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 529559f56af..53340ecede1 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -266,8 +266,8 @@ def test_handler_alexa(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'alexa': { @@ -309,8 +309,8 @@ def test_handler_google_actions(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('homeassistant.components.cloud.Cloud.initialize', - return_value=mock_coro(True)): + with patch('homeassistant.components.cloud.Cloud.async_start', + return_value=mock_coro()): setup = yield from async_setup_component(hass, 'cloud', { 'cloud': { 'google_actions': { diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 7d601c7a78d..73f2b9ff5a4 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,10 +7,12 @@ from datetime import datetime, timedelta from astral import Astral import pytest +from homeassistant.core import callback from homeassistant.setup import setup_component import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( + async_call_later, track_point_in_utc_time, track_point_in_time, track_utc_time_change, @@ -52,7 +54,7 @@ class TestEventHelpers(unittest.TestCase): runs = [] track_point_in_utc_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(before_birthday) self.hass.block_till_done() @@ -68,14 +70,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(1, len(runs)) track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) self._send_time_changed(after_birthday) self.hass.block_till_done() self.assertEqual(2, len(runs)) unsub = track_point_in_time( - self.hass, lambda x: runs.append(1), birthday_paulus) + self.hass, callback(lambda x: runs.append(1)), birthday_paulus) unsub() self._send_time_changed(after_birthday) @@ -642,3 +644,22 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + + +@asyncio.coroutine +def test_async_call_later(hass): + """Test calling an action later.""" + def action(): pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + remove = async_call_later(hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert hass is hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + assert remove is mock() From 18aa1037ddf7757e2792147c929649408add6d4b Mon Sep 17 00:00:00 2001 From: Slava Date: Sat, 10 Feb 2018 21:59:04 +0100 Subject: [PATCH 4/4] Update limitlessled requirement to v1.0.9 (#12275) * Update limitlessled requirement to v1.0.9 * trigger cla * take back empty line --- homeassistant/components/light/limitlessled.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index aad2abdd183..0c6b1143bbd 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['limitlessled==1.0.8'] +REQUIREMENTS = ['limitlessled==1.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f50c010072e..3ca5b9fc763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ liffylights==0.9.4 lightify==1.0.6.1 # homeassistant.components.light.limitlessled -limitlessled==1.0.8 +limitlessled==1.0.9 # homeassistant.components.linode linode-api==4.1.4b2