From b5bf5f9bd0c73d782bca54b11f41c6325b0fee65 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 18 Oct 2018 21:35:25 +0200 Subject: [PATCH 1/5] Add ws get, set card --- homeassistant/components/lovelace/__init__.py | 150 +++++++++++++++++- 1 file changed, 144 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e3f4522580b7e..1ee3740411a28 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -14,21 +14,39 @@ DOMAIN = 'lovelace' REQUIREMENTS = ['ruamel.yaml==0.15.72'] +LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' +JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +WS_TYPE_GET_CARD = 'lovelace/config/card/get' +WS_TYPE_SET_CARD = 'lovelace/config/card/set' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), }) -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_CARD, + vol.Required('card_id'): str, +}) + +SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_SET_CARD, + vol.Required('card_id'): str, + vol.Required('card_config'): str, +}) class WriteError(HomeAssistantError): """Error writing the data.""" +class CardNotFoundError(HomeAssistantError): + """Card not found in data.""" + + def save_yaml(fname: str, data: JSON_TYPE): """Save a YAML file.""" from ruamel.yaml import YAML @@ -45,7 +63,7 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error(str(exc)) raise HomeAssistantError(exc) except OSError as exc: - _LOGGER.exception('Saving YAML file failed: %s', fname) + _LOGGER.exception('Saving YAML file %s failed: %s', fname, exc) raise WriteError(exc) finally: if os.path.exists(tmp_fname): @@ -62,13 +80,14 @@ def load_yaml(fname: str) -> JSON_TYPE: from ruamel.yaml import YAML from ruamel.yaml.error import YAMLError yaml = YAML(typ='rt') + try: with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict return yaml.load(conf_file) or OrderedDict() except YAMLError as exc: - _LOGGER.error("YAML error: %s", exc) + _LOGGER.error("YAML error in %s: %s", fname, exc) raise HomeAssistantError(exc) except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) @@ -76,21 +95,81 @@ def load_yaml(fname: str) -> JSON_TYPE: def load_config(fname: str) -> JSON_TYPE: - """Load a YAML file and adds id to card if not present.""" + """Load a YAML file and adds id to views and cards if not present.""" config = load_yaml(fname) - # Check if all cards have an ID or else add one + # Check if all views and cards have an id or else add one updated = False + index = 0 for view in config.get('views', []): + if 'id' not in view: + updated = True + view['id'] = index + view.move_to_end('id', last=False) for card in view.get('cards', []): if 'id' not in card: updated = True card['id'] = uuid.uuid4().hex card.move_to_end('id', last=False) + index += 1 if updated: save_yaml(fname, config) return config +def object_to_yaml(data: JSON_TYPE) -> str: + """create yaml string from object.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + from ruamel.yaml.compat import StringIO + yaml = YAML(typ='rt') + yaml.indent(sequence=4, offset=2) + stream = StringIO() + try: + yaml.dump(data, stream) + return stream.getvalue() + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def yaml_to_object(data: str) -> JSON_TYPE: + """create object from yaml string.""" + from ruamel.yaml import YAML + from ruamel.yaml.error import YAMLError + yaml = YAML(typ='rt') + yaml.preserve_quotes = True + try: + return yaml.load(data) + except YAMLError as exc: + _LOGGER.error("YAML error: %s", exc) + raise HomeAssistantError(exc) + + +def get_card(fname: str, card_id: str) -> JSON_TYPE: + """Load a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + return object_to_yaml(card) + + raise CardNotFoundError("Card with ID: {} was not found in {}.".format(card_id, fname)) + + +def set_card(fname: str, card_id: str, card_config: str) -> bool: + """Save a specific card config for id.""" + config = load_yaml(fname) + for view in config.get('views', []): + for card in view.get('cards', []): + if card.get('id') == card_id: + card.update(yaml_to_object(card_config)) + save_yaml(fname, config) + # Do we want to return config on save? + return True + + raise CardNotFoundError("Card with ID: {} was not found in {}.".format(card_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -102,6 +181,14 @@ async def async_setup(hass, config): WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_CARD, websocket_lovelace_get_card, + SCHEMA_GET_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_SET_CARD, websocket_lovelace_set_card, + SCHEMA_SET_CARD) + return True @@ -111,7 +198,7 @@ async def websocket_lovelace_config(hass, connection, msg): error = None try: config = await hass.async_add_executor_job( - load_config, hass.config.path('ui-lovelace.yaml')) + load_config, hass.config.path(LOVELACE_CONFIG_FILE)) message = websocket_api.result_message( msg['id'], config ) @@ -125,3 +212,54 @@ async def websocket_lovelace_config(hass, connection, msg): message = websocket_api.error_message(msg['id'], *error) connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_get_card(hass, connection, msg): + """Send lovelace card config over websocket config.""" + error = None + try: + card = await hass.async_add_executor_job( + get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id']) + message = websocket_api.result_message( + msg['id'], card + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_set_card(hass, connection, msg): + """Receive lovelace card config over websocket and save.""" + error = None + try: + result = await hass.async_add_executor_job( + set_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config']) + 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 CardNotFoundError: + error = ('card_not_found', + 'Could not find card in ui-lovelace.yaml.') + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) From 677755fa05df8794d3400ed449d77e0840f7a850 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 18 Oct 2018 23:01:54 +0200 Subject: [PATCH 2/5] lint+fix test --- homeassistant/components/lovelace/__init__.py | 18 +++++++----- tests/components/lovelace/test_init.py | 29 ++++++++++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 1ee3740411a28..b2d6bede24eff 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -2,9 +2,10 @@ import logging import uuid import os -from os import O_WRONLY, O_CREAT, O_TRUNC +from os import O_CREAT, O_TRUNC, O_WRONLY from collections import OrderedDict -from typing import Union, List, Dict +from typing import Dict, List, Union + import voluptuous as vol from homeassistant.components import websocket_api @@ -117,7 +118,7 @@ def load_config(fname: str) -> JSON_TYPE: def object_to_yaml(data: JSON_TYPE) -> str: - """create yaml string from object.""" + """Create yaml string from object.""" from ruamel.yaml import YAML from ruamel.yaml.error import YAMLError from ruamel.yaml.compat import StringIO @@ -133,11 +134,10 @@ def object_to_yaml(data: JSON_TYPE) -> str: def yaml_to_object(data: str) -> JSON_TYPE: - """create object from yaml string.""" + """Create object from yaml string.""" from ruamel.yaml import YAML from ruamel.yaml.error import YAMLError yaml = YAML(typ='rt') - yaml.preserve_quotes = True try: return yaml.load(data) except YAMLError as exc: @@ -153,7 +153,8 @@ def get_card(fname: str, card_id: str) -> JSON_TYPE: if card.get('id') == card_id: return object_to_yaml(card) - raise CardNotFoundError("Card with ID: {} was not found in {}.".format(card_id, fname)) + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) def set_card(fname: str, card_id: str, card_config: str) -> bool: @@ -167,7 +168,8 @@ def set_card(fname: str, card_id: str, card_config: str) -> bool: # Do we want to return config on save? return True - raise CardNotFoundError("Card with ID: {} was not found in {}.".format(card_id, fname)) + raise CardNotFoundError( + "Card with ID: {} was not found in {}.".format(card_id, fname)) async def async_setup(hass, config): @@ -245,7 +247,7 @@ async def websocket_lovelace_set_card(hass, connection, msg): error = None try: result = await hass.async_add_executor_job( - set_card, hass.config.path(LOVELACE_CONFIG_FILE), + set_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], msg['card_config']) message = websocket_api.result_message( msg['id'], result diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 5e4cf2d803753..5bbec3a9c6612 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -55,6 +55,8 @@ # Title of the view. Will be used as the tooltip for tab icon title: Second view cards: + - id: test + type: entities # Entities card will take a list of entities and show their state. - type: entities # Title of the entities card @@ -79,6 +81,7 @@ title: Home views: - title: Dashboard + id: dashboard icon: mdi:home cards: - id: testid @@ -148,8 +151,11 @@ def test_add_id(self): fname = self._path_for("test6") with patch('homeassistant.components.lovelace.load_yaml', return_value=self.yaml.load(TEST_YAML_A)): - data = load_config(fname) + with patch('homeassistant.components.lovelace.save_yaml', + return_value=None): + data = load_config(fname) assert 'id' in data['views'][0]['cards'][0] + assert 'id' in data['views'][1] def test_id_not_changed(self): """Test if id is not changed if already exists.""" @@ -272,3 +278,24 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_get_card(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'test', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + assert msg['result'] == 'id: test\ntype: entities\n' From 38cf4ca148debb1e2f9da9b66ed8aa6e6a00b919 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Oct 2018 22:45:25 +0200 Subject: [PATCH 3/5] Add test for set --- homeassistant/components/lovelace/__init__.py | 1 - tests/components/lovelace/test_init.py | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index b2d6bede24eff..02bd3242504a3 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -165,7 +165,6 @@ def set_card(fname: str, card_id: str, card_config: str) -> bool: if card.get('id') == card_id: card.update(yaml_to_object(card_config)) save_yaml(fname, config) - # Do we want to return config on save? return True raise CardNotFoundError( diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 5bbec3a9c6612..d7500f5f2bfae 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -299,3 +299,28 @@ async def test_lovelace_get_card(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result'] == 'id: test\ntype: entities\n' + + +async def test_lovelace_set_card(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + with patch('homeassistant.components.lovelace.save_yaml', + return_value=None) as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0] + assert result[1]['views'][1]['cards'][0]['type'] == 'glance' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] From 61f6d931f6b7ca0b6ff13dece5dc6c4ce1425b7c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 20 Oct 2018 16:04:30 +0200 Subject: [PATCH 4/5] Added more tests, catch unsupported yaml constructors Like !include will now give an error in the frontend. --- homeassistant/components/lovelace/__init__.py | 53 ++++-- tests/components/lovelace/test_init.py | 158 +++++++++++++++--- 2 files changed, 179 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 02bd3242504a3..28c1b25f4c514 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -31,12 +31,14 @@ SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_CARD, vol.Required('card_id'): str, + vol.Optional('format', default='yaml'): str, }) SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_SET_CARD, vol.Required('card_id'): str, - vol.Required('card_config'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('format', default='yaml'): str, }) @@ -48,6 +50,10 @@ class CardNotFoundError(HomeAssistantError): """Card not found in data.""" +class UnsupportedYamlError(HomeAssistantError): + """Unsupported YAML.""" + + def save_yaml(fname: str, data: JSON_TYPE): """Save a YAML file.""" from ruamel.yaml import YAML @@ -76,10 +82,20 @@ def save_yaml(fname: str, data: JSON_TYPE): _LOGGER.error("YAML replacement cleanup failed: %s", exc) +def _yaml_unsupported(loader, node): + raise UnsupportedYamlError( + 'Unsupported YAML, you can not use {} in ui-lovelace.yaml' + .format(node.tag)) + + def load_yaml(fname: str) -> JSON_TYPE: """Load a YAML file.""" from ruamel.yaml import YAML + from ruamel.yaml.constructor import RoundTripConstructor from ruamel.yaml.error import YAMLError + + RoundTripConstructor.add_constructor(None, _yaml_unsupported) + yaml = YAML(typ='rt') try: @@ -104,13 +120,13 @@ def load_config(fname: str) -> JSON_TYPE: for view in config.get('views', []): if 'id' not in view: updated = True - view['id'] = index - view.move_to_end('id', last=False) + view.insert(0, 'id', index, + comment="Automatically created id") for card in view.get('cards', []): if 'id' not in card: updated = True - card['id'] = uuid.uuid4().hex - card.move_to_end('id', last=False) + card.insert(0, 'id', uuid.uuid4().hex, + comment="Automatically created id") index += 1 if updated: save_yaml(fname, config) @@ -145,25 +161,33 @@ def yaml_to_object(data: str) -> JSON_TYPE: raise HomeAssistantError(exc) -def get_card(fname: str, card_id: str) -> JSON_TYPE: +def get_card(fname: str, card_id: str, format: str) -> JSON_TYPE: """Load a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): if card.get('id') == card_id: - return object_to_yaml(card) + if format == 'yaml': + return object_to_yaml(card) + elif format == 'json': + return card + else: + raise HomeAssistantError( + 'Format {} not supported'.format(format)) raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) -def set_card(fname: str, card_id: str, card_config: str) -> bool: +def set_card(fname: str, card_id: str, card_config: str, format: str) -> bool: """Save a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): if card.get('id') == card_id: - card.update(yaml_to_object(card_config)) + if format == 'yaml': + card_config = yaml_to_object(card_config) + card.update(card_config) save_yaml(fname, config) return True @@ -206,6 +230,8 @@ async def websocket_lovelace_config(hass, connection, msg): except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -221,13 +247,16 @@ async def websocket_lovelace_get_card(hass, connection, msg): error = None try: card = await hass.async_add_executor_job( - get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id']) + get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], + msg.get('format', 'yaml')) message = websocket_api.result_message( msg['id'], card ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) except CardNotFoundError: error = ('card_not_found', 'Could not find card in ui-lovelace.yaml.') @@ -247,13 +276,15 @@ async def websocket_lovelace_set_card(hass, connection, msg): try: result = await hass.async_add_executor_job( set_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['card_config']) + msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) message = websocket_api.result_message( msg['id'], result ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) except CardNotFoundError: error = ('card_not_found', 'Could not find card in ui-lovelace.yaml.') diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index d7500f5f2bfae..c637267cc7e2c 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -9,7 +9,8 @@ from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components.lovelace import (load_yaml, - save_yaml, load_config) + save_yaml, load_config, + UnsupportedYamlError) TEST_YAML_A = """\ title: My Awesome Home @@ -105,6 +106,15 @@ type: vertical-stack """ +# Test unsupported YAML +TEST_UNSUP_YAML = """\ +title: Home +views: + - title: Dashboard + icon: mdi:home + cards: !include cards.yaml +""" + class TestYAML(unittest.TestCase): """Test lovelace.yaml save and load.""" @@ -150,10 +160,9 @@ def test_add_id(self): """Test if id is added.""" fname = self._path_for("test6") with patch('homeassistant.components.lovelace.load_yaml', - return_value=self.yaml.load(TEST_YAML_A)): - with patch('homeassistant.components.lovelace.save_yaml', - return_value=None): - data = load_config(fname) + return_value=self.yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml'): + data = load_config(fname) assert 'id' in data['views'][0]['cards'][0] assert 'id' in data['views'][1] @@ -262,7 +271,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client): async def test_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" + """Test lovelace_ui command load error.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) @@ -280,8 +289,27 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client): assert msg['error']['code'] == 'load_error' +async def test_lovelace_ui_load_json_err(hass, hass_ws_client): + """Test lovelace_ui command load error.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_config', + side_effect=UnsupportedYamlError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unsupported_error' + + async def test_lovelace_get_card(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" + """Test get_card command.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -301,26 +329,114 @@ async def test_lovelace_get_card(hass, hass_ws_client): assert msg['result'] == 'id: test\ntype: entities\n' -async def test_lovelace_set_card(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" +async def test_lovelace_get_card_not_found(hass, hass_ws_client): + """Test get_card command cannot find card.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') with patch('homeassistant.components.lovelace.load_yaml', return_value=yaml.load(TEST_YAML_A)): - with patch('homeassistant.components.lovelace.save_yaml', - return_value=None) as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/set', - 'card_id': 'test', - 'card_config': 'id: test\ntype: glance\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0] - assert result[1]['views'][1]['cards'][0]['type'] == 'glance' + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'not_found', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): + """Test get_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/get', + 'card_id': 'testid', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_set_card(hass, hass_ws_client): + """Test set_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 1, 'cards', 0, 'type'], + list_ok=True) == 'glance' assert msg['id'] == 5 assert msg['type'] == TYPE_RESULT assert msg['success'] + + +async def test_lovelace_set_card_not_found(hass, hass_ws_client): + """Test set_card command cannot find card.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'not_found', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'card_not_found' + + +async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): + """Test set_card command bad yaml.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.yaml_to_object', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/set', + 'card_id': 'test', + 'card_config': 'id: test\ntype: glance\n', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'save_error' From e9e013663cde420648cf405c74f41f26460c94b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 20 Oct 2018 17:02:17 +0200 Subject: [PATCH 5/5] lint --- homeassistant/components/lovelace/__init__.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 28c1b25f4c514..2c28b52ec6ec5 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -161,31 +161,28 @@ def yaml_to_object(data: str) -> JSON_TYPE: raise HomeAssistantError(exc) -def get_card(fname: str, card_id: str, format: str) -> JSON_TYPE: +def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: """Load a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): if card.get('id') == card_id: - if format == 'yaml': + if data_format == 'yaml': return object_to_yaml(card) - elif format == 'json': - return card - else: - raise HomeAssistantError( - 'Format {} not supported'.format(format)) + return card raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) -def set_card(fname: str, card_id: str, card_config: str, format: str) -> bool: +def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ + -> bool: """Save a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): if card.get('id') == card_id: - if format == 'yaml': + if data_format == 'yaml': card_config = yaml_to_object(card_config) card.update(card_config) save_yaml(fname, config)