From b601fa52ba5ad275fca183cd975c1a88a19e2de0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Mar 2019 13:02:55 -0700 Subject: [PATCH 01/71] Bumped version to 0.89.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85097dcb652..c4033b81dfd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 07022c46f2d118428a06cc15aa86782d24d6d5c9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Mar 2019 13:03:58 -0700 Subject: [PATCH 02/71] Version bump to 0.90.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c4033b81dfd..7b74c25d08a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 89 +MINOR_VERSION = 90 PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 42c9472a746169e9ee9b2544e20914fee83bbe5b Mon Sep 17 00:00:00 2001 From: Phil Hawthorne Date: Thu, 14 Mar 2019 13:08:23 +1100 Subject: [PATCH 03/71] Remove UTF8 decoding for Waze (#22020) Removes the UFT8 decoding for the Waze sensor, which broke in 0.89 Fixes #21739 --- homeassistant/components/sensor/waze_travel_time.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 83b4f3ad934..96a4c747293 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -218,7 +218,6 @@ class WazeTravelTime(Entity): route = sorted(routes, key=(lambda key: routes[key][0]))[0] duration, distance = routes[route] - route = bytes(route, 'ISO-8859-1').decode('UTF-8') self._state = { 'duration': duration, 'distance': distance, From c78e332df36706e82edb135e2bc034090da882c6 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 14 Mar 2019 18:18:25 +0100 Subject: [PATCH 04/71] Bring back the boiler status (#22021) --- homeassistant/components/netatmo/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 409358c2f04..2d8b06dd466 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -187,7 +187,7 @@ class NetatmoThermostat(ClimateDevice): "module_id": self._data.room_status[self._room_id]['module_id'] } if module_type == NA_THERM: - state_attributes["boiler_status"] = self.current_operation + state_attributes["boiler_status"] = self._data.boilerstatus elif module_type == NA_VALVE: state_attributes["heating_power_request"] = \ self._data.room_status[self._room_id]['heating_power_request'] From 90c878a7eda68df641334cdd5cdc97af6016c933 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 14 Mar 2019 18:58:32 +0100 Subject: [PATCH 05/71] Update additional platforms to use new MQTT message callback (#22030) * Move additional platforms to new MQTT callback * Fix automation.mqtt --- .../components/alarm_control_panel/manual_mqtt.py | 12 ++++++------ homeassistant/components/automation/mqtt.py | 12 ++++++------ homeassistant/components/device_tracker/mqtt_json.py | 8 ++++---- .../components/mqtt_eventstream/__init__.py | 4 ++-- homeassistant/components/owntracks/__init__.py | 8 ++++---- homeassistant/components/sensor/arwn.py | 6 +++--- homeassistant/components/sensor/mqtt_room.py | 6 +++--- homeassistant/components/snips/__init__.py | 8 ++++---- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 693c15fa424..9bee2b81d61 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -342,18 +342,18 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): ) @callback - def message_received(topic, payload, qos): + def message_received(msg): """Run when new MQTT message has been received.""" - if payload == self._payload_disarm: + if msg.payload == self._payload_disarm: self.async_alarm_disarm(self._code) - elif payload == self._payload_arm_home: + elif msg.payload == self._payload_arm_home: self.async_alarm_arm_home(self._code) - elif payload == self._payload_arm_away: + elif msg.payload == self._payload_arm_away: self.async_alarm_arm_away(self._code) - elif payload == self._payload_arm_night: + elif msg.payload == self._payload_arm_night: self.async_alarm_arm_night(self._code) else: - _LOGGER.warning("Received unexpected payload: %s", payload) + _LOGGER.warning("Received unexpected payload: %s", msg.payload) return await mqtt.async_subscribe( diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 5f52da745ee..ff89cd47024 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -29,18 +29,18 @@ async def async_trigger(hass, config, action, automation_info): encoding = config[CONF_ENCODING] or None @callback - def mqtt_automation_listener(msg_topic, msg_payload, qos): + def mqtt_automation_listener(mqttmsg): """Listen for MQTT messages.""" - if payload is None or payload == msg_payload: + if payload is None or payload == mqttmsg.payload: data = { 'platform': 'mqtt', - 'topic': msg_topic, - 'payload': msg_payload, - 'qos': qos, + 'topic': mqttmsg.topic, + 'payload': mqttmsg.payload, + 'qos': mqttmsg.qos, } try: - data['payload_json'] = json.loads(msg_payload) + data['payload_json'] = json.loads(mqttmsg.payload) except ValueError: pass diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 3a820d189f4..0a1b327dca9 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -41,17 +41,17 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): for dev_id, topic in devices.items(): @callback - def async_message_received(topic, payload, qos, dev_id=dev_id): + def async_message_received(msg, dev_id=dev_id): """Handle received MQTT message.""" try: - data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) + data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(msg.payload)) except vol.MultipleInvalid: _LOGGER.error("Skipping update for following data " "because of missing or malformatted data: %s", - payload) + msg.payload) return except ValueError: - _LOGGER.error("Error parsing JSON payload: %s", payload) + _LOGGER.error("Error parsing JSON payload: %s", msg.payload) return kwargs = _parse_see_args(dev_id, data) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 6e545d19fe2..fb6a94f1870 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -74,9 +74,9 @@ def async_setup(hass, config): # Process events from a remote server that are received on a queue. @callback - def _event_receiver(topic, payload, qos): + def _event_receiver(msg): """Receive events published by and fire them on this hass instance.""" - event = json.loads(payload) + event = json.loads(msg.payload) event_type = event.get('event_type') event_data = event.get('event_data') diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index c0d3d152270..df6b815e4c5 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -99,16 +99,16 @@ async def async_connect_mqtt(hass, component): """Subscribe to MQTT topic.""" context = hass.data[DOMAIN]['context'] - async def async_handle_mqtt_message(topic, payload, qos): + async def async_handle_mqtt_message(msg): """Handle incoming OwnTracks message.""" try: - message = json.loads(payload) + message = json.loads(msg.payload) except ValueError: # If invalid JSON - _LOGGER.error("Unable to parse payload as JSON: %s", payload) + _LOGGER.error("Unable to parse payload as JSON: %s", msg.payload) return - message['topic'] = topic + message['topic'] = msg.topic hass.helpers.dispatcher.async_dispatcher_send( DOMAIN, hass, context, message) diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 2b79e4c3a9a..95825f4ca13 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -61,7 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ARWN platform.""" @callback - def async_sensor_event_received(topic, payload, qos): + def async_sensor_event_received(msg): """Process events as sensors. When a new event on our topic (arwn/#) is received we map it @@ -74,8 +74,8 @@ async def async_setup_platform(hass, config, async_add_entities, This lets us dynamically incorporate sensors without any configuration on our side. """ - event = json.loads(payload) - sensors = discover_sensors(topic, event) + event = json.loads(msg.payload) + sensors = discover_sensors(msg.topic, event) if not sensors: return diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index b52f039281c..36f99719da4 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -90,16 +90,16 @@ class MQTTRoomSensor(Entity): self.async_schedule_update_ha_state() @callback - def message_received(topic, payload, qos): + def message_received(msg): """Handle new MQTT messages.""" try: - data = MQTT_PAYLOAD(payload) + data = MQTT_PAYLOAD(msg.payload) except vol.MultipleInvalid as error: _LOGGER.debug( "Skipping update because of malformatted data: %s", error) return - device = _parse_update_data(topic, data) + device = _parse_update_data(msg.topic, data) if device.get(CONF_DEVICE_ID) == self._device_id: if self._distance is None or self._updated is None: update_state(**device) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 20cc7137ef8..0cc96d66b1a 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -95,14 +95,14 @@ async def async_setup(hass, config): if CONF_FEEDBACK in config[DOMAIN]: async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK]) - async def message_received(topic, payload, qos): + async def message_received(msg): """Handle new messages on MQTT.""" - _LOGGER.debug("New intent: %s", payload) + _LOGGER.debug("New intent: %s", msg.payload) try: - request = json.loads(payload) + request = json.loads(msg.payload) except TypeError: - _LOGGER.error('Received invalid JSON: %s', payload) + _LOGGER.error('Received invalid JSON: %s', msg.payload) return if (request['intent']['confidenceScore'] From 7057958e3e082a0e4a42ccfe5ed099b4c15ac396 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 14 Mar 2019 09:10:36 -0700 Subject: [PATCH 06/71] Fix lifx light async error (#22031) --- homeassistant/components/lifx/light.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index c0b6158f186..19a9f7583ec 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -24,7 +24,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -250,7 +250,7 @@ class LIFXManager: async def service_handler(service): """Apply a service.""" tasks = [] - for light in self.service_to_entities(service): + for light in await self.async_service_to_entities(service): if service.service == SERVICE_LIFX_SET_STATE: task = light.set_state(**service.data) tasks.append(self.hass.async_create_task(task)) @@ -265,7 +265,7 @@ class LIFXManager: """Register the LIFX effects as hass service calls.""" async def service_handler(service): """Apply a service, i.e. start an effect.""" - entities = self.service_to_entities(service) + entities = await self.async_service_to_entities(service) if entities: await self.start_effect( entities, service.service, **service.data) @@ -314,9 +314,9 @@ class LIFXManager: elif service == SERVICE_EFFECT_STOP: await self.effects_conductor.stop(bulbs) - def service_to_entities(self, service): + async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" - entity_ids = extract_entity_ids(self.hass, service) + entity_ids = await async_extract_entity_ids(self.hass, service) if entity_ids: entities = [entity for entity in self.entities.values() if entity.entity_id in entity_ids] From 707d32495bad25ad345fc21798d5de970d4cf968 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Mar 2019 00:18:31 +0100 Subject: [PATCH 07/71] Fix Google Assistant User with Cloud (#22042) * Fix Google Assistant User with Cloud * Fix User Agent ID * respell * Fix object * Fix tests * fix lint * Fix lint --- homeassistant/components/cloud/__init__.py | 10 +++ homeassistant/components/cloud/client.py | 12 ++-- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/prefs.py | 14 +++- tests/components/cloud/test_init.py | 77 ++++++++++++++++++---- 5 files changed, 94 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index c854fe69be9..2e324f06738 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( @@ -136,12 +137,21 @@ async def async_setup(hass, config): else: kwargs = {CONF_MODE: DEFAULT_MODE} + # Alexa/Google custom config alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({}) google_conf = kwargs.pop(CONF_GOOGLE_ACTIONS, None) or GACTIONS_SCHEMA({}) + # Cloud settings prefs = CloudPreferences(hass) await prefs.async_initialize() + # Cloud user + if not prefs.cloud_user: + user = await hass.auth.async_create_system_user( + 'Home Assistant Cloud', [GROUP_ID_ADMIN]) + await prefs.async_update(cloud_user=user.id) + + # Initialize Cloud websession = hass.helpers.aiohttp_client.async_get_clientsession() client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) cloud = hass.data[DOMAIN] = Cloud(client, **kwargs) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 063a9daf00a..f73c16b1904 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -136,12 +136,16 @@ class CloudClient(Interface): if not self._prefs.google_enabled: return ga.turned_off_response(payload) - cloud = self._hass.data[DOMAIN] - return await ga.async_handle_message( - self._hass, self.google_config, - cloud.claims['cognito:username'], payload + answer = await ga.async_handle_message( + self._hass, self.google_config, self.prefs.cloud_user, payload ) + # Fix AgentUserId + cloud = self._hass.data[DOMAIN] + answer['payload']['agentUserId'] = cloud.claims['cognito:username'] + + return answer + async def async_webhook_message( self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud webhook message to client.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 65e026389f0..fdedacd6dbb 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -7,6 +7,7 @@ PREF_ENABLE_GOOGLE = 'google_enabled' PREF_ENABLE_REMOTE = 'remote_enabled' PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock' PREF_CLOUDHOOKS = 'cloudhooks' +PREF_CLOUD_USER = 'cloud_user' CONF_ALEXA = 'alexa' CONF_ALIASES = 'aliases' diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 263c17935cb..16ff8f0c213 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,7 +1,7 @@ """Preference management for cloud.""" from .const import ( DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS) + PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -26,14 +26,16 @@ class CloudPreferences: PREF_ENABLE_GOOGLE: True, PREF_ENABLE_REMOTE: False, PREF_GOOGLE_ALLOW_UNLOCK: False, - PREF_CLOUDHOOKS: {} + PREF_CLOUDHOOKS: {}, + PREF_CLOUD_USER: None, } self._prefs = prefs async def async_update(self, *, google_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF, - google_allow_unlock=_UNDEF, cloudhooks=_UNDEF): + google_allow_unlock=_UNDEF, cloudhooks=_UNDEF, + cloud_user=_UNDEF): """Update user preferences.""" for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), @@ -41,6 +43,7 @@ class CloudPreferences: (PREF_ENABLE_REMOTE, remote_enabled), (PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock), (PREF_CLOUDHOOKS, cloudhooks), + (PREF_CLOUD_USER, cloud_user), ): if value is not _UNDEF: self._prefs[key] = value @@ -75,3 +78,8 @@ class CloudPreferences: def cloudhooks(self): """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) + + @property + def cloud_user(self) -> str: + """Return ID from Home Assistant Cloud system user.""" + return self._prefs.get(PREF_CLOUD_USER) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index d3e2e50f3a7..0de395c8bbc 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,24 +1,21 @@ """Test the cloud component.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN - +from homeassistant.components.cloud.prefs import STORAGE_KEY +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.setup import async_setup_component from tests.common import mock_coro -async def test_constructor_loads_info_from_config(): +async def test_constructor_loads_info_from_config(hass): """Test non-dev mode loads info from SERVERS constant.""" - hass = MagicMock(data={}) - - with patch( - "homeassistant.components.cloud.prefs.CloudPreferences." - "async_initialize", - return_value=mock_coro() - ): - result = await cloud.async_setup(hass, { + with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + result = await async_setup_component(hass, 'cloud', { + 'http': {}, 'cloud': { cloud.CONF_MODE: cloud.MODE_DEV, 'cognito_client_id': 'test-cognito_client_id', @@ -79,3 +76,57 @@ async def test_startup_shutdown_events(hass, mock_cloud_fixture): await hass.async_block_till_done() assert mock_stop.called + + +async def test_setup_existing_cloud_user(hass, hass_storage): + """Test setup with API push default data.""" + user = await hass.auth.async_create_system_user('Cloud test') + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'data': { + 'cloud_user': user.id + } + } + with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): + result = await async_setup_component(hass, 'cloud', { + 'http': {}, + 'cloud': { + cloud.CONF_MODE: cloud.MODE_DEV, + 'cognito_client_id': 'test-cognito_client_id', + 'user_pool_id': 'test-user_pool_id', + 'region': 'test-region', + 'relayer': 'test-relayer', + } + }) + assert result + + assert hass_storage[STORAGE_KEY]['data']['cloud_user'] == user.id + + +async def test_setup_setup_cloud_user(hass, hass_storage): + """Test setup with API push default data.""" + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'data': { + 'cloud_user': None + } + } + with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): + result = await async_setup_component(hass, 'cloud', { + 'http': {}, + 'cloud': { + cloud.CONF_MODE: cloud.MODE_DEV, + 'cognito_client_id': 'test-cognito_client_id', + 'user_pool_id': 'test-user_pool_id', + 'region': 'test-region', + 'relayer': 'test-relayer', + } + }) + assert result + + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]['data']['cloud_user'] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN From fb895bba804927da756b5a48f67ed6f03ed6f3ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Mar 2019 16:25:00 -0700 Subject: [PATCH 08/71] Bumped version to 0.90.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7b74c25d08a..b1a0fbb1e4f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9e6a7a635797d127575debf238abf8f2a8212d9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 10:43:32 -0700 Subject: [PATCH 09/71] Updated frontend to 20190315.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 70c2bab0829..a8d2cbc35b9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190313.0'] +REQUIREMENTS = ['home-assistant-frontend==20190315.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c930723d00b..9200b803b8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190313.0 +home-assistant-frontend==20190315.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7f1b77559a..36f94167565 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190313.0 +home-assistant-frontend==20190315.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From b336322e9eac684230b256e2c310013c7389e512 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 13 Mar 2019 15:22:43 -0700 Subject: [PATCH 10/71] Mobile App: Require encryption for registrations that support it (#21852) ## Description: **Related issue (if applicable):** fixes #21758 ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. --- homeassistant/components/mobile_app/const.py | 1 + .../components/mobile_app/webhook.py | 25 ++++++++++++------- tests/components/mobile_app/test_webhook.py | 15 +++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 60b4cde4708..11b6f3e9865 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -41,6 +41,7 @@ ATTR_WEBHOOK_ENCRYPTED = 'encrypted' ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data' ATTR_WEBHOOK_TYPE = 'type' +ERR_ENCRYPTION_REQUIRED = 'encryption_required' ERR_INVALID_COMPONENT = 'invalid_component' ERR_RENDER_FAILURE = 'render_failure' ERR_SAVE_FAILURE = 'save_failure' diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 61188b50e1b..9efd1fcd9f8 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -20,15 +20,16 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType -from .const import (ATTR_APP_COMPONENT, DATA_DELETED_IDS, - ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, - DATA_REGISTRATIONS, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, - ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, - ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, - CONF_SECRET, DOMAIN, ERR_RENDER_FAILURE, - WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, - WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, +from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, + ATTR_EVENT_TYPE, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, + ATTR_TEMPLATE_VARIABLES, ATTR_WEBHOOK_DATA, + ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, + ATTR_WEBHOOK_TYPE, CONF_SECRET, DATA_DELETED_IDS, + DATA_REGISTRATIONS, DOMAIN, ERR_ENCRYPTION_REQUIRED, + ERR_RENDER_FAILURE, WEBHOOK_PAYLOAD_SCHEMA, + WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION) from .helpers import (_decrypt_payload, empty_okay_response, error_response, @@ -78,6 +79,12 @@ async def handle_webhook(store: Store, hass: HomeAssistantType, _LOGGER.warning('Received invalid JSON from mobile_app') return empty_okay_response(status=HTTP_BAD_REQUEST) + if (ATTR_WEBHOOK_ENCRYPTED not in req_data and + registration[ATTR_SUPPORTS_ENCRYPTION]): + _LOGGER.warning("Refusing to accept unencrypted webhook from %s", + registration[ATTR_DEVICE_NAME]) + return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required") + try: req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data) except vol.Invalid as ex: diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index a935110754c..bbdfcde93e7 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -149,3 +149,18 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 decrypted_data = decrypted_data.decode("utf-8") assert json.loads(decrypted_data) == {'rendered': 'Hello world'} + + +async def test_webhook_requires_encryption(webhook_client): # noqa: F811 + """Test that encrypted registrations only accept encrypted data.""" + resp = await webhook_client.post( + '/api/webhook/mobile_app_test', + json=RENDER_TEMPLATE + ) + + assert resp.status == 400 + + webhook_json = await resp.json() + assert 'error' in webhook_json + assert webhook_json['success'] is False + assert webhook_json['error']['code'] == 'encryption_required' From c67113ad55652da1eee9231c7b5105dd7918b021 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 13 Mar 2019 15:33:37 -0700 Subject: [PATCH 11/71] Mobile App: Support rendering multiple templates at once (#21851) * Support rendering multiple templates at once * Only catch TemplateError and dont log the error --- homeassistant/components/mobile_app/const.py | 7 ++-- .../components/mobile_app/webhook.py | 32 +++++++++---------- tests/components/mobile_app/const.py | 4 ++- tests/components/mobile_app/test_http_api.py | 6 ++-- tests/components/mobile_app/test_webhook.py | 6 ++-- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 11b6f3e9865..3f1a6bc988c 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -43,7 +43,6 @@ ATTR_WEBHOOK_TYPE = 'type' ERR_ENCRYPTION_REQUIRED = 'encryption_required' ERR_INVALID_COMPONENT = 'invalid_component' -ERR_RENDER_FAILURE = 'render_failure' ERR_SAVE_FAILURE = 'save_failure' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' @@ -99,8 +98,10 @@ FIRE_EVENT_SCHEMA = vol.Schema({ }) RENDER_TEMPLATE_SCHEMA = vol.Schema({ - vol.Required(ATTR_TEMPLATE): cv.string, - vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, + str: { + vol.Required(ATTR_TEMPLATE): cv.template, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, + } }) WEBHOOK_SCHEMAS = { diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 9efd1fcd9f8..e8372c8648d 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -15,7 +15,7 @@ from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, from homeassistant.core import EventOrigin from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, TemplateError) -from homeassistant.helpers import template +from homeassistant.helpers.template import attach from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType @@ -26,10 +26,9 @@ from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, CONF_SECRET, DATA_DELETED_IDS, DATA_REGISTRATIONS, DOMAIN, ERR_ENCRYPTION_REQUIRED, - ERR_RENDER_FAILURE, WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, + WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION) from .helpers import (_decrypt_payload, empty_okay_response, error_response, @@ -132,17 +131,18 @@ async def handle_webhook(store: Store, hass: HomeAssistantType, return empty_okay_response(headers=headers) if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: - try: - tpl = template.Template(data[ATTR_TEMPLATE], hass) - rendered = tpl.async_render(data.get(ATTR_TEMPLATE_VARIABLES)) - return webhook_response({"rendered": rendered}, - registration=registration, headers=headers) - # noqa: E722 pylint: disable=broad-except - except (ValueError, TemplateError, Exception) as ex: - _LOGGER.error("Error when rendering template during mobile_app " - "webhook (device name: %s): %s", - registration[ATTR_DEVICE_NAME], ex) - return error_response(ERR_RENDER_FAILURE, str(ex), headers=headers) + resp = {} + for key, item in data.items(): + try: + tpl = item[ATTR_TEMPLATE] + attach(hass, tpl) + resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) + # noqa: E722 pylint: disable=broad-except + except TemplateError as ex: + resp[key] = {"error": str(ex)} + + return webhook_response(resp, registration=registration, + headers=headers) if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: try: diff --git a/tests/components/mobile_app/const.py b/tests/components/mobile_app/const.py index 919a2a6e1fb..6dfe050191b 100644 --- a/tests/components/mobile_app/const.py +++ b/tests/components/mobile_app/const.py @@ -49,7 +49,9 @@ REGISTER_CLEARTEXT = { RENDER_TEMPLATE = { 'type': 'render_template', 'data': { - 'template': 'Hello world' + 'one': { + 'template': 'Hello world' + } } } diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 195d33e15b2..7861e63459a 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -5,7 +5,7 @@ import pytest from homeassistant.components.mobile_app.const import CONF_SECRET from homeassistant.const import CONF_WEBHOOK_ID -from .const import REGISTER +from .const import REGISTER, RENDER_TEMPLATE from . import authed_api_client # noqa: F401 @@ -35,7 +35,7 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 key = key[:keylen] key = key.ljust(keylen, b'\0') - payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + payload = json.dumps(RENDER_TEMPLATE['data']).encode("utf-8") data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") @@ -62,7 +62,7 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 encoder=Base64Encoder) decrypted_data = decrypted_data.decode("utf-8") - assert json.loads(decrypted_data) == {'rendered': 'Hello world'} + assert json.loads(decrypted_data) == {'one': 'Hello world'} async def test_register_invalid_component(authed_api_client): # noqa: F811 diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index bbdfcde93e7..75e8903c494 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -24,7 +24,7 @@ async def test_webhook_handle_render_template(webhook_client): # noqa: F811 assert resp.status == 200 json = await resp.json() - assert json == {'rendered': 'Hello world'} + assert json == {'one': 'Hello world'} async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 F811 @@ -123,7 +123,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 key = key[:keylen] key = key.ljust(keylen, b'\0') - payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + payload = json.dumps(RENDER_TEMPLATE['data']).encode("utf-8") data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") @@ -148,7 +148,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 encoder=Base64Encoder) decrypted_data = decrypted_data.decode("utf-8") - assert json.loads(decrypted_data) == {'rendered': 'Hello world'} + assert json.loads(decrypted_data) == {'one': 'Hello world'} async def test_webhook_requires_encryption(webhook_client): # noqa: F811 From 3fd1e8d38285b8ca491e6982576ae1eefc8ddaca Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 13 Mar 2019 15:38:53 -0700 Subject: [PATCH 12/71] Mobile App: Update Location schema updates & device ID generation (#21849) * Update location schema * Generate a random device ID at registration time for later use with device_tracker.see * Remove host name from device_tracker.see payload * Drop consider_home from the payload * Remove stale consider_home in schema * Remove source_type --- homeassistant/components/mobile_app/const.py | 24 ++++++++++- .../components/mobile_app/http_api.py | 12 ++++-- .../components/mobile_app/webhook.py | 42 ++++++++++++++----- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 3f1a6bc988c..7a497d76454 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,7 +1,10 @@ """Constants for mobile_app.""" import voluptuous as vol -from homeassistant.components.device_tracker import SERVICE_SEE_PAYLOAD_SCHEMA +from homeassistant.components.device_tracker import (ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME) from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) from homeassistant.helpers import config_validation as cv @@ -23,6 +26,7 @@ ATTR_APP_DATA = 'app_data' ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_APP_VERSION = 'app_version' +ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_NAME = 'device_name' ATTR_MANUFACTURER = 'manufacturer' ATTR_MODEL = 'model' @@ -36,6 +40,11 @@ ATTR_EVENT_TYPE = 'event_type' ATTR_TEMPLATE = 'template' ATTR_TEMPLATE_VARIABLES = 'variables' +ATTR_SPEED = 'speed' +ATTR_ALTITUDE = 'altitude' +ATTR_COURSE = 'course' +ATTR_VERTICAL_ACCURACY = 'vertical_accuracy' + ATTR_WEBHOOK_DATA = 'data' ATTR_WEBHOOK_ENCRYPTED = 'encrypted' ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data' @@ -104,10 +113,21 @@ RENDER_TEMPLATE_SCHEMA = vol.Schema({ } }) +UPDATE_LOCATION_SCHEMA = vol.Schema({ + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Required(ATTR_GPS): cv.gps, + vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): cv.positive_int, + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, +}) + WEBHOOK_SCHEMAS = { WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, - WEBHOOK_TYPE_UPDATE_LOCATION: SERVICE_SEE_PAYLOAD_SCHEMA, + WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA, WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA, } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 30083cc86b1..4948407b63b 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,4 +1,5 @@ """Provides an HTTP API for mobile_app.""" +import uuid from typing import Dict from aiohttp.web import Response, Request @@ -15,10 +16,11 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import get_component -from .const import (ATTR_APP_COMPONENT, ATTR_SUPPORTS_ENCRYPTION, - CONF_CLOUDHOOK_URL, CONF_SECRET, CONF_USER_ID, - DATA_REGISTRATIONS, DOMAIN, ERR_INVALID_COMPONENT, - ERR_SAVE_FAILURE, REGISTRATION_SCHEMA) +from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, + ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET, + CONF_USER_ID, DATA_REGISTRATIONS, DOMAIN, + ERR_INVALID_COMPONENT, ERR_SAVE_FAILURE, + REGISTRATION_SCHEMA) from .helpers import error_response, supports_encryption, savable_state @@ -66,6 +68,8 @@ class RegistrationsView(HomeAssistantView): data[CONF_CLOUDHOOK_URL] = \ await async_create_cloudhook(hass, webhook_id) + data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") + data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index e8372c8648d..4d3e0aef4c6 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -6,7 +6,9 @@ from typing import Dict from aiohttp.web import HTTPBadRequest, Response, Request import voluptuous as vol -from homeassistant.components.device_tracker import (DOMAIN as DT_DOMAIN, +from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, + ATTR_DEV_ID, + DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) from homeassistant.components.webhook import async_register as webhook_register @@ -20,15 +22,19 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType -from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, - ATTR_EVENT_TYPE, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, - ATTR_TEMPLATE_VARIABLES, ATTR_WEBHOOK_DATA, - ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, - ATTR_WEBHOOK_TYPE, CONF_SECRET, DATA_DELETED_IDS, - DATA_REGISTRATIONS, DOMAIN, ERR_ENCRYPTION_REQUIRED, - WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, - WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, +from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY, + ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, + ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS, + ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, + ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, + ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, + ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, + ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, + CONF_SECRET, DATA_DELETED_IDS, DATA_REGISTRATIONS, DOMAIN, + ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA, + WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION) from .helpers import (_decrypt_payload, empty_okay_response, error_response, @@ -145,9 +151,23 @@ async def handle_webhook(store: Store, hass: HomeAssistantType, headers=headers) if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: + see_payload = { + ATTR_DEV_ID: registration[ATTR_DEVICE_ID], + ATTR_LOCATION_NAME: data.get(ATTR_LOCATION_NAME), + ATTR_GPS: data.get(ATTR_GPS), + ATTR_GPS_ACCURACY: data.get(ATTR_GPS_ACCURACY), + ATTR_BATTERY: data.get(ATTR_BATTERY), + ATTR_ATTRIBUTES: { + ATTR_SPEED: data.get(ATTR_SPEED), + ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), + ATTR_COURSE: data.get(ATTR_COURSE), + ATTR_VERTICAL_ACCURACY: data.get(ATTR_VERTICAL_ACCURACY), + } + } + try: await hass.services.async_call(DT_DOMAIN, - DT_SEE, data, + DT_SEE, see_payload, blocking=True, context=context) # noqa: E722 pylint: disable=broad-except except (vol.Invalid, ServiceNotFound, Exception) as ex: From f7dcfe28b680db4b60cc36ba50b2c7c94f8648f9 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 14 Mar 2019 12:57:50 -0700 Subject: [PATCH 13/71] Mobile App: Register devices into the registry (#21856) * Register devices into the registry * Switch to device ID instead of webhook ID * Rearchitect mobile_app to support config entries * Kill DATA_REGISTRATIONS by migrating registrations into config entries * Fix tests * Improve how we get the config_entry_id * Remove single_instance_allowed * Simplify setup_registration * Move webhook registering functions into __init__.py since they are only ever used once * Kill get_registration websocket command * Support description_placeholders in async_abort * Add link to mobile_app implementing apps in abort dialog * Store config entry and device registry entry in hass.data instead of looking it up * Add testing to ensure that the config entry is created at registration * Fix busted async_abort test * Remove unnecessary check for entry is None --- .../mobile_app/.translations/en.json | 14 +++ .../components/mobile_app/__init__.py | 103 +++++++++++++++--- homeassistant/components/mobile_app/const.py | 5 +- .../components/mobile_app/helpers.py | 4 +- .../components/mobile_app/http_api.py | 37 ++----- .../components/mobile_app/strings.json | 14 +++ .../components/mobile_app/webhook.py | 73 +++++-------- .../components/mobile_app/websocket_api.py | 48 ++------ homeassistant/config_entries.py | 1 + homeassistant/data_entry_flow.py | 6 +- .../components/config/test_config_entries.py | 1 + tests/components/mobile_app/__init__.py | 65 ++++++----- tests/components/mobile_app/test_http_api.py | 29 ++++- tests/components/mobile_app/test_webhook.py | 39 ++++--- .../mobile_app/test_websocket_api.py | 27 ----- 15 files changed, 254 insertions(+), 212 deletions(-) create mode 100644 homeassistant/components/mobile_app/.translations/en.json create mode 100644 homeassistant/components/mobile_app/strings.json diff --git a/homeassistant/components/mobile_app/.translations/en.json b/homeassistant/components/mobile_app/.translations/en.json new file mode 100644 index 00000000000..646151a5229 --- /dev/null +++ b/homeassistant/components/mobile_app/.translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Mobile App", + "step": { + "confirm": { + "title": "Mobile App", + "description": "Do you want to set up the Mobile App component?" + } + }, + "abort": { + "install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps." + } + } +} diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 0d95bfe6832..1c348ea0782 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,11 +1,18 @@ """Integrates Native Apps to Home Assistant.""" +from homeassistant import config_entries +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.webhook import async_register as webhook_register +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import (DATA_DELETED_IDS, DATA_REGISTRATIONS, DATA_STORE, DOMAIN, - STORAGE_KEY, STORAGE_VERSION) +from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, + DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION) -from .http_api import register_http_handlers -from .webhook import register_deleted_webhooks, setup_registration +from .http_api import RegistrationsView +from .webhook import handle_webhook from .websocket_api import register_websocket_handlers DEPENDENCIES = ['device_tracker', 'http', 'webhook'] @@ -15,24 +22,88 @@ REQUIREMENTS = ['PyNaCl==1.3.0'] async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the mobile app component.""" + hass.data[DOMAIN] = { + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + } + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() if app_config is None: - app_config = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}} + app_config = { + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + } - if hass.data.get(DOMAIN) is None: - hass.data[DOMAIN] = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}} - - hass.data[DOMAIN][DATA_DELETED_IDS] = app_config.get(DATA_DELETED_IDS, []) - hass.data[DOMAIN][DATA_REGISTRATIONS] = app_config.get(DATA_REGISTRATIONS, - {}) + hass.data[DOMAIN] = app_config hass.data[DOMAIN][DATA_STORE] = store - for registration in app_config[DATA_REGISTRATIONS].values(): - setup_registration(hass, store, registration) - - register_http_handlers(hass, store) + hass.http.register_view(RegistrationsView()) register_websocket_handlers(hass) - register_deleted_webhooks(hass, store) + + for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: + try: + webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id, + handle_webhook) + except ValueError: + pass return True + + +async def async_setup_entry(hass, entry): + """Set up a mobile_app entry.""" + registration = entry.data + + webhook_id = registration[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] = entry + + device_registry = await dr.async_get_registry(hass) + + identifiers = { + (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), + (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]) + } + + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers=identifiers, + manufacturer=registration[ATTR_MANUFACTURER], + model=registration[ATTR_MODEL], + name=registration[ATTR_DEVICE_NAME], + sw_version=registration[ATTR_OS_VERSION] + ) + + hass.data[DOMAIN][DATA_DEVICES][webhook_id] = device + + registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME]) + webhook_register(hass, DOMAIN, registration_name, webhook_id, + handle_webhook) + + if ATTR_APP_COMPONENT in registration: + load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {}, + {DOMAIN: {}}) + + return True + + +@config_entries.HANDLERS.register(DOMAIN) +class MobileAppFlowHandler(config_entries.ConfigFlow): + """Handle a Mobile App config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + placeholders = { + 'apps_url': + 'https://www.home-assistant.io/components/mobile_app/#apps' + } + + return self.async_abort(reason='install_app', + description_placeholders=placeholders) + + async def async_step_registration(self, user_input=None): + """Handle a flow initialized during registration.""" + return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME], + data=user_input) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 7a497d76454..3ba029fec0e 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -17,8 +17,9 @@ CONF_CLOUDHOOK_URL = 'cloudhook_url' CONF_SECRET = 'secret' CONF_USER_ID = 'user_id' +DATA_CONFIG_ENTRIES = 'config_entries' DATA_DELETED_IDS = 'deleted_ids' -DATA_REGISTRATIONS = 'registrations' +DATA_DEVICES = 'devices' DATA_STORE = 'store' ATTR_APP_COMPONENT = 'app_component' @@ -26,6 +27,7 @@ ATTR_APP_DATA = 'app_data' ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_APP_VERSION = 'app_version' +ATTR_CONFIG_ENTRY_ID = 'entry_id' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_NAME = 'device_name' ATTR_MANUFACTURER = 'manufacturer' @@ -52,7 +54,6 @@ ATTR_WEBHOOK_TYPE = 'type' ERR_ENCRYPTION_REQUIRED = 'encryption_required' ERR_INVALID_COMPONENT = 'invalid_component' -ERR_SAVE_FAILURE = 'save_failure' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 1f67170a72c..5ec3b99b291 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -11,8 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, - CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, - DATA_REGISTRATIONS, DOMAIN) + CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -125,7 +124,6 @@ def savable_state(hass: HomeAssistantType) -> Dict: """Return a clean object containing things that should be saved.""" return { DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], - DATA_REGISTRATIONS: hass.data[DOMAIN][DATA_REGISTRATIONS] } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 4948407b63b..8076d217cac 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -8,29 +8,16 @@ from homeassistant.auth.util import generate_secret from homeassistant.components.cloud import async_create_cloudhook from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import (HTTP_CREATED, HTTP_INTERNAL_SERVER_ERROR, - CONF_WEBHOOK_ID) +from homeassistant.const import (HTTP_CREATED, CONF_WEBHOOK_ID) -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import get_component from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET, - CONF_USER_ID, DATA_REGISTRATIONS, DOMAIN, - ERR_INVALID_COMPONENT, ERR_SAVE_FAILURE, + CONF_USER_ID, DOMAIN, ERR_INVALID_COMPONENT, REGISTRATION_SCHEMA) -from .helpers import error_response, supports_encryption, savable_state - -from .webhook import setup_registration - - -def register_http_handlers(hass: HomeAssistantType, store: Store) -> bool: - """Register the HTTP handlers/views.""" - hass.http.register_view(RegistrationsView(store)) - return True +from .helpers import error_response, supports_encryption class RegistrationsView(HomeAssistantView): @@ -39,10 +26,6 @@ class RegistrationsView(HomeAssistantView): url = '/api/mobile_app/registrations' name = 'api:mobile_app:register' - def __init__(self, store: Store) -> None: - """Initialize the view.""" - self._store = store - @RequestDataValidator(REGISTRATION_SCHEMA) async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" @@ -79,16 +62,10 @@ class RegistrationsView(HomeAssistantView): data[CONF_USER_ID] = request['hass_user'].id - hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = data - - try: - await self._store.async_save(savable_state(hass)) - except HomeAssistantError: - return error_response(ERR_SAVE_FAILURE, - "Error saving registration", - status=HTTP_INTERNAL_SERVER_ERROR) - - setup_registration(hass, self._store, data) + ctx = {'source': 'registration'} + await hass.async_create_task( + hass.config_entries.flow.async_init(DOMAIN, context=ctx, + data=data)) return self.json({ CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL), diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json new file mode 100644 index 00000000000..646151a5229 --- /dev/null +++ b/homeassistant/components/mobile_app/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Mobile App", + "step": { + "confirm": { + "title": "Mobile App", + "description": "Do you want to set up the Mobile App component?" + } + }, + "abort": { + "install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps." + } + } +} diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 4d3e0aef4c6..1fab29160b7 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,7 +1,5 @@ """Webhook handlers for mobile_app.""" -from functools import partial import logging -from typing import Dict from aiohttp.web import HTTPBadRequest, Response, Request import voluptuous as vol @@ -10,27 +8,24 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, ATTR_DEV_ID, DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) -from homeassistant.components.webhook import async_register as webhook_register from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, CONF_WEBHOOK_ID, HTTP_BAD_REQUEST) from homeassistant.core import EventOrigin -from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, - TemplateError) +from homeassistant.exceptions import (ServiceNotFound, TemplateError) +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import attach -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType -from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY, - ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS, - ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, +from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, + ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, - CONF_SECRET, DATA_DELETED_IDS, DATA_REGISTRATIONS, DOMAIN, + CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DOMAIN, ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, @@ -38,45 +33,24 @@ from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY, WEBHOOK_TYPE_UPDATE_REGISTRATION) from .helpers import (_decrypt_payload, empty_okay_response, error_response, - registration_context, safe_registration, savable_state, + registration_context, safe_registration, webhook_response) _LOGGER = logging.getLogger(__name__) -def register_deleted_webhooks(hass: HomeAssistantType, store: Store): - """Register previously deleted webhook IDs so we can return 410.""" - for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: - try: - webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id, - partial(handle_webhook, store)) - except ValueError: - pass - - -def setup_registration(hass: HomeAssistantType, store: Store, - registration: Dict) -> None: - """Register the webhook for a registration and loads the app component.""" - registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME]) - webhook_id = registration[CONF_WEBHOOK_ID] - webhook_register(hass, DOMAIN, registration_name, webhook_id, - partial(handle_webhook, store)) - - if ATTR_APP_COMPONENT in registration: - load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {}, - {DOMAIN: {}}) - - -async def handle_webhook(store: Store, hass: HomeAssistantType, - webhook_id: str, request: Request) -> Response: +async def handle_webhook(hass: HomeAssistantType, webhook_id: str, + request: Request) -> Response: """Handle webhook callback.""" if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) headers = {} - registration = hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + + registration = config_entry.data try: req_data = await request.json() @@ -179,13 +153,22 @@ async def handle_webhook(store: Store, hass: HomeAssistantType, if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: new_registration = {**registration, **data} - hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = new_registration + device_registry = await dr.async_get_registry(hass) - try: - await store.async_save(savable_state(hass)) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), + (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]) + }, + manufacturer=new_registration[ATTR_MANUFACTURER], + model=new_registration[ATTR_MODEL], + name=new_registration[ATTR_DEVICE_NAME], + sw_version=new_registration[ATTR_OS_VERSION] + ) + + hass.config_entries.async_update_entry(config_entry, + data=new_registration) return webhook_response(safe_registration(new_registration), registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 5f6a25cbcec..7bc1e59d623 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -17,16 +17,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_DELETED_IDS, - DATA_REGISTRATIONS, DATA_STORE, DOMAIN) +from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_CONFIG_ENTRIES, + DATA_DELETED_IDS, DATA_STORE, DOMAIN) from .helpers import safe_registration, savable_state def register_websocket_handlers(hass: HomeAssistantType) -> bool: """Register the websocket handlers.""" - async_register_command(hass, websocket_get_registration) - async_register_command(hass, websocket_get_user_registrations) async_register_command(hass, websocket_delete_registration) @@ -34,39 +32,6 @@ def register_websocket_handlers(hass: HomeAssistantType) -> bool: return True -@ws_require_user() -@async_response -@websocket_command({ - vol.Required('type'): 'mobile_app/get_registration', - vol.Required(CONF_WEBHOOK_ID): cv.string, -}) -async def websocket_get_registration( - hass: HomeAssistantType, connection: ActiveConnection, - msg: dict) -> None: - """Return the registration for the given webhook_id.""" - user = connection.user - - webhook_id = msg.get(CONF_WEBHOOK_ID) - if webhook_id is None: - connection.send_error(msg['id'], ERR_INVALID_FORMAT, - "Webhook ID not provided") - return - - registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id) - - if registration is None: - connection.send_error(msg['id'], ERR_NOT_FOUND, - "Webhook ID not found in storage") - return - - if registration[CONF_USER_ID] != user.id and not user.is_admin: - return error_message( - msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner') - - connection.send_message( - result_message(msg['id'], safe_registration(registration))) - - @ws_require_user() @async_response @websocket_command({ @@ -87,7 +52,8 @@ async def websocket_get_user_registrations( user_registrations = [] - for registration in hass.data[DOMAIN][DATA_REGISTRATIONS].values(): + for config_entry in hass.config_entries.async_entries(domain=DOMAIN): + registration = config_entry.data if connection.user.is_admin or registration[CONF_USER_ID] is user_id: user_registrations.append(safe_registration(registration)) @@ -113,7 +79,9 @@ async def websocket_delete_registration(hass: HomeAssistantType, "Webhook ID not provided") return - registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id) + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + + registration = config_entry.data if registration is None: connection.send_error(msg['id'], ERR_NOT_FOUND, @@ -124,7 +92,7 @@ async def websocket_delete_registration(hass: HomeAssistantType, return error_message( msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner') - del hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] + await hass.config_entries.async_remove(config_entry.entry_id) hass.data[DOMAIN][DATA_DELETED_IDS].append(webhook_id) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1036c02fd0d..e00d7204a79 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -161,6 +161,7 @@ FLOWS = [ 'locative', 'luftdaten', 'mailgun', + 'mobile_app', 'mqtt', 'nest', 'openuv', diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 57265cf696d..acd0befda4e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -170,11 +170,13 @@ class FlowHandler: } @callback - def async_abort(self, *, reason: str) -> Dict: + def async_abort(self, *, reason: str, + description_placeholders: Optional[Dict] = None) -> Dict: """Abort the config flow.""" return { 'type': RESULT_TYPE_ABORT, 'flow_id': self.flow_id, 'handler': self.handler, - 'reason': reason + 'reason': reason, + 'description_placeholders': description_placeholders, } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 87ed83d9a7e..d5e4331f7b9 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -226,6 +226,7 @@ def test_abort(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'description_placeholders': None, 'handler': 'test', 'reason': 'bla', 'type': 'abort' diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py index 1f91eb7e442..bed275a534d 100644 --- a/tests/components/mobile_app/__init__.py +++ b/tests/components/mobile_app/__init__.py @@ -2,48 +2,59 @@ # pylint: disable=redefined-outer-name,unused-import import pytest +from tests.common import mock_device_registry + from homeassistant.setup import async_setup_component -from homeassistant.components.mobile_app.const import (DATA_DELETED_IDS, - DATA_REGISTRATIONS, - CONF_SECRET, - CONF_USER_ID, DOMAIN, +from homeassistant.components.mobile_app.const import (DATA_CONFIG_ENTRIES, + DATA_DELETED_IDS, + DATA_DEVICES, + DOMAIN, STORAGE_KEY, STORAGE_VERSION) -from homeassistant.const import CONF_WEBHOOK_ID + +from .const import REGISTER, REGISTER_CLEARTEXT @pytest.fixture -def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): +def registry(hass): + """Return a configured device registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +async def create_registrations(authed_api_client): + """Return two new registrations.""" + enc_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER + ) + + assert enc_reg.status == 201 + enc_reg_json = await enc_reg.json() + + clear_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT + ) + + assert clear_reg.status == 201 + clear_reg_json = await clear_reg.json() + + return (enc_reg_json, clear_reg_json) + + +@pytest.fixture +async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): """mobile_app mock client.""" hass_storage[STORAGE_KEY] = { 'version': STORAGE_VERSION, 'data': { - DATA_REGISTRATIONS: { - 'mobile_app_test': { - CONF_SECRET: '58eb127991594dad934d1584bdee5f27', - 'supports_encryption': True, - CONF_WEBHOOK_ID: 'mobile_app_test', - 'device_name': 'Test Device', - CONF_USER_ID: hass_admin_user.id, - }, - 'mobile_app_test_cleartext': { - 'supports_encryption': False, - CONF_WEBHOOK_ID: 'mobile_app_test_cleartext', - 'device_name': 'Test Device (Cleartext)', - CONF_USER_ID: hass_admin_user.id, - } - }, - DATA_DELETED_IDS: [], + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, } } - assert hass.loop.run_until_complete(async_setup_component( - hass, DOMAIN, { - DOMAIN: {} - })) + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + return await aiohttp_client(hass.http.app) @pytest.fixture diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 7861e63459a..eb9d1f54d93 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -2,14 +2,15 @@ # pylint: disable=redefined-outer-name,unused-import import pytest -from homeassistant.components.mobile_app.const import CONF_SECRET +from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.setup import async_setup_component from .const import REGISTER, RENDER_TEMPLATE from . import authed_api_client # noqa: F401 -async def test_registration(hass_client, authed_api_client): # noqa: F811 +async def test_registration(hass, hass_client): # noqa: F811 """Test that registrations happen.""" try: # pylint: disable=unused-import @@ -21,7 +22,11 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 import json - resp = await authed_api_client.post( + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + api_client = await hass_client() + + resp = await api_client.post( '/api/mobile_app/registrations', json=REGISTER ) @@ -30,6 +35,20 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 assert CONF_WEBHOOK_ID in register_json assert CONF_SECRET in register_json + entries = hass.config_entries.async_entries(DOMAIN) + + assert entries[0].data['app_data'] == REGISTER['app_data'] + assert entries[0].data['app_id'] == REGISTER['app_id'] + assert entries[0].data['app_name'] == REGISTER['app_name'] + assert entries[0].data['app_version'] == REGISTER['app_version'] + assert entries[0].data['device_name'] == REGISTER['device_name'] + assert entries[0].data['manufacturer'] == REGISTER['manufacturer'] + assert entries[0].data['model'] == REGISTER['model'] + assert entries[0].data['os_name'] == REGISTER['os_name'] + assert entries[0].data['os_version'] == REGISTER['os_version'] + assert entries[0].data['supports_encryption'] == \ + REGISTER['supports_encryption'] + keylen = SecretBox.KEY_SIZE key = register_json[CONF_SECRET].encode("utf-8") key = key[:keylen] @@ -46,9 +65,7 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 'encrypted_data': data, } - webhook_client = await hass_client() - - resp = await webhook_client.post( + resp = await api_client.post( '/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]), json=container ) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 75e8903c494..a70e8ba1275 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,5 +1,6 @@ """Webhook tests for mobile_app.""" # pylint: disable=redefined-outer-name,unused-import +import logging import pytest from homeassistant.components.mobile_app.const import CONF_SECRET @@ -8,16 +9,20 @@ from homeassistant.core import callback from tests.common import async_mock_service -from . import authed_api_client, webhook_client # noqa: F401 +from . import (authed_api_client, create_registrations, # noqa: F401 + webhook_client) # noqa: F401 from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE) +_LOGGER = logging.getLogger(__name__) -async def test_webhook_handle_render_template(webhook_client): # noqa: F811 + +async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 """Test that we render templates properly.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=RENDER_TEMPLATE ) @@ -27,12 +32,13 @@ async def test_webhook_handle_render_template(webhook_client): # noqa: F811 assert json == {'one': 'Hello world'} -async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 F811 +async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: E501 F811 """Test that we call services properly.""" calls = async_mock_service(hass, 'test', 'mobile_app') resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=CALL_SERVICE ) @@ -41,7 +47,8 @@ async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 assert len(calls) == 1 -async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811 +async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 """Test that we can fire events.""" events = [] @@ -53,7 +60,7 @@ async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811 hass.bus.async_listen('test_event', store_event) resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=FIRE_EVENT ) @@ -93,10 +100,12 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa assert CONF_SECRET not in update_json -async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): # noqa: E501 F811 +async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 + create_registrations, # noqa: F401, F811, E501 + caplog): # noqa: E501 F811 """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), data='not json' ) @@ -106,7 +115,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): # assert 'invalid JSON' in caplog.text -async def test_webhook_handle_decryption(webhook_client): # noqa: F811 +async def test_webhook_handle_decryption(webhook_client, # noqa: F811 + create_registrations): # noqa: F401, F811, E501 """Test that we can encrypt/decrypt properly.""" try: # pylint: disable=unused-import @@ -119,7 +129,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 import json keylen = SecretBox.KEY_SIZE - key = "58eb127991594dad934d1584bdee5f27".encode("utf-8") + key = create_registrations[0]['secret'].encode("utf-8") key = key[:keylen] key = key.ljust(keylen, b'\0') @@ -135,7 +145,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 } resp = await webhook_client.post( - '/api/webhook/mobile_app_test', + '/api/webhook/{}'.format(create_registrations[0]['webhook_id']), json=container ) @@ -151,10 +161,11 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 assert json.loads(decrypted_data) == {'one': 'Hello world'} -async def test_webhook_requires_encryption(webhook_client): # noqa: F811 +async def test_webhook_requires_encryption(webhook_client, # noqa: F811 + create_registrations): # noqa: F401, F811, E501 """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test', + '/api/webhook/{}'.format(create_registrations[0]['webhook_id']), json=RENDER_TEMPLATE ) diff --git a/tests/components/mobile_app/test_websocket_api.py b/tests/components/mobile_app/test_websocket_api.py index 614fd33974b..ee656159d2e 100644 --- a/tests/components/mobile_app/test_websocket_api.py +++ b/tests/components/mobile_app/test_websocket_api.py @@ -9,33 +9,6 @@ from . import authed_api_client, setup_ws, webhook_client # noqa: F401 from .const import (CALL_SERVICE, REGISTER) -async def test_webocket_get_registration(hass, setup_ws, authed_api_client, # noqa: E501 F811 - hass_ws_client): - """Test get_registration websocket command.""" - register_resp = await authed_api_client.post( - '/api/mobile_app/registrations', json=REGISTER - ) - - assert register_resp.status == 201 - register_json = await register_resp.json() - assert CONF_WEBHOOK_ID in register_json - assert CONF_SECRET in register_json - - client = await hass_ws_client(hass) - await client.send_json({ - 'id': 5, - 'type': 'mobile_app/get_registration', - CONF_WEBHOOK_ID: register_json[CONF_WEBHOOK_ID], - }) - - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result']['app_id'] == 'io.homeassistant.mobile_app_test' - - async def test_webocket_get_user_registrations(hass, aiohttp_client, hass_ws_client, hass_read_only_access_token): From f0b7d76e269b4492dd957bd0a82c6d5893d2ac3a Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 14 Mar 2019 17:24:53 -0700 Subject: [PATCH 14/71] Mobile App: Sensors (#21854) ## Description: **Related issue (if applicable):** fixes #21782 ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. --- .../components/mobile_app/__init__.py | 36 +++-- .../components/mobile_app/binary_sensor.py | 54 +++++++ homeassistant/components/mobile_app/const.py | 56 ++++++- homeassistant/components/mobile_app/entity.py | 98 +++++++++++++ .../components/mobile_app/helpers.py | 5 +- homeassistant/components/mobile_app/sensor.py | 58 ++++++++ .../components/mobile_app/webhook.py | 107 ++++++++++++-- tests/components/mobile_app/__init__.py | 8 +- tests/components/mobile_app/test_entity.py | 137 ++++++++++++++++++ 9 files changed, 529 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/mobile_app/binary_sensor.py create mode 100644 homeassistant/components/mobile_app/entity.py create mode 100644 homeassistant/components/mobile_app/sensor.py create mode 100644 tests/components/mobile_app/test_entity.py diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1c348ea0782..ecbe8d70847 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -3,13 +3,13 @@ from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.webhook import async_register as webhook_register from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, +from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, + ATTR_MODEL, ATTR_OS_VERSION, DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, - DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION) + DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, + STORAGE_VERSION) from .http_api import RegistrationsView from .webhook import handle_webhook @@ -22,19 +22,25 @@ REQUIREMENTS = ['PyNaCl==1.3.0'] async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the mobile app component.""" - hass.data[DOMAIN] = { - DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, - } - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() if app_config is None: app_config = { - DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + DATA_BINARY_SENSOR: {}, + DATA_CONFIG_ENTRIES: {}, + DATA_DELETED_IDS: [], + DATA_DEVICES: {}, + DATA_SENSOR: {} } - hass.data[DOMAIN] = app_config - hass.data[DOMAIN][DATA_STORE] = store + hass.data[DOMAIN] = { + DATA_BINARY_SENSOR: app_config.get(DATA_BINARY_SENSOR, {}), + DATA_CONFIG_ENTRIES: {}, + DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), + DATA_DEVICES: {}, + DATA_SENSOR: app_config.get(DATA_SENSOR, {}), + DATA_STORE: store, + } hass.http.register_view(RegistrationsView()) register_websocket_handlers(hass) @@ -79,9 +85,11 @@ async def async_setup_entry(hass, entry): webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - if ATTR_APP_COMPONENT in registration: - load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {}, - {DOMAIN: {}}) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, + DATA_BINARY_SENSOR)) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR)) return True diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py new file mode 100644 index 00000000000..289a50584c9 --- /dev/null +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -0,0 +1,54 @@ +"""Binary sensor platform for mobile_app.""" +from functools import partial + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import callback +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import (ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE, + DATA_DEVICES, DOMAIN) + +from .entity import MobileAppEntity + +DEPENDENCIES = ['mobile_app'] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up mobile app binary sensor from a config entry.""" + entities = list() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + for config in hass.data[DOMAIN][ENTITY_TYPE].values(): + if config[CONF_WEBHOOK_ID] != webhook_id: + continue + + device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] + + entities.append(MobileAppBinarySensor(config, device, config_entry)) + + async_add_entities(entities) + + @callback + def handle_sensor_registration(webhook_id, data): + if data[CONF_WEBHOOK_ID] != webhook_id: + return + + device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] + + async_add_entities([MobileAppBinarySensor(data, device, config_entry)]) + + async_dispatcher_connect(hass, + '{}_{}_register'.format(DOMAIN, ENTITY_TYPE), + partial(handle_sensor_registration, webhook_id)) + + +class MobileAppBinarySensor(MobileAppEntity, BinarySensorDevice): + """Representation of an mobile app binary sensor.""" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._config[ATTR_SENSOR_STATE] diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 3ba029fec0e..d38df31b214 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,6 +1,9 @@ """Constants for mobile_app.""" import voluptuous as vol +from homeassistant.components.binary_sensor import (DEVICE_CLASSES as + BINARY_SENSOR_CLASSES) +from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.device_tracker import (ATTR_BATTERY, ATTR_GPS, ATTR_GPS_ACCURACY, @@ -17,9 +20,11 @@ CONF_CLOUDHOOK_URL = 'cloudhook_url' CONF_SECRET = 'secret' CONF_USER_ID = 'user_id' +DATA_BINARY_SENSOR = 'binary_sensor' DATA_CONFIG_ENTRIES = 'config_entries' DATA_DELETED_IDS = 'deleted_ids' DATA_DEVICES = 'devices' +DATA_SENSOR = 'sensor' DATA_STORE = 'store' ATTR_APP_COMPONENT = 'app_component' @@ -54,16 +59,22 @@ ATTR_WEBHOOK_TYPE = 'type' ERR_ENCRYPTION_REQUIRED = 'encryption_required' ERR_INVALID_COMPONENT = 'invalid_component' +ERR_SENSOR_NOT_REGISTERED = 'not_registered' +ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor' WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' +WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states' WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION] + WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_UPDATE_REGISTRATION, + WEBHOOK_TYPE_UPDATE_SENSOR_STATES] REGISTRATION_SCHEMA = vol.Schema({ @@ -91,7 +102,7 @@ UPDATE_REGISTRATION_SCHEMA = vol.Schema({ WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({ vol.Required(ATTR_WEBHOOK_TYPE): cv.string, # vol.In(WEBHOOK_TYPES) - vol.Required(ATTR_WEBHOOK_DATA, default={}): dict, + vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, }) @@ -125,10 +136,49 @@ UPDATE_LOCATION_SCHEMA = vol.Schema({ vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, }) +ATTR_SENSOR_ATTRIBUTES = 'attributes' +ATTR_SENSOR_DEVICE_CLASS = 'device_class' +ATTR_SENSOR_ICON = 'icon' +ATTR_SENSOR_NAME = 'name' +ATTR_SENSOR_STATE = 'state' +ATTR_SENSOR_TYPE = 'type' +ATTR_SENSOR_TYPE_BINARY_SENSOR = 'binary_sensor' +ATTR_SENSOR_TYPE_SENSOR = 'sensor' +ATTR_SENSOR_UNIQUE_ID = 'unique_id' +ATTR_SENSOR_UOM = 'unit_of_measurement' + +SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] + +COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)) + +SIGNAL_SENSOR_UPDATE = DOMAIN + '_sensor_update' + +REGISTER_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All(vol.Lower, + vol.In(COMBINED_CLASSES)), + vol.Required(ATTR_SENSOR_NAME): cv.string, + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + vol.Required(ATTR_SENSOR_UOM): cv.string, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon, +}) + +UPDATE_SENSOR_STATE_SCHEMA = vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, +})]) + WEBHOOK_SCHEMAS = { WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, + WEBHOOK_TYPE_REGISTER_SENSOR: REGISTER_SENSOR_SCHEMA, WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA, WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA, + WEBHOOK_TYPE_UPDATE_SENSOR_STATES: UPDATE_SENSOR_STATE_SCHEMA, } diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py new file mode 100644 index 00000000000..05736b3a689 --- /dev/null +++ b/homeassistant/components/mobile_app/entity.py @@ -0,0 +1,98 @@ +"""A entity class for mobile_app.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, + ATTR_MODEL, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, + DOMAIN, SIGNAL_SENSOR_UPDATE) + + +class MobileAppEntity(Entity): + """Representation of an mobile app entity.""" + + def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry): + """Initialize the sensor.""" + self._config = config + self._device = device + self._entry = entry + self._registration = entry.data + self._sensor_id = "{}_{}".format(self._registration[CONF_WEBHOOK_ID], + config[ATTR_SENSOR_UNIQUE_ID]) + self._entity_type = config[ATTR_SENSOR_TYPE] + self.unsub_dispatcher = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self.unsub_dispatcher = async_dispatcher_connect(self.hass, + SIGNAL_SENSOR_UPDATE, + self._handle_update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self.unsub_dispatcher is not None: + self.unsub_dispatcher() + + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + + @property + def name(self): + """Return the name of the mobile app sensor.""" + return self._config[ATTR_SENSOR_NAME] + + @property + def device_class(self): + """Return the device class.""" + return self._config.get(ATTR_SENSOR_DEVICE_CLASS) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return self._config[ATTR_SENSOR_ATTRIBUTES] + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._config[ATTR_SENSOR_ICON] + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return self._sensor_id + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + 'identifiers': { + (ATTR_DEVICE_ID, self._registration[ATTR_DEVICE_ID]), + (CONF_WEBHOOK_ID, self._registration[CONF_WEBHOOK_ID]) + }, + 'manufacturer': self._registration[ATTR_MANUFACTURER], + 'model': self._registration[ATTR_MODEL], + 'device_name': self._registration[ATTR_DEVICE_NAME], + 'sw_version': self._registration[ATTR_OS_VERSION], + 'config_entries': self._device.config_entries + } + + async def async_update(self): + """Get the latest state of the sensor.""" + data = self.hass.data[DOMAIN] + try: + self._config = data[self._entity_type][self._sensor_id] + except KeyError: + return + + @callback + def _handle_update(self, data): + """Handle async event updates.""" + self._config = data + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 5ec3b99b291..60bd8b4e1d6 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -11,7 +11,8 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, - CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN) + CONF_SECRET, CONF_USER_ID, DATA_BINARY_SENSOR, + DATA_DELETED_IDS, DATA_SENSOR, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -123,7 +124,9 @@ def safe_registration(registration: Dict) -> Dict: def savable_state(hass: HomeAssistantType) -> Dict: """Return a clean object containing things that should be saved.""" return { + DATA_BINARY_SENSOR: hass.data[DOMAIN][DATA_BINARY_SENSOR], DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], + DATA_SENSOR: hass.data[DOMAIN][DATA_SENSOR], } diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py new file mode 100644 index 00000000000..c6a53ce57ec --- /dev/null +++ b/homeassistant/components/mobile_app/sensor.py @@ -0,0 +1,58 @@ +"""Sensor platform for mobile_app.""" +from functools import partial + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import (ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, + ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN) + +from .entity import MobileAppEntity + +DEPENDENCIES = ['mobile_app'] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up mobile app sensor from a config entry.""" + entities = list() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + for config in hass.data[DOMAIN][ENTITY_TYPE].values(): + if config[CONF_WEBHOOK_ID] != webhook_id: + continue + + device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] + + entities.append(MobileAppSensor(config, device, config_entry)) + + async_add_entities(entities) + + @callback + def handle_sensor_registration(webhook_id, data): + if data[CONF_WEBHOOK_ID] != webhook_id: + return + + device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] + + async_add_entities([MobileAppSensor(data, device, config_entry)]) + + async_dispatcher_connect(hass, + '{}_{}_register'.format(DOMAIN, ENTITY_TYPE), + partial(handle_sensor_registration, webhook_id)) + + +class MobileAppSensor(MobileAppEntity): + """Representation of an mobile app sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._config[ATTR_SENSOR_STATE] + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._config[ATTR_SENSOR_UOM] diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 1fab29160b7..aafa6046d11 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -10,30 +10,38 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, SERVICE_SEE as DT_SEE) from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, - CONF_WEBHOOK_ID, HTTP_BAD_REQUEST) + CONF_WEBHOOK_ID, HTTP_BAD_REQUEST, + HTTP_CREATED) from homeassistant.core import EventOrigin -from homeassistant.exceptions import (ServiceNotFound, TemplateError) +from homeassistant.exceptions import (HomeAssistantError, + ServiceNotFound, TemplateError) from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, - ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, - CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DOMAIN, - ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA, + CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, + DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED, + ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, + SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION) + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_REGISTER_SENSOR, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_UPDATE_REGISTRATION, + WEBHOOK_TYPE_UPDATE_SENSOR_STATES) + from .helpers import (_decrypt_payload, empty_okay_response, error_response, - registration_context, safe_registration, + registration_context, safe_registration, savable_state, webhook_response) @@ -79,6 +87,10 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) + if webhook_type not in WEBHOOK_SCHEMAS: + _LOGGER.error('Received invalid webhook type: %s', webhook_type) + return empty_okay_response() + try: data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) except vol.Invalid as ex: @@ -172,3 +184,80 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, return webhook_response(safe_registration(new_registration), registration=registration, headers=headers) + + if webhook_type == WEBHOOK_TYPE_REGISTER_SENSOR: + entity_type = data[ATTR_SENSOR_TYPE] + + unique_id = data[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = "{}_{}".format(webhook_id, unique_id) + + if unique_store_key in hass.data[DOMAIN][entity_type]: + _LOGGER.error("Refusing to re-register existing sensor %s!", + unique_id) + return error_response(ERR_SENSOR_DUPLICATE_UNIQUE_ID, + "{} {} already exists!".format(entity_type, + unique_id), + status=409) + + data[CONF_WEBHOOK_ID] = webhook_id + + hass.data[DOMAIN][entity_type][unique_store_key] = data + + try: + await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + except HomeAssistantError as ex: + _LOGGER.error("Error registering sensor: %s", ex) + return empty_okay_response() + + register_signal = '{}_{}_register'.format(DOMAIN, + data[ATTR_SENSOR_TYPE]) + async_dispatcher_send(hass, register_signal, data) + + return webhook_response({"status": "registered"}, + registration=registration, status=HTTP_CREATED, + headers=headers) + + if webhook_type == WEBHOOK_TYPE_UPDATE_SENSOR_STATES: + resp = {} + for sensor in data: + entity_type = sensor[ATTR_SENSOR_TYPE] + + unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = "{}_{}".format(webhook_id, unique_id) + + if unique_store_key not in hass.data[DOMAIN][entity_type]: + _LOGGER.error("Refusing to update non-registered sensor: %s", + unique_store_key) + err_msg = '{} {} is not registered'.format(entity_type, + unique_id) + resp[unique_id] = { + 'success': False, + 'error': { + 'code': ERR_SENSOR_NOT_REGISTERED, + 'message': err_msg + } + } + continue + + entry = hass.data[DOMAIN][entity_type][unique_store_key] + + new_state = {**entry, **sensor} + + hass.data[DOMAIN][entity_type][unique_store_key] = new_state + + safe = savable_state(hass) + + try: + await hass.data[DOMAIN][DATA_STORE].async_save(safe) + except HomeAssistantError as ex: + _LOGGER.error("Error updating mobile_app registration: %s", ex) + return empty_okay_response() + + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) + + resp[unique_id] = {"status": "okay"} + + return webhook_response(resp, registration=registration, + headers=headers) diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py index bed275a534d..cf617ff0528 100644 --- a/tests/components/mobile_app/__init__.py +++ b/tests/components/mobile_app/__init__.py @@ -6,9 +6,9 @@ from tests.common import mock_device_registry from homeassistant.setup import async_setup_component -from homeassistant.components.mobile_app.const import (DATA_CONFIG_ENTRIES, +from homeassistant.components.mobile_app.const import (DATA_BINARY_SENSOR, DATA_DELETED_IDS, - DATA_DEVICES, + DATA_SENSOR, DOMAIN, STORAGE_KEY, STORAGE_VERSION) @@ -48,7 +48,9 @@ async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): hass_storage[STORAGE_KEY] = { 'version': STORAGE_VERSION, 'data': { - DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + DATA_BINARY_SENSOR: {}, + DATA_DELETED_IDS: [], + DATA_SENSOR: {} } } diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py new file mode 100644 index 00000000000..d8cb91a8bc6 --- /dev/null +++ b/tests/components/mobile_app/test_entity.py @@ -0,0 +1,137 @@ +"""Entity tests for mobile_app.""" +# pylint: disable=redefined-outer-name,unused-import +import logging + +from . import (authed_api_client, create_registrations, # noqa: F401 + webhook_client) # noqa: F401 + +_LOGGER = logging.getLogger(__name__) + + +async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401, F811, E501 + """Test that sensors can be registered and updated.""" + webhook_id = create_registrations[1]['webhook_id'] + webhook_url = '/api/webhook/{}'.format(webhook_id) + + reg_resp = await webhook_client.post( + webhook_url, + json={ + 'type': 'register_sensor', + 'data': { + 'attributes': { + 'foo': 'bar' + }, + 'device_class': 'battery', + 'icon': 'mdi:battery', + 'name': 'Battery State', + 'state': 100, + 'type': 'sensor', + 'unique_id': 'battery_state', + 'unit_of_measurement': '%' + } + } + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {'status': 'registered'} + + # 3 because we require device_tracker which adds zone.home and + # group.all_devices + assert len(hass.states.async_all()) == 3 + + entity = hass.states.async_all()[2] + + assert entity.attributes['device_class'] == 'battery' + assert entity.attributes['icon'] == 'mdi:battery' + assert entity.attributes['unit_of_measurement'] == '%' + assert entity.attributes['foo'] == 'bar' + assert entity.domain == 'sensor' + assert entity.name == 'Battery State' + assert entity.state == '100' + + update_resp = await webhook_client.post( + webhook_url, + json={ + 'type': 'update_sensor_states', + 'data': [ + { + 'icon': 'mdi:battery-unknown', + 'state': 123, + 'type': 'sensor', + 'unique_id': 'battery_state' + } + ] + } + ) + + assert update_resp.status == 200 + + updated_entity = hass.states.async_all()[2] + + assert updated_entity.state == '123' + + +async def test_sensor_must_register(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F401, F811, E501 + """Test that sensors must be registered before updating.""" + webhook_id = create_registrations[1]['webhook_id'] + webhook_url = '/api/webhook/{}'.format(webhook_id) + resp = await webhook_client.post( + webhook_url, + json={ + 'type': 'update_sensor_states', + 'data': [ + { + 'state': 123, + 'type': 'sensor', + 'unique_id': 'battery_state' + } + ] + } + ) + + assert resp.status == 200 + + json = await resp.json() + assert json['battery_state']['success'] is False + assert json['battery_state']['error']['code'] == 'not_registered' + + +async def test_sensor_id_no_dupes(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F401, F811, E501 + """Test that sensors must have a unique ID.""" + webhook_id = create_registrations[1]['webhook_id'] + webhook_url = '/api/webhook/{}'.format(webhook_id) + + payload = { + 'type': 'register_sensor', + 'data': { + 'attributes': { + 'foo': 'bar' + }, + 'device_class': 'battery', + 'icon': 'mdi:battery', + 'name': 'Battery State', + 'state': 100, + 'type': 'sensor', + 'unique_id': 'battery_state', + 'unit_of_measurement': '%' + } + } + + reg_resp = await webhook_client.post(webhook_url, json=payload) + + assert reg_resp.status == 201 + + reg_json = await reg_resp.json() + assert reg_json == {'status': 'registered'} + + dupe_resp = await webhook_client.post(webhook_url, json=payload) + + assert dupe_resp.status == 409 + + dupe_json = await dupe_resp.json() + assert dupe_json['success'] is False + assert dupe_json['error']['code'] == 'duplicate_unique_id' From 25a7f71ec27370cafd0abaf6fbc6e352622e60b3 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 14 Mar 2019 02:25:07 -0700 Subject: [PATCH 15/71] Bump androidtv to 0.0.11 (#22025) --- homeassistant/components/androidtv/media_player.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index ab43dc8c6ea..458fdff87fd 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv ANDROIDTV_DOMAIN = 'androidtv' -REQUIREMENTS = ['androidtv==0.0.10'] +REQUIREMENTS = ['androidtv==0.0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9200b803b8f..c2a75a11d80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -158,7 +158,7 @@ alpha_vantage==2.1.0 amcrest==1.2.5 # homeassistant.components.androidtv.media_player -androidtv==0.0.10 +androidtv==0.0.11 # homeassistant.components.switch.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From 4835fb2c57c4937110000eb7e7dca4e7e577ed4d Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 13 Mar 2019 21:55:30 -0700 Subject: [PATCH 16/71] Mobile App: Enable loading via discovery (surprise inside!) (#22027) ![](http://funpeep.com/wp-content/uploads/2014/04/Cute-White-Cat-Wallpaper.jpg) --- homeassistant/components/discovery/__init__.py | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 2f94cf48f4d..6a561e570c5 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==2.4.0'] +REQUIREMENTS = ['netdisco==2.5.0'] DOMAIN = 'discovery' @@ -40,6 +40,7 @@ SERVICE_HUE = 'philips_hue' SERVICE_IGD = 'igd' SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_KONNECTED = 'konnected' +SERVICE_MOBILE_APP = 'hass_mobile_app' SERVICE_NETGEAR = 'netgear_router' SERVICE_OCTOPRINT = 'octoprint' SERVICE_ROKU = 'roku' @@ -63,6 +64,7 @@ CONFIG_ENTRY_HANDLERS = { } SERVICE_HANDLERS = { + SERVICE_MOBILE_APP: ('mobile_app', None), SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), SERVICE_WEMO: ('wemo', None), diff --git a/requirements_all.txt b/requirements_all.txt index c2a75a11d80..4f8d598665b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ nessclient==0.9.14 netdata==0.1.2 # homeassistant.components.discovery -netdisco==2.4.0 +netdisco==2.5.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 11ebb3f24eea916b2a427f4542d37a396f5ed055 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 13 Mar 2019 22:05:56 -0700 Subject: [PATCH 17/71] Mobile App: Discovery to default configuration.yaml, zeroconf to default_config (#22028) * Move discovery into default configuration.yaml * Add zeroconf to default_config --- homeassistant/components/default_config/__init__.py | 2 +- homeassistant/config.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index badc403c7c8..888a4d51c95 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -6,7 +6,6 @@ DEPENDENCIES = ( 'cloud', 'config', 'conversation', - 'discovery', 'frontend', 'history', 'logbook', @@ -17,6 +16,7 @@ DEPENDENCIES = ( 'sun', 'system_health', 'updater', + 'zeroconf', ) diff --git a/homeassistant/config.py b/homeassistant/config.py index db59e2c2744..19b8087e538 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -75,6 +75,9 @@ introduction: # http: # base_url: example.duckdns.org:8123 +# Discover some devices automatically +discovery: + # Sensors sensor: # Weather prediction From 0029dc3813b42abdc5b7c642babeced1ad5a9a77 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 14 Mar 2019 19:46:59 -0700 Subject: [PATCH 18/71] Mobile App: Expose Cloud Remote UI FQDN in registration response (#22055) * Add a callback to get the cloud remote UI FQDN * Expose Cloud Remote UI FQDN in the registration response * Return a URL instead of FQDN --- homeassistant/components/cloud/__init__.py | 10 ++++++++++ homeassistant/components/mobile_app/const.py | 1 + homeassistant/components/mobile_app/http_api.py | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2e324f06738..3e3d6f975e9 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -118,6 +118,16 @@ async def async_delete_cloudhook(hass, webhook_id: str) -> None: await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id) +@bind_hass +@callback +def async_remote_ui_url(hass) -> str: + """Get the remote UI URL.""" + if not async_is_logged_in(hass): + raise CloudNotAvailable + + return "https://" + hass.data[DOMAIN].remote.instance_domain + + def is_cloudhook_request(request): """Test if a request came from a cloudhook. diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index d38df31b214..3aa4626da29 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -17,6 +17,7 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 CONF_CLOUDHOOK_URL = 'cloudhook_url' +CONF_REMOTE_UI_URL = 'remote_ui_url' CONF_SECRET = 'secret' CONF_USER_ID = 'user_id' diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 8076d217cac..2ae8f441e52 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -5,7 +5,9 @@ from typing import Dict from aiohttp.web import Response, Request from homeassistant.auth.util import generate_secret -from homeassistant.components.cloud import async_create_cloudhook +from homeassistant.components.cloud import (async_create_cloudhook, + async_remote_ui_url, + CloudNotAvailable) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import (HTTP_CREATED, CONF_WEBHOOK_ID) @@ -13,7 +15,8 @@ from homeassistant.const import (HTTP_CREATED, CONF_WEBHOOK_ID) from homeassistant.loader import get_component from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, - ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET, + ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, + CONF_REMOTE_UI_URL, CONF_SECRET, CONF_USER_ID, DOMAIN, ERR_INVALID_COMPONENT, REGISTRATION_SCHEMA) @@ -67,8 +70,15 @@ class RegistrationsView(HomeAssistantView): hass.config_entries.flow.async_init(DOMAIN, context=ctx, data=data)) + remote_ui_url = None + try: + remote_ui_url = async_remote_ui_url(hass) + except CloudNotAvailable: + pass + return self.json({ CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL), + CONF_REMOTE_UI_URL: remote_ui_url, CONF_SECRET: data.get(CONF_SECRET), CONF_WEBHOOK_ID: data[CONF_WEBHOOK_ID], }, status_code=HTTP_CREATED) From 8f103454687869d2fd0e71e20fb84783efc4032a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 07:41:34 -0700 Subject: [PATCH 19/71] Return config entry ID after creation (#22060) --- .../components/config/config_entries.py | 22 ++++++++++++++++++- .../components/config/test_config_entries.py | 10 +++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 65f65cbcec5..8865ff39cea 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -118,6 +118,16 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): # pylint: disable=no-value-for-parameter return await super().post(request) + def _prepare_result_json(self, result): + """Convert result to JSON.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return super()._prepare_result_json(result) + + data = result.copy() + data['result'] = data['result'].entry_id + data.pop('data') + return data + class ConfigManagerFlowResourceView(FlowManagerResourceView): """View to interact with the flow manager.""" @@ -143,6 +153,16 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) + def _prepare_result_json(self, result): + """Convert result to JSON.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return super()._prepare_result_json(result) + + data = result.copy() + data['result'] = data['result'].entry_id + data.pop('data') + return data + class ConfigManagerAvailableFlowView(HomeAssistantView): """View to query available flows.""" @@ -175,7 +195,7 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): return await super().post(request) -class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): +class OptionManagerFlowResourceView(FlowManagerResourceView): """View to interact with the option flow manager.""" url = '/api/config/config_entries/options/flow/{flow_id}' diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index d5e4331f7b9..852a5adf6a2 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -255,6 +255,10 @@ def test_create_account(hass, client): json={'handler': 'test'}) assert resp.status == 200 + + entries = hass.config_entries.async_entries('test') + assert len(entries) == 1 + data = yield from resp.json() data.pop('flow_id') assert data == { @@ -262,6 +266,7 @@ def test_create_account(hass, client): 'title': 'Test Entry', 'type': 'create_entry', 'version': 1, + 'result': entries[0].entry_id, 'description': None, 'description_placeholders': None, } @@ -317,6 +322,10 @@ def test_two_step_flow(hass, client): '/api/config/config_entries/flow/{}'.format(flow_id), json={'user_title': 'user-title'}) assert resp.status == 200 + + entries = hass.config_entries.async_entries('test') + assert len(entries) == 1 + data = yield from resp.json() data.pop('flow_id') assert data == { @@ -324,6 +333,7 @@ def test_two_step_flow(hass, client): 'type': 'create_entry', 'title': 'user-title', 'version': 1, + 'result': entries[0].entry_id, 'description': None, 'description_placeholders': None, } From 3ec8b5a170e5f0e85e6f81f92507ad76ce54f229 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 15 Mar 2019 10:01:15 -0700 Subject: [PATCH 20/71] Correct context (#22061) --- homeassistant/components/alexa/smart_home.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index a856a3d8e82..c87b2c3f624 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,21 +1,21 @@ """Support for alexa Smart Home Skill API.""" import asyncio -from collections import OrderedDict -from datetime import datetime import json import logging import math +from collections import OrderedDict +from datetime import datetime from uuid import uuid4 import aiohttp import async_timeout +import homeassistant.core as ha +import homeassistant.util.color as color_util from homeassistant.components import ( alert, automation, binary_sensor, cover, fan, group, http, input_boolean, light, lock, media_player, scene, script, sensor, switch) from homeassistant.components.climate import const as climate -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.event import async_track_state_change from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES, @@ -25,14 +25,14 @@ from homeassistant.const import ( SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, SERVICE_VOLUME_SET, SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) -import homeassistant.core as ha -import homeassistant.util.color as color_util +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.event import async_track_state_change from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature +from .auth import Auth from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_ENDPOINT, \ CONF_ENTITY_CONFIG, CONF_FILTER, DATE_FORMAT, DEFAULT_TIMEOUT -from .auth import Auth _LOGGER = logging.getLogger(__name__) @@ -1115,12 +1115,15 @@ class SmartHomeView(http.HomeAssistantView): the response. """ hass = request.app['hass'] + user = request[http.KEY_HASS_USER] message = await request.json() _LOGGER.debug("Received Alexa Smart Home request: %s", message) response = await async_handle_message( - hass, self.smart_home_config, message) + hass, self.smart_home_config, message, + context=ha.Context(user_id=user.id) + ) _LOGGER.debug("Sending Alexa Smart Home response: %s", response) return b'' if response is None else self.json(response) From ac1aeb35a6b950f5850f6ca1f2347431b1b79b74 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Mar 2019 18:39:53 +0100 Subject: [PATCH 21/71] Binary Sensor for Remote UI & Fix timezone (#22076) * Binary Sensor for Remote UI * Fix lint * Revert make hass public * Add tests --- homeassistant/components/cloud/__init__.py | 4 +- .../components/cloud/binary_sensor.py | 73 +++++++++++++++++++ homeassistant/components/cloud/client.py | 15 +++- homeassistant/components/cloud/const.py | 2 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_binary_sensor.py | 32 ++++++++ 7 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/cloud/binary_sensor.py create mode 100644 tests/components/cloud/test_binary_sensor.py diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 3e3d6f975e9..9f6e678e417 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD) from .prefs import CloudPreferences -REQUIREMENTS = ['hass-nabucasa==0.5'] +REQUIREMENTS = ['hass-nabucasa==0.7'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -193,4 +193,6 @@ async def async_setup(hass, config): DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler) await http_api.async_setup(hass) + hass.async_create_task(hass.helpers.discovery.async_load_platform( + 'binary_sensor', DOMAIN, {}, config)) return True diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py new file mode 100644 index 00000000000..874c3420c58 --- /dev/null +++ b/homeassistant/components/cloud/binary_sensor.py @@ -0,0 +1,73 @@ +"""Support for Home Assistant Cloud binary sensors.""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN + +DEPENDENCIES = ['cloud'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the cloud binary sensors.""" + if discovery_info is None: + return + cloud = hass.data[DOMAIN] + + async_add_entities([CloudRemoteBinary(cloud)]) + + +class CloudRemoteBinary(BinarySensorDevice): + """Representation of an Cloud Remote UI Connection binary sensor.""" + + def __init__(self, cloud): + """Initialize the binary sensor.""" + self.cloud = cloud + self._unsub_dispatcher = None + + @property + def name(self) -> str: + """Return the name of the binary sensor, if any.""" + return "Remote UI" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "cloud-remote-ui-connectivity" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.cloud.remote.is_connected + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.cloud.remote.certificate is not None + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return False + + async def async_added_to_hass(self): + """Register update dispatcher.""" + @callback + def async_state_update(data): + """Update callback.""" + self.async_write_ha_state() + + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update) + + async def async_will_remove_from_hass(self): + """Register update dispatcher.""" + if self._unsub_dispatcher is not None: + self._unsub_dispatcher() + self._unsub_dispatcher = None diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f73c16b1904..7fdfc786515 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -6,15 +6,18 @@ from typing import Any, Dict import aiohttp from hass_nabucasa.client import CloudClient as Interface +from homeassistant.core import callback from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import ( helpers as ga_h, smart_home as ga) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.aiohttp import MockRequest from . import utils -from .const import CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN +from .const import ( + CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE) from .prefs import CloudPreferences @@ -115,13 +118,19 @@ class CloudClient(Interface): self._alexa_config = None self._google_config = None - async def async_user_message( - self, identifier: str, title: str, message: str) -> None: + @callback + def user_message(self, identifier: str, title: str, message: str) -> None: """Create a message for user to UI.""" self._hass.components.persistent_notification.async_create( message, title, identifier ) + @callback + def dispatcher_message(self, identifier: str, data: Any = None) -> None: + """Match cloud notification to dispatcher.""" + if identifier.startwith("remote_"): + async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data) + async def async_alexa_message( self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index fdedacd6dbb..2816e3f6dc9 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -25,3 +25,5 @@ CONF_ACME_DIRECTORY_SERVER = 'acme_directory_server' MODE_DEV = "development" MODE_PROD = "production" + +DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update' diff --git a/requirements_all.txt b/requirements_all.txt index 4f8d598665b..a5f728ea232 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ habitipy==0.2.0 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.5 +hass-nabucasa==0.7 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36f94167565..65993daefa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -114,7 +114,7 @@ ha-ffmpeg==1.11 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.5 +hass-nabucasa==0.7 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py new file mode 100644 index 00000000000..938829b809b --- /dev/null +++ b/tests/components/cloud/test_binary_sensor.py @@ -0,0 +1,32 @@ +"""Tests for the cloud binary sensor.""" +from unittest.mock import Mock + +from homeassistant.setup import async_setup_component +from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE + + +async def test_remote_connection_sensor(hass): + """Test the remote connection sensor.""" + assert await async_setup_component(hass, 'cloud', {'cloud': {}}) + cloud = hass.data['cloud'] = Mock() + cloud.remote.certificate = None + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.remote_ui') + assert state is not None + assert state.state == 'unavailable' + + cloud.remote.is_connected = False + cloud.remote.certificate = object() + hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.remote_ui') + assert state.state == 'off' + + cloud.remote.is_connected = True + hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.remote_ui') + assert state.state == 'on' From b18aef8d317563700482baca9d5ac41670f590ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 07:47:13 -0700 Subject: [PATCH 22/71] Fix test --- tests/components/mobile_app/test_entity.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index d8cb91a8bc6..f399f842745 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -37,11 +37,8 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401 json = await reg_resp.json() assert json == {'status': 'registered'} - # 3 because we require device_tracker which adds zone.home and - # group.all_devices - assert len(hass.states.async_all()) == 3 - - entity = hass.states.async_all()[2] + entity = hass.states.get('sensor.battery_state') + assert entity is not None assert entity.attributes['device_class'] == 'battery' assert entity.attributes['icon'] == 'mdi:battery' From 3d404c43c800df095bd5a30c25bb5c8fcde01541 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 09:14:20 -0700 Subject: [PATCH 23/71] Fix more test --- tests/components/mobile_app/test_entity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index f399f842745..5dc285cfe9e 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -65,8 +65,7 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401 assert update_resp.status == 200 - updated_entity = hass.states.async_all()[2] - + updated_entity = hass.states.get('sensor.battery_state') assert updated_entity.state == '123' From ff6b86b5a825bee623e88cd2e12fa04c179e1480 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 10:59:55 -0700 Subject: [PATCH 24/71] Bumped version to 0.90.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b1a0fbb1e4f..8fa218efd86 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From aa81819683110093058058b3625b5579ebfb629d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 11:11:59 -0700 Subject: [PATCH 25/71] Fix func --- homeassistant/components/cloud/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 7fdfc786515..da89f8331a9 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -128,7 +128,7 @@ class CloudClient(Interface): @callback def dispatcher_message(self, identifier: str, data: Any = None) -> None: """Match cloud notification to dispatcher.""" - if identifier.startwith("remote_"): + if identifier.startswith("remote_"): async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data) async def async_alexa_message( From 592447927258ac196e442e351cb0cc626fcb20d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 23:17:41 -0700 Subject: [PATCH 26/71] Updated frontend to 20190315.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index a8d2cbc35b9..5d5585ddd23 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190315.0'] +REQUIREMENTS = ['home-assistant-frontend==20190315.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a5f728ea232..d2cdfad49bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190315.0 +home-assistant-frontend==20190315.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65993daefa7..752ee21d614 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190315.0 +home-assistant-frontend==20190315.1 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From a46b64d227a0e8635a8fc3c84b1d2697892212d3 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 15 Mar 2019 12:25:09 -0700 Subject: [PATCH 27/71] Bump androidtv to 0.0.12 (#22072) --- homeassistant/components/androidtv/media_player.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 458fdff87fd..1282a40cac5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv ANDROIDTV_DOMAIN = 'androidtv' -REQUIREMENTS = ['androidtv==0.0.11'] +REQUIREMENTS = ['androidtv==0.0.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d2cdfad49bb..22509e84f7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -158,7 +158,7 @@ alpha_vantage==2.1.0 amcrest==1.2.5 # homeassistant.components.androidtv.media_player -androidtv==0.0.11 +androidtv==0.0.12 # homeassistant.components.switch.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From 68d1a5322a4adfb1ff5e7eb71485a5e7627519bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 19:26:10 -0700 Subject: [PATCH 28/71] Prevent cloud remote UI when using 127.0.0.1 as trusted network (#22093) * Prevent cloud remote UI when using trusted networks * Limit to 127.0.0.1 trusted network * Update error msg * Disable ipv6 loopback --- homeassistant/components/cloud/const.py | 4 + homeassistant/components/cloud/http_api.py | 73 +++++++++------- homeassistant/components/cloud/prefs.py | 35 +++++++- tests/components/cloud/test_http_api.py | 99 ++++++++++++++++++++++ 4 files changed, 177 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 2816e3f6dc9..1286832c0c7 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -27,3 +27,7 @@ MODE_DEV = "development" MODE_PROD = "production" DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update' + + +class InvalidTrustedNetworks(Exception): + """Raised when invalid trusted networks config.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 61b3b8576ec..212bdfb4bf8 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -18,7 +18,7 @@ from homeassistant.components.google_assistant import smart_home as google_sh from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, - PREF_GOOGLE_ALLOW_UNLOCK) + PREF_GOOGLE_ALLOW_UNLOCK, InvalidTrustedNetworks) _LOGGER = logging.getLogger(__name__) @@ -58,7 +58,11 @@ SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) -_CLOUD_ERRORS = {} +_CLOUD_ERRORS = { + InvalidTrustedNetworks: + (500, 'Remote UI not compatible with 127.0.0.1/::1' + ' as a trusted network.') +} async def async_setup(hass): @@ -106,7 +110,9 @@ async def async_setup(hass): auth.PasswordChangeRequired: (400, 'Password change required.'), asyncio.TimeoutError: - (502, 'Unable to reach the Home Assistant cloud.') + (502, 'Unable to reach the Home Assistant cloud.'), + aiohttp.ClientError: + (500, 'Error making internal request'), }) @@ -120,12 +126,7 @@ def _handle_cloud_errors(handler): return result except Exception as err: # pylint: disable=broad-except - err_info = _CLOUD_ERRORS.get(err.__class__) - if err_info is None: - _LOGGER.exception( - "Unexpected error processing request for %s", request.path) - err_info = (502, 'Unexpected error: {}'.format(err)) - status, msg = err_info + status, msg = _process_cloud_exception(err, request.path) return view.json_message( msg, status_code=status, message_code=err.__class__.__name__.lower()) @@ -133,6 +134,31 @@ def _handle_cloud_errors(handler): return error_handler +def _ws_handle_cloud_errors(handler): + """Websocket decorator to handle auth errors.""" + @wraps(handler) + async def error_handler(hass, connection, msg): + """Handle exceptions that raise from the wrapped handler.""" + try: + return await handler(hass, connection, msg) + + except Exception as err: # pylint: disable=broad-except + err_status, err_msg = _process_cloud_exception(err, msg['type']) + connection.send_error(msg['id'], err_status, err_msg) + + return error_handler + + +def _process_cloud_exception(exc, where): + """Process a cloud exception.""" + err_info = _CLOUD_ERRORS.get(exc.__class__) + if err_info is None: + _LOGGER.exception( + "Unexpected error processing request for %s", where) + err_info = (502, 'Unexpected error: {}'.format(exc)) + return err_info + + class GoogleActionsSyncView(HomeAssistantView): """Trigger a Google Actions Smart Home Sync.""" @@ -295,26 +321,6 @@ def _require_cloud_login(handler): return with_cloud_auth -def _handle_aiohttp_errors(handler): - """Websocket decorator that handlers aiohttp errors. - - Can only wrap async handlers. - """ - @wraps(handler) - async def with_error_handling(hass, connection, msg): - """Handle aiohttp errors.""" - try: - await handler(hass, connection, msg) - except asyncio.TimeoutError: - connection.send_message(websocket_api.error_message( - msg['id'], 'timeout', 'Command timed out.')) - except aiohttp.ClientError: - connection.send_message(websocket_api.error_message( - msg['id'], 'unknown', 'Error making request.')) - - return with_error_handling - - @_require_cloud_login @websocket_api.async_response async def websocket_subscription(hass, connection, msg): @@ -363,7 +369,7 @@ async def websocket_update_prefs(hass, connection, msg): @_require_cloud_login @websocket_api.async_response -@_handle_aiohttp_errors +@_ws_handle_cloud_errors async def websocket_hook_create(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -373,6 +379,7 @@ async def websocket_hook_create(hass, connection, msg): @_require_cloud_login @websocket_api.async_response +@_ws_handle_cloud_errors async def websocket_hook_delete(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] @@ -417,25 +424,27 @@ def _account_data(cloud): @_require_cloud_login @websocket_api.async_response +@_ws_handle_cloud_errors @websocket_api.websocket_command({ 'type': 'cloud/remote/connect' }) async def websocket_remote_connect(hass, connection, msg): """Handle request for connect remote.""" cloud = hass.data[DOMAIN] - await cloud.remote.connect() await cloud.client.prefs.async_update(remote_enabled=True) + await cloud.remote.connect() connection.send_result(msg['id'], _account_data(cloud)) @_require_cloud_login @websocket_api.async_response +@_ws_handle_cloud_errors @websocket_api.websocket_command({ 'type': 'cloud/remote/disconnect' }) async def websocket_remote_disconnect(hass, connection, msg): """Handle request for disconnect remote.""" cloud = hass.data[DOMAIN] - await cloud.remote.disconnect() await cloud.client.prefs.async_update(remote_enabled=False) + await cloud.remote.disconnect() connection.send_result(msg['id'], _account_data(cloud)) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 16ff8f0c213..b0244f6b1fb 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,7 +1,10 @@ """Preference management for cloud.""" +from ipaddress import ip_address + from .const import ( DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER) + PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER, + InvalidTrustedNetworks) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -13,6 +16,7 @@ class CloudPreferences: def __init__(self, hass): """Initialize cloud prefs.""" + self._hass = hass self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._prefs = None @@ -48,6 +52,9 @@ class CloudPreferences: if value is not _UNDEF: self._prefs[key] = value + if remote_enabled is True and self._has_local_trusted_network: + raise InvalidTrustedNetworks + await self._store.async_save(self._prefs) def as_dict(self): @@ -57,7 +64,15 @@ class CloudPreferences: @property def remote_enabled(self): """Return if remote is enabled on start.""" - return self._prefs.get(PREF_ENABLE_REMOTE, False) + enabled = self._prefs.get(PREF_ENABLE_REMOTE, False) + + if not enabled: + return False + + if self._has_local_trusted_network: + return False + + return True @property def alexa_enabled(self): @@ -83,3 +98,19 @@ class CloudPreferences: def cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" return self._prefs.get(PREF_CLOUD_USER) + + @property + def _has_local_trusted_network(self) -> bool: + """Return if we allow localhost to bypass auth.""" + local4 = ip_address('127.0.0.1') + local6 = ip_address('::1') + + for prv in self._hass.auth.auth_providers: + if prv.type != 'trusted_networks': + continue + + for network in prv.trusted_networks: + if local4 in network or local6 in network: + return True + + return False diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 3ab4b1030fa..6c50a158cad 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -7,6 +7,7 @@ from jose import jwt from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED +from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.cloud.const import ( PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_ALLOW_UNLOCK, DOMAIN) @@ -589,3 +590,101 @@ async def test_disabling_remote(hass, hass_ws_client, setup_api, assert not cloud.client.remote_autostart assert len(mock_disconnect.mock_calls) == 1 + + +async def test_enabling_remote_trusted_networks_local4( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.auth._providers[('trusted_networks', None)] = \ + tn_auth.TrustedNetworksAuthProvider( + hass, None, tn_auth.CONFIG_SCHEMA({ + 'type': 'trusted_networks', + 'trusted_networks': [ + '127.0.0.1' + ] + }) + ) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as a trusted network.' + + assert len(mock_connect.mock_calls) == 0 + + +async def test_enabling_remote_trusted_networks_local6( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.auth._providers[('trusted_networks', None)] = \ + tn_auth.TrustedNetworksAuthProvider( + hass, None, tn_auth.CONFIG_SCHEMA({ + 'type': 'trusted_networks', + 'trusted_networks': [ + '::1' + ] + }) + ) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as a trusted network.' + + assert len(mock_connect.mock_calls) == 0 + + +async def test_enabling_remote_trusted_networks_other( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.auth._providers[('trusted_networks', None)] = \ + tn_auth.TrustedNetworksAuthProvider( + hass, None, tn_auth.CONFIG_SCHEMA({ + 'type': 'trusted_networks', + 'trusted_networks': [ + '192.168.0.0/24' + ] + }) + ) + + client = await hass_ws_client(hass) + cloud = hass.data[DOMAIN] + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + return_value=mock_coro() + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert response['success'] + assert cloud.client.remote_autostart + + assert len(mock_connect.mock_calls) == 1 From 7a88c58ffa54c19965c7726941f8e106849a3ce6 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sat, 16 Mar 2019 02:19:32 -0400 Subject: [PATCH 29/71] Beta Fix: FFMPEG and Stream component (#22091) * remove stream_source from ffmpeg and onvif and add to generic ip cam * fix tests --- homeassistant/components/camera/ffmpeg.py | 5 ----- homeassistant/components/camera/generic.py | 8 ++++++++ homeassistant/components/camera/onvif.py | 5 ----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 83ffdd499e9..db9e73f3e1b 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -76,8 +76,3 @@ class FFmpegCamera(Camera): def name(self): """Return the name of this camera.""" return self._name - - @property - def stream_source(self): - """Return the source of the stream.""" - return self._input diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index ae7e849c234..c8d6721ac18 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -28,12 +28,14 @@ _LOGGER = logging.getLogger(__name__) CONF_CONTENT_TYPE = 'content_type' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_STILL_IMAGE_URL = 'still_image_url' +CONF_STREAM_SOURCE = 'stream_source' CONF_FRAMERATE = 'framerate' DEFAULT_NAME = 'Generic Camera' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STILL_IMAGE_URL): cv.template, + vol.Optional(CONF_STREAM_SOURCE, default=None): vol.Any(None, cv.string), vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean, @@ -62,6 +64,7 @@ class GenericCamera(Camera): self._authentication = device_info.get(CONF_AUTHENTICATION) self._name = device_info.get(CONF_NAME) self._still_image_url = device_info[CONF_STILL_IMAGE_URL] + self._stream_source = device_info[CONF_STREAM_SOURCE] self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._frame_interval = 1 / device_info[CONF_FRAMERATE] @@ -141,3 +144,8 @@ class GenericCamera(Camera): def name(self): """Return the name of this device.""" return self._name + + @property + def stream_source(self): + """Return the source of the stream.""" + return self._stream_source diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index b0bd029a80c..da0bae7c50b 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -230,8 +230,3 @@ class ONVIFHassCamera(Camera): def name(self): """Return the name of this camera.""" return self._name - - @property - def stream_source(self): - """Return the source of the stream.""" - return self._input From 7b224dde23b12acdc6d75647c49ea1c480693b59 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Mar 2019 23:20:19 -0700 Subject: [PATCH 30/71] Bumped version to 0.90.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8fa218efd86..d0c6a72f5e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 16ac1d4600b5461969457251be3d10303ee24eef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Mar 2019 23:23:28 -0700 Subject: [PATCH 31/71] Updated frontend to 20190316.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5d5585ddd23..286ece850a6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190315.1'] +REQUIREMENTS = ['home-assistant-frontend==20190316.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 22509e84f7c..4feca7efeba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190315.1 +home-assistant-frontend==20190316.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 752ee21d614..38b478f78b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190315.1 +home-assistant-frontend==20190316.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From f21856418518811bb9a002202e49535dfeeb852f Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 17 Mar 2019 02:16:05 -0400 Subject: [PATCH 32/71] delete previously removed service option from services yaml (#22123) --- homeassistant/components/camera/services.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index ec00ce3ef5c..575f1fe76f7 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -50,9 +50,6 @@ play_stream: format: description: (Optional) Stream format supported by media player. example: 'hls' - keepalive: - description: (Optional) Keep the stream worker alive for fast access. - example: 'true' local_file_update_file_path: description: Update the file_path for a local_file camera. From 872ee3eb210cbb404d5be897ca00cb51ce66ac42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Mar 2019 23:26:48 -0700 Subject: [PATCH 33/71] Bumped version to 0.90.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d0c6a72f5e5..1838e02eb23 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c37dcacf54f9cf127144712f02d77a5c25bb2faf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Mar 2019 16:54:31 -0700 Subject: [PATCH 34/71] Updated frontend to 20190318.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 286ece850a6..96619face25 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190316.0'] +REQUIREMENTS = ['home-assistant-frontend==20190318.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 4feca7efeba..3138d3754ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190316.0 +home-assistant-frontend==20190318.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38b478f78b2..cfdd432baa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190316.0 +home-assistant-frontend==20190318.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From 22624715a9af7bb2fccda4ecc685f44b26fb3c53 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 17 Mar 2019 03:42:49 -0700 Subject: [PATCH 35/71] Remove hass.config from aws_lambda notify payload (#22125) --- homeassistant/components/notify/aws_lambda.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index 28fedf6434d..17df1ba8f5a 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the AWS Lambda notification service.""" - context_str = json.dumps({'hass': hass.config.as_dict(), - 'custom': config[CONF_CONTEXT]}, cls=JSONEncoder) + context_str = json.dumps({'custom': config[CONF_CONTEXT]}, cls=JSONEncoder) context_b64 = base64.b64encode(context_str.encode('utf-8')) context = context_b64.decode('utf-8') From cc00f3cd2ec0ae74dd256aa97072374434baf1de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Mar 2019 19:13:06 -0700 Subject: [PATCH 36/71] Allow non-admins to listen to certain events (#22137) --- .../components/websocket_api/commands.py | 39 +++++++--- .../components/websocket_api/permissions.py | 23 ++++++ .../components/websocket_api/test_commands.py | 73 +++++++++++++++++++ 3 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/websocket_api/permissions.py diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b64fac0ed51..32bbd90aad1 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,7 +1,9 @@ """Commands part of Websocket API.""" import voluptuous as vol -from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED +from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.const import ( + MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED) from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.exceptions import Unauthorized, ServiceNotFound, \ HomeAssistantError @@ -42,20 +44,37 @@ def handle_subscribe_events(hass, connection, msg): Async friendly. """ - if not connection.user.is_admin: + from .permissions import SUBSCRIBE_WHITELIST + + event_type = msg['event_type'] + + if (event_type not in SUBSCRIBE_WHITELIST and + not connection.user.is_admin): raise Unauthorized - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return + if event_type == EVENT_STATE_CHANGED: + @callback + def forward_events(event): + """Forward state changed events to websocket.""" + if not connection.user.permissions.check_entity( + event.data['entity_id'], POLICY_READ): + return - connection.send_message(messages.event_message( - msg['id'], event.as_dict() - )) + connection.send_message(messages.event_message(msg['id'], event)) + + else: + @callback + def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + connection.send_message(messages.event_message( + msg['id'], event.as_dict() + )) connection.subscriptions[msg['id']] = hass.bus.async_listen( - msg['event_type'], forward_events) + event_type, forward_events) connection.send_message(messages.result_message(msg['id'])) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py new file mode 100644 index 00000000000..b98b21d184e --- /dev/null +++ b/homeassistant/components/websocket_api/permissions.py @@ -0,0 +1,23 @@ +"""Permission constants for the websocket API. + +Separate file to avoid circular imports. +""" +from homeassistant.const import ( + EVENT_COMPONENT_LOADED, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_THEMES_UPDATED) +from homeassistant.components.persistent_notification import ( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + +# These are events that do not contain any sensitive data +# Except for state_changed, which is handled accordingly. +SUBSCRIBE_WHITELIST = { + EVENT_COMPONENT_LOADED, + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_THEMES_UPDATED, +} diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 8e0f751abed..4f3be31b22c 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -333,3 +333,76 @@ async def test_get_states_not_allows_nan(hass, websocket_client): msg = await websocket_client.receive_json() assert not msg['success'] assert msg['error']['code'] == const.ERR_UNKNOWN_ERROR + + +async def test_subscribe_unsubscribe_events_whitelist( + hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe events on whitelist.""" + hass_admin_user.groups = [] + + await websocket_client.send_json({ + 'id': 5, + 'type': 'subscribe_events', + 'event_type': 'not-in-whitelist' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == 'unauthorized' + + await websocket_client.send_json({ + 'id': 6, + 'type': 'subscribe_events', + 'event_type': 'themes_updated' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + hass.bus.async_fire('themes_updated') + + with timeout(3, loop=hass.loop): + msg = await websocket_client.receive_json() + + assert msg['id'] == 6 + assert msg['type'] == 'event' + event = msg['event'] + assert event['event_type'] == 'themes_updated' + assert event['origin'] == 'LOCAL' + + +async def test_subscribe_unsubscribe_events_state_changed( + hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe state_changed events.""" + hass_admin_user.groups = [] + hass_admin_user.mock_policy({ + 'entities': { + 'entity_ids': { + 'light.permitted': True + } + } + }) + + await websocket_client.send_json({ + 'id': 7, + 'type': 'subscribe_events', + 'event_type': 'state_changed' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 7 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + hass.states.async_set('light.not_permitted', 'on') + hass.states.async_set('light.permitted', 'on') + + msg = await websocket_client.receive_json() + assert msg['id'] == 7 + assert msg['type'] == 'event' + assert msg['event']['event_type'] == 'state_changed' + assert msg['event']['data']['entity_id'] == 'light.permitted' From 33a70758838374b437386ffeace62d38dba039d2 Mon Sep 17 00:00:00 2001 From: WebSpider Date: Mon, 18 Mar 2019 13:54:24 +0100 Subject: [PATCH 37/71] Bump tado version (#22145) * Bump python-tado, new API endpoint * Change references of old API endpoint to new * Update REQUIREMENTS --- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/device_tracker.py | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 767e29ba0b9..56fc0cb704c 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle -REQUIREMENTS = ['python-tado==0.2.3'] +REQUIREMENTS = ['python-tado==0.2.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 7812bbd812b..8804bef5616 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -52,9 +52,9 @@ class TadoDeviceScanner(DeviceScanner): # If there's a home_id, we need a different API URL if self.home_id is None: - self.tadoapiurl = 'https://my.tado.com/api/v2/me' + self.tadoapiurl = 'https://auth.tado.com/api/v2/me' else: - self.tadoapiurl = 'https://my.tado.com/api/v2' \ + self.tadoapiurl = 'https://auth.tado.com/api/v2' \ '/homes/{home_id}/mobileDevices' # The API URL always needs a username and password diff --git a/requirements_all.txt b/requirements_all.txt index 3138d3754ae..d07c4addef4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1394,7 +1394,7 @@ python-songpal==0.0.9.1 python-synology==0.2.0 # homeassistant.components.tado -python-tado==0.2.3 +python-tado==0.2.8 # homeassistant.components.telegram_bot python-telegram-bot==11.1.0 From 1c9b750e36c4141f20b04935840f55a92e902477 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Mon, 18 Mar 2019 09:27:34 -0400 Subject: [PATCH 38/71] Fix resetting access token on streams with keepalive (#22148) --- homeassistant/components/stream/__init__.py | 6 ++++++ homeassistant/components/stream/core.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 1d04791a11a..3f715af0e04 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -120,10 +120,16 @@ class Stream: """Remove provider output stream.""" if provider.format in self._outputs: del self._outputs[provider.format] + self.check_idle() if not self._outputs: self.stop() + def check_idle(self): + """Reset access token if all providers are idle.""" + if all([p.idle for p in self._outputs.values()]): + self.access_token = None + def start(self): """Start a stream.""" if self._thread is None or not self._thread.isAlive(): diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 3d6ffa0e20c..665803d38eb 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -43,6 +43,7 @@ class StreamOutput: def __init__(self, stream) -> None: """Initialize a stream output.""" + self.idle = False self._stream = stream self._cursor = None self._event = asyncio.Event() @@ -77,10 +78,11 @@ class StreamOutput: def get_segment(self, sequence: int = None) -> Any: """Retrieve a specific segment, or the whole list.""" + self.idle = False # Reset idle timeout if self._unsub is not None: self._unsub() - self._unsub = async_call_later(self._stream.hass, 300, self._cleanup) + self._unsub = async_call_later(self._stream.hass, 300, self._timeout) if not sequence: return self._segments @@ -109,7 +111,7 @@ class StreamOutput: # Start idle timeout when we start recieving data if self._unsub is None: self._unsub = async_call_later( - self._stream.hass, 300, self._cleanup) + self._stream.hass, 300, self._timeout) if segment is None: self._event.set() @@ -124,7 +126,15 @@ class StreamOutput: self._event.clear() @callback - def _cleanup(self, _now=None): + def _timeout(self, _now=None): + """Handle stream timeout.""" + if self._stream.keepalive: + self.idle = True + self._stream.check_idle() + else: + self._cleanup() + + def _cleanup(self): """Remove provider.""" self._segments = [] self._stream.remove_provider(self) From d75d75e49fd0b68de3deb2697b7eb8b1205085d9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 19 Mar 2019 00:58:48 +0100 Subject: [PATCH 39/71] Remove config check over supervisor (#22156) * Remove config check over supervisor * Fix lint * Fix tests --- homeassistant/components/hassio/__init__.py | 28 ++++---------- homeassistant/components/hassio/handler.py | 7 ---- tests/components/hassio/test_handler.py | 11 ------ tests/components/hassio/test_init.py | 43 ++++++--------------- 4 files changed, 19 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e070c889f31..7f85c8cfc3f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import SERVICE_CHECK_CONFIG +import homeassistant.config as conf_util from homeassistant.const import ( ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) from homeassistant.core import DOMAIN as HASS_DOMAIN, callback @@ -130,23 +131,6 @@ def is_hassio(hass): return DOMAIN in hass.config.components -@bind_hass -async def async_check_config(hass): - """Check configuration over Hass.io API.""" - hassio = hass.data[DOMAIN] - - try: - result = await hassio.check_homeassistant_config() - except HassioAPIError as err: - _LOGGER.error("Error on Hass.io API: %s", err) - raise HomeAssistantError() from None - else: - if result['result'] == "error": - return result['message'] - - return None - - async def async_setup(hass, config): """Set up the Hass.io component.""" # Check local setup @@ -259,9 +243,13 @@ async def async_setup(hass, config): await hassio.stop_homeassistant() return - error = await async_check_config(hass) - if error: - _LOGGER.error(error) + try: + errors = await conf_util.async_check_ha_config_file(hass) + except HomeAssistantError: + return + + if errors: + _LOGGER.error(errors) hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", "Config validating", "{0}.check_config".format(HASS_DOMAIN)) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 46e32c9f7c3..7eb3245c0df 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -97,13 +97,6 @@ class HassIO: """ return self.send_command("/homeassistant/stop") - def check_homeassistant_config(self): - """Check Home-Assistant config with Hass.io API. - - This method return a coroutine. - """ - return self.send_command("/homeassistant/check", timeout=600) - @_api_data def retrieve_discovery_messages(self): """Return all discovery data from Hass.io API. diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index db3917a2201..3e7b9e95d92 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -74,17 +74,6 @@ async def test_api_homeassistant_restart(hassio_handler, aioclient_mock): assert aioclient_mock.call_count == 1 -async def test_api_homeassistant_config(hassio_handler, aioclient_mock): - """Test setup with API HomeAssistant config.""" - aioclient_mock.post( - "http://127.0.0.1/homeassistant/check", json={ - 'result': 'ok', 'data': {'test': 'bla'}}) - - data = await hassio_handler.check_homeassistant_config() - assert data['data']['test'] == 'bla' - assert aioclient_mock.call_count == 1 - - async def test_api_addon_info(hassio_handler, aioclient_mock): """Test setup with API Add-on info.""" aioclient_mock.get( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 435e03a1755..1326805fc93 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -7,8 +7,7 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.setup import async_setup_component -from homeassistant.components.hassio import ( - STORAGE_KEY, async_check_config) +from homeassistant.components.hassio import STORAGE_KEY from tests.common import mock_coro @@ -311,8 +310,6 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'}) aioclient_mock.post( "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) yield from hass.services.async_call('homeassistant', 'stop') yield from hass.async_block_till_done() @@ -322,32 +319,14 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): yield from hass.services.async_call('homeassistant', 'check_config') yield from hass.async_block_till_done() + assert aioclient_mock.call_count == 2 + + with patch( + 'homeassistant.config.async_check_ha_config_file', + return_value=mock_coro() + ) as mock_check_config: + yield from hass.services.async_call('homeassistant', 'restart') + yield from hass.async_block_till_done() + assert mock_check_config.called + assert aioclient_mock.call_count == 3 - - yield from hass.services.async_call('homeassistant', 'restart') - yield from hass.async_block_till_done() - - assert aioclient_mock.call_count == 5 - - -@asyncio.coroutine -def test_check_config_ok(hassio_env, hass, aioclient_mock): - """Check Config that is okay.""" - assert (yield from async_setup_component(hass, 'hassio', {})) - - aioclient_mock.post( - "http://127.0.0.1/homeassistant/check", json={'result': 'ok'}) - - assert (yield from async_check_config(hass)) is None - - -@asyncio.coroutine -def test_check_config_fail(hassio_env, hass, aioclient_mock): - """Check Config that is wrong.""" - assert (yield from async_setup_component(hass, 'hassio', {})) - - aioclient_mock.post( - "http://127.0.0.1/homeassistant/check", json={ - 'result': 'error', 'message': "Error"}) - - assert (yield from async_check_config(hass)) == "Error" From 592edd10ef518c6687b15e9f39a31fe9652cd46f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Mar 2019 00:56:57 +0100 Subject: [PATCH 40/71] Upgrade toonapilib to 3.2.2 + lower interval (#22160) --- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/toon/binary_sensor.py | 2 +- homeassistant/components/toon/climate.py | 2 +- homeassistant/components/toon/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 0ca0a414fa5..d718b5895e4 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -REQUIREMENTS = ['toonapilib==3.2.1'] +REQUIREMENTS = ['toonapilib==3.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index a50a67085ec..694b7d1d033 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -17,7 +17,7 @@ DEPENDENCIES = ['toon'] _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=300) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 13f1c1269a1..f09dc010c79 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=300) HA_TOON = { STATE_AUTO: 'Comfort', diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index e263bda9fc7..f58c8ef4840 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -16,7 +16,7 @@ DEPENDENCIES = ['toon'] _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=300) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, diff --git a/requirements_all.txt b/requirements_all.txt index d07c4addef4..b13da82945b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1707,7 +1707,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonapilib==3.2.1 +toonapilib==3.2.2 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfdd432baa7..5cf0720d2ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,7 +298,7 @@ srpenergy==1.0.5 statsd==3.2.1 # homeassistant.components.toon -toonapilib==3.2.1 +toonapilib==3.2.2 # homeassistant.components.camera.uvc uvcclient==0.11.0 From ad0ec663538bc88f0ad0346dfbeaa17084ab7064 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Mar 2019 17:04:49 -0700 Subject: [PATCH 41/71] Bumped version to 0.90.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1838e02eb23..5a10155d7df 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From fff6927f9ce98e7034346b5760642a6a640a9f15 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Mar 2019 11:38:05 -0700 Subject: [PATCH 42/71] Updated frontend to 20190319.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 96619face25..ee779589461 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190318.0'] +REQUIREMENTS = ['home-assistant-frontend==20190319.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index b13da82945b..18452f1af82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190318.0 +home-assistant-frontend==20190319.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cf0720d2ad..c2f117997a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190318.0 +home-assistant-frontend==20190319.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From f202114ead9f29b3ac88fa4a5a03c41f94d2fb01 Mon Sep 17 00:00:00 2001 From: uchagani Date: Tue, 19 Mar 2019 03:51:42 -0400 Subject: [PATCH 43/71] bump total_connect_client to 0.24 (#22166) --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 3b0725658d4..ba8155fde93 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.22'] +REQUIREMENTS = ['total_connect_client==0.24'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 18452f1af82..ac407958cb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ todoist-python==7.0.17 toonapilib==3.2.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.22 +total_connect_client==0.24 # homeassistant.components.tplink_lte tp-connected==0.0.4 From b85189e6998a1d0bf4fc6ace2997c878c663566d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 19 Mar 2019 15:10:30 +0100 Subject: [PATCH 44/71] Update Hass-NabuCasa 0.8 (#22177) --- homeassistant/components/cloud/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 9f6e678e417..ff1b2344ac8 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD) from .prefs import CloudPreferences -REQUIREMENTS = ['hass-nabucasa==0.7'] +REQUIREMENTS = ['hass-nabucasa==0.8'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ac407958cb8..a6d62468308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ habitipy==0.2.0 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.7 +hass-nabucasa==0.8 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2f117997a4..5a357a5c0b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -114,7 +114,7 @@ ha-ffmpeg==1.11 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.7 +hass-nabucasa==0.8 # homeassistant.components.mqtt.server hbmqtt==0.9.4 From e6ffc790f2a82dbe0a9f9c486c3bc17bee4c1ab5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Mar 2019 11:33:50 -0700 Subject: [PATCH 45/71] Always load Hass.io component on Hass.io (#22185) * Always load Hass.io component on Hass.io * Lint * Lint --- homeassistant/bootstrap.py | 22 +++++++++++++++---- .../components/discovery/__init__.py | 5 ----- tests/components/discovery/test_init.py | 16 -------------- tests/test_bootstrap.py | 11 +++++++++- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 444b4a9f855..3d05eb06e6c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -127,10 +127,7 @@ async def async_from_config_dict(config: Dict[str, Any], hass.config_entries = config_entries.ConfigEntries(hass, config) await hass.config_entries.async_initialize() - # Filter out the repeating and common config section [homeassistant] - components = set(key.split(' ')[0] for key in config.keys() - if key != core.DOMAIN) - components.update(hass.config_entries.async_domains()) + components = _get_components(hass, config) # Resolve all dependencies of all components. for component in list(components): @@ -391,3 +388,20 @@ async def async_mount_local_lib_path(config_dir: str) -> str: if lib_dir not in sys.path: sys.path.insert(0, lib_dir) return deps_dir + + +@core.callback +def _get_components(hass: core.HomeAssistant, config: Dict[str, Any]): + """Get components to set up.""" + # Filter out the repeating and common config section [homeassistant] + components = set(key.split(' ')[0] for key in config.keys() + if key != core.DOMAIN) + + # Add config entry domains + components.update(hass.config_entries.async_domains()) + + # Make sure the Hass.io component is loaded + if 'HASSIO' in os.environ: + components.add('hassio') + + return components diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 6a561e570c5..d4816213f50 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -9,7 +9,6 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired. import json from datetime import timedelta import logging -import os import voluptuous as vol @@ -199,10 +198,6 @@ async def async_setup(hass, config): """Schedule the first discovery when Home Assistant starts up.""" async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) - # Discovery for local services - if 'HASSIO' in os.environ: - hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) return True diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index d4566bc0b03..28d30a9167f 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,6 +1,5 @@ """The tests for the discovery component.""" import asyncio -import os from unittest.mock import patch, MagicMock import pytest @@ -142,21 +141,6 @@ def test_discover_duplicates(hass): SERVICE_NO_PLATFORM_COMPONENT, BASE_CONFIG) -@asyncio.coroutine -def test_load_component_hassio(hass): - """Test load hassio component.""" - def discover(netdisco): - """Fake discovery.""" - return [] - - with patch.dict(os.environ, {'HASSIO': "FAKE_HASSIO"}), \ - patch('homeassistant.components.hassio.async_setup', - return_value=mock_coro(return_value=True)) as mock_hassio: - yield from mock_discovery(hass, discover) - - assert mock_hassio.called - - async def test_discover_config_flow(hass): """Test discovery triggering a config flow.""" discovery_info = { diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 978b0b9d450..1b62c5244e4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -34,7 +34,7 @@ def test_from_config_file(hass): } with patch_yaml_files(files, True): - yield from bootstrap.async_from_config_file('config.yaml') + yield from bootstrap.async_from_config_file('config.yaml', hass) assert components == hass.config.components @@ -103,3 +103,12 @@ async def test_async_from_config_file_not_mount_deps_folder(loop): await bootstrap.async_from_config_file('mock-path', hass) assert len(mock_mount.mock_calls) == 0 + + +async def test_load_hassio(hass): + """Test that we load Hass.io component.""" + with patch.dict(os.environ, {}, clear=True): + assert bootstrap._get_components(hass, {}) == set() + + with patch.dict(os.environ, {'HASSIO': '1'}): + assert bootstrap._get_components(hass, {}) == {'hassio'} From b8f246356aa0a98d853817697a9f4c8c692dc68f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Mar 2019 11:41:08 -0700 Subject: [PATCH 46/71] Bumped version to 0.90.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a10155d7df..fa841c3f246 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 268d129ea9676800ce56f46105b6b3127e4fe62c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Mar 2019 14:04:19 -0700 Subject: [PATCH 47/71] Updated frontend to 20190319.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ee779589461..5c061a7f857 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190319.0'] +REQUIREMENTS = ['home-assistant-frontend==20190319.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a6d62468308..7aeed6d9278 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190319.0 +home-assistant-frontend==20190319.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a357a5c0b3..1875248a7a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190319.0 +home-assistant-frontend==20190319.1 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From 7cf1f4f9fe4228645f9f876cfebea2181b6846e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Mar 2019 16:48:31 -0700 Subject: [PATCH 48/71] Bumped version to 0.90.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fa841c3f246..8a405b9a7bd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1bf49ce5a3712ff500904c446fa667764b3913e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Mar 2019 07:45:09 -0700 Subject: [PATCH 49/71] Updated frontend to 20190320.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5c061a7f857..6e05299ec52 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190319.1'] +REQUIREMENTS = ['home-assistant-frontend==20190320.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 7aeed6d9278..87e9f53bba9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190319.1 +home-assistant-frontend==20190320.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1875248a7a0..935bc5689e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190319.1 +home-assistant-frontend==20190320.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From d4cd39e43efd223ccc4430ae7f63527e8d5c7837 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 20 Mar 2019 22:49:27 +0800 Subject: [PATCH 50/71] Fixed typing errors (#22207) --- homeassistant/bootstrap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3d05eb06e6c..d532d9cdb86 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -5,7 +5,7 @@ import os import sys from time import time from collections import OrderedDict -from typing import Any, Optional, Dict +from typing import Any, Optional, Dict, Set import voluptuous as vol @@ -391,14 +391,15 @@ async def async_mount_local_lib_path(config_dir: str) -> str: @core.callback -def _get_components(hass: core.HomeAssistant, config: Dict[str, Any]): +def _get_components(hass: core.HomeAssistant, + config: Dict[str, Any]) -> Set[str]: """Get components to set up.""" # Filter out the repeating and common config section [homeassistant] components = set(key.split(' ')[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - components.update(hass.config_entries.async_domains()) + components.update(hass.config_entries.async_domains()) # type: ignore # Make sure the Hass.io component is loaded if 'HASSIO' in os.environ: From 9d8054e6e2bc07e513a88c79dee2d135f3124894 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Mar 2019 07:51:23 -0700 Subject: [PATCH 51/71] Bumped version to 0.90.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8a405b9a7bd..df0146cde62 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ee1c270c8949404cb8f670739d6d1864a15fc457 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Mar 2019 12:56:59 -0700 Subject: [PATCH 52/71] Updated frontend to 20190321.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 6e05299ec52..30b9d350df6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190320.0'] +REQUIREMENTS = ['home-assistant-frontend==20190321.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 87e9f53bba9..c9878e43ff8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190320.0 +home-assistant-frontend==20190321.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 935bc5689e1..885ca887d65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -126,7 +126,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190320.0 +home-assistant-frontend==20190321.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From a84ba90c9e974c5741a0735407f401bad0346652 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 20 Mar 2019 22:15:21 -0400 Subject: [PATCH 53/71] Fix ZHA force polled entities. (#22222) ## Description: Fix "force_polled" ZHA entities. ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 --- homeassistant/components/zha/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index d0848222549..1e98118e09f 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -179,7 +179,7 @@ class ZhaEntity(RestoreEntity, entity.Entity): async def async_update(self): """Retrieve latest state.""" - for channel in self.cluster_channels: + for channel in self.cluster_channels.values(): if hasattr(channel, 'async_update'): await channel.async_update() From 1aab2840125f00ef0e9bfe2d596fc61605483311 Mon Sep 17 00:00:00 2001 From: Karim Roukoz Date: Thu, 21 Mar 2019 12:39:30 -0400 Subject: [PATCH 54/71] Bump total-connect-client to 0.25, fixing issue with Total Connect (#22230) * Bump total-connect-client to 0.25 * Bump version in requirements_all.txt --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index ba8155fde93..a272a22abe5 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.24'] +REQUIREMENTS = ['total_connect_client==0.25'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c9878e43ff8..88e1c0492c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ todoist-python==7.0.17 toonapilib==3.2.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.24 +total_connect_client==0.25 # homeassistant.components.tplink_lte tp-connected==0.0.4 From 0f730310a4a0e485836234cfdae08896b5b26cb3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Mar 2019 13:03:40 -0700 Subject: [PATCH 55/71] Bumped version to 0.90.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index df0146cde62..ba33a566c9a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 56cbe8a73f02a989ad0eab026c52441d804124b2 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Thu, 21 Mar 2019 10:31:55 -0400 Subject: [PATCH 56/71] Stream fixes (#22238) * fix issues with out of order packets, and empty first packet on some IP camera models * do not skip the first packet --- homeassistant/components/stream/worker.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 3a3e19d9703..d0196761968 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -63,21 +63,29 @@ def stream_worker(hass, stream, quit_event): first_packet = True sequence = 1 audio_packets = {} + last_dts = None while not quit_event.is_set(): try: packet = next(container.demux(video_stream)) if packet.dts is None: + if first_packet: + continue # If we get a "flushing" packet, the stream is done - raise StopIteration + raise StopIteration("No dts in packet") except (av.AVError, StopIteration) as ex: # End of stream, clear listeners and stop thread for fmt, _ in outputs.items(): hass.loop.call_soon_threadsafe( stream.outputs[fmt].put, None) - _LOGGER.error("Error demuxing stream: %s", ex) + _LOGGER.error("Error demuxing stream: %s", str(ex)) break + # Skip non monotonically increasing dts in feed + if not first_packet and last_dts >= packet.dts: + continue + last_dts = packet.dts + # Reset segment on every keyframe if packet.is_keyframe: # Save segment to outputs From b2bb70b5aa2ce73f4d586730a964f119b0b1f0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Thu, 21 Mar 2019 16:24:30 +0100 Subject: [PATCH 57/71] Allow on/off on tado climate component. (#22242) * Allow on/off on tado climate component. Bump python-tado version. Patch from @wmalgadey * Revert wrongly change in tado device tracker --- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/climate.py | 29 +++++++++++++++++-- .../components/tado/device_tracker.py | 4 +-- requirements_all.txt | 2 +- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 56fc0cb704c..6808729685e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle -REQUIREMENTS = ['python-tado==0.2.8'] +REQUIREMENTS = ['python-tado==0.2.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index d5f152bbd76..c68a427cb47 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -4,9 +4,10 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF) +from homeassistant.const import ( + ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,8 @@ OPERATION_LIST = { CONST_MODE_OFF: 'Off', } -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_ON_OFF) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -199,6 +201,27 @@ class TadoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp + @property + def is_on(self): + """Return true if heater is on.""" + return self._device_is_active + + def turn_off(self): + """Turn device off.""" + _LOGGER.info("Switching mytado.com to OFF for zone %s", + self.zone_name) + + self._current_operation = CONST_MODE_OFF + self._control_heating() + + def turn_on(self): + """Turn device on.""" + _LOGGER.info("Switching mytado.com to %s mode for zone %s", + self._overlay_mode, self.zone_name) + + self._current_operation = self._overlay_mode + self._control_heating() + def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 8804bef5616..7812bbd812b 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -52,9 +52,9 @@ class TadoDeviceScanner(DeviceScanner): # If there's a home_id, we need a different API URL if self.home_id is None: - self.tadoapiurl = 'https://auth.tado.com/api/v2/me' + self.tadoapiurl = 'https://my.tado.com/api/v2/me' else: - self.tadoapiurl = 'https://auth.tado.com/api/v2' \ + self.tadoapiurl = 'https://my.tado.com/api/v2' \ '/homes/{home_id}/mobileDevices' # The API URL always needs a username and password diff --git a/requirements_all.txt b/requirements_all.txt index 88e1c0492c9..9cdda45585f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1394,7 +1394,7 @@ python-songpal==0.0.9.1 python-synology==0.2.0 # homeassistant.components.tado -python-tado==0.2.8 +python-tado==0.2.9 # homeassistant.components.telegram_bot python-telegram-bot==11.1.0 From f47a50aa243f1fbd196fbbd2d5397510505ab6a0 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 21 Mar 2019 11:13:20 -0500 Subject: [PATCH 58/71] Fix validate webhook requirements (#22248) --- .../components/smartthings/smartapp.py | 2 ++ tests/components/smartthings/test_init.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 0b64bac5956..548a38711bd 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -65,6 +65,8 @@ def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" if cloud.async_active_subscription(hass): return True + if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: + return True return get_webhook_url(hass).lower().startswith('https://') diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index a5edc93fce6..4daf37cac55 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -189,6 +189,33 @@ async def test_config_entry_loads_platforms( assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) +async def test_config_entry_loads_unconnected_cloud( + hass, config_entry, app, installed_app, + device, smartthings_mock, subscription_factory, scene): + """Test entry loads during startup when cloud isn't connected.""" + setattr(hass.config_entries, '_entries', [config_entry]) + hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" + hass.config.api.base_url = 'http://0.0.0.0' + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro(return_value=[scene]) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + with patch.object(hass.config_entries, 'async_forward_entry_setup', + return_value=mock_coro()) as forward_mock: + assert await smartthings.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + async def test_unload_entry(hass, config_entry): """Test entries are unloaded correctly.""" connect_disconnect = Mock() From 8a314d7da0c12e9ef1c1f69714026ed3f9d03724 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 21 Mar 2019 19:39:24 +0100 Subject: [PATCH 59/71] Update Hass-NabuCasa 0.9 (#22258) --- homeassistant/components/cloud/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index ff1b2344ac8..75874d6759e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD) from .prefs import CloudPreferences -REQUIREMENTS = ['hass-nabucasa==0.8'] +REQUIREMENTS = ['hass-nabucasa==0.9'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9cdda45585f..8703c4a0ca2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ habitipy==0.2.0 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.8 +hass-nabucasa==0.9 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 885ca887d65..7dd40c05d8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -114,7 +114,7 @@ ha-ffmpeg==1.11 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.8 +hass-nabucasa==0.9 # homeassistant.components.mqtt.server hbmqtt==0.9.4 From ff93591aaf4dbe028e078c64bf393adfd0106459 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Mar 2019 13:22:08 -0700 Subject: [PATCH 60/71] Lint --- homeassistant/components/tado/climate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index c68a427cb47..f698906caa7 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,7 +1,6 @@ """Support for Tado to create a climate device for each zone.""" import logging -from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF) From 8085e9206a7437b8ccd2c1bd7be454b12eaf6d16 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 21 Mar 2019 22:21:13 +0100 Subject: [PATCH 61/71] Update hass-nabucasa 0.10 (#22267) --- homeassistant/components/cloud/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 75874d6759e..9517971b16d 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD) from .prefs import CloudPreferences -REQUIREMENTS = ['hass-nabucasa==0.9'] +REQUIREMENTS = ['hass-nabucasa==0.10'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8703c4a0ca2..45d9386cbd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ habitipy==0.2.0 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.9 +hass-nabucasa==0.10 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dd40c05d8e..3340bb40d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -114,7 +114,7 @@ ha-ffmpeg==1.11 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.9 +hass-nabucasa==0.10 # homeassistant.components.mqtt.server hbmqtt==0.9.4 From 4d0a28fa56e9ae1fe89eaf265aa226724bcb605c Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sun, 24 Mar 2019 17:08:59 -0700 Subject: [PATCH 62/71] Fix ps4 no creds with additional device (#22300) * Fix no creds with additional device. * Update config_flow.py --- homeassistant/components/ps4/config_flow.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index d000ed1f7e7..e0b41dfadd5 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -79,7 +79,11 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): # If entry exists check that devices found aren't configured. if self.hass.config_entries.async_entries(DOMAIN): + creds = {} for entry in self.hass.config_entries.async_entries(DOMAIN): + # Retrieve creds from entry + creds['data'] = entry.data[CONF_TOKEN] + # Retrieve device data from entry conf_devices = entry.data['devices'] for c_device in conf_devices: if c_device['host'] in device_list: @@ -88,6 +92,11 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): # If list is empty then all devices are configured. if not device_list: return self.async_abort(reason='devices_configured') + # Add existing creds for linking. Should be only 1. + if not creds: + # Abort if creds is missing. + return self.async_abort(reason='credential_error') + self.creds = creds['data'] # Login to PS4 with user data. if user_input is not None: From 24a55834f9ce4d9e22c25a3eb6d3216587034c71 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 24 Mar 2019 20:13:20 -0400 Subject: [PATCH 63/71] Prefer TCP for RTSP streams (#22338) ## Description: For RTSP streams, set the `prefer_tcp` FFMPEG flag. This should resolve some of the "green feed" issues that some users are reporting, likely due to packets being lost over UDP on their network. Resources: [FFMPEG protocols documentation](https://ffmpeg.org/ffmpeg-protocols.html#rtsp) ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. --- homeassistant/components/stream/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 3f715af0e04..c881ec1276a 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -44,6 +44,11 @@ def request_stream(hass, stream_source, *, fmt='hls', if options is None: options = {} + # For RTSP streams, prefer TCP + if isinstance(stream_source, str) \ + and stream_source[:7] == 'rtsp://' and not options: + options['rtsp_flags'] = 'prefer_tcp' + try: streams = hass.data[DOMAIN][ATTR_STREAMS] stream = streams.get(stream_source) From a2508b4f52fcc75156795003162e4a336cec9e58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Mar 2019 17:43:15 +0100 Subject: [PATCH 64/71] Update hass-nabucasa & fix state (#22385) * Update hass-nabucasa & fix state * Fix lint --- homeassistant/components/cloud/__init__.py | 2 +- homeassistant/components/cloud/binary_sensor.py | 12 ++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_binary_sensor.py | 3 +++ 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 9517971b16d..76a768385f8 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD) from .prefs import CloudPreferences -REQUIREMENTS = ['hass-nabucasa==0.10'] +REQUIREMENTS = ['hass-nabucasa==0.11'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 874c3420c58..19a6528e321 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Home Assistant Cloud binary sensors.""" +import asyncio + from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -8,6 +9,9 @@ from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN DEPENDENCIES = ['cloud'] +WAIT_UNTIL_CHANGE = 3 + + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the cloud binary sensors.""" @@ -58,10 +62,10 @@ class CloudRemoteBinary(BinarySensorDevice): async def async_added_to_hass(self): """Register update dispatcher.""" - @callback - def async_state_update(data): + async def async_state_update(data): """Update callback.""" - self.async_write_ha_state() + await asyncio.sleep(WAIT_UNTIL_CHANGE) + self.async_schedule_update_ha_state() self._unsub_dispatcher = async_dispatcher_connect( self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update) diff --git a/requirements_all.txt b/requirements_all.txt index 45d9386cbd8..793cc4796b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ habitipy==0.2.0 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.10 +hass-nabucasa==0.11 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3340bb40d9c..fcaf93701fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -114,7 +114,7 @@ ha-ffmpeg==1.11 hangups==0.4.6 # homeassistant.components.cloud -hass-nabucasa==0.10 +hass-nabucasa==0.11 # homeassistant.components.mqtt.server hbmqtt==0.9.4 diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 938829b809b..f6d8783a609 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -7,6 +7,9 @@ from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" + from homeassistant.components.cloud import binary_sensor as bin_sensor + bin_sensor.WAIT_UNTIL_CHANGE = 0 + assert await async_setup_component(hass, 'cloud', {'cloud': {}}) cloud = hass.data['cloud'] = Mock() cloud.remote.certificate = None From fac214828d710353ecd3c53be0237a938cf77157 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 26 Mar 2019 15:38:25 +0100 Subject: [PATCH 65/71] Enable hass.io panel without ping (#22388) * Enable hass.io panel without ping * fix tests --- homeassistant/components/hassio/__init__.py | 3 +-- homeassistant/components/hassio/handler.py | 2 +- tests/components/hassio/test_init.py | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7f85c8cfc3f..f89a6539cd0 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -145,8 +145,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) if not await hassio.is_connected(): - _LOGGER.error("Not connected with Hass.io") - return False + _LOGGER.warning("Not connected with Hass.io / system to busy!") store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7eb3245c0df..7eddc639690 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -62,7 +62,7 @@ class HassIO: This method return a coroutine. """ - return self.send_command("/supervisor/ping", method="get") + return self.send_command("/supervisor/ping", method="get", timeout=15) @_api_data def get_homeassistant_info(self): diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1326805fc93..dafb8f1a028 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -196,15 +196,16 @@ def test_fail_setup_without_environ_var(hass): @asyncio.coroutine -def test_fail_setup_cannot_connect(hass): +def test_fail_setup_cannot_connect(hass, caplog): """Fail setup if cannot connect.""" with patch.dict(os.environ, MOCK_ENVIRON), \ patch('homeassistant.components.hassio.HassIO.is_connected', Mock(return_value=mock_coro(None))): result = yield from async_setup_component(hass, 'hassio', {}) - assert not result + assert result - assert not hass.components.hassio.is_hassio() + assert hass.components.hassio.is_hassio() + assert "Not connected with Hass.io / system to busy!" in caplog.text @asyncio.coroutine From 8a6d9cc0e428f22a70e1ade1639dca2a62102548 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Tue, 26 Mar 2019 03:39:09 -0400 Subject: [PATCH 66/71] reset unsub to None on timeout (#22404) --- homeassistant/components/stream/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 665803d38eb..59c0a6b650f 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -128,6 +128,7 @@ class StreamOutput: @callback def _timeout(self, _now=None): """Handle stream timeout.""" + self._unsub = None if self._stream.keepalive: self.idle = True self._stream.check_idle() From ab1f1316ba80ff7fe73f981f3a8e3527eaac9279 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Mar 2019 07:40:10 -0700 Subject: [PATCH 67/71] Bumped version to 0.90.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ba33a566c9a..d8bd84a079a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 90 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 44dc85204c48e259a610b973ebe8a87659bc544c Mon Sep 17 00:00:00 2001 From: mvn23 Date: Thu, 14 Mar 2019 13:11:04 +0100 Subject: [PATCH 68/71] Bump pyotgw to 0.4b2 (#21973) --- homeassistant/components/opentherm_gw/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7676806cfdf..d66059c55a0 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyotgw==0.4b1'] +REQUIREMENTS = ['pyotgw==0.4b2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 793cc4796b4..814f80c1597 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1205,7 +1205,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b1 +pyotgw==0.4b2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 42e75dc45c292c0d3c1ca307ac140830e2e667a8 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Mar 2019 16:32:51 +0100 Subject: [PATCH 69/71] Fix opentherm_gw blocks HA startup when gateway unreachable. (#22106) --- homeassistant/components/opentherm_gw/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index d66059c55a0..4fa24604edc 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -8,8 +8,9 @@ from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.const import ( ATTR_DATE, ATTR_ID, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, - CONF_MONITORED_VARIABLES, CONF_NAME, EVENT_HOMEASSISTANT_STOP, - PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE) + CONF_MONITORED_VARIABLES, CONF_NAME, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, + PRECISION_WHOLE) from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -115,14 +116,18 @@ async def async_setup(hass, config): DATA_GW_VARS: pyotgw.vars, DATA_LATEST_STATUS: {} } - hass.async_create_task(connect_and_subscribe( - hass, conf[CONF_DEVICE], gateway)) hass.async_create_task(register_services(hass, gateway)) hass.async_create_task(async_load_platform( hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE), config)) if monitored_vars: hass.async_create_task(setup_monitored_vars( hass, config, monitored_vars)) + + def schedule_connect(event): + """Schedule the connect_and_subscribe coroutine.""" + hass.async_create_task( + connect_and_subscribe(hass, conf[CONF_DEVICE], gateway)) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_connect) return True From ddefb7421509d170d201ece7a007d25894e42ebb Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Mar 2019 16:34:31 +0100 Subject: [PATCH 70/71] Fix TypeError in current_temperature if no temperature is known. (#22112) Don't set opentherm_gw climate temperatures to 0 on init. --- homeassistant/components/opentherm_gw/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 584be4c0c64..1a7c031638f 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -37,8 +37,8 @@ class OpenThermGateway(ClimateDevice): self.floor_temp = config.get(CONF_FLOOR_TEMP) self.temp_precision = config.get(CONF_PRECISION) self._current_operation = STATE_IDLE - self._current_temperature = 0.0 - self._target_temperature = 0.0 + self._current_temperature = None + self._target_temperature = None self._away_mode_a = None self._away_mode_b = None self._away_state_a = False @@ -124,6 +124,8 @@ class OpenThermGateway(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" + if self._current_temperature is None: + return if self.floor_temp is True: if self.temp_precision == PRECISION_HALVES: return int(2 * self._current_temperature) / 2 From 24ba434e6a271b16e9164179bb2e7ff90e2fadb7 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Mar 2019 23:51:50 +0100 Subject: [PATCH 71/71] Improve opentherm gw startup (#22121) * Improve fix in c90f0d5 (#22106). Schedule connect coroutine directly on the loop rather than waiting for EVENT_HOMEASSISTANT_START. * Remove unused import. --- homeassistant/components/opentherm_gw/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 4fa24604edc..acb277c0ef5 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -8,9 +8,8 @@ from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.const import ( ATTR_DATE, ATTR_ID, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, - CONF_MONITORED_VARIABLES, CONF_NAME, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, - PRECISION_WHOLE) + CONF_MONITORED_VARIABLES, CONF_NAME, EVENT_HOMEASSISTANT_STOP, + PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE) from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -122,12 +121,9 @@ async def async_setup(hass, config): if monitored_vars: hass.async_create_task(setup_monitored_vars( hass, config, monitored_vars)) - - def schedule_connect(event): - """Schedule the connect_and_subscribe coroutine.""" - hass.async_create_task( - connect_and_subscribe(hass, conf[CONF_DEVICE], gateway)) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_connect) + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task( + connect_and_subscribe(hass, conf[CONF_DEVICE], gateway)) return True