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
This commit is contained in:
Paulus Schoutsen 2018-12-10 08:57:17 +01:00 committed by GitHub
parent a744dc270b
commit a521b885bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 177 additions and 1313 deletions

View file

@ -250,8 +250,7 @@ async def async_setup(hass, config):
await asyncio.wait( await asyncio.wait(
[async_register_built_in_panel(hass, panel) for panel in ( [async_register_built_in_panel(hass, panel) for panel in (
'dev-event', 'dev-info', 'dev-service', 'dev-state', 'dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', 'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')],
'states', 'profile')],
loop=hass.loop) loop=hass.loop)
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel

View file

@ -7,507 +7,139 @@ at https://www.home-assistant.io/lovelace/
from functools import wraps from functools import wraps
import logging import logging
import os import os
from typing import Dict, List, Union
import time import time
import uuid
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.ruamel_yaml as yaml from homeassistant.util.yaml import load_yaml
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'lovelace' 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' 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_LOVELACE_UI = 'lovelace/config'
WS_TYPE_MIGRATE_CONFIG = 'lovelace/config/migrate'
WS_TYPE_SAVE_CONFIG = 'lovelace/config/save' 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({ SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): vol.Required('type'): WS_TYPE_GET_LOVELACE_UI,
vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI),
vol.Optional('force', default=False): bool, 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({ SCHEMA_SAVE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SAVE_CONFIG, vol.Required('type'): WS_TYPE_SAVE_CONFIG,
vol.Required('config'): vol.Any(str, dict), 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): class ConfigNotFound(HomeAssistantError):
"""Card not found in data.""" """When no config available."""
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)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the Lovelace commands.""" """Set up the Lovelace commands."""
# Backwards compat. Added in 0.80. Remove after 0.85 # Pass in default to `get` because defaults not set if loaded as dep
hass.components.websocket_api.async_register_command( mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE)
OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
SCHEMA_GET_LOVELACE_UI) 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( hass.components.websocket_api.async_register_command(
WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
SCHEMA_GET_LOVELACE_UI) 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( hass.components.websocket_api.async_register_command(
WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config, WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config,
SCHEMA_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 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): def handle_yaml_errors(func):
"""Handle error with WebSocket calls.""" """Handle error with WebSocket calls."""
@wraps(func) @wraps(func)
@ -518,19 +150,8 @@ def handle_yaml_errors(func):
message = websocket_api.result_message( message = websocket_api.result_message(
msg['id'], result msg['id'], result
) )
except FileNotFoundError: except ConfigNotFound:
error = ('file_not_found', error = 'config_not_found', 'No config 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 HomeAssistantError as err: except HomeAssistantError as err:
error = 'error', str(err) error = 'error', str(err)
@ -546,117 +167,11 @@ def handle_yaml_errors(func):
@handle_yaml_errors @handle_yaml_errors
async def websocket_lovelace_config(hass, connection, msg): async def websocket_lovelace_config(hass, connection, msg):
"""Send Lovelace UI config over WebSocket configuration.""" """Send Lovelace UI config over WebSocket configuration."""
return await hass.async_add_executor_job(load_config, hass, return await hass.data[DOMAIN].async_load(msg['force'])
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))
@websocket_api.async_response @websocket_api.async_response
@handle_yaml_errors @handle_yaml_errors
async def websocket_lovelace_save_config(hass, connection, msg): async def websocket_lovelace_save_config(hass, connection, msg):
"""Save Lovelace UI configuration.""" """Save Lovelace UI configuration."""
return await hass.async_add_executor_job( await hass.data[DOMAIN].async_save(msg['config'])
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'])

View file

@ -1,748 +1,98 @@
"""Test the Lovelace initialization.""" """Test the Lovelace initialization."""
from unittest.mock import patch from unittest.mock import patch
from ruamel.yaml import YAML
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components import frontend, lovelace
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
"""
def test_add_id(): async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage):
"""Test if id is added.""" """Test we load lovelace config from storage."""
yaml = YAML(typ='rt') 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) client = await hass_ws_client(hass)
with patch('homeassistant.components.lovelace.load_config', # Fetch data
return_value={'hello': 'world'}): await client.send_json({
await client.send_json({ 'id': 5,
'id': 5, 'type': 'lovelace/config'
'type': 'frontend/lovelace_config', })
}) response = await client.receive_json()
msg = await client.receive_json() assert not response['success']
assert response['error']['code'] == 'config_not_found'
assert msg['id'] == 5 # Store new config
assert msg['type'] == TYPE_RESULT await client.send_json({
assert msg['success'] 'id': 6,
assert msg['result'] == {'hello': 'world'} '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): async def test_lovelace_from_yaml(hass, hass_ws_client):
"""Test lovelace_ui command cannot find file.""" """Test we load lovelace config from yaml."""
await async_setup_component(hass, 'lovelace') 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) client = await hass_ws_client(hass)
with patch('homeassistant.components.lovelace.load_config', # Fetch data
side_effect=FileNotFoundError): 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({ await client.send_json({
'id': 5, 'id': 7,
'type': 'frontend/lovelace_config', 'type': 'lovelace/config'
}) })
msg = await client.receive_json() response = await client.receive_json()
assert msg['id'] == 5 assert response['success']
assert msg['type'] == TYPE_RESULT assert response['result'] == {'hello': 'yo'}
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']