diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 2c28b52ec6e..141f3c98334 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -18,10 +18,15 @@ REQUIREMENTS = ['ruamel.yaml==0.15.72'] LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +FORMAT_YAML = 'yaml' +FORMAT_JSON = 'json' + OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' + WS_TYPE_GET_CARD = 'lovelace/config/card/get' -WS_TYPE_SET_CARD = 'lovelace/config/card/set' +WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' +WS_TYPE_ADD_CARD = 'lovelace/config/card/add' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, @@ -31,14 +36,25 @@ SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_CARD, vol.Required('card_id'): str, - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) -SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SET_CARD, +SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE_CARD, vol.Required('card_id'): str, vol.Required('card_config'): vol.Any(str, Dict), - vol.Optional('format', default='yaml'): str, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), +}) + +SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_ADD_CARD, + vol.Required('view_id'): str, + vol.Required('card_config'): vol.Any(str, Dict), + vol.Optional('position'): int, + vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, + FORMAT_YAML), }) @@ -50,6 +66,10 @@ class CardNotFoundError(HomeAssistantError): """Card not found in data.""" +class ViewNotFoundError(HomeAssistantError): + """View not found in data.""" + + class UnsupportedYamlError(HomeAssistantError): """Unsupported YAML.""" @@ -161,37 +181,61 @@ def yaml_to_object(data: str) -> JSON_TYPE: raise HomeAssistantError(exc) -def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE: +def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\ + -> JSON_TYPE: """Load a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - return object_to_yaml(card) - return card + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + return object_to_yaml(card) + return card raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) -def set_card(fname: str, card_id: str, card_config: str, data_format: str)\ - -> bool: +def update_card(fname: str, card_id: str, card_config: str, + data_format: str = FORMAT_YAML): """Save a specific card config for id.""" config = load_yaml(fname) for view in config.get('views', []): for card in view.get('cards', []): - if card.get('id') == card_id: - if data_format == 'yaml': - card_config = yaml_to_object(card_config) - card.update(card_config) - save_yaml(fname, config) - return True + if card.get('id') != card_id: + continue + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + card.update(card_config) + save_yaml(fname, config) + return raise CardNotFoundError( "Card with ID: {} was not found in {}.".format(card_id, fname)) +def add_card(fname: str, view_id: str, card_config: str, + position: int = None, data_format: str = FORMAT_YAML): + """Add a card to a view.""" + config = load_yaml(fname) + for view in config.get('views', []): + if view.get('id') != view_id: + continue + cards = view.get('cards', []) + if data_format == FORMAT_YAML: + card_config = yaml_to_object(card_config) + if position is None: + cards.append(card_config) + else: + cards.insert(position, card_config) + save_yaml(fname, config) + return + + raise ViewNotFoundError( + "View with ID: {} was not found in {}.".format(view_id, fname)) + + async def async_setup(hass, config): """Set up the Lovelace commands.""" # Backwards compat. Added in 0.80. Remove after 0.85 @@ -208,8 +252,12 @@ async def async_setup(hass, config): SCHEMA_GET_CARD) hass.components.websocket_api.async_register_command( - WS_TYPE_SET_CARD, websocket_lovelace_set_card, - SCHEMA_SET_CARD) + WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card, + SCHEMA_UPDATE_CARD) + + hass.components.websocket_api.async_register_command( + WS_TYPE_ADD_CARD, websocket_lovelace_add_card, + SCHEMA_ADD_CARD) return True @@ -245,7 +293,7 @@ async def websocket_lovelace_get_card(hass, connection, msg): try: card = await hass.async_add_executor_job( get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], - msg.get('format', 'yaml')) + msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( msg['id'], card ) @@ -254,9 +302,8 @@ async def websocket_lovelace_get_card(hass, connection, msg): 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) except HomeAssistantError as err: error = 'load_error', str(err) @@ -267,24 +314,51 @@ async def websocket_lovelace_get_card(hass, connection, msg): @websocket_api.async_response -async def websocket_lovelace_set_card(hass, connection, msg): +async def websocket_lovelace_update_card(hass, connection, msg): """Receive lovelace card config over websocket and save.""" error = None try: - result = await hass.async_add_executor_job( - set_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['card_config'], msg.get('format', 'yaml')) + await hass.async_add_executor_job( + update_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML)) message = websocket_api.result_message( - msg['id'], result + msg['id'], True ) except FileNotFoundError: error = ('file_not_found', 'Could not find ui-lovelace.yaml in your config dir.') except UnsupportedYamlError as err: error = 'unsupported_error', str(err) - except CardNotFoundError: - error = ('card_not_found', - 'Could not find card in ui-lovelace.yaml.') + except CardNotFoundError as err: + error = 'card_not_found', str(err) + except HomeAssistantError as err: + error = 'save_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message(message) + + +@websocket_api.async_response +async def websocket_lovelace_add_card(hass, connection, msg): + """Add new card to view over websocket and save.""" + error = None + try: + await hass.async_add_executor_job( + add_card, hass.config.path(LOVELACE_CONFIG_FILE), + msg['view_id'], msg['card_config'], msg.get('position'), + msg.get('format', FORMAT_YAML)) + message = websocket_api.result_message( + msg['id'], True + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except UnsupportedYamlError as err: + error = 'unsupported_error', str(err) + except ViewNotFoundError as err: + error = 'view_not_found', str(err) except HomeAssistantError as err: error = 'save_error', str(err) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index c637267cc7e..1ce0f9ff602 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -370,8 +370,8 @@ async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): assert msg['error']['code'] == 'load_error' -async def test_lovelace_set_card(hass, hass_ws_client): - """Test set_card command.""" +async def test_lovelace_update_card(hass, hass_ws_client): + """Test update_card command.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -382,7 +382,7 @@ async def test_lovelace_set_card(hass, hass_ws_client): as save_yaml_mock: await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -396,8 +396,8 @@ async def test_lovelace_set_card(hass, hass_ws_client): assert msg['success'] -async def test_lovelace_set_card_not_found(hass, hass_ws_client): - """Test set_card command cannot find card.""" +async def test_lovelace_update_card_not_found(hass, hass_ws_client): + """Test update_card command cannot find card.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -406,7 +406,7 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): return_value=yaml.load(TEST_YAML_A)): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'not_found', 'card_config': 'id: test\ntype: glance\n', }) @@ -418,8 +418,8 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client): assert msg['error']['code'] == 'card_not_found' -async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): - """Test set_card command bad yaml.""" +async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client): + """Test update_card command bad yaml.""" await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) yaml = YAML(typ='rt') @@ -430,7 +430,7 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): side_effect=HomeAssistantError): await client.send_json({ 'id': 5, - 'type': 'lovelace/config/card/set', + 'type': 'lovelace/config/card/update', 'card_id': 'test', 'card_config': 'id: test\ntype: glance\n', }) @@ -440,3 +440,56 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert msg['success'] is False assert msg['error']['code'] == 'save_error' + + +async def test_lovelace_add_card(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 2, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + + +async def test_lovelace_add_card_position(hass, hass_ws_client): + """Test add_card command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + yaml = YAML(typ='rt') + + with patch('homeassistant.components.lovelace.load_yaml', + return_value=yaml.load(TEST_YAML_A)), \ + patch('homeassistant.components.lovelace.save_yaml') \ + as save_yaml_mock: + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config/card/add', + 'view_id': 'example', + 'position': 0, + 'card_config': 'id: test\ntype: added\n', + }) + msg = await client.receive_json() + + result = save_yaml_mock.call_args_list[0][0][1] + assert result.mlget(['views', 0, 'cards', 0, 'type'], + list_ok=True) == 'added' + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success']