2018-11-03 09:24:02 +00:00
|
|
|
"""
|
|
|
|
Support for the Lovelace UI.
|
|
|
|
|
|
|
|
For more details about this component, please refer to the documentation
|
|
|
|
at https://www.home-assistant.io/lovelace/
|
|
|
|
"""
|
2018-10-31 12:49:54 +00:00
|
|
|
from functools import wraps
|
2018-11-03 09:24:02 +00:00
|
|
|
import logging
|
2018-11-06 01:12:31 +00:00
|
|
|
import os
|
2018-10-22 12:45:13 +00:00
|
|
|
from typing import Dict, List, Union
|
2018-11-06 01:12:31 +00:00
|
|
|
import time
|
2018-11-03 09:24:02 +00:00
|
|
|
import uuid
|
2018-10-22 12:45:13 +00:00
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components import websocket_api
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2018-11-03 09:24:02 +00:00
|
|
|
import homeassistant.util.ruamel_yaml as yaml
|
2018-09-25 06:39:35 +00:00
|
|
|
|
2018-10-17 14:31:06 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2018-11-03 09:24:02 +00:00
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
DOMAIN = 'lovelace'
|
2018-11-06 01:12:31 +00:00
|
|
|
LOVELACE_DATA = 'lovelace'
|
2018-09-25 06:39:35 +00:00
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
|
|
|
|
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
FORMAT_YAML = 'yaml'
|
|
|
|
FORMAT_JSON = 'json'
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
|
|
|
|
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'
|
2018-10-27 21:51:40 +00:00
|
|
|
WS_TYPE_MIGRATE_CONFIG = 'lovelace/config/migrate'
|
2018-11-23 21:56:58 +00:00
|
|
|
WS_TYPE_SAVE_CONFIG = 'lovelace/config/save'
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
WS_TYPE_GET_CARD = 'lovelace/config/card/get'
|
2018-10-23 19:48:35 +00:00
|
|
|
WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update'
|
|
|
|
WS_TYPE_ADD_CARD = 'lovelace/config/card/add'
|
2018-10-26 10:56:14 +00:00
|
|
|
WS_TYPE_MOVE_CARD = 'lovelace/config/card/move'
|
2018-10-26 15:29:33 +00:00
|
|
|
WS_TYPE_DELETE_CARD = 'lovelace/config/card/delete'
|
2018-10-17 14:31:06 +00:00
|
|
|
|
2018-11-01 08:44:38 +00:00
|
|
|
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'
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
2018-11-03 09:24:02 +00:00
|
|
|
vol.Required('type'):
|
|
|
|
vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI),
|
2018-09-25 06:39:35 +00:00
|
|
|
})
|
|
|
|
|
2018-10-27 21:51:40 +00:00
|
|
|
SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|
|
|
vol.Required('type'): WS_TYPE_MIGRATE_CONFIG,
|
|
|
|
})
|
|
|
|
|
2018-11-23 21:56:58 +00:00
|
|
|
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),
|
|
|
|
})
|
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|
|
|
vol.Required('type'): WS_TYPE_GET_CARD,
|
|
|
|
vol.Required('card_id'): str,
|
2018-11-03 09:24:02 +00:00
|
|
|
vol.Optional('format', default=FORMAT_YAML):
|
|
|
|
vol.Any(FORMAT_JSON, FORMAT_YAML),
|
2018-10-22 12:45:13 +00:00
|
|
|
})
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|
|
|
vol.Required('type'): WS_TYPE_UPDATE_CARD,
|
2018-10-22 12:45:13 +00:00
|
|
|
vol.Required('card_id'): str,
|
2018-11-22 11:48:50 +00:00
|
|
|
vol.Required('card_config'): vol.Any(str, dict),
|
2018-11-03 09:24:02 +00:00
|
|
|
vol.Optional('format', default=FORMAT_YAML):
|
|
|
|
vol.Any(FORMAT_JSON, FORMAT_YAML),
|
2018-10-23 19:48:35 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|
|
|
vol.Required('type'): WS_TYPE_ADD_CARD,
|
|
|
|
vol.Required('view_id'): str,
|
2018-11-22 11:48:50 +00:00
|
|
|
vol.Required('card_config'): vol.Any(str, dict),
|
2018-10-23 19:48:35 +00:00
|
|
|
vol.Optional('position'): int,
|
2018-11-03 09:24:02 +00:00
|
|
|
vol.Optional('format', default=FORMAT_YAML):
|
|
|
|
vol.Any(FORMAT_JSON, FORMAT_YAML),
|
2018-10-22 12:45:13 +00:00
|
|
|
})
|
2018-10-17 14:31:06 +00:00
|
|
|
|
2018-10-26 10:56:14 +00:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
|
2018-10-26 15:29:33 +00:00
|
|
|
SCHEMA_DELETE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|
|
|
vol.Required('type'): WS_TYPE_DELETE_CARD,
|
|
|
|
vol.Required('card_id'): str,
|
|
|
|
})
|
|
|
|
|
2018-11-01 08:44:38 +00:00
|
|
|
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,
|
2018-11-22 11:48:50 +00:00
|
|
|
vol.Required('view_config'): vol.Any(str, dict),
|
2018-11-01 08:44:38 +00:00
|
|
|
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,
|
2018-11-22 11:48:50 +00:00
|
|
|
vol.Required('view_config'): vol.Any(str, dict),
|
2018-11-01 08:44:38 +00:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
|
2018-10-17 14:31:06 +00:00
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
class CardNotFoundError(HomeAssistantError):
|
|
|
|
"""Card not found in data."""
|
|
|
|
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
class ViewNotFoundError(HomeAssistantError):
|
|
|
|
"""View not found in data."""
|
|
|
|
|
|
|
|
|
2018-10-26 10:56:14 +00:00
|
|
|
class DuplicateIdError(HomeAssistantError):
|
|
|
|
"""Duplicate ID's."""
|
|
|
|
|
|
|
|
|
2018-11-06 01:12:31 +00:00
|
|
|
def load_config(hass) -> JSON_TYPE:
|
2018-10-27 21:51:40 +00:00
|
|
|
"""Load a YAML file."""
|
2018-11-06 01:12:31 +00:00
|
|
|
fname = hass.config.path(LOVELACE_CONFIG_FILE)
|
|
|
|
|
|
|
|
# Check for a cached version of the config
|
|
|
|
if 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()
|
|
|
|
for view in config.get('views', []):
|
|
|
|
view_id = str(view.get('id', ''))
|
|
|
|
if view_id:
|
|
|
|
if view_id in seen_view_ids:
|
|
|
|
raise DuplicateIdError(
|
|
|
|
'ID `{}` has multiple occurances in views'.format(view_id))
|
|
|
|
seen_view_ids.add(view_id)
|
|
|
|
for card in view.get('cards', []):
|
|
|
|
card_id = str(card.get('id', ''))
|
|
|
|
if card_id:
|
|
|
|
if card_id in seen_card_ids:
|
|
|
|
raise DuplicateIdError(
|
|
|
|
'ID `{}` has multiple occurances in cards'
|
|
|
|
.format(card_id))
|
|
|
|
seen_card_ids.add(card_id)
|
|
|
|
hass.data[LOVELACE_DATA] = (config, time.time())
|
|
|
|
return config
|
2018-10-27 21:51:40 +00:00
|
|
|
|
|
|
|
|
2018-10-31 12:49:54 +00:00
|
|
|
def migrate_config(fname: str) -> None:
|
|
|
|
"""Add id to views and cards if not present and check duplicates."""
|
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-17 14:31:06 +00:00
|
|
|
updated = False
|
2018-10-26 10:56:14 +00:00
|
|
|
seen_card_ids = set()
|
|
|
|
seen_view_ids = set()
|
2018-10-22 12:45:13 +00:00
|
|
|
index = 0
|
2018-10-17 14:31:06 +00:00
|
|
|
for view in config.get('views', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
view_id = str(view.get('id', ''))
|
|
|
|
if not view_id:
|
2018-10-22 12:45:13 +00:00
|
|
|
updated = True
|
2018-11-03 09:24:02 +00:00
|
|
|
view.insert(0, 'id', index, comment="Automatically created id")
|
2018-10-26 10:56:14 +00:00
|
|
|
else:
|
|
|
|
if view_id in seen_view_ids:
|
|
|
|
raise DuplicateIdError(
|
2018-11-03 09:24:02 +00:00
|
|
|
'ID `{}` has multiple occurrences in views'.format(
|
|
|
|
view_id))
|
2018-10-26 10:56:14 +00:00
|
|
|
seen_view_ids.add(view_id)
|
2018-10-17 14:31:06 +00:00
|
|
|
for card in view.get('cards', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
card_id = str(card.get('id', ''))
|
|
|
|
if not card_id:
|
2018-10-17 14:31:06 +00:00
|
|
|
updated = True
|
2018-10-22 12:45:13 +00:00
|
|
|
card.insert(0, 'id', uuid.uuid4().hex,
|
|
|
|
comment="Automatically created id")
|
2018-10-26 10:56:14 +00:00
|
|
|
else:
|
|
|
|
if card_id in seen_card_ids:
|
|
|
|
raise DuplicateIdError(
|
2018-11-03 09:24:02 +00:00
|
|
|
'ID `{}` has multiple occurrences in cards'
|
2018-10-26 10:56:14 +00:00
|
|
|
.format(card_id))
|
|
|
|
seen_card_ids.add(card_id)
|
2018-10-22 12:45:13 +00:00
|
|
|
index += 1
|
2018-10-17 14:31:06 +00:00
|
|
|
if updated:
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
|
2018-11-23 21:56:58 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\
|
|
|
|
-> JSON_TYPE:
|
2018-10-22 12:45:13 +00:00
|
|
|
"""Load a specific card config for id."""
|
2018-10-31 12:49:54 +00:00
|
|
|
round_trip = data_format == FORMAT_YAML
|
|
|
|
|
|
|
|
config = yaml.load_yaml(fname, round_trip)
|
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
for view in config.get('views', []):
|
|
|
|
for card in view.get('cards', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(card.get('id', '')) != card_id:
|
2018-10-23 19:48:35 +00:00
|
|
|
continue
|
|
|
|
if data_format == FORMAT_YAML:
|
2018-10-31 12:49:54 +00:00
|
|
|
return yaml.object_to_yaml(card)
|
2018-10-23 19:48:35 +00:00
|
|
|
return card
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
raise CardNotFoundError(
|
|
|
|
"Card with ID: {} was not found in {}.".format(card_id, fname))
|
|
|
|
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
def update_card(fname: str, card_id: str, card_config: str,
|
2018-10-31 12:49:54 +00:00
|
|
|
data_format: str = FORMAT_YAML) -> None:
|
2018-10-22 12:45:13 +00:00
|
|
|
"""Save a specific card config for id."""
|
2018-10-31 12:49:54 +00:00
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-22 12:45:13 +00:00
|
|
|
for view in config.get('views', []):
|
|
|
|
for card in view.get('cards', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(card.get('id', '')) != card_id:
|
2018-10-23 19:48:35 +00:00
|
|
|
continue
|
|
|
|
if data_format == FORMAT_YAML:
|
2018-10-31 12:49:54 +00:00
|
|
|
card_config = yaml.yaml_to_object(card_config)
|
2018-11-01 08:44:38 +00:00
|
|
|
card.clear()
|
2018-10-23 19:48:35 +00:00
|
|
|
card.update(card_config)
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-23 19:48:35 +00:00
|
|
|
return
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
raise CardNotFoundError(
|
|
|
|
"Card with ID: {} was not found in {}.".format(card_id, fname))
|
|
|
|
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
def add_card(fname: str, view_id: str, card_config: str,
|
2018-10-31 12:49:54 +00:00
|
|
|
position: int = None, data_format: str = FORMAT_YAML) -> None:
|
2018-10-23 19:48:35 +00:00
|
|
|
"""Add a card to a view."""
|
2018-10-31 12:49:54 +00:00
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-23 19:48:35 +00:00
|
|
|
for view in config.get('views', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(view.get('id', '')) != view_id:
|
2018-10-23 19:48:35 +00:00
|
|
|
continue
|
|
|
|
cards = view.get('cards', [])
|
|
|
|
if data_format == FORMAT_YAML:
|
2018-10-31 12:49:54 +00:00
|
|
|
card_config = yaml.yaml_to_object(card_config)
|
2018-10-23 19:48:35 +00:00
|
|
|
if position is None:
|
|
|
|
cards.append(card_config)
|
|
|
|
else:
|
|
|
|
cards.insert(position, card_config)
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-23 19:48:35 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
raise ViewNotFoundError(
|
|
|
|
"View with ID: {} was not found in {}.".format(view_id, fname))
|
|
|
|
|
|
|
|
|
2018-10-31 12:49:54 +00:00
|
|
|
def move_card(fname: str, card_id: str, position: int = None) -> None:
|
2018-10-26 10:56:14 +00:00
|
|
|
"""Move a card to a different position."""
|
|
|
|
if position is None:
|
2018-11-03 09:24:02 +00:00
|
|
|
raise HomeAssistantError(
|
|
|
|
'Position is required if view is not specified.')
|
2018-10-31 12:49:54 +00:00
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-26 10:56:14 +00:00
|
|
|
for view in config.get('views', []):
|
|
|
|
for card in view.get('cards', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(card.get('id', '')) != card_id:
|
2018-10-26 10:56:14 +00:00
|
|
|
continue
|
|
|
|
cards = view.get('cards')
|
|
|
|
cards.insert(position, cards.pop(cards.index(card)))
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-26 10:56:14 +00:00
|
|
|
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,
|
2018-10-31 12:49:54 +00:00
|
|
|
position: int = None) -> None:
|
2018-10-26 10:56:14 +00:00
|
|
|
"""Move a card to a different view."""
|
2018-10-31 12:49:54 +00:00
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-26 10:56:14 +00:00
|
|
|
for view in config.get('views', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(view.get('id', '')) == view_id:
|
2018-10-26 10:56:14 +00:00
|
|
|
destination = view.get('cards')
|
|
|
|
for card in view.get('cards'):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(card.get('id', '')) != card_id:
|
2018-10-26 10:56:14 +00:00
|
|
|
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)
|
|
|
|
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-26 10:56:14 +00:00
|
|
|
|
|
|
|
|
2018-11-01 08:44:38 +00:00
|
|
|
def delete_card(fname: str, card_id: str) -> None:
|
2018-10-26 15:29:33 +00:00
|
|
|
"""Delete a card from view."""
|
2018-10-31 12:49:54 +00:00
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-10-26 15:29:33 +00:00
|
|
|
for view in config.get('views', []):
|
|
|
|
for card in view.get('cards', []):
|
2018-10-31 12:49:54 +00:00
|
|
|
if str(card.get('id', '')) != card_id:
|
2018-10-26 15:29:33 +00:00
|
|
|
continue
|
|
|
|
cards = view.get('cards')
|
|
|
|
cards.pop(cards.index(card))
|
2018-10-31 12:49:54 +00:00
|
|
|
yaml.save_yaml(fname, config)
|
2018-10-26 15:29:33 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
raise CardNotFoundError(
|
|
|
|
"Card with ID: {} was not found in {}.".format(card_id, fname))
|
|
|
|
|
|
|
|
|
2018-11-01 08:44:38 +00:00
|
|
|
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)
|
2018-11-06 01:12:31 +00:00
|
|
|
found = None
|
2018-11-01 08:44:38 +00:00
|
|
|
for view in config.get('views', []):
|
2018-11-06 01:12:31 +00:00
|
|
|
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))
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-11-06 01:12:31 +00:00
|
|
|
del found['cards']
|
|
|
|
if data_format == FORMAT_YAML:
|
|
|
|
return yaml.object_to_yaml(found)
|
|
|
|
return found
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def update_view(fname: str, view_id: str, view_config, data_format:
|
|
|
|
str = FORMAT_YAML) -> None:
|
|
|
|
"""Update view."""
|
|
|
|
config = yaml.load_yaml(fname, True)
|
2018-11-06 01:12:31 +00:00
|
|
|
found = None
|
2018-11-01 08:44:38 +00:00
|
|
|
for view in config.get('views', []):
|
2018-11-06 01:12:31 +00:00
|
|
|
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)
|
|
|
|
view_config['cards'] = found.get('cards', [])
|
|
|
|
found.clear()
|
|
|
|
found.update(view_config)
|
|
|
|
yaml.save_yaml(fname, config)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
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 position is None:
|
|
|
|
views.append(view_config)
|
|
|
|
else:
|
|
|
|
views.insert(position, view_config)
|
|
|
|
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', [])
|
2018-11-06 01:12:31 +00:00
|
|
|
found = None
|
2018-11-01 08:44:38 +00:00
|
|
|
for view in views:
|
2018-11-06 01:12:31 +00:00
|
|
|
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))
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-11-06 01:12:31 +00:00
|
|
|
views.insert(position, views.pop(views.index(found)))
|
|
|
|
yaml.save_yaml(fname, config)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def delete_view(fname: str, view_id: str) -> None:
|
|
|
|
"""Delete a view."""
|
|
|
|
config = yaml.load_yaml(fname, True)
|
|
|
|
views = config.get('views', [])
|
2018-11-06 01:12:31 +00:00
|
|
|
found = None
|
2018-11-01 08:44:38 +00:00
|
|
|
for view in views:
|
2018-11-06 01:12:31 +00:00
|
|
|
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))
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-11-06 01:12:31 +00:00
|
|
|
views.pop(views.index(found))
|
|
|
|
yaml.save_yaml(fname, config)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
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)
|
|
|
|
|
2018-11-23 21:56:58 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
|
|
|
WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
|
|
|
|
SCHEMA_GET_LOVELACE_UI)
|
|
|
|
|
2018-10-27 21:51:40 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
|
|
|
WS_TYPE_MIGRATE_CONFIG, websocket_lovelace_migrate_config,
|
|
|
|
SCHEMA_MIGRATE_CONFIG)
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-23 21:56:58 +00:00
|
|
|
WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config,
|
|
|
|
SCHEMA_SAVE_CONFIG)
|
2018-09-25 06:39:35 +00:00
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_GET_CARD, websocket_lovelace_get_card, SCHEMA_GET_CARD)
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-10-23 19:48:35 +00:00
|
|
|
WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card,
|
|
|
|
SCHEMA_UPDATE_CARD)
|
|
|
|
|
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_ADD_CARD, websocket_lovelace_add_card, SCHEMA_ADD_CARD)
|
2018-10-22 12:45:13 +00:00
|
|
|
|
2018-10-26 10:56:14 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_MOVE_CARD, websocket_lovelace_move_card, SCHEMA_MOVE_CARD)
|
2018-10-26 10:56:14 +00:00
|
|
|
|
2018-10-26 15:29:33 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
|
|
|
WS_TYPE_DELETE_CARD, websocket_lovelace_delete_card,
|
|
|
|
SCHEMA_DELETE_CARD)
|
|
|
|
|
2018-11-01 08:44:38 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_GET_VIEW, websocket_lovelace_get_view, SCHEMA_GET_VIEW)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
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(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_ADD_VIEW, websocket_lovelace_add_view, SCHEMA_ADD_VIEW)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
hass.components.websocket_api.async_register_command(
|
2018-11-03 09:24:02 +00:00
|
|
|
WS_TYPE_MOVE_VIEW, websocket_lovelace_move_view, SCHEMA_MOVE_VIEW)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
hass.components.websocket_api.async_register_command(
|
|
|
|
WS_TYPE_DELETE_VIEW, websocket_lovelace_delete_view,
|
|
|
|
SCHEMA_DELETE_VIEW)
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-10-31 12:49:54 +00:00
|
|
|
def handle_yaml_errors(func):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Handle error with WebSocket calls."""
|
2018-10-31 12:49:54 +00:00
|
|
|
@wraps(func)
|
|
|
|
async def send_with_error_handling(hass, connection, msg):
|
|
|
|
error = None
|
|
|
|
try:
|
|
|
|
result = await func(hass, connection, msg)
|
|
|
|
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)
|
2018-11-06 01:12:31 +00:00
|
|
|
except DuplicateIdError as err:
|
|
|
|
error = 'duplicate_id', str(err)
|
2018-10-31 12:49:54 +00:00
|
|
|
except CardNotFoundError as err:
|
|
|
|
error = 'card_not_found', str(err)
|
|
|
|
except ViewNotFoundError as err:
|
|
|
|
error = 'view_not_found', str(err)
|
|
|
|
except HomeAssistantError as err:
|
|
|
|
error = 'error', str(err)
|
|
|
|
|
|
|
|
if error is not None:
|
|
|
|
message = websocket_api.error_message(msg['id'], *error)
|
|
|
|
|
|
|
|
connection.send_message(message)
|
|
|
|
|
|
|
|
return send_with_error_handling
|
|
|
|
|
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-09-25 06:39:35 +00:00
|
|
|
async def websocket_lovelace_config(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Send Lovelace UI config over WebSocket configuration."""
|
2018-11-06 01:12:31 +00:00
|
|
|
return await hass.async_add_executor_job(load_config, hass)
|
2018-10-27 21:51:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-27 21:51:40 +00:00
|
|
|
async def websocket_lovelace_migrate_config(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Migrate Lovelace UI configuration."""
|
2018-10-31 12:49:54 +00:00
|
|
|
return await hass.async_add_executor_job(
|
|
|
|
migrate_config, hass.config.path(LOVELACE_CONFIG_FILE))
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
|
2018-11-23 21:56:58 +00:00
|
|
|
@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))
|
|
|
|
|
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-22 12:45:13 +00:00
|
|
|
async def websocket_lovelace_get_card(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Send Lovelace card config over WebSocket configuration."""
|
2018-10-31 12:49:54 +00:00
|
|
|
return await hass.async_add_executor_job(
|
|
|
|
get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'],
|
|
|
|
msg.get('format', FORMAT_YAML))
|
2018-10-22 12:45:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-23 19:48:35 +00:00
|
|
|
async def websocket_lovelace_update_card(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Receive Lovelace card configuration over WebSocket and save."""
|
2018-10-31 12:49:54 +00:00
|
|
|
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))
|
2018-10-23 19:48:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-23 19:48:35 +00:00
|
|
|
async def websocket_lovelace_add_card(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Add new card to view over WebSocket and save."""
|
2018-10-31 12:49:54 +00:00
|
|
|
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))
|
2018-10-26 10:56:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-26 10:56:14 +00:00
|
|
|
async def websocket_lovelace_move_card(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Move card to different position over WebSocket and save."""
|
2018-10-31 12:49:54 +00:00
|
|
|
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'))
|
2018-10-26 15:29:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
2018-10-31 12:49:54 +00:00
|
|
|
@handle_yaml_errors
|
2018-10-26 15:29:33 +00:00
|
|
|
async def websocket_lovelace_delete_card(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Delete card from Lovelace over WebSocket and save."""
|
2018-10-31 12:49:54 +00:00
|
|
|
return await hass.async_add_executor_job(
|
2018-11-03 09:24:02 +00:00
|
|
|
delete_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'])
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
|
|
|
@handle_yaml_errors
|
|
|
|
async def websocket_lovelace_get_view(hass, connection, msg):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Send Lovelace view config over WebSocket config."""
|
2018-11-01 08:44:38 +00:00
|
|
|
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):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Receive Lovelace card config over WebSocket and save."""
|
2018-11-01 08:44:38 +00:00
|
|
|
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):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Add new view over WebSocket and save."""
|
2018-11-01 08:44:38 +00:00
|
|
|
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):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Move view to different position over WebSocket and save."""
|
2018-11-01 08:44:38 +00:00
|
|
|
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):
|
2018-11-03 09:24:02 +00:00
|
|
|
"""Delete card from Lovelace over WebSocket and save."""
|
2018-11-01 08:44:38 +00:00
|
|
|
return await hass.async_add_executor_job(
|
2018-11-03 09:24:02 +00:00
|
|
|
delete_view, hass.config.path(LOVELACE_CONFIG_FILE), msg['view_id'])
|