From ebaf7f8c00db2bd5de793cbd62a4e8c9070d362c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Oct 2018 20:34:50 +0200 Subject: [PATCH 01/35] Bumped version to 0.81.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4dcc171d35c..d44a5139885 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -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 02f55b039c45d11881651a8a802b2f090ece3c71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:03:38 +0200 Subject: [PATCH 02/35] Update frontend to 20181023.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 36bb3507dda..55aa0700bef 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181021.0'] +REQUIREMENTS = ['home-assistant-frontend==20181023.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 84b8b2d57ce..90c0dd8c44c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181021.0 +home-assistant-frontend==20181023.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1568fd95607..e8fc3517121 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181021.0 +home-assistant-frontend==20181023.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 86ecd7555a4cbac2aead03aa1148f30c4095f872 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:04:25 +0200 Subject: [PATCH 03/35] Update translations --- .../components/ifttt/.translations/tr.json | 5 ++++ .../components/mailgun/.translations/en.json | 18 +++++++++++++++ .../components/mailgun/.translations/lb.json | 18 +++++++++++++++ .../components/mqtt/.translations/tr.json | 11 +++++++++ .../simplisafe/.translations/nl.json | 19 +++++++++++++++ .../simplisafe/.translations/tr.json | 12 ++++++++++ .../components/smhi/.translations/tr.json | 16 +++++++++++++ .../components/unifi/.translations/nl.json | 23 +++++++++++++++++++ .../components/unifi/.translations/tr.json | 12 ++++++++++ .../components/upnp/.translations/tr.json | 11 +++++++++ .../components/zwave/.translations/tr.json | 11 +++++++++ 11 files changed, 156 insertions(+) create mode 100644 homeassistant/components/ifttt/.translations/tr.json create mode 100644 homeassistant/components/mailgun/.translations/en.json create mode 100644 homeassistant/components/mailgun/.translations/lb.json create mode 100644 homeassistant/components/mqtt/.translations/tr.json create mode 100644 homeassistant/components/simplisafe/.translations/nl.json create mode 100644 homeassistant/components/simplisafe/.translations/tr.json create mode 100644 homeassistant/components/smhi/.translations/tr.json create mode 100644 homeassistant/components/unifi/.translations/nl.json create mode 100644 homeassistant/components/unifi/.translations/tr.json create mode 100644 homeassistant/components/upnp/.translations/tr.json create mode 100644 homeassistant/components/zwave/.translations/tr.json diff --git a/homeassistant/components/ifttt/.translations/tr.json b/homeassistant/components/ifttt/.translations/tr.json new file mode 100644 index 00000000000..80188b637f9 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/tr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/en.json b/homeassistant/components/mailgun/.translations/en.json new file mode 100644 index 00000000000..3abb8aba726 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Mailgun?", + "title": "Set up the Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/lb.json b/homeassistant/components/mailgun/.translations/lb.json new file mode 100644 index 00000000000..f84225444d9 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Mailgun anzeriichten?", + "title": "Mailgun Webhook ariichten" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/tr.json b/homeassistant/components/mqtt/.translations/tr.json new file mode 100644 index 00000000000..1b73b94d5a4 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "data": { + "discovery": "Ke\u015ffetmeyi etkinle\u015ftir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nl.json b/homeassistant/components/simplisafe/.translations/nl.json new file mode 100644 index 00000000000..c84593c0b23 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account bestaat al", + "invalid_credentials": "Ongeldige gebruikersgegevens" + }, + "step": { + "user": { + "data": { + "code": "Code (voor Home Assistant)", + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw gegevens in" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/tr.json b/homeassistant/components/simplisafe/.translations/tr.json new file mode 100644 index 00000000000..ec84b1b7c1c --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/tr.json b/homeassistant/components/smhi/.translations/tr.json new file mode 100644 index 00000000000..bb50f1e2a8d --- /dev/null +++ b/homeassistant/components/smhi/.translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "name_exists": "Bu ad zaten var", + "wrong_location": "Konum sadece \u0130sve\u00e7" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json new file mode 100644 index 00000000000..8e87dc4b2a6 --- /dev/null +++ b/homeassistant/components/unifi/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "user_privilege": "Gebruiker moet beheerder zijn" + }, + "error": { + "faulty_credentials": "Foutieve gebruikersgegevens", + "service_unavailable": "Geen service beschikbaar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "Stel de UniFi-controller in" + } + }, + "title": "UniFi-controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/tr.json b/homeassistant/components/unifi/.translations/tr.json new file mode 100644 index 00000000000..667a5e676fb --- /dev/null +++ b/homeassistant/components/unifi/.translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/tr.json b/homeassistant/components/upnp/.translations/tr.json new file mode 100644 index 00000000000..91503c17a07 --- /dev/null +++ b/homeassistant/components/upnp/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "enable_sensors": "Trafik sens\u00f6rleri ekleyin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/tr.json b/homeassistant/components/zwave/.translations/tr.json new file mode 100644 index 00000000000..c9762784d52 --- /dev/null +++ b/homeassistant/components/zwave/.translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)" + } + } + } + } +} \ No newline at end of file From 23316a8344bad8fb4aed2e34d3983c4c786f2d9f Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 23 Oct 2018 05:01:01 +1100 Subject: [PATCH 04/35] Geo location trigger added (#16967) * zone trigger supports entity id pattern * fixed lint error * fixed test code * initial version of new geo_location trigger * revert to original * simplified code and added tests * refactored geo_location trigger to be based on a source defined by the entity * amended test cases * small refactorings --- .../components/automation/geo_location.py | 74 +++++ homeassistant/const.py | 1 + .../automation/test_geo_location.py | 271 ++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 homeassistant/components/automation/geo_location.py create mode 100644 tests/components/automation/test_geo_location.py diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py new file mode 100644 index 00000000000..b2c9a9c093a --- /dev/null +++ b/homeassistant/components/automation/geo_location.py @@ -0,0 +1,74 @@ +""" +Offer geo location automation rules. + +For more details about this automation trigger, please refer to the +documentation at +https://home-assistant.io/docs/automation/trigger/#geo-location-trigger +""" +import voluptuous as vol + +from homeassistant.components.geo_location import DOMAIN +from homeassistant.core import callback +from homeassistant.const import ( + CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED) +from homeassistant.helpers import ( + condition, config_validation as cv) +from homeassistant.helpers.config_validation import entity_domain + +EVENT_ENTER = 'enter' +EVENT_LEAVE = 'leave' +DEFAULT_EVENT = EVENT_ENTER + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'geo_location', + vol.Required(CONF_SOURCE): cv.string, + vol.Required(CONF_ZONE): entity_domain('zone'), + vol.Required(CONF_EVENT, default=DEFAULT_EVENT): + vol.Any(EVENT_ENTER, EVENT_LEAVE), +}) + + +def source_match(state, source): + """Check if the state matches the provided source.""" + return state and state.attributes.get('source') == source + + +async def async_trigger(hass, config, action): + """Listen for state changes based on configuration.""" + source = config.get(CONF_SOURCE).lower() + zone_entity_id = config.get(CONF_ZONE) + trigger_event = config.get(CONF_EVENT) + + @callback + def state_change_listener(event): + """Handle specific state changes.""" + # Skip if the event is not a geo_location entity. + if not event.data.get('entity_id').startswith(DOMAIN): + return + # Skip if the event's source does not match the trigger's source. + from_state = event.data.get('old_state') + to_state = event.data.get('new_state') + if not source_match(from_state, source) \ + and not source_match(to_state, source): + return + + zone_state = hass.states.get(zone_entity_id) + from_match = condition.zone(hass, zone_state, from_state) + to_match = condition.zone(hass, zone_state, to_state) + + # pylint: disable=too-many-boolean-expressions + if trigger_event == EVENT_ENTER and not from_match and to_match or \ + trigger_event == EVENT_LEAVE and from_match and not to_match: + hass.async_run_job(action({ + 'trigger': { + 'platform': 'geo_location', + 'source': source, + 'entity_id': event.data.get('entity_id'), + 'from_state': from_state, + 'to_state': to_state, + 'zone': zone_state, + 'event': trigger_event, + }, + }, context=event.context)) + + return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) diff --git a/homeassistant/const.py b/homeassistant/const.py index d44a5139885..ae4a2b052bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -129,6 +129,7 @@ CONF_SENSOR_TYPE = 'sensor_type' CONF_SENSORS = 'sensors' CONF_SHOW_ON_MAP = 'show_on_map' CONF_SLAVE = 'slave' +CONF_SOURCE = 'source' CONF_SSL = 'ssl' CONF_STATE = 'state' CONF_STATE_TEMPLATE = 'state_template' diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py new file mode 100644 index 00000000000..130cdeef99c --- /dev/null +++ b/tests/components/automation/test_geo_location.py @@ -0,0 +1,271 @@ +"""The tests for the geo location trigger.""" +import unittest + +from homeassistant.components import automation, zone +from homeassistant.core import callback, Context +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common + + +class TestAutomationGeoLocation(unittest.TestCase): + """Test the geo location trigger.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + mock_component(self.hass, 'group') + assert setup_component(self.hass, zone.DOMAIN, { + 'zone': { + 'name': 'test', + 'latitude': 32.880837, + 'longitude': -117.237561, + 'radius': 250, + } + }) + + self.calls = [] + + @callback + def record_call(service): + """Record calls.""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_if_fires_on_zone_enter(self): + """Test for firing on zone enter.""" + context = Context() + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }, context=context) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + assert self.calls[0].context is context + self.assertEqual( + 'geo_location - geo_location.entity - hello - hello - test', + self.calls[0].data['some']) + + # Set out of zone again so we can trigger call + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758 + }) + self.hass.block_till_done() + + common.turn_off(self.hass) + self.hass.block_till_done() + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_for_enter_on_zone_leave(self): + """Test for not firing on zone leave.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758 + }) + self.hass.block_till_done() + + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_zone_leave(self): + """Test for firing on zone leave.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_for_leave_on_zone_enter(self): + """Test for not firing on zone enter.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }) + self.hass.block_till_done() + + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_zone_appear(self): + """Test for firing if entity appears in zone.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'enter', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + # Entity appears in zone without previously existing outside the zone. + context = Context() + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }, context=context) + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + assert self.calls[0].context is context + self.assertEqual( + 'geo_location - geo_location.entity - - hello - test', + self.calls[0].data['some']) + + def test_if_fires_on_zone_disappear(self): + """Test for firing if entity disappears from zone.""" + self.hass.states.set('geo_location.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564, + 'source': 'test_source' + }) + self.hass.block_till_done() + + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'geo_location', + 'source': 'test_source', + 'zone': 'zone.test', + 'event': 'leave', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + + } + } + }) + + # Entity disappears from zone without new coordinates outside the zone. + self.hass.states.async_remove('geo_location.entity') + self.hass.block_till_done() + + self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'geo_location - geo_location.entity - hello - - test', + self.calls[0].data['some']) From b5323cd89408cd79eb3a5dd3c7e2463b74ef1278 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Oct 2018 14:45:13 +0200 Subject: [PATCH 05/35] Add lovelace websocket get and set card (#17600) * Add ws get, set card * lint+fix test * Add test for set * Added more tests, catch unsupported yaml constructors Like !include will now give an error in the frontend. * lint --- homeassistant/components/lovelace/__init__.py | 187 +++++++++++++++++- tests/components/lovelace/test_init.py | 174 +++++++++++++++- 2 files changed, 348 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e3f4522580b..2c28b52ec6e 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -2,9 +2,10 @@ import logging import uuid import os -from os import O_WRONLY, O_CREAT, O_TRUNC +from os import O_CREAT, O_TRUNC, O_WRONLY from collections import OrderedDict -from typing import Union, List, Dict +from typing import Dict, List, Union + import voluptuous as vol from homeassistant.components import websocket_api @@ -14,21 +15,45 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'lovelace' REQUIREMENTS = ['ruamel.yaml==0.15.72'] +LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' +JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +WS_TYPE_GET_CARD = 'lovelace/config/card/get' +WS_TYPE_SET_CARD = 'lovelace/config/card/set' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), }) -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_CARD, + vol.Required('card_id'): str, + vol.Optional('format', default='yaml'): str, +}) + +SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_SET_CARD, + vol.Required('card_id'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('format', default='yaml'): str, +}) class WriteError(HomeAssistantError): """Error writing the data.""" +class CardNotFoundError(HomeAssistantError): + """Card not found in data.""" + + +class UnsupportedYamlError(HomeAssistantError): + """Unsupported YAML.""" + + def save_yaml(fname: str, data: JSON_TYPE): """Save a YAML file.""" from ruamel.yaml import YAML @@ -45,7 +70,7 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error(str(exc)) raise HomeAssistantError(exc) except OSError as exc: - _LOGGER.exception('Saving YAML file failed: %s', fname) + _LOGGER.exception('Saving YAML file %s failed: %s', fname, exc) raise WriteError(exc) finally: if os.path.exists(tmp_fname): @@ -57,18 +82,29 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error("YAML replacement cleanup failed: %s", exc) +def _yaml_unsupported(loader, node): + raise UnsupportedYamlError( + 'Unsupported YAML, you can not use {} in ui-lovelace.yaml' + .format(node.tag)) + + def load_yaml(fname: str) -> JSON_TYPE: """Load a YAML file.""" from ruamel.yaml import YAML + from ruamel.yaml.constructor import RoundTripConstructor from ruamel.yaml.error import YAMLError + + RoundTripConstructor.add_constructor(None, _yaml_unsupported) + yaml = YAML(typ='rt') + try: with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict return yaml.load(conf_file) or OrderedDict() except YAMLError as exc: - _LOGGER.error("YAML error: %s", exc) + _LOGGER.error("YAML error in %s: %s", fname, exc) raise HomeAssistantError(exc) except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) @@ -76,21 +112,86 @@ def load_yaml(fname: str) -> JSON_TYPE: def load_config(fname: str) -> JSON_TYPE: - """Load a YAML file and adds id to card if not present.""" + """Load a YAML file and adds id to views and cards if not present.""" config = load_yaml(fname) - # Check if all cards have an ID or else add one + # Check if all views and cards have an id or else add one updated = False + index = 0 for view in config.get('views', []): + if 'id' not in view: + updated = True + view.insert(0, 'id', index, + comment="Automatically created id") for card in view.get('cards', []): if 'id' not in card: updated = True - card['id'] = uuid.uuid4().hex - card.move_to_end('id', last=False) + card.insert(0, 'id', uuid.uuid4().hex, + comment="Automatically created id") + index += 1 if updated: save_yaml(fname, config) return config +def object_to_yaml(data: JSON_TYPE) -> str: + """Create yaml string from object.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + from ruamel.yaml.compat import StringIO + yaml = YAML(typ='rt') + yaml.indent(sequence=4, offset=2) + stream = StringIO() + try: + yaml.dump(data, stream) + return stream.getvalue() + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def yaml_to_object(data: str) -> JSON_TYPE: + """Create object from yaml string.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + yaml = YAML(typ='rt') + try: + return yaml.load(data) + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: + """Load a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + if data_format == 'yaml': + return object_to_yaml(card) + return card + + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) + + +def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ + -> bool: + """Save a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + if data_format == 'yaml': + card_config = yaml_to_object(card_config) + card.update(card_config) + save_yaml(fname, config) + return True + + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -102,6 +203,14 @@ async def async_setup(hass, config): WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_CARD, websocket_lovelace_get_card, + SCHEMA_GET_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_SET_CARD, websocket_lovelace_set_card, + SCHEMA_SET_CARD) + return True @@ -111,13 +220,15 @@ async def websocket_lovelace_config(hass, connection, msg): error = None try: config = await hass.async_add_executor_job( - load_config, hass.config.path('ui-lovelace.yaml')) + load_config, hass.config.path(LOVELACE_CONFIG_FILE)) message = websocket_api.result_message( msg['id'], config ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -125,3 +236,59 @@ async def websocket_lovelace_config(hass, connection, msg): message = websocket_api.error_message(msg['id'], *error) connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_get_card(hass, connection, msg): + """Send lovelace card config over websocket config.""" + error = None + try: + card = await hass.async_add_executor_job( + get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], + msg.get('format', 'yaml')) + message = websocket_api.result_message( + msg['id'], card + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_set_card(hass, connection, msg): + """Receive lovelace card config over websocket and save.""" + error = None + try: + result = await hass.async_add_executor_job( + set_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) + message = websocket_api.result_message( + msg['id'], result + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 5e4cf2d8037..c637267cc7e 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -9,7 +9,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components.lovelace import (load_yaml, - save_yaml, load_config) + save_yaml, load_config, + UnsupportedYamlError) TEST_YAML_A = """\ title: My Awesome Home @@ -55,6 +56,8 @@ views: # Title of the view. Will be used as the tooltip for tab icon title: Second view cards: + - id: test + type: entities # Entities card will take a list of entities and show their state. - type: entities # Title of the entities card @@ -79,6 +82,7 @@ TEST_YAML_B = """\ title: Home views: - title: Dashboard + id: dashboard icon: mdi:home cards: - id: testid @@ -102,6 +106,15 @@ views: type: vertical-stack """ +# Test unsupported YAML +TEST_UNSUP_YAML = """\ +title: Home +views: + - title: Dashboard + icon: mdi:home + cards: !include cards.yaml +""" + class TestYAML(unittest.TestCase): """Test lovelace.yaml save and load.""" @@ -147,9 +160,11 @@ class TestYAML(unittest.TestCase): """Test if id is added.""" fname = self._path_for("test6") with patch('homeassistant.components.lovelace.load_yaml', - return_value=self.yaml.load(TEST_YAML_A)): + return_value=self.yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml'): data = load_config(fname) assert 'id' in data['views'][0]['cards'][0] + assert 'id' in data['views'][1] def test_id_not_changed(self): """Test if id is not changed if already exists.""" @@ -256,7 +271,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client): async def test_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" + """Test lovelace_ui command load error.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) @@ -272,3 +287,156 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_ui_load_json_err(hass, hass_ws_client): + """Test lovelace_ui command load error.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_config', + side_effect=UnsupportedYamlError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unsupported_error' + + +async def test_lovelace_get_card(hass, hass_ws_client): + """Test get_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'test', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + assert msg['result'] == 'id: test\ntype: entities\n' + + +async def test_lovelace_get_card_not_found(hass, hass_ws_client): + """Test get_card command cannot find card.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'not_found', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): + """Test get_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'testid', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_set_card(hass, hass_ws_client): + """Test set_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 1, 'cards', 0, 'type'], + list_ok=True) == 'glance' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + + +async def test_lovelace_set_card_not_found(hass, hass_ws_client): + """Test set_card command cannot find card.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'not_found', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): + """Test set_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.yaml_to_object', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'save_error' From fe8dec27a38cfeeb00129a555e1a4e850b1e4251 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 23 Oct 2018 04:54:03 -0400 Subject: [PATCH 06/35] Fixed issue #16903 re exception with multiple simultanious writes (#17636) Reworked tests/components/emulated_hue/test_init.py to not be dependent on the specific internal implementation of util/jsonn.py --- homeassistant/util/json.py | 14 ++- tests/components/emulated_hue/test_init.py | 122 ++++++++++----------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 0a2a2a1edf3..b002c8e3147 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -4,7 +4,7 @@ from typing import Union, List, Dict import json import os -from os import O_WRONLY, O_CREAT, O_TRUNC +import tempfile from homeassistant.exceptions import HomeAssistantError @@ -46,13 +46,17 @@ def save_json(filename: str, data: Union[List, Dict], Returns True on success. """ - tmp_filename = filename + "__TEMP__" + tmp_filename = "" + tmp_path = os.path.split(filename)[0] try: json_data = json.dumps(data, sort_keys=True, indent=4) - mode = 0o600 if private else 0o644 - with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode), - 'w', encoding='utf-8') as fdesc: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8', + dir=tmp_path, delete=False) as fdesc: fdesc.write(json_data) + tmp_filename = fdesc.name + if not private: + os.chmod(tmp_filename, 0o644) os.replace(tmp_filename, filename) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 9b0a5cd9052..3de8e969140 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,7 +1,5 @@ """Test the Emulated Hue component.""" -import json - -from unittest.mock import patch, Mock, mock_open, MagicMock +from unittest.mock import patch, Mock, MagicMock from homeassistant.components.emulated_hue import Config @@ -14,30 +12,30 @@ def test_config_google_home_entity_id_to_number(): 'type': 'google_home' }) - mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={'1': 'light.test2'}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '2' - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '1': 'light.test2', '2': 'light.test' + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert json_saver.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert json_saver.call_count == 1 + + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): @@ -48,30 +46,30 @@ def test_config_google_home_entity_id_to_number_altered(): 'type': 'google_home' }) - mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={'21': 'light.test2'}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert json_saver.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert json_saver.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): @@ -82,29 +80,29 @@ def test_config_google_home_entity_id_to_number_empty(): 'type': 'google_home' }) - mop = mock_open(read_data='') - handle = mop() + with patch('homeassistant.components.emulated_hue.load_json', + return_value={}) as json_loader: + with patch('homeassistant.components.emulated_hue' + '.save_json') as json_saver: + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert json_saver.call_count == 1 + assert json_loader.call_count == 1 - with patch('homeassistant.util.json.open', mop, create=True): - with patch('homeassistant.util.json.os.open', return_value=0): - with patch('homeassistant.util.json.os.replace'): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + assert json_saver.mock_calls[0][1][1] == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert json_saver.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert json_saver.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): From 87133a0e77fce953a6a293117bb8f1b825bd2f5b Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Mon, 22 Oct 2018 00:04:47 -0500 Subject: [PATCH 07/35] Update flux library version (#17677) --- homeassistant/components/light/flux_led.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index f389d34cd5d..cab6957c265 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['flux_led==0.21'] +REQUIREMENTS = ['flux_led==0.22'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 90c0dd8c44c..591f9e6c71c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -376,7 +376,7 @@ fitbit==0.3.0 fixerio==1.0.0a0 # homeassistant.components.light.flux_led -flux_led==0.21 +flux_led==0.22 # homeassistant.components.sensor.foobot foobot_async==0.3.1 From 2d9a9649538e62beca78f4141f77930b316620d4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 23 Oct 2018 07:11:55 +0200 Subject: [PATCH 08/35] Update limitlessled to 1.1.3 (#17703) --- homeassistant/components/light/limitlessled.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index a5aeabba84d..2e2971cfdc2 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -20,7 +20,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, color_hs_to_RGB) from homeassistant.helpers.restore_state import async_get_last_state -REQUIREMENTS = ['limitlessled==1.1.2'] +REQUIREMENTS = ['limitlessled==1.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 591f9e6c71c..f24fb306de7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ liffylights==0.9.4 lightify==1.0.6.1 # homeassistant.components.light.limitlessled -limitlessled==1.1.2 +limitlessled==1.1.3 # homeassistant.components.linode linode-api==4.1.9b1 From 011cc624b65efb7f3bdf596c7334507a2c2f1413 Mon Sep 17 00:00:00 2001 From: Jaxom Nutt <40261038+JaxomCS@users.noreply.github.com> Date: Tue, 23 Oct 2018 16:28:49 +0800 Subject: [PATCH 09/35] Bug fix for clicksend (#17713) * Bug fix Current version causes 500 error since it is sending an array of from numbers to ClickSend. Changing the from number to 'hass' identifies all messages as coming from Home Assistant making them more recognisable and removes the bug. * Amendment Changed it to use 'hass' as the default instead of defaulting to the recipient which is the array. Would have worked if users set their own name but users who were using the default were experiencing the issue. * Added DEFAULT_SENDER variable --- homeassistant/components/notify/clicksend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py index c028da2c579..5506d6ed6d0 100644 --- a/homeassistant/components/notify/clicksend.py +++ b/homeassistant/components/notify/clicksend.py @@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) BASE_API_URL = 'https://rest.clicksend.com/v3' +DEFAULT_SENDER = 'hass' + HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} @@ -29,7 +31,7 @@ def validate_sender(config): """Set the optional sender name if sender name is not provided.""" if CONF_SENDER in config: return config - config[CONF_SENDER] = config[CONF_RECIPIENT] + config[CONF_SENDER] = DEFAULT_SENDER return config @@ -61,7 +63,7 @@ class ClicksendNotificationService(BaseNotificationService): self.username = config.get(CONF_USERNAME) self.api_key = config.get(CONF_API_KEY) self.recipients = config.get(CONF_RECIPIENT) - self.sender = config.get(CONF_SENDER, CONF_RECIPIENT) + self.sender = config.get(CONF_SENDER) def send_message(self, message="", **kwargs): """Send a message to a user.""" From 4750656f1a0efde8a80d03a4f8a2303cac1935a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Oct 2018 14:09:47 +0200 Subject: [PATCH 10/35] Bumped version to 0.81.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ae4a2b052bc..5bcfba2caff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -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 1788eaf037820262a698bdfa2efa45c3d1839d68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Oct 2018 22:15:21 +0200 Subject: [PATCH 11/35] Update frontend to 20181024.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 55aa0700bef..c155bcf81e3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181023.0'] +REQUIREMENTS = ['home-assistant-frontend==20181024.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index f24fb306de7..4f390c992fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181023.0 +home-assistant-frontend==20181024.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8fc3517121..7e93231c82e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181023.0 +home-assistant-frontend==20181024.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From edc1cbdc32a4881c1e72a4bf631016ae17cc9b87 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Wed, 24 Oct 2018 05:15:59 -0400 Subject: [PATCH 12/35] Elk-M1 climate (#17679) * Initial climate for Elk-M1. * Tidy * fix hound error * fix hound error --- homeassistant/components/climate/elkm1.py | 193 +++++++++++++++++++++ homeassistant/components/elkm1/__init__.py | 4 +- 2 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/climate/elkm1.py diff --git a/homeassistant/components/climate/elkm1.py b/homeassistant/components/climate/elkm1.py new file mode 100644 index 00000000000..6bd33b382dc --- /dev/null +++ b/homeassistant/components/climate/elkm1.py @@ -0,0 +1,193 @@ +""" +Support for control of Elk-M1 connected thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.elkm1/ +""" +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO, + STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) +from homeassistant.components.elkm1 import ( + DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) +from homeassistant.const import STATE_ON + +DEPENDENCIES = [ELK_DOMAIN] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Create the Elk-M1 thermostat platform.""" + if discovery_info is None: + return + + elk = hass.data[ELK_DOMAIN]['elk'] + async_add_entities(create_elk_entities( + hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) + + +class ElkThermostat(ElkEntity, ClimateDevice): + """Representation of an Elk-M1 Thermostat.""" + + def __init__(self, element, elk, elk_data): + """Initialize climate entity.""" + super().__init__(element, elk, elk_data) + self._state = None + + @property + def supported_features(self): + """Return the list of supported features.""" + return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT + | SUPPORT_TARGET_TEMPERATURE_HIGH + | SUPPORT_TARGET_TEMPERATURE_LOW) + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return self._temperature_unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._element.current_temp + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + from elkm1_lib.const import ThermostatMode + if (self._element.mode == ThermostatMode.HEAT.value) or ( + self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): + return self._element.heat_setpoint + if self._element.mode == ThermostatMode.COOL.value: + return self._element.cool_setpoint + return None + + @property + def target_temperature_high(self): + """Return the high target temperature.""" + return self._element.cool_setpoint + + @property + def target_temperature_low(self): + """Return the low target temperature.""" + return self._element.heat_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._element.humidity + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._state + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY] + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def is_aux_heat_on(self): + """Return if aux heater is on.""" + from elkm1_lib.const import ThermostatMode + return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value + + @property + def min_temp(self): + """Return the minimum temperature supported.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature supported.""" + return 99 + + @property + def current_fan_mode(self): + """Return the fan setting.""" + from elkm1_lib.const import ThermostatFan + if self._element.fan == ThermostatFan.AUTO.value: + return STATE_AUTO + if self._element.fan == ThermostatFan.ON.value: + return STATE_ON + return None + + def _elk_set(self, mode, fan): + from elkm1_lib.const import ThermostatSetting + if mode is not None: + self._element.set(ThermostatSetting.MODE.value, mode) + if fan is not None: + self._element.set(ThermostatSetting.FAN.value, fan) + + async def async_set_operation_mode(self, operation_mode): + """Set thermostat operation mode.""" + from elkm1_lib.const import ThermostatFan, ThermostatMode + settings = { + STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), + STATE_HEAT: (ThermostatMode.HEAT.value, None), + STATE_COOL: (ThermostatMode.COOL.value, None), + STATE_AUTO: (ThermostatMode.AUTO.value, None), + STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value) + } + self._elk_set(settings[operation_mode][0], settings[operation_mode][1]) + + async def async_turn_aux_heat_on(self): + """Turn auxiliary heater on.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) + + async def async_turn_aux_heat_off(self): + """Turn auxiliary heater off.""" + from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.HEAT.value, None) + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return [STATE_AUTO, STATE_ON] + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + from elkm1_lib.const import ThermostatFan + if fan_mode == STATE_AUTO: + self._elk_set(None, ThermostatFan.AUTO.value) + elif fan_mode == STATE_ON: + self._elk_set(None, ThermostatFan.ON.value) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + from elkm1_lib.const import ThermostatSetting + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if low_temp is not None: + self._element.set( + ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) + if high_temp is not None: + self._element.set( + ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) + + def _element_changed(self, element, changeset): + from elkm1_lib.const import ThermostatFan, ThermostatMode + mode_to_state = { + ThermostatMode.OFF.value: STATE_IDLE, + ThermostatMode.COOL.value: STATE_COOL, + ThermostatMode.HEAT.value: STATE_HEAT, + ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT, + ThermostatMode.AUTO.value: STATE_AUTO, + } + self._state = mode_to_state.get(self._element.mode) + if self._state == STATE_IDLE and \ + self._element.fan == ThermostatFan.ON.value: + self._state = STATE_FAN_ONLY diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 5c379c7438b..76594e16736 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,8 +35,8 @@ CONF_ENABLED = 'enabled' _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel', 'light', 'scene', 'sensor', - 'switch'] +SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene', + 'sensor', 'switch'] SPEAK_SERVICE_SCHEMA = vol.Schema({ vol.Required('number'): From 8de0824688e5f9b7a3952384afe5d7147e7f4a07 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 24 Oct 2018 05:53:45 -0400 Subject: [PATCH 13/35] Add cover to supported platforms (#17725) --- homeassistant/components/zwave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 74678cda0fc..35703d64974 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -66,7 +66,7 @@ DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 -SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'fan', +SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'fan', 'light', 'sensor', 'switch'] RENAME_NODE_SCHEMA = vol.Schema({ From 295a004326350440acc09e6df4a175acf84f4f6b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 23 Oct 2018 21:48:35 +0200 Subject: [PATCH 14/35] Lovelace ws: add card (#17730) * Change set to update * Add 'add card' * Woof. --- homeassistant/components/lovelace/__init__.py | 138 ++++++++++++++---- tests/components/lovelace/test_init.py | 71 +++++++-- 2 files changed, 168 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 2c28b52ec6e..141f3c98334 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -18,10 +18,15 @@ REQUIREMENTS = ['ruamel.yaml==0.15.72'] LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +FORMAT_YAML = 'yaml' +FORMAT_JSON = 'json' + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' + WS_TYPE_GET_CARD = 'lovelace/config/card/get' -WS_TYPE_SET_CARD = 'lovelace/config/card/set' +WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' +WS_TYPE_ADD_CARD = 'lovelace/config/card/add' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, @@ -31,14 +36,25 @@ SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_CARD, vol.Required('card_id'): str, - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) -SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SET_CARD, +SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE_CARD, vol.Required('card_id'): str, vol.Required('card_config'): vol.Any(str, Dict), - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), +}) + +SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_ADD_CARD, + vol.Required('view_id'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('position'): int, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) @@ -50,6 +66,10 @@ class CardNotFoundError(HomeAssistantError): """Card not found in data.""" +class ViewNotFoundError(HomeAssistantError): + """View not found in data.""" + + class UnsupportedYamlError(HomeAssistantError): """Unsupported YAML.""" @@ -161,37 +181,61 @@ def yaml_to_object(data: str) -> JSON_TYPE: raise HomeAssistantError(exc) -def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: +def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\ + -> JSON_TYPE: """Load a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - return object_to_yaml(card) - return card + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + return object_to_yaml(card) + return card raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) -def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ - -> bool: +def update_card(fname: str, card_id: str, card_config: str, + data_format: str = FORMAT_YAML): """Save a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - card_config = yaml_to_object(card_config) - card.update(card_config) - save_yaml(fname, config) - return True + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + card.update(card_config) + save_yaml(fname, config) + return raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) +def add_card(fname: str, view_id: str, card_config: str, + position: int = None, data_format: str = FORMAT_YAML): + """Add a card to a view.""" + config = load_yaml(fname) + for view in config.get('views', []): + if view.get('id') != view_id: + continue + cards = view.get('cards', []) + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + if position is None: + cards.append(card_config) + else: + cards.insert(position, card_config) + save_yaml(fname, config) + return + + raise ViewNotFoundError( + "View with ID: {} was not found in {}.".format(view_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -208,8 +252,12 @@ async def async_setup(hass, config): SCHEMA_GET_CARD) hass.components.websocket_api.async_register_command( - WS_TYPE_SET_CARD, websocket_lovelace_set_card, - SCHEMA_SET_CARD) + WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card, + SCHEMA_UPDATE_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_ADD_CARD, websocket_lovelace_add_card, + SCHEMA_ADD_CARD) return True @@ -245,7 +293,7 @@ async def websocket_lovelace_get_card(hass, connection, msg): try: card = await hass.async_add_executor_job( get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], - msg.get('format', 'yaml')) + msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( msg['id'], card ) @@ -254,9 +302,8 @@ async def websocket_lovelace_get_card(hass, connection, msg): 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -267,24 +314,51 @@ async def websocket_lovelace_get_card(hass, connection, msg): @websocket_api.async_response -async def websocket_lovelace_set_card(hass, connection, msg): +async def websocket_lovelace_update_card(hass, connection, msg): """Receive lovelace card config over websocket and save.""" error = None try: - result = await hass.async_add_executor_job( - set_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) + await hass.async_add_executor_job( + update_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( - msg['id'], result + msg['id'], True ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_add_card(hass, connection, msg): + """Add new card to view over websocket and save.""" + error = None + try: + await hass.async_add_executor_job( + add_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['view_id'], msg['card_config'], msg.get('position'), + msg.get('format', FORMAT_YAML)) + message = websocket_api.result_message( + msg['id'], True + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except ViewNotFoundError as err: + error = 'view_not_found', str(err) except HomeAssistantError as err: error = 'save_error', str(err) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index c637267cc7e..1ce0f9ff602 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -370,8 +370,8 @@ async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): assert msg['error']['code'] == 'load_error' -async def test_lovelace_set_card(hass, hass_ws_client): - """Test set_card command.""" +async def test_lovelace_update_card(hass, hass_ws_client): + """Test update_card command.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -382,7 +382,7 @@ async def test_lovelace_set_card(hass, hass_ws_client): as save_yaml_mock: await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -396,8 +396,8 @@ async def test_lovelace_set_card(hass, hass_ws_client): assert msg['success'] -async def test_lovelace_set_card_not_found(hass, hass_ws_client): - """Test set_card command cannot find card.""" +async def test_lovelace_update_card_not_found(hass, hass_ws_client): + """Test update_card command cannot find card.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -406,7 +406,7 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): return_value=yaml.load(TEST_YAML_A)): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'not_found', 'card_config': 'id: test\ntype: glance\n', }) @@ -418,8 +418,8 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): assert msg['error']['code'] == 'card_not_found' -async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): - """Test set_card command bad yaml.""" +async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client): + """Test update_card command bad yaml.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -430,7 +430,7 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): side_effect=HomeAssistantError): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -440,3 +440,56 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'save_error' + + +async def test_lovelace_add_card(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 2, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + + +async def test_lovelace_add_card_position(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'position': 0, + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 0, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] From 3d841681d70e367ee0e444c6846435c18afd608c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 24 Oct 2018 14:14:01 +0200 Subject: [PATCH 15/35] Remove day (fixes #17741) (#17743) --- homeassistant/components/sensor/fastdotcom.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index c6a56701f7c..761dc7c6a00 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' -CONF_DAY = 'day' CONF_MANUAL = 'manual' ICON = 'mdi:speedometer' @@ -34,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), vol.Optional(CONF_HOUR): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), - vol.Optional(CONF_DAY): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), vol.Optional(CONF_MANUAL, default=False): cv.boolean, }) @@ -109,8 +106,7 @@ class SpeedtestData: if not config.get(CONF_MANUAL): track_time_change( hass, self.update, second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR), - day=config.get(CONF_DAY)) + minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR)) def update(self, now): """Get the latest data from fast.com.""" From a3ec37834bfd8f2a1ecdca4666c147dc69856dca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Oct 2018 22:15:57 +0200 Subject: [PATCH 16/35] Bumped version to 0.81.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5bcfba2caff..edcaa88cecb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -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 ad903f991729900c9c5f33319581381def9a4b5f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:17:40 +0200 Subject: [PATCH 17/35] Bump frontend to 20181026.0 --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c155bcf81e3..0e92595ae78 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181024.0'] +REQUIREMENTS = ['home-assistant-frontend==20181026.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', From a4773dc3e440823e0d2c1260c5979f9616d71f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:18:10 +0200 Subject: [PATCH 18/35] Update translations --- .../components/cast/.translations/pt.json | 4 ++-- .../components/deconz/.translations/pt.json | 2 +- .../components/hangouts/.translations/pt.json | 6 ++--- .../components/hue/.translations/pt.json | 2 +- .../components/ifttt/.translations/ko.json | 2 +- .../components/ifttt/.translations/pl.json | 6 ++--- .../components/ifttt/.translations/pt.json | 2 +- .../components/ios/.translations/pt.json | 4 ++-- .../components/lifx/.translations/pt.json | 12 ++++++++-- .../components/mailgun/.translations/ca.json | 18 +++++++++++++++ .../components/mailgun/.translations/ko.json | 18 +++++++++++++++ .../components/mailgun/.translations/nl.json | 15 +++++++++++++ .../components/mailgun/.translations/pl.json | 18 +++++++++++++++ .../components/mailgun/.translations/pt.json | 18 +++++++++++++++ .../components/mailgun/.translations/ru.json | 18 +++++++++++++++ .../components/mailgun/.translations/sl.json | 18 +++++++++++++++ .../mailgun/.translations/zh-Hant.json | 18 +++++++++++++++ .../components/mqtt/.translations/pt.json | 6 ++--- .../components/nest/.translations/ko.json | 4 ++-- .../components/nest/.translations/pt.json | 2 +- .../components/openuv/.translations/pt.json | 2 +- .../components/smhi/.translations/fr.json | 16 ++++++++++++++ .../components/smhi/.translations/pt.json | 6 +++++ .../components/tradfri/.translations/pt.json | 2 +- .../components/twilio/.translations/ca.json | 18 +++++++++++++++ .../components/twilio/.translations/en.json | 18 +++++++++++++++ .../components/twilio/.translations/ko.json | 18 +++++++++++++++ .../components/twilio/.translations/nl.json | 14 ++++++++++++ .../components/twilio/.translations/no.json | 5 +++++ .../components/twilio/.translations/pl.json | 18 +++++++++++++++ .../components/twilio/.translations/ru.json | 18 +++++++++++++++ .../components/twilio/.translations/sl.json | 18 +++++++++++++++ .../twilio/.translations/zh-Hant.json | 18 +++++++++++++++ .../components/unifi/.translations/fr.json | 22 +++++++++++++++++++ .../components/unifi/.translations/nl.json | 5 ++++- .../components/unifi/.translations/no.json | 13 +++++++++++ .../components/unifi/.translations/pl.json | 2 +- .../components/upnp/.translations/pt.json | 6 ++--- .../components/zwave/.translations/fr.json | 21 ++++++++++++++++++ .../components/zwave/.translations/ko.json | 2 +- 40 files changed, 405 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/mailgun/.translations/ca.json create mode 100644 homeassistant/components/mailgun/.translations/ko.json create mode 100644 homeassistant/components/mailgun/.translations/nl.json create mode 100644 homeassistant/components/mailgun/.translations/pl.json create mode 100644 homeassistant/components/mailgun/.translations/pt.json create mode 100644 homeassistant/components/mailgun/.translations/ru.json create mode 100644 homeassistant/components/mailgun/.translations/sl.json create mode 100644 homeassistant/components/mailgun/.translations/zh-Hant.json create mode 100644 homeassistant/components/smhi/.translations/fr.json create mode 100644 homeassistant/components/twilio/.translations/ca.json create mode 100644 homeassistant/components/twilio/.translations/en.json create mode 100644 homeassistant/components/twilio/.translations/ko.json create mode 100644 homeassistant/components/twilio/.translations/nl.json create mode 100644 homeassistant/components/twilio/.translations/no.json create mode 100644 homeassistant/components/twilio/.translations/pl.json create mode 100644 homeassistant/components/twilio/.translations/ru.json create mode 100644 homeassistant/components/twilio/.translations/sl.json create mode 100644 homeassistant/components/twilio/.translations/zh-Hant.json create mode 100644 homeassistant/components/unifi/.translations/fr.json create mode 100644 homeassistant/components/unifi/.translations/no.json create mode 100644 homeassistant/components/zwave/.translations/fr.json diff --git a/homeassistant/components/cast/.translations/pt.json b/homeassistant/components/cast/.translations/pt.json index a6d28538396..85d1b14484d 100644 --- a/homeassistant/components/cast/.translations/pt.json +++ b/homeassistant/components/cast/.translations/pt.json @@ -7,9 +7,9 @@ "step": { "confirm": { "description": "Deseja configurar o Google Cast?", - "title": "" + "title": "Google Cast" } }, - "title": "" + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 1f7b8209089..eef2d5ce946 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -28,6 +28,6 @@ "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" } }, - "title": "deCONZ" + "title": "Gateway Zigbee deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pt.json b/homeassistant/components/hangouts/.translations/pt.json index 64c960a121a..a16c60128c1 100644 --- a/homeassistant/components/hangouts/.translations/pt.json +++ b/homeassistant/components/hangouts/.translations/pt.json @@ -5,7 +5,7 @@ "unknown": "Ocorreu um erro desconhecido." }, "error": { - "invalid_2fa": "Autoriza\u00e7\u00e3o por 2 factores inv\u00e1lida, por favor, tente novamente.", + "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." }, @@ -15,7 +15,7 @@ "2fa": "Pin 2FA" }, "description": "Vazio", - "title": "" + "title": "Autentica\u00e7\u00e3o de 2 Fatores" }, "user": { "data": { @@ -26,6 +26,6 @@ "title": "Login Google Hangouts" } }, - "title": "" + "title": "Google Hangouts" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index f7988d82d8c..d52540b0921 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 57ad8037753..2f033e4f4ee 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url})\ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index 3c3c2182503..7064364ebe6 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -5,12 +5,12 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Podaj nast\u0119puj\u0105ce informacje:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { - "description": "Jeste\u015b pewny, \u017ce chcesz skonfigurowa\u0107 IFTTT?", - "title": "Konfigurowanie apletu Webhook IFTTT" + "description": "Czy chcesz skonfigurowa\u0107 IFTTT?", + "title": "Konfiguracja apletu Webhook IFTTT" } }, "title": "IFTTT" diff --git a/homeassistant/components/ifttt/.translations/pt.json b/homeassistant/components/ifttt/.translations/pt.json index 34c6496d7b1..08b7aee6a08 100644 --- a/homeassistant/components/ifttt/.translations/pt.json +++ b/homeassistant/components/ifttt/.translations/pt.json @@ -13,6 +13,6 @@ "title": "Configurar o IFTTT Webhook Applet" } }, - "title": "" + "title": "IFTTT" } } \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pt.json b/homeassistant/components/ios/.translations/pt.json index 6752606d9f5..d38b9abb70b 100644 --- a/homeassistant/components/ios/.translations/pt.json +++ b/homeassistant/components/ios/.translations/pt.json @@ -6,9 +6,9 @@ "step": { "confirm": { "description": "Deseja configurar o componente iOS do Home Assistant?", - "title": "" + "title": "Home Assistant iOS" } }, - "title": "" + "title": "Home Assistant iOS" } } \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pt.json b/homeassistant/components/lifx/.translations/pt.json index 5d7fdf356ef..d5c93c33993 100644 --- a/homeassistant/components/lifx/.translations/pt.json +++ b/homeassistant/components/lifx/.translations/pt.json @@ -1,7 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede." - } + "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida." + }, + "step": { + "confirm": { + "description": "Deseja configurar o LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json new file mode 100644 index 00000000000..d644b9b8c73 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Mailgun] ({mailgun_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar Mailgun?", + "title": "Configureu el Webhook de Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json new file mode 100644 index 00000000000..0dd8cbdb47d --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Mailgun \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun Webhook]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Mailgun Webhook \uc124\uc815" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/nl.json b/homeassistant/components/mailgun/.translations/nl.json new file mode 100644 index 00000000000..d71c311b7f8 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Mailgun-berichten te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n enkele instantie is nodig." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Mailgun wilt instellen?", + "title": "Stel de Mailgun Webhook in" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/pl.json b/homeassistant/components/mailgun/.translations/pl.json new file mode 100644 index 00000000000..ba89efab0c2 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Mailgun.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Mailgun?", + "title": "Konfiguracja Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/pt.json b/homeassistant/components/mailgun/.translations/pt.json new file mode 100644 index 00000000000..963d3322d84 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens Mailgun.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks with Mailgun] ({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Mailgun?", + "title": "Configurar o Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json new file mode 100644 index 00000000000..62007a95809 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Mailgun.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/sl.json b/homeassistant/components/mailgun/.translations/sl.json new file mode 100644 index 00000000000..12dad4d8c7e --- /dev/null +++ b/homeassistant/components/mailgun/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Mailgun, mora biti Home Assistent dostopen prek interneta.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "Za po\u0161iljanje dogodkov Home Assistentu boste morali nastaviti [Webhooks z Mailgun]({mailgun_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite automations za obravnavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Mailgun?", + "title": "Nastavite Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json new file mode 100644 index 00000000000..4b9ab3a7abb --- /dev/null +++ b/homeassistant/components/mailgun/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Mailgun\uff1f", + "title": "\u8a2d\u5b9a Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json index 3b36345994d..21b9cbdf755 100644 --- a/homeassistant/components/mqtt/.translations/pt.json +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -9,14 +9,14 @@ "step": { "broker": { "data": { - "broker": "", + "broker": "Broker", "discovery": "Ativar descoberta", "password": "Palavra-passe", "port": "Porto", "username": "Utilizador" }, "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.", - "title": "" + "title": "MQTT" }, "hassio_confirm": { "data": { @@ -26,6 +26,6 @@ "title": "MQTT Broker atrav\u00e9s do add-on Hass.io" } }, - "title": "" + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json index 0caa70aeff2..a53a26bca5a 100644 --- a/homeassistant/components/nest/.translations/ko.json +++ b/homeassistant/components/nest/.translations/ko.json @@ -4,7 +4,7 @@ "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "error": { "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", @@ -24,7 +24,7 @@ "data": { "code": "\ud540 \ucf54\ub4dc" }, - "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", "title": "Nest \uacc4\uc815 \uc5f0\uacb0" } }, diff --git a/homeassistant/components/nest/.translations/pt.json b/homeassistant/components/nest/.translations/pt.json index 40743fe3ddb..5ea970d9fb3 100644 --- a/homeassistant/components/nest/.translations/pt.json +++ b/homeassistant/components/nest/.translations/pt.json @@ -28,6 +28,6 @@ "title": "Associar conta Nest" } }, - "title": "" + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pt.json b/homeassistant/components/openuv/.translations/pt.json index 36f875efc00..48283a74106 100644 --- a/homeassistant/components/openuv/.translations/pt.json +++ b/homeassistant/components/openuv/.translations/pt.json @@ -15,6 +15,6 @@ "title": "Preencha com as suas informa\u00e7\u00f5es" } }, - "title": "" + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/fr.json b/homeassistant/components/smhi/.translations/fr.json new file mode 100644 index 00000000000..d1378f183d5 --- /dev/null +++ b/homeassistant/components/smhi/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pt.json b/homeassistant/components/smhi/.translations/pt.json index a5c71885906..e814ffd5046 100644 --- a/homeassistant/components/smhi/.translations/pt.json +++ b/homeassistant/components/smhi/.translations/pt.json @@ -1,8 +1,14 @@ { "config": { + "error": { + "name_exists": "Nome j\u00e1 existe", + "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" + }, "step": { "user": { "data": { + "latitude": "Latitude", + "longitude": "Longitude", "name": "Nome" }, "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" diff --git a/homeassistant/components/tradfri/.translations/pt.json b/homeassistant/components/tradfri/.translations/pt.json index 05d3cbb57fe..e89cb6ac620 100644 --- a/homeassistant/components/tradfri/.translations/pt.json +++ b/homeassistant/components/tradfri/.translations/pt.json @@ -18,6 +18,6 @@ "title": "Introduzir c\u00f3digo de seguran\u00e7a" } }, - "title": "" + "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json new file mode 100644 index 00000000000..3179f420ede --- /dev/null +++ b/homeassistant/components/twilio/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Twilio] ({twilio_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar Twilio?", + "title": "Configureu el Webhook de Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/en.json b/homeassistant/components/twilio/.translations/en.json new file mode 100644 index 00000000000..3ee0421469c --- /dev/null +++ b/homeassistant/components/twilio/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Twilio?", + "title": "Set up the Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json new file mode 100644 index 00000000000..028919bff90 --- /dev/null +++ b/homeassistant/components/twilio/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twilio \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio Webhook]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Twilio \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Twilio Webhook \uc124\uc815" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nl.json b/homeassistant/components/twilio/.translations/nl.json new file mode 100644 index 00000000000..a053bf372a5 --- /dev/null +++ b/homeassistant/components/twilio/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n exemplaar is nodig." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Twilio wilt instellen?", + "title": "Stel de Twilio Webhook in" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json new file mode 100644 index 00000000000..86e5d9051b3 --- /dev/null +++ b/homeassistant/components/twilio/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/pl.json b/homeassistant/components/twilio/.translations/pl.json new file mode 100644 index 00000000000..19c835c4b8c --- /dev/null +++ b/homeassistant/components/twilio/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Twilio.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Twilio?", + "title": "Konfiguracja Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json new file mode 100644 index 00000000000..e758a47064e --- /dev/null +++ b/homeassistant/components/twilio/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Twilio.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/sl.json b/homeassistant/components/twilio/.translations/sl.json new file mode 100644 index 00000000000..0321cb05452 --- /dev/null +++ b/homeassistant/components/twilio/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Twilio, mora biti Home Assistent dostopen prek interneta.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [Webhooks z Twilio]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite avtomatizacijo za obravnavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Twilio?", + "title": "Nastavite Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json new file mode 100644 index 00000000000..2e85ef7b2de --- /dev/null +++ b/homeassistant/components/twilio/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Twilio]({twilio_url})\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Twilio\uff1f", + "title": "\u8a2d\u5b9a Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json new file mode 100644 index 00000000000..68e90811a3e --- /dev/null +++ b/homeassistant/components/unifi/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "user_privilege": "L'utilisateur doit \u00eatre administrateur" + }, + "error": { + "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur", + "service_unavailable": "Aucun service disponible" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "site": "ID du site", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 8e87dc4b2a6..7a1eea546a2 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Controller site is al geconfigureerd", "user_privilege": "Gebruiker moet beheerder zijn" }, "error": { @@ -13,7 +14,9 @@ "host": "Host", "password": "Wachtwoord", "port": "Poort", - "username": "Gebruikersnaam" + "site": "Site ID", + "username": "Gebruikersnaam", + "verify_ssl": "Controller gebruik van het juiste certificaat" }, "title": "Stel de UniFi-controller in" } diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json new file mode 100644 index 00000000000..7e9251dc026 --- /dev/null +++ b/homeassistant/components/unifi/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index f2f8082ac76..5382adcbf7d 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -18,7 +18,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu" }, - "title": "Skonfiguruj kontroler UniFi" + "title": "Konfiguracja kontrolera UniFi" } }, "title": "Kontroler UniFi" diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index 5e9b516d1c2..899a5def479 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -11,17 +11,17 @@ }, "step": { "init": { - "title": "" + "title": "UPnP/IGD" }, "user": { "data": { "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", "enable_sensors": "Adicionar sensores de tr\u00e1fego", - "igd": "" + "igd": "UPnP/IGD" }, "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" } }, - "title": "" + "title": "UPnP/IGD" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/fr.json b/homeassistant/components/zwave/.translations/fr.json new file mode 100644 index 00000000000..c667965bebc --- /dev/null +++ b/homeassistant/components/zwave/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave est d\u00e9j\u00e0 configur\u00e9", + "one_instance_only": "Le composant ne prend en charge qu'une seule instance Z-Wave" + }, + "error": { + "option_error": "La validation Z-Wave a \u00e9chou\u00e9. Le chemin d'acc\u00e8s \u00e0 la cl\u00e9 USB est-il correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", + "usb_path": "Chemin USB" + }, + "title": "Configurer Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json index d57f758ce25..e288019de0c 100644 --- a/homeassistant/components/zwave/.translations/ko.json +++ b/homeassistant/components/zwave/.translations/ko.json @@ -13,7 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uacbd\ub85c" }, - "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Z-Wave \uc124\uc815" } }, From af03390c4f2b76f361f5bfdd73f043861171398e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 24 Oct 2018 21:53:18 -0600 Subject: [PATCH 19/35] Fixed an incorrect reference in the entity registry (#17775) --- homeassistant/helpers/entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4a5daa182fa..5adf748dc58 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -185,7 +185,7 @@ class EntityRegistry: for listener_ref in new.update_listeners: listener = listener_ref() if listener is None: - to_remove.append(listener) + to_remove.append(listener_ref) else: try: listener.async_registry_updated(old, new) From bc67115df38a2a5ee6bac8659651f85aab8bc2af Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 25 Oct 2018 09:45:56 +0200 Subject: [PATCH 20/35] Update HAP-python to 2.3.0 (#17778) * Update HAP-python to 2.3.0 * Fix tests --- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/homekit/type_switches.py | 8 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_locks.py | 1 + tests/components/homekit/test_type_media_players.py | 1 + 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1c30de918e3..d4d8fe0216c 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,7 @@ from .const import ( from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -REQUIREMENTS = ['HAP-python==2.2.2'] +REQUIREMENTS = ['HAP-python==2.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 59ae17b5d9d..839abe5a580 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,7 +1,9 @@ """Class to hold all switch accessories.""" import logging -from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH +from pyhap.const import ( + CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, + CATEGORY_SPRINKLER, CATEGORY_SWITCH) from homeassistant.components.switch import DOMAIN from homeassistant.const import ( @@ -17,10 +19,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CATEGORY_SPRINKLER = 28 -CATEGORY_FAUCET = 29 -CATEGORY_SHOWER_HEAD = 30 - VALVE_TYPE = { TYPE_FAUCET: (CATEGORY_FAUCET, 3), TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), diff --git a/requirements_all.txt b/requirements_all.txt index 4f390c992fa..489b4fe9287 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,7 +34,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.3.0 # homeassistant.components.notify.mastodon Mastodon.py==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e93231c82e..c735d47d551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5.2 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.3.0 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index e7e52c65559..8132099bd3e 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -82,6 +82,7 @@ async def test_no_code(hass, hk_driver, config, events): # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, 'lock') + acc.char_target_state.value = 0 await hass.async_add_job(acc.char_target_state.client_update_value, 1) await hass.async_block_till_done() assert call_lock diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 6b23b3cc58e..745e4c162bc 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -64,6 +64,7 @@ async def test_media_player_set_state(hass, hk_driver, events): call_media_stop = async_mock_service(hass, DOMAIN, 'media_stop') call_toggle_mute = async_mock_service(hass, DOMAIN, 'volume_mute') + acc.chars[FEATURE_ON_OFF].value = False await hass.async_add_job(acc.chars[FEATURE_ON_OFF] .client_update_value, True) await hass.async_block_till_done() From c4b2c2bfcfe7bba97640ac1b3091a7d5243cf570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Thu, 25 Oct 2018 10:16:09 +0200 Subject: [PATCH 21/35] SMHI weather component not showing correct values in current forecast (#17783) * fixes not showing current forecast correctly * Update config_flow.py * Update smhi.py * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/smhi/__init__.py | 2 +- homeassistant/components/smhi/config_flow.py | 2 -- homeassistant/components/weather/smhi.py | 1 - requirements_all.txt | 4 +--- requirements_test_all.txt | 4 +--- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index d0e4b6ef487..2421addfd0c 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import Config, HomeAssistant from .config_flow import smhi_locations # noqa: F401 from .const import DOMAIN # noqa: F401 -REQUIREMENTS = ['smhi-pkg==1.0.4'] +REQUIREMENTS = ['smhi-pkg==1.0.5'] DEFAULT_NAME = 'smhi' diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3c9875ab797..e461c6d195d 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -20,8 +20,6 @@ from homeassistant.util import slugify from .const import DOMAIN, HOME_LOCATION_NAME -REQUIREMENTS = ['smhi-pkg==1.0.4'] - @callback def smhi_locations(hass: HomeAssistant): diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/weather/smhi.py index c24d3f8f091..3bbaab3f8ec 100644 --- a/homeassistant/components/weather/smhi.py +++ b/homeassistant/components/weather/smhi.py @@ -30,7 +30,6 @@ from homeassistant.components.smhi.const import ( ENTITY_ID_SENSOR_FORMAT, ATTR_SMHI_CLOUDINESS) DEPENDENCIES = ['smhi'] -REQUIREMENTS = ['smhi-pkg==1.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 489b4fe9287..304e2f51122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,9 +1378,7 @@ smappy==0.2.16 # smbus-cffi==0.5.1 # homeassistant.components.smhi -# homeassistant.components.smhi.config_flow -# homeassistant.components.weather.smhi -smhi-pkg==1.0.4 +smhi-pkg==1.0.5 # homeassistant.components.media_player.snapcast snapcast==2.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c735d47d551..cdb8f48fbd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,9 +228,7 @@ simplisafe-python==3.1.12 sleepyq==0.6 # homeassistant.components.smhi -# homeassistant.components.smhi.config_flow -# homeassistant.components.weather.smhi -smhi-pkg==1.0.4 +smhi-pkg==1.0.5 # homeassistant.components.climate.honeywell somecomfort==0.5.2 From 0c7b0bdb442849c3839a2c3b915dc3790c1e032e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Oct 2018 19:57:36 +0200 Subject: [PATCH 22/35] Fix unloading an entry can leave states around (#17786) * Add test that tests unloading on remove * Add more test things * Untangle entity remove code from entity platform * Don't add default implementation of async_will_remove * Keep entity weakref alive --- homeassistant/helpers/entity.py | 10 ++-- homeassistant/helpers/entity_platform.py | 19 ++---- tests/test_config_entries.py | 75 ++++++++++++++++++++---- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 987bdeae6ca..687ed0b6f8b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -363,14 +363,16 @@ class Entity: async def async_remove(self): """Remove entity from Home Assistant.""" + will_remove = getattr(self, 'async_will_remove_from_hass', None) + + if will_remove: + await will_remove() # pylint: disable=not-callable + if self._on_remove is not None: while self._on_remove: self._on_remove.pop()() - if self.platform is not None: - await self.platform.async_remove_entity(self.entity_id) - else: - self.hass.states.async_remove(self.entity_id) + self.hass.states.async_remove(self.entity_id) @callback def async_registry_updated(self, old, new): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3ab45577236..5fd580a33f0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -345,8 +345,10 @@ class EntityPlatform: raise HomeAssistantError( msg) - self.entities[entity.entity_id] = entity - component_entities.add(entity.entity_id) + entity_id = entity.entity_id + self.entities[entity_id] = entity + component_entities.add(entity_id) + entity.async_on_remove(lambda: self.entities.pop(entity_id)) if hasattr(entity, 'async_added_to_hass'): await entity.async_added_to_hass() @@ -365,7 +367,7 @@ class EntityPlatform: if not self.entities: return - tasks = [self._async_remove_entity(entity_id) + tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] await asyncio.wait(tasks, loop=self.hass.loop) @@ -376,7 +378,7 @@ class EntityPlatform: async def async_remove_entity(self, entity_id): """Remove entity id from platform.""" - await self._async_remove_entity(entity_id) + await self.entities[entity_id].async_remove() # Clean up polling job if no longer needed if (self._async_unsub_polling is not None and @@ -385,15 +387,6 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def _async_remove_entity(self, entity_id): - """Remove entity id from platform.""" - entity = self.entities.pop(entity_id) - - if hasattr(entity, 'async_will_remove_from_hass'): - await entity.async_will_remove_from_hass() - - self.hass.states.async_remove(entity_id) - async def _update_entity_states(self, now): """Update the states of all the polling entities. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 340118502b1..59777e2e6bb 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -11,7 +11,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import ( - MockModule, mock_coro, MockConfigEntry, async_fire_time_changed) + MockModule, mock_coro, MockConfigEntry, async_fire_time_changed, + MockPlatform, MockEntity) @pytest.fixture @@ -40,35 +41,87 @@ def test_call_setup_entry(hass): assert len(mock_setup_entry.mock_calls) == 1 -@asyncio.coroutine -def test_remove_entry(hass, manager): +async def test_remove_entry(hass, manager): """Test that we can remove an entry.""" - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + async def mock_setup_entry(hass, entry): + """Mock setting up entry.""" + hass.loop.create_task(hass.config_entries.async_forward_entry_setup( + entry, 'light')) + return True + async def mock_unload_entry(hass, entry): + """Mock unloading an entry.""" + result = await hass.config_entries.async_forward_entry_unload( + entry, 'light') + assert result + return result + + entity = MockEntity( + unique_id='1234', + name='Test Entity', + ) + + async def mock_setup_entry_platform(hass, entry, async_add_entities): + """Mock setting up platform.""" + async_add_entities([entity]) + + loader.set_component(hass, 'test', MockModule( + 'test', + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry + )) loader.set_component( - hass, 'test', - MockModule('comp', async_unload_entry=mock_unload_entry)) + hass, 'light.test', + MockPlatform(async_setup_entry=mock_setup_entry_platform)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry( + entry = MockConfigEntry( domain='test', entry_id='test2', - state=config_entries.ENTRY_STATE_LOADED - ).add_to_manager(manager) + ) + entry.add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) + # Check all config entries exist assert [item.entry_id for item in manager.async_entries()] == \ ['test1', 'test2', 'test3'] - result = yield from manager.async_remove('test2') + # Setup entry + await entry.async_setup(hass) + await hass.async_block_till_done() + # Check entity state got added + assert hass.states.get('light.test_entity') is not None + # Group all_lights, light.test_entity + assert len(hass.states.async_all()) == 2 + + # Check entity got added to entity registry + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 1 + entity_entry = list(ent_reg.entities.values())[0] + assert entity_entry.config_entry_id == entry.entry_id + + # Remove entry + result = await manager.async_remove('test2') + await hass.async_block_till_done() + + # Check that unload went well and so no need to restart assert result == { 'require_restart': False } + + # Check that config entry was removed. assert [item.entry_id for item in manager.async_entries()] == \ ['test1', 'test3'] - assert len(mock_unload_entry.mock_calls) == 1 + # Check that entity state has been removed + assert hass.states.get('light.test_entity') is None + # Just Group all_lights + assert len(hass.states.async_all()) == 1 + + # Check that entity registry entry no longer references config_entry_id + entity_entry = list(ent_reg.entities.values())[0] + assert entity_entry.config_entry_id is None @asyncio.coroutine From 121a59abe0fc37ab217429c99594fecc5455e491 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 10:22:26 +0200 Subject: [PATCH 23/35] Bumped version to 0.81.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index edcaa88cecb..5882bd62314 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From dcf8aba1507a60b438a25b4e05da32a5b0557941 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Oct 2018 12:55:44 +0200 Subject: [PATCH 24/35] frontend bump --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 304e2f51122..ecb09679d42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181024.0 +home-assistant-frontend==20181026.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdb8f48fbd0..7da93c5e0bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181024.0 +home-assistant-frontend==20181026.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From a0d4595f78fb6d55b8ccaa3596b6c0aa8375515e Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 27 Oct 2018 12:36:47 -0700 Subject: [PATCH 25/35] Switch to using Client from twilio.rest rather than the deleted TwilioRestClient (#17885) --- homeassistant/components/twilio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twilio.py b/homeassistant/components/twilio.py index 9f9767e4675..b32f9ee8efc 100644 --- a/homeassistant/components/twilio.py +++ b/homeassistant/components/twilio.py @@ -34,9 +34,9 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Twilio component.""" - from twilio.rest import TwilioRestClient + from twilio.rest import Client conf = config[DOMAIN] - hass.data[DATA_TWILIO] = TwilioRestClient( + hass.data[DATA_TWILIO] = Client( conf.get(CONF_ACCOUNT_SID), conf.get(CONF_AUTH_TOKEN)) hass.http.register_view(TwilioReceiveDataView()) return True From ea75e3bfa83c0ec91eaed6424ddf1b1ff4e9dcef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Oct 2018 12:14:37 +0200 Subject: [PATCH 26/35] bump frontend to 20181026.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 0e92595ae78..d2b671df8b0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181026.0'] +REQUIREMENTS = ['home-assistant-frontend==20181026.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index ecb09679d42..5ad0e9aae8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181026.0 +home-assistant-frontend==20181026.1 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7da93c5e0bc..d7a7c761b4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ hdate==0.6.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181026.0 +home-assistant-frontend==20181026.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From edf29749799ef5a88aa6541f55c114a9b11f3d38 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 27 Oct 2018 11:07:08 -0600 Subject: [PATCH 27/35] Fixes an issue with OpenUV config import failing (#17831) * Fixes an issue with OpenUV config import failing * Update * Update __init__.py * Update config_flow.py --- homeassistant/components/openuv/__init__.py | 44 +++++++++++-------- .../components/openuv/config_flow.py | 27 ++++-------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index a45d9ceb0d6..1dc158bc2ab 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -109,26 +109,30 @@ async def async_setup(hass, config): return True conf = config[DOMAIN] - latitude = conf.get(CONF_LATITUDE) - longitude = conf.get(CONF_LONGITUDE) - identifier = '{0}, {1}'.format(latitude, longitude) + identifier = '{0}, {1}'.format( + conf.get(CONF_LATITUDE, hass.config.latitude), + conf.get(CONF_LONGITUDE, hass.config.longitude)) if identifier in configured_instances(hass): return True + data = { + CONF_API_KEY: conf[CONF_API_KEY], + CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], + CONF_SENSORS: conf[CONF_SENSORS], + CONF_SCAN_INTERVAL: conf[CONF_SCAN_INTERVAL], + } + + if CONF_LATITUDE in conf: + data[CONF_LATITUDE] = conf[CONF_LATITUDE] + if CONF_LONGITUDE in conf: + data[CONF_LONGITUDE] = conf[CONF_LONGITUDE] + if CONF_ELEVATION in conf: + data[CONF_ELEVATION] = conf[CONF_ELEVATION] + hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, - context={'source': SOURCE_IMPORT}, - data={ - CONF_API_KEY: conf[CONF_API_KEY], - CONF_LATITUDE: latitude, - CONF_LONGITUDE: longitude, - CONF_ELEVATION: conf.get(CONF_ELEVATION), - CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], - CONF_SENSORS: conf[CONF_SENSORS], - CONF_SCAN_INTERVAL: conf[CONF_SCAN_INTERVAL], - })) + DOMAIN, context={'source': SOURCE_IMPORT}, data=data)) return True @@ -143,10 +147,11 @@ async def async_setup_entry(hass, config_entry): openuv = OpenUV( Client( config_entry.data[CONF_API_KEY], - config_entry.data[CONF_LATITUDE], - config_entry.data[CONF_LONGITUDE], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), websession, - altitude=config_entry.data[CONF_ELEVATION]), + altitude=config_entry.data.get( + CONF_ELEVATION, hass.config.elevation)), config_entry.data.get(CONF_BINARY_SENSORS, {}).get( CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)), config_entry.data.get(CONF_SENSORS, {}).get( @@ -158,8 +163,9 @@ async def async_setup_entry(hass, config_entry): raise ConfigEntryNotReady for component in ('binary_sensor', 'sensor'): - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - config_entry, component)) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + config_entry, component)) async def refresh(event_time): """Refresh OpenUV data.""" diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 27ffe5c3985..11301baf5c5 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -17,7 +17,8 @@ def configured_instances(hass): """Return a set of configured OpenUV instances.""" return set( '{0}, {1}'.format( - entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE]) + entry.data.get(CONF_LATITUDE, hass.config.latitude), + entry.data.get(CONF_LONGITUDE, hass.config.longitude)) for entry in hass.config_entries.async_entries(DOMAIN)) @@ -36,12 +37,9 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): """Show the form to the user.""" data_schema = vol.Schema({ vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_LATITUDE, default=self.hass.config.latitude): - cv.latitude, - vol.Optional(CONF_LONGITUDE, default=self.hass.config.longitude): - cv.longitude, - vol.Optional(CONF_ELEVATION, default=self.hass.config.elevation): - vol.Coerce(float), + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ELEVATION): vol.Coerce(float), }) return self.async_show_form( @@ -61,11 +59,9 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): if not user_input: return await self._show_form() - latitude = user_input[CONF_LATITUDE] - longitude = user_input[CONF_LONGITUDE] - elevation = user_input[CONF_ELEVATION] - - identifier = '{0}, {1}'.format(latitude, longitude) + identifier = '{0}, {1}'.format( + user_input.get(CONF_LATITUDE, self.hass.config.latitude), + user_input.get(CONF_LONGITUDE, self.hass.config.longitude)) if identifier in configured_instances(self.hass): return await self._show_form({CONF_LATITUDE: 'identifier_exists'}) @@ -78,11 +74,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): scan_interval = user_input.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input.update({ - CONF_LATITUDE: latitude, - CONF_LONGITUDE: longitude, - CONF_ELEVATION: elevation, - CONF_SCAN_INTERVAL: scan_interval.seconds, - }) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds return self.async_create_entry(title=identifier, data=user_input) From 21686c92634b6e5bff3f665cfe68215abb6fe4b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Oct 2018 21:34:33 +0200 Subject: [PATCH 28/35] Allow a list ofr update entity (#17860) * Allow a list ofr update entity * Update services.yaml * Update services.yaml --- homeassistant/components/__init__.py | 9 ++++++--- homeassistant/components/services.yaml | 6 ++++++ tests/components/test_init.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index bdb89dd60fa..8715f0baa96 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -31,7 +31,7 @@ SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' SERVICE_CHECK_CONFIG = 'check_config' SERVICE_UPDATE_ENTITY = 'update_entity' SCHEMA_UPDATE_ENTITY = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_id + ATTR_ENTITY_ID: cv.entity_ids }) @@ -142,8 +142,11 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: async def async_handle_update_service(call): """Service handler for updating an entity.""" - await hass.helpers.entity_component.async_update_entity( - call.data[ATTR_ENTITY_ID]) + tasks = [hass.helpers.entity_component.async_update_entity(entity) + for entity in call.data[ATTR_ENTITY_ID]] + + if tasks: + await asyncio.wait(tasks) hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 62da489aab4..e8512d67fc4 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -527,6 +527,12 @@ homeassistant: entity_id: description: The entity_id of the device to turn off. example: light.living_room + update_entity: + description: Force one or more entities to update its data + fields: + entity_id: + description: One or multiple entity_ids to update. Can be a list. + example: light.living_room xiaomi_aqara: play_ringtone: diff --git a/tests/components/test_init.py b/tests/components/test_init.py index b9152bbdd6a..139a97463ea 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -364,7 +364,7 @@ async def test_entity_update(hass): with patch('homeassistant.helpers.entity_component.async_update_entity', return_value=mock_coro()) as mock_update: await hass.services.async_call('homeassistant', 'update_entity', { - 'entity_id': 'light.kitchen' + 'entity_id': ['light.kitchen'] }, blocking=True) assert len(mock_update.mock_calls) == 1 From 1f07909a1425144694cf083ecf70a8339f98358b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Oct 2018 23:51:40 +0200 Subject: [PATCH 29/35] Move migrate to separate WS command (#17890) --- homeassistant/components/lovelace/__init__.py | 38 +++++++++++++++++++ tests/components/lovelace/test_init.py | 10 ++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 141f3c98334..c45791160ac 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -24,6 +24,7 @@ FORMAT_JSON = 'json' OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +WS_TYPE_MIGRATE_CONFIG = 'lovelace/config/migrate' WS_TYPE_GET_CARD = 'lovelace/config/card/get' WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' WS_TYPE_ADD_CARD = 'lovelace/config/card/add' @@ -33,6 +34,10 @@ SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ OLD_WS_TYPE_GET_LOVELACE_UI), }) +SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_MIGRATE_CONFIG, +}) + SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_CARD, vol.Required('card_id'): str, @@ -132,6 +137,11 @@ def load_yaml(fname: str) -> JSON_TYPE: def load_config(fname: str) -> JSON_TYPE: + """Load a YAML file.""" + return load_yaml(fname) + + +def migrate_config(fname: str) -> JSON_TYPE: """Load a YAML file and adds id to views and cards if not present.""" config = load_yaml(fname) # Check if all views and cards have an id or else add one @@ -243,6 +253,10 @@ async def async_setup(hass, config): OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) + hass.components.websocket_api.async_register_command( + WS_TYPE_MIGRATE_CONFIG, websocket_lovelace_migrate_config, + SCHEMA_MIGRATE_CONFIG) + hass.components.websocket_api.async_register_command( WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) @@ -286,6 +300,30 @@ async def websocket_lovelace_config(hass, connection, msg): connection.send_message(message) +@websocket_api.async_response +async def websocket_lovelace_migrate_config(hass, connection, msg): + """Migrate lovelace UI config.""" + error = None + try: + config = await hass.async_add_executor_job( + migrate_config, hass.config.path(LOVELACE_CONFIG_FILE)) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + @websocket_api.async_response async def websocket_lovelace_get_card(hass, connection, msg): """Send lovelace card config over websocket config.""" diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 1ce0f9ff602..c728d1c581c 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -8,8 +8,8 @@ from ruamel.yaml import YAML from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.components.lovelace import (load_yaml, - save_yaml, load_config, +from homeassistant.components.lovelace import (load_yaml, migrate_config, + save_yaml, UnsupportedYamlError) TEST_YAML_A = """\ @@ -162,7 +162,7 @@ class TestYAML(unittest.TestCase): with patch('homeassistant.components.lovelace.load_yaml', return_value=self.yaml.load(TEST_YAML_A)), \ patch('homeassistant.components.lovelace.save_yaml'): - data = load_config(fname) + data = migrate_config(fname) assert 'id' in data['views'][0]['cards'][0] assert 'id' in data['views'][1] @@ -171,8 +171,8 @@ class TestYAML(unittest.TestCase): fname = self._path_for("test7") with patch('homeassistant.components.lovelace.load_yaml', return_value=self.yaml.load(TEST_YAML_B)): - data = load_config(fname) - self.assertEqual(data, self.yaml.load(TEST_YAML_B)) + data = migrate_config(fname) + assert data == self.yaml.load(TEST_YAML_B) async def test_deprecated_lovelace_ui(hass, hass_ws_client): From 29c9081ca1a3d584b608b9bda69f076920507b75 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Oct 2018 23:54:01 +0200 Subject: [PATCH 30/35] Bumped version to 0.81.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5882bd62314..effdec4db39 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -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 04e0fd1d469b8384ccb22dd24155180d5c3ad0d8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 29 Oct 2018 19:09:54 +0100 Subject: [PATCH 31/35] Fix controller not being stored when setup fails and sequentially fails the retry functionality (#17927) --- homeassistant/components/unifi/__init__.py | 9 +++++---- tests/components/unifi/test_init.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 26b60aecf42..1e07d8cb83d 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -32,20 +32,21 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the UniFi component.""" - controller = UniFiController(hass, config_entry) - if DOMAIN not in hass.data: hass.data[DOMAIN] = {} + + controller = UniFiController(hass, config_entry) + controller_id = CONTROLLER_ID.format( host=config_entry.data[CONF_CONTROLLER][CONF_HOST], site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] ) + hass.data[DOMAIN][controller_id] = controller + if not await controller.async_setup(): return False - hass.data[DOMAIN][controller_id] = controller - if controller.mac is None: return True diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 400dd3fd93e..0115801eec6 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -54,7 +54,7 @@ async def test_successful_config_entry(hass): async def test_controller_fail_setup(hass): - """Test that configured options for a host are loaded via config entry.""" + """Test that a failed setup still stores controller.""" entry = MockConfigEntry(domain=unifi.DOMAIN, data={ 'controller': { 'host': '0.0.0.0', @@ -75,7 +75,7 @@ async def test_controller_fail_setup(hass): controller_id = unifi.CONTROLLER_ID.format( host='0.0.0.0', site='default' ) - assert controller_id not in hass.data[unifi.DOMAIN] + assert controller_id in hass.data[unifi.DOMAIN] async def test_controller_no_mac(hass): From 3a8891d9ac59cd0cbd54ab92afc66b067868609c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Oct 2018 19:21:21 +0100 Subject: [PATCH 32/35] Pass hass_config to load_platform (#17952) * Pass hass_config to load_platform * Fix tests * Lint --- homeassistant/components/apple_tv.py | 10 ++++----- homeassistant/components/elkm1/__init__.py | 3 ++- homeassistant/components/evohome.py | 2 +- homeassistant/components/google.py | 22 +++++++++++-------- homeassistant/components/maxcube.py | 4 ++-- homeassistant/components/melissa.py | 4 ++-- .../components/mysensors/__init__.py | 2 +- homeassistant/components/mysensors/gateway.py | 21 ++++++++++-------- homeassistant/components/octoprint.py | 5 +++-- homeassistant/components/opentherm_gw.py | 11 +++++----- homeassistant/components/smappee.py | 4 ++-- homeassistant/components/spc.py | 4 ++-- homeassistant/components/spider.py | 2 +- homeassistant/helpers/discovery.py | 9 ++++---- tests/components/device_tracker/test_init.py | 2 +- tests/components/light/test_mqtt_json.py | 2 +- tests/components/lock/test_mqtt.py | 2 +- tests/components/notify/test_demo.py | 3 ++- tests/components/test_google.py | 4 ++-- tests/helpers/test_discovery.py | 9 ++++---- tests/helpers/test_entity_component.py | 2 +- 21 files changed, 70 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 012e71a08a7..b8774d76873 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -163,7 +163,7 @@ async def async_setup(hass, config): async def atv_discovered(service, info): """Set up an Apple TV that was auto discovered.""" - await _setup_atv(hass, { + await _setup_atv(hass, config, { CONF_NAME: info['name'], CONF_HOST: info['host'], CONF_LOGIN_ID: info['properties']['hG'], @@ -172,7 +172,7 @@ async def async_setup(hass, config): discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) - tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])] + tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])] if tasks: await asyncio.wait(tasks, loop=hass.loop) @@ -187,7 +187,7 @@ async def async_setup(hass, config): return True -async def _setup_atv(hass, atv_config): +async def _setup_atv(hass, hass_config, atv_config): """Set up an Apple TV.""" import pyatv name = atv_config.get(CONF_NAME) @@ -212,10 +212,10 @@ async def _setup_atv(hass, atv_config): } hass.async_create_task(discovery.async_load_platform( - hass, 'media_player', DOMAIN, atv_config)) + hass, 'media_player', DOMAIN, atv_config, hass_config)) hass.async_create_task(discovery.async_load_platform( - hass, 'remote', DOMAIN, atv_config)) + hass, 'remote', DOMAIN, atv_config, hass_config)) class AppleTVPowerManager: diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 76594e16736..aa7b9973c8e 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -146,7 +146,8 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} for component in SUPPORTED_DOMAINS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {})) + discovery.async_load_platform(hass, component, DOMAIN, {}, + hass_config)) return True diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome.py index ceeec407b05..397d3b9f6c0 100644 --- a/homeassistant/components/evohome.py +++ b/homeassistant/components/evohome.py @@ -140,6 +140,6 @@ def setup(hass, config): _LOGGER.debug("setup(), location = %s", tmp_loc) - load_platform(hass, 'climate', DOMAIN) + load_platform(hass, 'climate', DOMAIN, {}, config) return True diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index e37b3ba7ff7..49cb195d6c9 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -88,7 +88,7 @@ DEVICE_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def do_authentication(hass, config): +def do_authentication(hass, hass_config, config): """Notify user of actions and authenticate. Notify user of user_code and verification_url then poll @@ -145,7 +145,7 @@ def do_authentication(hass, config): storage = Storage(hass.config.path(TOKEN_FILE)) storage.put(credentials) - do_setup(hass, config) + do_setup(hass, hass_config, config) listener() hass.components.persistent_notification.create( 'We are all setup now. Check {} for calendars that have ' @@ -167,14 +167,15 @@ def setup(hass, config): token_file = hass.config.path(TOKEN_FILE) if not os.path.isfile(token_file): - do_authentication(hass, conf) + do_authentication(hass, config, conf) else: - do_setup(hass, conf) + do_setup(hass, config, conf) return True -def setup_services(hass, track_new_found_calendars, calendar_service): +def setup_services(hass, hass_config, track_new_found_calendars, + calendar_service): """Set up the service listeners.""" def _found_calendar(call): """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" @@ -190,7 +191,8 @@ def setup_services(hass, track_new_found_calendars, calendar_service): ) discovery.load_platform(hass, 'calendar', DOMAIN, - hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]) + hass.data[DATA_INDEX][calendar[CONF_CAL_ID]], + hass_config) hass.services.register( DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) @@ -210,7 +212,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service): return True -def do_setup(hass, config): +def do_setup(hass, hass_config, config): """Run the setup after we have everything configured.""" # Load calendars the user has configured hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) @@ -218,13 +220,15 @@ def do_setup(hass, config): calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) track_new_found_calendars = convert(config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW) - setup_services(hass, track_new_found_calendars, calendar_service) + setup_services(hass, hass_config, track_new_found_calendars, + calendar_service) # Ensure component is loaded setup_component(hass, 'calendar', config) for calendar in hass.data[DATA_INDEX].values(): - discovery.load_platform(hass, 'calendar', DOMAIN, calendar) + discovery.load_platform(hass, 'calendar', DOMAIN, calendar, + hass_config) # Look for any new calendars hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index b574f0bcb15..9980d554232 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -73,8 +73,8 @@ def setup(hass, config): if connection_failed >= len(gateways): return False - load_platform(hass, 'climate', DOMAIN) - load_platform(hass, 'binary_sensor', DOMAIN) + load_platform(hass, 'climate', DOMAIN, {}, config) + load_platform(hass, 'binary_sensor', DOMAIN, {}, config) return True diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py index f5a757dbcf3..49e7d62f5cb 100644 --- a/homeassistant/components/melissa.py +++ b/homeassistant/components/melissa.py @@ -39,6 +39,6 @@ def setup(hass, config): api = melissa.Melissa(username=username, password=password) hass.data[DATA_MELISSA] = api - load_platform(hass, 'sensor', DOMAIN, {}) - load_platform(hass, 'climate', DOMAIN, {}) + load_platform(hass, 'sensor', DOMAIN, {}, config) + load_platform(hass, 'climate', DOMAIN, {}, config) return True diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 4f00247495a..883175340ce 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -111,7 +111,7 @@ async def async_setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways - hass.async_create_task(finish_setup(hass, gateways)) + hass.async_create_task(finish_setup(hass, config, gateways)) return True diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 558e944f727..cb1dad922f8 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -137,7 +137,7 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): gateway.metric = hass.config.units.is_metric gateway.optimistic = conf[CONF_OPTIMISTIC] gateway.device = device - gateway.event_callback = _gw_callback_factory(hass) + gateway.event_callback = _gw_callback_factory(hass, config) gateway.nodes_config = gateway_conf[CONF_NODES] if persistence: await gateway.start_persistence() @@ -145,12 +145,13 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): return gateway -async def finish_setup(hass, gateways): +async def finish_setup(hass, hass_config, gateways): """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] start_tasks = [] for gateway in gateways.values(): - discover_tasks.append(_discover_persistent_devices(hass, gateway)) + discover_tasks.append(_discover_persistent_devices( + hass, hass_config, gateway)) start_tasks.append(_gw_start(hass, gateway)) if discover_tasks: # Make sure all devices and platforms are loaded before gateway start. @@ -159,7 +160,7 @@ async def finish_setup(hass, gateways): await asyncio.wait(start_tasks, loop=hass.loop) -async def _discover_persistent_devices(hass, gateway): +async def _discover_persistent_devices(hass, hass_config, gateway): """Discover platforms for devices loaded via persistence file.""" tasks = [] new_devices = defaultdict(list) @@ -170,17 +171,18 @@ async def _discover_persistent_devices(hass, gateway): for platform, dev_ids in validated.items(): new_devices[platform].extend(dev_ids) for platform, dev_ids in new_devices.items(): - tasks.append(_discover_mysensors_platform(hass, platform, dev_ids)) + tasks.append(_discover_mysensors_platform( + hass, hass_config, platform, dev_ids)) if tasks: await asyncio.wait(tasks, loop=hass.loop) @callback -def _discover_mysensors_platform(hass, platform, new_devices): +def _discover_mysensors_platform(hass, hass_config, platform, new_devices): """Discover a MySensors platform.""" task = hass.async_create_task(discovery.async_load_platform( hass, platform, DOMAIN, - {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}, hass_config)) return task @@ -215,7 +217,7 @@ async def _gw_start(hass, gateway): hass.data.pop(gateway_ready_key, None) -def _gw_callback_factory(hass): +def _gw_callback_factory(hass, hass_config): """Return a new callback for the gateway.""" @callback def mysensors_callback(msg): @@ -246,7 +248,8 @@ def _gw_callback_factory(hass): else: new_dev_ids.append(dev_id) if new_dev_ids: - _discover_mysensors_platform(hass, platform, new_dev_ids) + _discover_mysensors_platform( + hass, hass_config, platform, new_dev_ids) for signal in set(signals): # Only one signal per device is needed. # A device can have multiple platforms, ie multiple schemas. diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 2a39ac2c44a..b626e9a93b5 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -114,11 +114,12 @@ def setup(hass, config): sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS] load_platform(hass, 'sensor', DOMAIN, {'name': name, 'base_url': base_url, - 'sensors': sensors}) + 'sensors': sensors}, config) b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] load_platform(hass, 'binary_sensor', DOMAIN, {'name': name, 'base_url': base_url, - 'sensors': b_sensors}) + 'sensors': b_sensors}, + config) success = True return success diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw.py index 08807a2d2a6..7152a58afcc 100644 --- a/homeassistant/components/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw.py @@ -64,9 +64,10 @@ async def async_setup(hass, config): hass.async_create_task(connect_and_subscribe( hass, conf[CONF_DEVICE], gateway)) hass.async_create_task(async_load_platform( - hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE))) + hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE), config)) if monitored_vars: - hass.async_create_task(setup_monitored_vars(hass, monitored_vars)) + hass.async_create_task(setup_monitored_vars( + hass, config, monitored_vars)) return True @@ -82,7 +83,7 @@ async def connect_and_subscribe(hass, device_path, gateway): gateway.subscribe(handle_report) -async def setup_monitored_vars(hass, monitored_vars): +async def setup_monitored_vars(hass, config, monitored_vars): """Set up requested sensors.""" gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] sensor_type_map = { @@ -200,6 +201,6 @@ async def setup_monitored_vars(hass, monitored_vars): _LOGGER.error("Monitored variable not supported: %s", var) if binary_sensors: hass.async_create_task(async_load_platform( - hass, COMP_BINARY_SENSOR, DOMAIN, binary_sensors)) + hass, COMP_BINARY_SENSOR, DOMAIN, binary_sensors, config)) if sensors: - await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors) + await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors, config) diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index 7904f0a6cce..d8b7a68a506 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -66,8 +66,8 @@ def setup(hass, config): return False hass.data[DATA_SMAPPEE] = smappee - load_platform(hass, 'switch', DOMAIN) - load_platform(hass, 'sensor', DOMAIN) + load_platform(hass, 'switch', DOMAIN, {}, config) + load_platform(hass, 'sensor', DOMAIN, {}, config) return True diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index 5aa987bd0a8..dd1931e27f1 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -63,11 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, {})) + hass, 'binary_sensor', DOMAIN, {}, config)) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN, {})) + hass, 'alarm_control_panel', DOMAIN, {}, config)) # start listening for incoming events over websocket spc.start() diff --git a/homeassistant/components/spider.py b/homeassistant/components/spider.py index 48632be6bad..a10fa129fe5 100644 --- a/homeassistant/components/spider.py +++ b/homeassistant/components/spider.py @@ -56,7 +56,7 @@ def setup(hass, config): } for component in SPIDER_COMPONENTS: - load_platform(hass, component, DOMAIN, {}) + load_platform(hass, component, DOMAIN, {}, config) _LOGGER.debug("Connection with Spider API succeeded") return True diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 698cee0fcc2..405861eeb75 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -114,8 +114,7 @@ def async_listen_platform(hass, component, callback): @bind_hass -def load_platform(hass, component, platform, discovered=None, - hass_config=None): +def load_platform(hass, component, platform, discovered, hass_config): """Load a component and platform dynamically. Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be @@ -132,8 +131,8 @@ def load_platform(hass, component, platform, discovered=None, @bind_hass -async def async_load_platform(hass, component, platform, discovered=None, - hass_config=None): +async def async_load_platform(hass, component, platform, discovered, + hass_config): """Load a component and platform dynamically. Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be @@ -149,6 +148,8 @@ async def async_load_platform(hass, component, platform, discovered=None, This method is a coroutine. """ + assert hass_config, 'You need to pass in the real hass config' + if component in DEPENDENCY_BLACKLIST: raise HomeAssistantError( 'Cannot discover the {} component.'.format(component)) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index b1b68ff92df..c2c39e17f64 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -180,7 +180,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): assert device_tracker.DOMAIN not in self.hass.config.components discovery.load_platform( self.hass, device_tracker.DOMAIN, 'demo', {'test_key': 'test_val'}, - {}) + {'demo': {}}) self.hass.block_till_done() assert device_tracker.DOMAIN in self.hass.config.components assert mock_demo_setup_scanner.called diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 46db2f61fb3..f388c638b0d 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -674,7 +674,7 @@ class TestLightMQTTJSON(unittest.TestCase): async def test_discovery_removal(hass, mqtt_mock, caplog): """Test removal of discovered mqtt_json lights.""" - await async_start(hass, 'homeassistant', {}) + await async_start(hass, 'homeassistant', {'mqtt': {}}) data = ( '{ "name": "Beer",' ' "platform": "mqtt_json",' diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 4d2378ff9fa..ac0a83d4e2d 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -180,7 +180,7 @@ class TestLockMQTT(unittest.TestCase): async def test_discovery_removal_lock(hass, mqtt_mock, caplog): """Test removal of discovered lock.""" - await async_start(hass, 'homeassistant', {}) + await async_start(hass, 'homeassistant', {'mqtt': {}}) data = ( '{ "name": "Beer",' ' "command_topic": "test_topic" }' diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 3be2ddcb86b..a18af7ba684 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -66,7 +66,8 @@ class TestNotifyDemo(unittest.TestCase): """Test discovery of notify demo platform.""" assert notify.DOMAIN not in self.hass.config.components discovery.load_platform( - self.hass, 'notify', 'demo', {'test_key': 'test_val'}, {}) + self.hass, 'notify', 'demo', {'test_key': 'test_val'}, + {'notify': {}}) self.hass.block_till_done() assert notify.DOMAIN in self.hass.config.components assert mock_demo_get_service.called diff --git a/tests/components/test_google.py b/tests/components/test_google.py index b8dc29b5dea..2be58c7e9d4 100644 --- a/tests/components/test_google.py +++ b/tests/components/test_google.py @@ -85,8 +85,8 @@ class TestGoogle(unittest.TestCase): calendar_service = google.GoogleCalendarService( self.hass.config.path(google.TOKEN_FILE)) - self.assertTrue(google.setup_services(self.hass, True, - calendar_service)) + assert google.setup_services( + self.hass, {'google': {}}, True, calendar_service) # self.hass.services.call('google', 'found_calendar', calendar, # blocking=True) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 64f90ee7452..ffafd3ca146 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -79,15 +79,15 @@ class TestHelpersDiscovery: platform_callback) discovery.load_platform(self.hass, 'test_component', 'test_platform', - 'discovery info') + 'discovery info', {'test_component': {}}) self.hass.block_till_done() assert mock_setup_component.called assert mock_setup_component.call_args[0] == \ - (self.hass, 'test_component', None) + (self.hass, 'test_component', {'test_component': {}}) self.hass.block_till_done() discovery.load_platform(self.hass, 'test_component_2', 'test_platform', - 'discovery info') + 'discovery info', {'test_component': {}}) self.hass.block_till_done() assert len(calls) == 1 @@ -202,7 +202,8 @@ class TestHelpersDiscovery: async def test_load_platform_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - await discovery.async_load_platform(None, 'config', 'zwave') + await discovery.async_load_platform(None, 'config', 'zwave', {}, + {'config': {}}) async def test_discover_forbids_config(): diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index c853d0b3447..2bef8c0b53e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -131,7 +131,7 @@ class TestHelpersEntityComponent(unittest.TestCase): component.setup({}) discovery.load_platform(self.hass, DOMAIN, 'platform_test', - {'msg': 'discovery_info'}) + {'msg': 'discovery_info'}, {DOMAIN: {}}) self.hass.block_till_done() From 66d0fb7dbf9ed49ba17a5b2ebebdbe08d205f7af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Oct 2018 19:22:59 +0100 Subject: [PATCH 33/35] Bumped version to 0.81.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index effdec4db39..70d6eb18f1f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 81 -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 a91d89413228496acc2ed27b8dd4c7dde096fbc5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Oct 2018 21:16:05 +0100 Subject: [PATCH 34/35] Update requests to 2.20.0 (#17978) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fa0d675f4b1..193bb42dba0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 -requests==2.19.1 +requests==2.20.0 voluptuous==0.11.5 voluptuous-serialize==2.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5ad0e9aae8f..caaadd4c6f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 -requests==2.19.1 +requests==2.20.0 voluptuous==0.11.5 voluptuous-serialize==2.0.0 diff --git a/setup.py b/setup.py index 90f2e8357fd..5bca2cc43db 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.13,<4', - 'requests==2.19.1', + 'requests==2.20.0', 'voluptuous==0.11.5', 'voluptuous-serialize==2.0.0', ] From 1e03f945b57933af1cf61023c1b6f359e9cc4ccf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Oct 2018 21:25:17 +0100 Subject: [PATCH 35/35] Don't use keyset (#17984) --- homeassistant/components/cloud/__init__.py | 68 +--------------------- tests/components/cloud/test_init.py | 25 ++++---- 2 files changed, 13 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 3bfc5909b0b..ba2d41a9feb 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -4,20 +4,16 @@ Component to integrate the Home Assistant cloud. For more details about this component, please refer to the documentation at https://home-assistant.io/components/cloud/ """ -import asyncio from datetime import datetime, timedelta import json import logging import os -import aiohttp -import async_timeout import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME) from homeassistant.helpers import entityfilter, config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import helpers as ga_h @@ -129,7 +125,6 @@ class Cloud: self._google_actions = google_actions self._gactions_config = None self._prefs = None - self.jwt_keyset = None self.id_token = None self.access_token = None self.refresh_token = None @@ -262,13 +257,6 @@ class Cloud: } self._prefs = prefs - success = await self._fetch_jwt_keyset() - - # Fetching keyset can fail if internet is not up yet. - if not success: - self.hass.helpers.event.async_call_later(5, self.async_start) - return - def load_config(): """Load config.""" # Ensure config dir exists @@ -288,14 +276,6 @@ class Cloud: if info is None: return - # Validate tokens - try: - for token in 'id_token', 'access_token': - self._decode_claims(info[token]) - except ValueError as err: # Raised when token is invalid - _LOGGER.warning("Found invalid token %s: %s", token, err) - return - self.id_token = info['id_token'] self.access_token = info['access_token'] self.refresh_token = info['refresh_token'] @@ -311,49 +291,7 @@ class Cloud: self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled await self._store.async_save(self._prefs) - async def _fetch_jwt_keyset(self): - """Fetch the JWT keyset for the Cognito instance.""" - session = async_get_clientsession(self.hass) - url = ("https://cognito-idp.us-east-1.amazonaws.com/" - "{}/.well-known/jwks.json".format(self.user_pool_id)) - - try: - with async_timeout.timeout(10, loop=self.hass.loop): - req = await session.get(url) - self.jwt_keyset = await req.json() - - return True - - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Error fetching Cognito keyset: %s", err) - return False - - def _decode_claims(self, token): + def _decode_claims(self, token): # pylint: disable=no-self-use """Decode the claims in a token.""" - from jose import jwt, exceptions as jose_exceptions - try: - header = jwt.get_unverified_header(token) - except jose_exceptions.JWTError as err: - raise ValueError(str(err)) from None - kid = header.get('kid') - - if kid is None: - raise ValueError("No kid in header") - - # Locate the key for this kid - key = None - for key_dict in self.jwt_keyset['keys']: - if key_dict['kid'] == kid: - key = key_dict - break - if not key: - raise ValueError( - "Unable to locate kid ({}) in keyset".format(kid)) - - try: - return jwt.decode( - token, key, audience=self.cognito_client_id, options={ - 'verify_exp': False, - }) - except jose_exceptions.JWTError as err: - raise ValueError(str(err)) from None + from jose import jwt + return jwt.get_unverified_claims(token) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 61518f0f0e8..44d56566f75 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -32,8 +32,7 @@ def test_constructor_loads_info_from_constant(): 'google_actions_sync_url': 'test-google_actions_sync_url', 'subscription_info_url': 'test-subscription-info-url' } - }), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', - return_value=mock_coro(True)): + }): result = yield from cloud.async_setup(hass, { 'cloud': {cloud.CONF_MODE: 'beer'} }) @@ -54,17 +53,15 @@ def test_constructor_loads_info_from_config(): """Test non-dev mode loads info from SERVERS constant.""" hass = MagicMock(data={}) - with patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', - return_value=mock_coro(True)): - result = yield from cloud.async_setup(hass, { - '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', - } - }) + result = yield from cloud.async_setup(hass, { + '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 cl = hass.data['cloud'] @@ -89,8 +86,6 @@ async def test_initialize_loads_info(mock_os, hass): cl.iot.connect.return_value = mock_coro() with patch('homeassistant.components.cloud.open', mopen, create=True), \ - patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', - return_value=mock_coro(True)), \ patch('homeassistant.components.cloud.Cloud._decode_claims'): await cl.async_start(None)