From 4fd4e84b724c92dd035ba201ae3f6e84d41a1006 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Dec 2018 09:34:21 +0100 Subject: [PATCH 01/31] Bumped version to 0.84.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b4a94d318f6..ae7460bfea9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -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 1b03a35fa1f0cd3a2da33fd330320e2768317a25 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Dec 2018 07:12:59 +0100 Subject: [PATCH 02/31] Updated frontend to 20181207.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 43a4839bf43..325ae2a1d30 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==20181205.0'] +REQUIREMENTS = ['home-assistant-frontend==20181207.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c56bf8888e5..e782320bf96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181205.0 +home-assistant-frontend==20181207.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c63fb9a1200..85ee3e7c900 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181205.0 +home-assistant-frontend==20181207.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 578fe371c6c4105e82115905bc789ed15e574c92 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Dec 2018 10:38:26 +0100 Subject: [PATCH 03/31] Revert #17745 (#19064) --- homeassistant/components/google_assistant/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d688491fe89..f0294c3bcb2 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -48,7 +48,7 @@ def async_register_http(hass, cfg): entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - expose_by_default or entity.domain in exposed_domains + expose_by_default and entity.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being From 3eb646eb0dfefaa84343fac3f8c614749b712c7b Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 6 Dec 2018 18:05:15 +0200 Subject: [PATCH 04/31] Fix missing colorTemperatureInKelvin from Alexa responses (#19069) * Fix missing colorTemperatureInKelvin from Alexa responses * Update smart_home.py * Add test --- homeassistant/components/alexa/smart_home.py | 14 ++++++++++++++ tests/components/alexa/test_smart_home.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2a61533a2b9..f06b853087f 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -504,6 +504,20 @@ class _AlexaColorTemperatureController(_AlexaInterface): def name(self): return 'Alexa.ColorTemperatureController' + def properties_supported(self): + return [{'name': 'colorTemperatureInKelvin'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'colorTemperatureInKelvin': + raise _UnsupportedProperty(name) + if 'color_temp' in self.entity.attributes: + return color_util.color_temperature_mired_to_kelvin( + self.entity.attributes['color_temp']) + return 0 + class _AlexaPercentageController(_AlexaInterface): """Implements Alexa.PercentageController. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3cfb8068177..ddf66d1c617 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1354,6 +1354,25 @@ async def test_report_colored_light_state(hass): }) +async def test_report_colored_temp_light_state(hass): + """Test ColorTemperatureController reports color temp correctly.""" + hass.states.async_set( + 'light.test_on', 'on', {'friendly_name': "Test light On", + 'color_temp': 240, + 'supported_features': 2}) + hass.states.async_set( + 'light.test_off', 'off', {'friendly_name': "Test light Off", + 'supported_features': 2}) + + properties = await reported_properties(hass, 'light.test_on') + properties.assert_equal('Alexa.ColorTemperatureController', + 'colorTemperatureInKelvin', 4166) + + properties = await reported_properties(hass, 'light.test_off') + properties.assert_equal('Alexa.ColorTemperatureController', + 'colorTemperatureInKelvin', 0) + + async def reported_properties(hass, endpoint): """Use ReportState to get properties and return them. From ff9427d46351e02618de8c75e54ad90dc36b8a73 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 7 Dec 2018 07:09:05 +0100 Subject: [PATCH 05/31] Force refresh Lovelace (#19073) * Force refresh Lovelace * Check config on load * Update __init__.py * Update __init__.py --- homeassistant/components/lovelace/__init__.py | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 0d9b6a6d9fe..f6a8a3fd688 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -48,6 +48,7 @@ WS_TYPE_DELETE_VIEW = 'lovelace/config/view/delete' 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), + vol.Optional('force', default=False): bool, }) SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ @@ -144,12 +145,12 @@ class DuplicateIdError(HomeAssistantError): """Duplicate ID's.""" -def load_config(hass) -> JSON_TYPE: +def load_config(hass, force: bool) -> JSON_TYPE: """Load a YAML file.""" fname = hass.config.path(LOVELACE_CONFIG_FILE) # Check for a cached version of the config - if LOVELACE_DATA in hass.data: + if not force and LOVELACE_DATA in hass.data: config, last_update = hass.data[LOVELACE_DATA] modtime = os.path.getmtime(fname) if config and last_update > modtime: @@ -158,23 +159,29 @@ def load_config(hass) -> JSON_TYPE: config = yaml.load_yaml(fname, False) seen_card_ids = set() seen_view_ids = set() + if 'views' in config and not isinstance(config['views'], list): + raise HomeAssistantError("Views should be a list.") for view in config.get('views', []): - view_id = view.get('id') - if view_id: - view_id = str(view_id) - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in views'.format(view_id)) - seen_view_ids.add(view_id) + if 'id' in view and not isinstance(view['id'], (str, int)): + raise HomeAssistantError( + "Your config contains view(s) with invalid ID(s).") + view_id = str(view.get('id', '')) + if view_id in seen_view_ids: + raise DuplicateIdError( + 'ID `{}` has multiple occurances in views'.format(view_id)) + seen_view_ids.add(view_id) + if 'cards' in view and not isinstance(view['cards'], list): + raise HomeAssistantError("Cards should be a list.") for card in view.get('cards', []): - card_id = card.get('id') - if card_id: - card_id = str(card_id) - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in cards' - .format(card_id)) - seen_card_ids.add(card_id) + if 'id' in card and not isinstance(card['id'], (str, int)): + raise HomeAssistantError( + "Your config contains card(s) with invalid ID(s).") + card_id = str(card.get('id', '')) + if card_id in seen_card_ids: + raise DuplicateIdError( + 'ID `{}` has multiple occurances in cards' + .format(card_id)) + seen_card_ids.add(card_id) hass.data[LOVELACE_DATA] = (config, time.time()) return config @@ -539,7 +546,8 @@ def handle_yaml_errors(func): @handle_yaml_errors async def websocket_lovelace_config(hass, connection, msg): """Send Lovelace UI config over WebSocket configuration.""" - return await hass.async_add_executor_job(load_config, hass) + return await hass.async_add_executor_job(load_config, hass, + msg.get('force', False)) @websocket_api.async_response From da160066c34e728faffd207f2eec9cdaf2b864ae Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 7 Dec 2018 07:06:35 +0100 Subject: [PATCH 06/31] Upgrade aiolifx to 0.6.7 (#19077) --- homeassistant/components/lifx/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 52df3d47ca1..f2713197ed1 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -8,7 +8,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN DOMAIN = 'lifx' -REQUIREMENTS = ['aiolifx==0.6.6'] +REQUIREMENTS = ['aiolifx==0.6.7'] CONF_SERVER = 'server' CONF_BROADCAST = 'broadcast' diff --git a/requirements_all.txt b/requirements_all.txt index e782320bf96..8bcbd449635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ aiohue==1.5.0 aioimaplib==0.7.13 # homeassistant.components.lifx -aiolifx==0.6.6 +aiolifx==0.6.7 # homeassistant.components.light.lifx aiolifx_effects==0.2.1 From 393ada03120db66b74530a4bc78d82eb1abe8b14 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Dec 2018 07:14:19 +0100 Subject: [PATCH 07/31] Bumped version to 0.84.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ae7460bfea9..0f59d771bde 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -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 945b84a7dffee4a3d9e1f9e7ea33973e32b6f76b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 09:53:53 +0100 Subject: [PATCH 08/31] Updated frontend to 20181210.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 325ae2a1d30..0fe7a2dfa6e 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==20181207.0'] +REQUIREMENTS = ['home-assistant-frontend==20181210.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 8bcbd449635..4c86c53608c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181207.0 +home-assistant-frontend==20181210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85ee3e7c900..6f766f9b07c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181207.0 +home-assistant-frontend==20181210.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 048f219a7ff3f2985e671e095f49dc8b3791c5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 7 Dec 2018 11:06:38 +0100 Subject: [PATCH 09/31] Upgrade pyatv to 0.3.12 (#19085) --- homeassistant/components/apple_tv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index ff17b6d5e39..73cabdfbae6 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyatv==0.3.11'] +REQUIREMENTS = ['pyatv==0.3.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4c86c53608c..240bc1f9db1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -861,7 +861,7 @@ pyarlo==0.2.2 pyatmo==1.4 # homeassistant.components.apple_tv -pyatv==0.3.11 +pyatv==0.3.12 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From e6c224fa404de3ceb688c3ffd4ef71141573ec48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 7 Dec 2018 19:33:06 +0100 Subject: [PATCH 10/31] Upgrade Tibber lib (#19098) --- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index fce4312ad68..8462b646a22 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.8.5'] +REQUIREMENTS = ['pyTibber==0.8.6'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 240bc1f9db1..1dd5be162a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.8.5 +pyTibber==0.8.6 # homeassistant.components.switch.dlink pyW215==0.6.0 From 3528d865b75f5226982207da59b4a1d36c6b4d54 Mon Sep 17 00:00:00 2001 From: Nick Horvath Date: Fri, 7 Dec 2018 13:20:05 -0500 Subject: [PATCH 11/31] Bump skybellpy version to fix api issue (#19100) --- homeassistant/components/skybell.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/skybell.py b/homeassistant/components/skybell.py index 3f27c91e7c5..b3c3b63bd84 100644 --- a/homeassistant/components/skybell.py +++ b/homeassistant/components/skybell.py @@ -14,7 +14,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['skybellpy==0.1.2'] +REQUIREMENTS = ['skybellpy==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1dd5be162a2..41246cf6758 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ simplisafe-python==3.1.14 sisyphus-control==2.1 # homeassistant.components.skybell -skybellpy==0.1.2 +skybellpy==0.2.0 # homeassistant.components.notify.slack slacker==0.11.0 From 76c26da4cb6cf4a6534b1a2ee05dfba989b928ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 08:57:17 +0100 Subject: [PATCH 12/31] Lovelace using storage (#19101) * Add MVP * Remove unused code * Fix * Add force back * Fix tests * Storage keyed * Error out when storage doesnt find config * Use old load_yaml * Set config for panel correct * Use instance cache var * Make config option --- homeassistant/components/frontend/__init__.py | 3 +- homeassistant/components/lovelace/__init__.py | 675 ++------------- tests/components/lovelace/test_init.py | 812 ++---------------- 3 files changed, 177 insertions(+), 1313 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0fe7a2dfa6e..5013a451adc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -250,8 +250,7 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', - 'states', 'profile')], + 'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index f6a8a3fd688..68c322b3956 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -7,507 +7,139 @@ at https://www.home-assistant.io/lovelace/ from functools import wraps import logging import os -from typing import Dict, List, Union import time -import uuid import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.ruamel_yaml as yaml +from homeassistant.util.yaml import load_yaml _LOGGER = logging.getLogger(__name__) DOMAIN = 'lovelace' -LOVELACE_DATA = 'lovelace' +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +CONF_MODE = 'mode' +MODE_YAML = 'yaml' +MODE_STORAGE = 'storage' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_MODE, default=MODE_STORAGE): + vol.All(vol.Lower, vol.In([MODE_YAML, MODE_STORAGE])), + }), +}, extra=vol.ALLOW_EXTRA) + 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_MIGRATE_CONFIG = 'lovelace/config/migrate' WS_TYPE_SAVE_CONFIG = 'lovelace/config/save' -WS_TYPE_GET_CARD = 'lovelace/config/card/get' -WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' -WS_TYPE_ADD_CARD = 'lovelace/config/card/add' -WS_TYPE_MOVE_CARD = 'lovelace/config/card/move' -WS_TYPE_DELETE_CARD = 'lovelace/config/card/delete' - -WS_TYPE_GET_VIEW = 'lovelace/config/view/get' -WS_TYPE_UPDATE_VIEW = 'lovelace/config/view/update' -WS_TYPE_ADD_VIEW = 'lovelace/config/view/add' -WS_TYPE_MOVE_VIEW = 'lovelace/config/view/move' -WS_TYPE_DELETE_VIEW = 'lovelace/config/view/delete' - 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), + vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, vol.Optional('force', default=False): bool, }) -SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MIGRATE_CONFIG, -}) - SCHEMA_SAVE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_SAVE_CONFIG, vol.Required('config'): vol.Any(str, dict), - vol.Optional('format', default=FORMAT_JSON): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -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=FORMAT_YAML): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -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=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), -}) - -SCHEMA_MOVE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MOVE_CARD, - vol.Required('card_id'): str, - vol.Optional('new_position'): int, - vol.Optional('new_view_id'): str, -}) - -SCHEMA_DELETE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE_CARD, - vol.Required('card_id'): str, -}) - -SCHEMA_GET_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_VIEW, - vol.Required('view_id'): str, - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_UPDATE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE_VIEW, - vol.Required('view_id'): str, - vol.Required('view_config'): vol.Any(str, dict), - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_ADD_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_ADD_VIEW, - vol.Required('view_config'): vol.Any(str, dict), - vol.Optional('position'): int, - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_MOVE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MOVE_VIEW, - vol.Required('view_id'): str, - vol.Required('new_position'): int, -}) - -SCHEMA_DELETE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE_VIEW, - vol.Required('view_id'): str, }) -class CardNotFoundError(HomeAssistantError): - """Card not found in data.""" - - -class ViewNotFoundError(HomeAssistantError): - """View not found in data.""" - - -class DuplicateIdError(HomeAssistantError): - """Duplicate ID's.""" - - -def load_config(hass, force: bool) -> JSON_TYPE: - """Load a YAML file.""" - fname = hass.config.path(LOVELACE_CONFIG_FILE) - - # Check for a cached version of the config - if not force and LOVELACE_DATA in hass.data: - config, last_update = hass.data[LOVELACE_DATA] - modtime = os.path.getmtime(fname) - if config and last_update > modtime: - return config - - config = yaml.load_yaml(fname, False) - seen_card_ids = set() - seen_view_ids = set() - if 'views' in config and not isinstance(config['views'], list): - raise HomeAssistantError("Views should be a list.") - for view in config.get('views', []): - if 'id' in view and not isinstance(view['id'], (str, int)): - raise HomeAssistantError( - "Your config contains view(s) with invalid ID(s).") - view_id = str(view.get('id', '')) - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in views'.format(view_id)) - seen_view_ids.add(view_id) - if 'cards' in view and not isinstance(view['cards'], list): - raise HomeAssistantError("Cards should be a list.") - for card in view.get('cards', []): - if 'id' in card and not isinstance(card['id'], (str, int)): - raise HomeAssistantError( - "Your config contains card(s) with invalid ID(s).") - card_id = str(card.get('id', '')) - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in cards' - .format(card_id)) - seen_card_ids.add(card_id) - hass.data[LOVELACE_DATA] = (config, time.time()) - return config - - -def migrate_config(fname: str) -> None: - """Add id to views and cards if not present and check duplicates.""" - config = yaml.load_yaml(fname, True) - updated = False - seen_card_ids = set() - seen_view_ids = set() - index = 0 - for view in config.get('views', []): - view_id = str(view.get('id', '')) - if not view_id: - updated = True - view.insert(0, 'id', index, comment="Automatically created id") - else: - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurrences in views'.format( - view_id)) - seen_view_ids.add(view_id) - for card in view.get('cards', []): - card_id = str(card.get('id', '')) - if not card_id: - updated = True - card.insert(0, 'id', uuid.uuid4().hex, - comment="Automatically created id") - else: - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurrences in cards' - .format(card_id)) - seen_card_ids.add(card_id) - index += 1 - if updated: - yaml.save_yaml(fname, config) - - -def save_config(fname: str, config, data_format: str = FORMAT_JSON) -> None: - """Save config to file.""" - if data_format == FORMAT_YAML: - config = yaml.yaml_to_object(config) - yaml.save_yaml(fname, config) - - -def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\ - -> JSON_TYPE: - """Load a specific card config for id.""" - round_trip = data_format == FORMAT_YAML - - config = yaml.load_yaml(fname, round_trip) - - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - if data_format == FORMAT_YAML: - return yaml.object_to_yaml(card) - return card - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def update_card(fname: str, card_id: str, card_config: str, - data_format: str = FORMAT_YAML) -> None: - """Save a specific card config for id.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - if data_format == FORMAT_YAML: - card_config = yaml.yaml_to_object(card_config) - card.clear() - card.update(card_config) - yaml.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) -> None: - """Add a card to a view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - if str(view.get('id', '')) != view_id: - continue - cards = view.get('cards', []) - if not cards and 'cards' in view: - del view['cards'] - if data_format == FORMAT_YAML: - card_config = yaml.yaml_to_object(card_config) - if 'id' not in card_config: - card_config['id'] = uuid.uuid4().hex - if position is None: - cards.append(card_config) - else: - cards.insert(position, card_config) - if 'cards' not in view: - view['cards'] = cards - yaml.save_yaml(fname, config) - return - - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - -def move_card(fname: str, card_id: str, position: int = None) -> None: - """Move a card to a different position.""" - if position is None: - raise HomeAssistantError( - 'Position is required if view is not specified.') - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - cards = view.get('cards') - cards.insert(position, cards.pop(cards.index(card))) - yaml.save_yaml(fname, config) - return - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def move_card_view(fname: str, card_id: str, view_id: str, - position: int = None) -> None: - """Move a card to a different view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - destination = view.get('cards') - for card in view.get('cards'): - if str(card.get('id', '')) != card_id: - continue - origin = view.get('cards') - card_to_move = card - - if 'destination' not in locals(): - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - if 'card_to_move' not in locals(): - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - origin.pop(origin.index(card_to_move)) - - if position is None: - destination.append(card_to_move) - else: - destination.insert(position, card_to_move) - - yaml.save_yaml(fname, config) - - -def delete_card(fname: str, card_id: str) -> None: - """Delete a card from view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - cards = view.get('cards') - cards.pop(cards.index(card)) - yaml.save_yaml(fname, config) - return - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def get_view(fname: str, view_id: str, data_format: str = FORMAT_YAML) -> None: - """Get view without it's cards.""" - round_trip = data_format == FORMAT_YAML - config = yaml.load_yaml(fname, round_trip) - found = None - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - del found['cards'] - if data_format == FORMAT_YAML: - return yaml.object_to_yaml(found) - return found - - -def update_view(fname: str, view_id: str, view_config, data_format: - str = FORMAT_YAML) -> None: - """Update view.""" - config = yaml.load_yaml(fname, True) - found = None - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - if data_format == FORMAT_YAML: - view_config = yaml.yaml_to_object(view_config) - if not view_config.get('cards') and found.get('cards'): - view_config['cards'] = found.get('cards', []) - if not view_config.get('badges') and found.get('badges'): - view_config['badges'] = found.get('badges', []) - found.clear() - found.update(view_config) - yaml.save_yaml(fname, config) - - -def add_view(fname: str, view_config: str, - position: int = None, data_format: str = FORMAT_YAML) -> None: - """Add a view.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - if data_format == FORMAT_YAML: - view_config = yaml.yaml_to_object(view_config) - if 'id' not in view_config: - view_config['id'] = uuid.uuid4().hex - if position is None: - views.append(view_config) - else: - views.insert(position, view_config) - if 'views' not in config: - config['views'] = views - yaml.save_yaml(fname, config) - - -def move_view(fname: str, view_id: str, position: int) -> None: - """Move a view to a different position.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - found = None - for view in views: - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - views.insert(position, views.pop(views.index(found))) - yaml.save_yaml(fname, config) - - -def delete_view(fname: str, view_id: str) -> None: - """Delete a view.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - found = None - for view in views: - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - views.pop(views.index(found)) - yaml.save_yaml(fname, config) +class ConfigNotFound(HomeAssistantError): + """When no config available.""" async def async_setup(hass, config): """Set up the Lovelace commands.""" - # Backwards compat. Added in 0.80. Remove after 0.85 - hass.components.websocket_api.async_register_command( - OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, - SCHEMA_GET_LOVELACE_UI) + # Pass in default to `get` because defaults not set if loaded as dep + mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE) + + await hass.components.frontend.async_register_built_in_panel( + DOMAIN, config={ + 'mode': mode + }) + + if mode == MODE_YAML: + hass.data[DOMAIN] = LovelaceYAML(hass) + else: + hass.data[DOMAIN] = LovelaceStorage(hass) hass.components.websocket_api.async_register_command( 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_SAVE_CONFIG, websocket_lovelace_save_config, SCHEMA_SAVE_CONFIG) - 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_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) - - hass.components.websocket_api.async_register_command( - WS_TYPE_MOVE_CARD, websocket_lovelace_move_card, SCHEMA_MOVE_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE_CARD, websocket_lovelace_delete_card, - SCHEMA_DELETE_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_VIEW, websocket_lovelace_get_view, SCHEMA_GET_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE_VIEW, websocket_lovelace_update_view, - SCHEMA_UPDATE_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_ADD_VIEW, websocket_lovelace_add_view, SCHEMA_ADD_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_MOVE_VIEW, websocket_lovelace_move_view, SCHEMA_MOVE_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE_VIEW, websocket_lovelace_delete_view, - SCHEMA_DELETE_VIEW) - return True +class LovelaceStorage: + """Class to handle Storage based Lovelace config.""" + + def __init__(self, hass): + """Initialize Lovelace config based on storage helper.""" + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data = None + + async def async_load(self, force): + """Load config.""" + if self._data is None: + data = await self._store.async_load() + self._data = data if data else {'config': None} + + config = self._data['config'] + + if config is None: + raise ConfigNotFound + + return config + + async def async_save(self, config): + """Save config.""" + self._data = {'config': config} + await self._store.async_save(config) + + +class LovelaceYAML: + """Class to handle YAML-based Lovelace config.""" + + def __init__(self, hass): + """Initialize the YAML config.""" + self.hass = hass + self._cache = None + + async def async_load(self, force): + """Load config.""" + return await self.hass.async_add_executor_job(self._load_config, force) + + def _load_config(self, force): + """Load the actual config.""" + fname = self.hass.config.path(LOVELACE_CONFIG_FILE) + # Check for a cached version of the config + if not force and self._cache is not None: + config, last_update = self._cache + modtime = os.path.getmtime(fname) + if config and last_update > modtime: + return config + + try: + config = load_yaml(fname) + except FileNotFoundError: + raise ConfigNotFound from None + + self._cache = (config, time.time()) + return config + + async def async_save(self, config): + """Save config.""" + raise HomeAssistantError('Not supported') + + def handle_yaml_errors(func): """Handle error with WebSocket calls.""" @wraps(func) @@ -518,19 +150,8 @@ def handle_yaml_errors(func): 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 yaml.UnsupportedYamlError as err: - error = 'unsupported_error', str(err) - except yaml.WriteError as err: - error = 'write_error', str(err) - except DuplicateIdError as err: - error = 'duplicate_id', str(err) - except CardNotFoundError as err: - error = 'card_not_found', str(err) - except ViewNotFoundError as err: - error = 'view_not_found', str(err) + except ConfigNotFound: + error = 'config_not_found', 'No config found.' except HomeAssistantError as err: error = 'error', str(err) @@ -546,117 +167,11 @@ def handle_yaml_errors(func): @handle_yaml_errors async def websocket_lovelace_config(hass, connection, msg): """Send Lovelace UI config over WebSocket configuration.""" - return await hass.async_add_executor_job(load_config, hass, - msg.get('force', False)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_migrate_config(hass, connection, msg): - """Migrate Lovelace UI configuration.""" - return await hass.async_add_executor_job( - migrate_config, hass.config.path(LOVELACE_CONFIG_FILE)) + return await hass.data[DOMAIN].async_load(msg['force']) @websocket_api.async_response @handle_yaml_errors async def websocket_lovelace_save_config(hass, connection, msg): """Save Lovelace UI configuration.""" - return await hass.async_add_executor_job( - save_config, hass.config.path(LOVELACE_CONFIG_FILE), msg['config'], - msg.get('format', FORMAT_JSON)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_get_card(hass, connection, msg): - """Send Lovelace card config over WebSocket configuration.""" - return await hass.async_add_executor_job( - get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_update_card(hass, connection, msg): - """Receive Lovelace card configuration over WebSocket and save.""" - return 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)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_add_card(hass, connection, msg): - """Add new card to view over WebSocket and save.""" - return 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)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_move_card(hass, connection, msg): - """Move card to different position over WebSocket and save.""" - if 'new_view_id' in msg: - return await hass.async_add_executor_job( - move_card_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['new_view_id'], msg.get('new_position')) - - return await hass.async_add_executor_job( - move_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg.get('new_position')) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_delete_card(hass, connection, msg): - """Delete card from Lovelace over WebSocket and save.""" - return await hass.async_add_executor_job( - delete_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id']) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_get_view(hass, connection, msg): - """Send Lovelace view config over WebSocket config.""" - return await hass.async_add_executor_job( - get_view, hass.config.path(LOVELACE_CONFIG_FILE), msg['view_id'], - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_update_view(hass, connection, msg): - """Receive Lovelace card config over WebSocket and save.""" - return await hass.async_add_executor_job( - update_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_id'], msg['view_config'], msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_add_view(hass, connection, msg): - """Add new view over WebSocket and save.""" - return await hass.async_add_executor_job( - add_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_config'], msg.get('position'), - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_move_view(hass, connection, msg): - """Move view to different position over WebSocket and save.""" - return await hass.async_add_executor_job( - move_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_id'], msg['new_position']) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_delete_view(hass, connection, msg): - """Delete card from Lovelace over WebSocket and save.""" - return await hass.async_add_executor_job( - delete_view, hass.config.path(LOVELACE_CONFIG_FILE), msg['view_id']) + await hass.data[DOMAIN].async_save(msg['config']) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index e296d14c6f8..ea856b464c3 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,748 +1,98 @@ """Test the Lovelace initialization.""" from unittest.mock import patch -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 migrate_config -from homeassistant.util.ruamel_yaml import UnsupportedYamlError - -TEST_YAML_A = """\ -title: My Awesome Home -# Include external resources -resources: - - url: /local/my-custom-card.js - type: js - - url: /local/my-webfont.css - type: css - -# Exclude entities from "Unused entities" view -excluded_entities: - - weblink.router -views: - # View tab title. - - title: Example - # Optional unique id for direct access /lovelace/${id} - id: example - # Optional background (overwrites the global background). - background: radial-gradient(crimson, skyblue) - # Each view can have a different theme applied. - theme: dark-mode - # The cards to show on this view. - cards: - # The filter card will filter entities for their state - - type: entity-filter - entities: - - device_tracker.paulus - - device_tracker.anne_there - state_filter: - - 'home' - card: - type: glance - title: People that are home - - # The picture entity card will represent an entity with a picture - - type: picture-entity - image: https://www.home-assistant.io/images/default-social.png - entity: light.bed_light - - # Specify a tab icon if you want the view tab to be an icon. - - icon: mdi:home-assistant - # Title of the view. Will be used as the tooltip for tab icon - title: Second view - cards: - - id: test - type: entities - title: Test card - # Entities card will take a list of entities and show their state. - - type: entities - # Title of the entities card - title: Example - # The entities here will be shown in the same order as specified. - # Each entry is an entity ID or a map with extra options. - entities: - - light.kitchen - - switch.ac - - entity: light.living_room - # Override the name to use - name: LR Lights - - # The markdown card will render markdown text. - - type: markdown - title: Lovelace - content: > - Welcome to your **Lovelace UI**. -""" - -TEST_YAML_B = """\ -title: Home -views: - - title: Dashboard - id: dashboard - icon: mdi:home - cards: - - id: testid - type: vertical-stack - cards: - - type: picture-entity - entity: group.sample - name: Sample - image: /local/images/sample.jpg - tap_action: toggle -""" - -# Test data that can not be loaded as YAML -TEST_BAD_YAML = """\ -title: Home -views: - - title: Dashboard - icon: mdi:home - cards: - - id: testid - type: vertical-stack -""" - -# Test unsupported YAML -TEST_UNSUP_YAML = """\ -title: Home -views: - - title: Dashboard - icon: mdi:home - cards: !include cards.yaml -""" +from homeassistant.components import frontend, lovelace -def test_add_id(): - """Test if id is added.""" - yaml = YAML(typ='rt') +async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): + """Test we load lovelace config from storage.""" + assert await async_setup_component(hass, 'lovelace', {}) + assert hass.data[frontend.DATA_PANELS]['lovelace'].config == { + 'mode': 'storage' + } - fname = "dummy.yaml" - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - migrate_config(fname) - - result = save_yaml_mock.call_args_list[0][0][1] - assert 'id' in result['views'][0]['cards'][0] - assert 'id' in result['views'][1] - - -def test_id_not_changed(): - """Test if id is not changed if already exists.""" - yaml = YAML(typ='rt') - - fname = "dummy.yaml" - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_B)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - migrate_config(fname) - assert save_yaml_mock.call_count == 0 - - -async def test_deprecated_lovelace_ui(hass, hass_ws_client): - """Test lovelace_ui command.""" - await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_config', - return_value={'hello': 'world'}): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() + # Fetch data + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert not response['success'] + assert response['error']['code'] == 'config_not_found' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'hello': 'world'} + # Store new config + await client.send_json({ + 'id': 6, + 'type': 'lovelace/config/save', + 'config': { + 'yo': 'hello' + } + }) + response = await client.receive_json() + assert response['success'] + assert hass_storage[lovelace.STORAGE_KEY]['data'] == { + 'yo': 'hello' + } + + # Load new config + await client.send_json({ + 'id': 7, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert response['success'] + + assert response['result'] == { + 'yo': 'hello' + } -async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') +async def test_lovelace_from_yaml(hass, hass_ws_client): + """Test we load lovelace config from yaml.""" + assert await async_setup_component(hass, 'lovelace', { + 'lovelace': { + 'mode': 'YAML' + } + }) + assert hass.data[frontend.DATA_PANELS]['lovelace'].config == { + 'mode': 'yaml' + } + client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_config', - side_effect=FileNotFoundError): + # Fetch data + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert not response['success'] + + assert response['error']['code'] == 'config_not_found' + + # Store new config not allowed + await client.send_json({ + 'id': 6, + 'type': 'lovelace/config/save', + 'config': { + 'yo': 'hello' + } + }) + response = await client.receive_json() + assert not response['success'] + + # Patch data + with patch('homeassistant.components.lovelace.load_yaml', return_value={ + 'hello': 'yo' + }): await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', + 'id': 7, + 'type': 'lovelace/config' }) - msg = await client.receive_json() + response = await client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'file_not_found' - - -async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'frontend/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'] == 'error' - - -async def test_lovelace_ui(hass, hass_ws_client): - """Test lovelace_ui command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - return_value={'hello': 'world'}): - 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'] - assert msg['result'] == {'hello': 'world'} - - -async def test_lovelace_ui_not_found(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=FileNotFoundError): - 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'] == 'file_not_found' - - -async def test_lovelace_ui_load_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=HomeAssistantError): - 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'] == '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.util.ruamel_yaml.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\ntitle: Test card\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.util.ruamel_yaml.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.util.ruamel_yaml.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'] == 'error' - - -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') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - '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_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') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - '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_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') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.yaml_to_object', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - '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'] == '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.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.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.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.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'] - - -async def test_lovelace_move_card_position(hass, hass_ws_client): - """Test move_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_position': 2, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 1, 'cards', 2, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_card_view(hass, hass_ws_client): - """Test move_card to view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_view_id': 'example', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 2, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_card_view_position(hass, hass_ws_client): - """Test move_card to view with position command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_view_id': 'example', - 'new_position': 1, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 1, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_delete_card(hass, hass_ws_client): - """Test delete_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/delete', - 'card_id': 'test', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - cards = result.mlget(['views', 1, 'cards'], list_ok=True) - assert len(cards) == 2 - assert cards[0]['title'] == 'Example' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_get_view(hass, hass_ws_client): - """Test get_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/get', - 'view_id': 'example', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert "".join(msg['result'].split()) == "".join('title: Example\n # \ - Optional unique id for direct\ - access /lovelace/${id}\nid: example\n # Optional\ - background (overwrites the global background).\n\ - background: radial-gradient(crimson, skyblue)\n\ - # Each view can have a different theme applied.\n\ - theme: dark-mode\n'.split()) - - -async def test_lovelace_get_view_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.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/get', - 'view_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'] == 'view_not_found' - - -async def test_lovelace_update_view(hass, hass_ws_client): - """Test update_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - origyaml = yaml.load(TEST_YAML_A) - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=origyaml), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/update', - 'view_id': 'example', - 'view_config': 'id: example2\ntitle: New title\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - orig_view = origyaml.mlget(['views', 0], list_ok=True) - new_view = result.mlget(['views', 0], list_ok=True) - assert new_view['title'] == 'New title' - assert new_view['cards'] == orig_view['cards'] - assert 'theme' not in new_view - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_add_view(hass, hass_ws_client): - """Test add_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/add', - 'view_config': 'id: test\ntitle: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 2, 'title'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_add_view_position(hass, hass_ws_client): - """Test add_view command with position.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/add', - 'position': 0, - 'view_config': 'id: test\ntitle: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'title'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_view_position(hass, hass_ws_client): - """Test move_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/move', - 'view_id': 'example', - 'new_position': 1, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 1, 'title'], - list_ok=True) == 'Example' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_delete_view(hass, hass_ws_client): - """Test delete_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/delete', - 'view_id': 'example', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - views = result.get('views', []) - assert len(views) == 1 - assert views[0]['title'] == 'Second view' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] + assert response['success'] + assert response['result'] == {'hello': 'yo'} From a5072f0fe4b3b0ee5efd366a293b84e343b1362b Mon Sep 17 00:00:00 2001 From: arigilder <43716164+arigilder@users.noreply.github.com> Date: Sat, 8 Dec 2018 23:27:01 -0500 Subject: [PATCH 13/31] Remove marking device tracker stale if state is stale (#19133) --- homeassistant/components/device_tracker/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 16d9022c98f..202883713c7 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -383,7 +383,6 @@ class DeviceTracker: for device in self.devices.values(): if (device.track and device.last_update_home) and \ device.stale(now): - device.mark_stale() self.hass.async_create_task(device.async_update_ha_state(True)) async def async_setup_tracked_device(self): From 18bc772cbbb74569883c50dca6d6f7460b7a4805 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 09:55:19 +0100 Subject: [PATCH 14/31] Bumped version to 0.84.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0f59d771bde..52cb452c126 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -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 55aaa894c335f2b764a885b9115a30f55d1c69c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:50:09 +0100 Subject: [PATCH 15/31] Updated frontend to 20181210.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 5013a451adc..5e9f96a4073 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==20181210.0'] +REQUIREMENTS = ['home-assistant-frontend==20181210.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 41246cf6758..366f9425b1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.0 +home-assistant-frontend==20181210.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f766f9b07c..0509f33ac87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.0 +home-assistant-frontend==20181210.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From fa9fe4067a08b911bc3c581419ae498c6e12b535 Mon Sep 17 00:00:00 2001 From: Eric Nagley Date: Mon, 10 Dec 2018 06:31:52 -0500 Subject: [PATCH 16/31] Google assistant fix target temp for *F values. (#19083) * home-assistant/home-assistant#18524 : Add rounding to *F temps * home-assistant/home-assistant#18524 : Linting * simplify round behavior * fix trailing whitespace (thanks github editor) --- homeassistant/components/google_assistant/trait.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f2cb819fcc9..e0776d4c636 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -588,8 +588,11 @@ class TemperatureSettingTrait(_Trait): max_temp = self.state.attributes[climate.ATTR_MAX_TEMP] if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - temp = temp_util.convert(params['thermostatTemperatureSetpoint'], - TEMP_CELSIUS, unit) + temp = temp_util.convert( + params['thermostatTemperatureSetpoint'], TEMP_CELSIUS, + unit) + if unit == TEMP_FAHRENHEIT: + temp = round(temp) if temp < min_temp or temp > max_temp: raise SmartHomeError( @@ -607,6 +610,8 @@ class TemperatureSettingTrait(_Trait): temp_high = temp_util.convert( params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS, unit) + if unit == TEMP_FAHRENHEIT: + temp_high = round(temp_high) if temp_high < min_temp or temp_high > max_temp: raise SmartHomeError( @@ -615,7 +620,10 @@ class TemperatureSettingTrait(_Trait): "{} and {}".format(min_temp, max_temp)) temp_low = temp_util.convert( - params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, unit) + params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, + unit) + if unit == TEMP_FAHRENHEIT: + temp_low = round(temp_low) if temp_low < min_temp or temp_low > max_temp: raise SmartHomeError( From d7459c73e0fb4d42ae4fee6f229c7796e141c85b Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 01:15:04 -0500 Subject: [PATCH 17/31] home-assistant/home-assistant#18645: Fix climate mode mapping. --- homeassistant/components/google_assistant/trait.py | 5 ++++- tests/components/google_assistant/__init__.py | 2 +- tests/components/google_assistant/test_google_assistant.py | 4 ++-- tests/components/google_assistant/test_trait.py | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index e0776d4c636..3a46f529c35 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -517,7 +517,10 @@ class TemperatureSettingTrait(_Trait): climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', climate.STATE_OFF: 'off', - climate.STATE_AUTO: 'heatcool', + climate.STATE_AUTO: 'auto', + climate.STATE_FAN_ONLY: 'fan-only', + climate.STATE_DRY: 'dry', + climate.STATE_ECO: 'eco' } google_to_hass = {value: key for key, value in hass_to_google.items()} diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 03cc327a5c5..949960598d6 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -223,7 +223,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,heatcool,off', + 'availableThermostatModes': 'heat,cool,auto,off', 'thermostatTemperatureUnit': 'C', }, }, { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 89e9090da98..0da2781a01f 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -214,7 +214,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureSetpointLow': 21 } assert devices['climate.hvac'] == { @@ -271,7 +271,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureSetpointLow': -6.1, } assert devices['climate.hvac'] == { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e9169c9bbbe..cb709ed084c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -679,11 +679,11 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_MAX_TEMP: 80 }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,heatcool', + 'availableThermostatModes': 'off,cool,heat,auto', 'thermostatTemperatureUnit': 'F', } assert trt.query_attributes() == { - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureAmbient': 21.1, 'thermostatHumidityAmbient': 25, 'thermostatTemperatureSetpointLow': 18.3, @@ -709,7 +709,7 @@ async def test_temperature_setting_climate_range(hass): calls = async_mock_service( hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, { - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', }) assert len(calls) == 1 assert calls[0].data == { From d8f6331318f29cd2e6dd5165fa028fe9d6944a85 Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 01:50:48 -0500 Subject: [PATCH 18/31] home-assistant/home-assistant#18645: Remove un-used constants. --- homeassistant/components/google_assistant/const.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index aca960f9c0a..bfeb0fcadf5 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -19,8 +19,6 @@ DEFAULT_EXPOSED_DOMAINS = [ 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', ] DEFAULT_ALLOW_UNLOCK = False -CLIMATE_MODE_HEATCOOL = 'heatcool' -CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} PREFIX_TYPES = 'action.devices.types.' TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' From 0454a5fa3fd1a13e6cc4b9832947a28048d69175 Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 10:00:56 -0500 Subject: [PATCH 19/31] home-assistant/home-assistant#18645: revert heat-cool -> auto change --- homeassistant/components/google_assistant/trait.py | 2 +- tests/components/google_assistant/__init__.py | 2 +- tests/components/google_assistant/test_google_assistant.py | 4 ++-- tests/components/google_assistant/test_trait.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3a46f529c35..7153115e3ef 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -517,7 +517,7 @@ class TemperatureSettingTrait(_Trait): climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', climate.STATE_OFF: 'off', - climate.STATE_AUTO: 'auto', + climate.STATE_AUTO: 'heatcool', climate.STATE_FAN_ONLY: 'fan-only', climate.STATE_DRY: 'dry', climate.STATE_ECO: 'eco' diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 949960598d6..03cc327a5c5 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -223,7 +223,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,auto,off', + 'availableThermostatModes': 'heat,cool,heatcool,off', 'thermostatTemperatureUnit': 'C', }, }, { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0da2781a01f..89e9090da98 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -214,7 +214,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureSetpointLow': 21 } assert devices['climate.hvac'] == { @@ -271,7 +271,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureSetpointLow': -6.1, } assert devices['climate.hvac'] == { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index cb709ed084c..e9169c9bbbe 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -679,11 +679,11 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_MAX_TEMP: 80 }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,auto', + 'availableThermostatModes': 'off,cool,heat,heatcool', 'thermostatTemperatureUnit': 'F', } assert trt.query_attributes() == { - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureAmbient': 21.1, 'thermostatHumidityAmbient': 25, 'thermostatTemperatureSetpointLow': 18.3, @@ -709,7 +709,7 @@ async def test_temperature_setting_climate_range(hass): calls = async_mock_service( hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, { - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', }) assert len(calls) == 1 assert calls[0].data == { From cb874fefbbf9a266c9189aa2fba667dc6c654f14 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:24:56 +0100 Subject: [PATCH 20/31] Drop OwnTracks bad packets (#19161) --- .../components/owntracks/__init__.py | 22 ++++++++++----- tests/components/owntracks/test_init.py | 27 ++++++++++++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 0bb7a2390b7..7dc88be9764 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -118,9 +118,18 @@ async def async_connect_mqtt(hass, component): async def handle_webhook(hass, webhook_id, request): - """Handle webhook callback.""" + """Handle webhook callback. + + iOS sets the "topic" as part of the payload. + Android does not set a topic but adds headers to the request. + """ context = hass.data[DOMAIN]['context'] - message = await request.json() + + try: + message = await request.json() + except ValueError: + _LOGGER.warning('Received invalid JSON from OwnTracks') + return json_response([]) # Android doesn't populate topic if 'topic' not in message: @@ -129,11 +138,10 @@ async def handle_webhook(hass, webhook_id, request): device = headers.get('X-Limit-D', user) if user is None: - _LOGGER.warning('Set a username in Connection -> Identification') - return json_response( - {'error': 'You need to supply username.'}, - status=400 - ) + _LOGGER.warning('No topic or user found in message. If on Android,' + ' set a username in Connection -> Identification') + # Keep it as a 200 response so the incorrect packet is discarded + return json_response([]) topic_base = re.sub('/#$', '', context.mqtt_topic) message['topic'] = '{}/{}/{}'.format(topic_base, user, device) diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index ba362da905a..3d2d8d03e7c 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -110,7 +110,7 @@ def test_handle_value_error(mock_client): @asyncio.coroutine -def test_returns_error_missing_username(mock_client): +def test_returns_error_missing_username(mock_client, caplog): """Test that an error is returned when username is missing.""" resp = yield from mock_client.post( '/api/webhook/owntracks_test', @@ -120,10 +120,29 @@ def test_returns_error_missing_username(mock_client): } ) - assert resp.status == 400 - + # Needs to be 200 or OwnTracks keeps retrying bad packet. + assert resp.status == 200 json = yield from resp.json() - assert json == {'error': 'You need to supply username.'} + assert json == [] + assert 'No topic or user found' in caplog.text + + +@asyncio.coroutine +def test_returns_error_incorrect_json(mock_client, caplog): + """Test that an error is returned when username is missing.""" + resp = yield from mock_client.post( + '/api/webhook/owntracks_test', + data='not json', + headers={ + 'X-Limit-d': 'Pixel', + } + ) + + # Needs to be 200 or OwnTracks keeps retrying bad packet. + assert resp.status == 200 + json = yield from resp.json() + assert json == [] + assert 'invalid JSON' in caplog.text @asyncio.coroutine From e3b10085111896ed19fa28d79a5dac4151c46e22 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:25:08 +0100 Subject: [PATCH 21/31] Fix lovelace save (#19162) --- homeassistant/components/lovelace/__init__.py | 4 ++-- tests/components/lovelace/test_init.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 68c322b3956..e6f122bce19 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -101,8 +101,8 @@ class LovelaceStorage: async def async_save(self, config): """Save config.""" - self._data = {'config': config} - await self._store.async_save(config) + self._data['config'] = config + await self._store.async_save(self._data) class LovelaceYAML: diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index ea856b464c3..15548b28cfb 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -34,7 +34,7 @@ async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): response = await client.receive_json() assert response['success'] assert hass_storage[lovelace.STORAGE_KEY]['data'] == { - 'yo': 'hello' + 'config': {'yo': 'hello'} } # Load new config From 2da5a022856b8782117d78d104d1ace54bcc4481 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:58:51 +0100 Subject: [PATCH 22/31] Add raw service data to event (#19163) --- homeassistant/core.py | 6 ++++-- tests/test_core.py | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2a40d604ee0..37d1134ef29 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1098,9 +1098,11 @@ class ServiceRegistry: raise ServiceNotFound(domain, service) from None if handler.schema: - service_data = handler.schema(service_data) + processed_data = handler.schema(service_data) + else: + processed_data = service_data - service_call = ServiceCall(domain, service, service_data, context) + service_call = ServiceCall(domain, service, processed_data, context) self._hass.bus.async_fire(EVENT_CALL_SERVICE, { ATTR_DOMAIN: domain.lower(), diff --git a/tests/test_core.py b/tests/test_core.py index 724233cbf98..5ee9f5cdb05 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,6 +8,7 @@ from unittest.mock import patch, MagicMock from datetime import datetime, timedelta from tempfile import TemporaryDirectory +import voluptuous as vol import pytz import pytest @@ -21,7 +22,7 @@ from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED) + EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant, async_mock_service @@ -1000,3 +1001,27 @@ async def test_service_executed_with_subservices(hass): assert len(calls) == 4 assert [call.service for call in calls] == [ 'outer', 'inner', 'inner', 'outer'] + + +async def test_service_call_event_contains_original_data(hass): + """Test that service call event contains original data.""" + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, callback) + + calls = async_mock_service(hass, 'test', 'service', vol.Schema({ + 'number': vol.Coerce(int) + })) + + await hass.services.async_call('test', 'service', { + 'number': '23' + }, blocking=True) + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data['service_data']['number'] == '23' + assert len(calls) == 1 + assert calls[0].data['number'] == 23 From e94eb686a65c3e84aeada381a009051389929a53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 13:00:41 +0100 Subject: [PATCH 23/31] Bumped version to 0.84.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 52cb452c126..4684ca347ed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 029d006bebb85f9eaf3a69b1c10410a3cbf09671 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Dec 2018 10:29:55 +0100 Subject: [PATCH 24/31] Updated frontend to 20181211.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 5e9f96a4073..8caca591305 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==20181210.1'] +REQUIREMENTS = ['home-assistant-frontend==20181211.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 366f9425b1e..ee749157a7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.1 +home-assistant-frontend==20181211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0509f33ac87..e3ad8015a4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.1 +home-assistant-frontend==20181211.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From aa45ff83bd6882968ce5726223a9554246a4f38b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Dec 2018 06:50:54 +0100 Subject: [PATCH 25/31] Fix cloud defaults (#19172) --- homeassistant/components/cloud/__init__.py | 3 +-- homeassistant/components/cloud/prefs.py | 8 +++----- tests/components/cloud/test_cloudhooks.py | 2 +- tests/components/cloud/test_iot.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 329f83768ce..fd5b413043e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -252,8 +252,7 @@ class Cloud: return json.loads(file.read()) info = await self.hass.async_add_job(load_config) - - await self.prefs.async_initialize(bool(info)) + await self.prefs.async_initialize() if info is None: return diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index c4aa43c91d2..32362df2fa9 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -16,19 +16,17 @@ class CloudPreferences: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._prefs = None - async def async_initialize(self, logged_in): + async def async_initialize(self): """Finish initializing the preferences.""" prefs = await self._store.async_load() if prefs is None: - # Backwards compat: we enable alexa/google if already logged in prefs = { - PREF_ENABLE_ALEXA: logged_in, - PREF_ENABLE_GOOGLE: logged_in, + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, PREF_GOOGLE_ALLOW_UNLOCK: False, PREF_CLOUDHOOKS: {} } - await self._store.async_save(prefs) self._prefs = prefs diff --git a/tests/components/cloud/test_cloudhooks.py b/tests/components/cloud/test_cloudhooks.py index b65046331a7..9306a6c6ef3 100644 --- a/tests/components/cloud/test_cloudhooks.py +++ b/tests/components/cloud/test_cloudhooks.py @@ -17,7 +17,7 @@ def mock_cloudhooks(hass): cloud.iot = Mock(async_send_message=Mock(return_value=mock_coro())) cloud.cloudhook_create_url = 'https://webhook-create.url' cloud.prefs = prefs.CloudPreferences(hass) - hass.loop.run_until_complete(cloud.prefs.async_initialize(True)) + hass.loop.run_until_complete(cloud.prefs.async_initialize()) return cloudhooks.Cloudhooks(cloud) diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index b11de7da4e4..2133a803aef 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -411,7 +411,7 @@ async def test_refresh_token_expired(hass): async def test_webhook_msg(hass): """Test webhook msg.""" cloud = Cloud(hass, MODE_DEV, None, None) - await cloud.prefs.async_initialize(True) + await cloud.prefs.async_initialize() await cloud.prefs.async_update(cloudhooks={ 'hello': { 'webhook_id': 'mock-webhook-id', From e98476e02684234f1ea3bd38f5e9af6cb27e823b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Dec 2018 10:33:58 +0100 Subject: [PATCH 26/31] Bumped version to 0.84.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4684ca347ed..2d03be50cf7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a0bc96c20db0ebcea83ef7a6047a98e091fe230b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 11:44:50 +0100 Subject: [PATCH 27/31] Revert PR #18602 (#19188) --- homeassistant/components/camera/mjpeg.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 5c6d7e18075..2819b0e6ec4 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -59,15 +59,21 @@ async def async_setup_platform(hass, config, async_add_entities, def extract_image_from_mjpeg(stream): """Take in a MJPEG stream object, return the jpg from it.""" - data = bytes() - data_start = b"\xff\xd8" - data_end = b"\xff\xd9" - for chunk in stream: - end_idx = chunk.find(data_end) - if end_idx != -1: - return data[data.find(data_start):] + chunk[:end_idx + 2] + data = b'' + for chunk in stream: data += chunk + jpg_end = data.find(b'\xff\xd9') + + if jpg_end == -1: + continue + + jpg_start = data.find(b'\xff\xd8') + + if jpg_start == -1: + continue + + return data[jpg_start:jpg_end + 2] class MjpegCamera(Camera): From 404fbe388c1daa12aefca139b75bc7d854ff8090 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 11:45:42 +0100 Subject: [PATCH 28/31] Bumped version to 0.84.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d03be50cf7..cd71c6d994c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From f65e57bf7bcf2d997f9895142f2e5ebeac5100d4 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 12 Dec 2018 09:54:25 -0500 Subject: [PATCH 29/31] Add automation and script events to logbook event types (#19219) --- homeassistant/components/logbook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 78da5733a06..79ee728ddd7 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -60,7 +60,8 @@ CONFIG_SCHEMA = vol.Schema({ ALL_EVENT_TYPES = [ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED + EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED, + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED ] LOG_MESSAGE_SCHEMA = vol.Schema({ From ced96775fe71bb6bc42c6a644ffae47794cc3802 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 17:17:27 +0100 Subject: [PATCH 30/31] Fix owntracks topic in encrypted ios (#19220) * Fix owntracks topic * Warn if per-topic secret and using HTTP --- homeassistant/components/device_tracker/owntracks.py | 9 +++++++-- homeassistant/components/owntracks/__init__.py | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index ae2b9d6146b..e85ebbe6fe1 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -316,14 +316,19 @@ async def async_handle_waypoints_message(hass, context, message): @HANDLERS.register('encrypted') async def async_handle_encrypted_message(hass, context, message): """Handle an encrypted message.""" - plaintext_payload = _decrypt_payload(context.secret, message['topic'], + if 'topic' not in message and isinstance(context.secret, dict): + _LOGGER.error("You cannot set per topic secrets when using HTTP") + return + + plaintext_payload = _decrypt_payload(context.secret, message.get('topic'), message['data']) if plaintext_payload is None: return decrypted = json.loads(plaintext_payload) - decrypted['topic'] = message['topic'] + if 'topic' in message and 'topic' not in decrypted: + decrypted['topic'] = message['topic'] await async_handle_message(hass, context, decrypted) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 7dc88be9764..5e6a99741e8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -137,15 +137,16 @@ async def handle_webhook(hass, webhook_id, request): user = headers.get('X-Limit-U') device = headers.get('X-Limit-D', user) - if user is None: + if user: + topic_base = re.sub('/#$', '', context.mqtt_topic) + message['topic'] = '{}/{}/{}'.format(topic_base, user, device) + + elif message['_type'] != 'encrypted': _LOGGER.warning('No topic or user found in message. If on Android,' ' set a username in Connection -> Identification') # Keep it as a 200 response so the incorrect packet is discarded return json_response([]) - topic_base = re.sub('/#$', '', context.mqtt_topic) - message['topic'] = '{}/{}/{}'.format(topic_base, user, device) - hass.helpers.dispatcher.async_dispatcher_send( DOMAIN, hass, context, message) return json_response([]) From e2bf3ac0956ed2ce569e49a0c774933b74e92f08 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 17:18:47 +0100 Subject: [PATCH 31/31] Bumped version to 0.84.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cd71c6d994c..21494691a46 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -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)