From a521b885bf14e14a75791a2d7411aafac2d6416d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 08:57:17 +0100 Subject: [PATCH] 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 36fbe14aefd..f14a3b0b324 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'}