Lovelace: Duplicate ID check on load config + caching (#18152)

* Add caching + dupl. ID check

* duplicate imports...

* lint

* remove for/else

* found

* Missed one...
This commit is contained in:
Bram Kragten 2018-11-06 02:12:31 +01:00 committed by Charles Garwood
parent 7077e19cf8
commit 24c110ad3c

View file

@ -6,7 +6,9 @@ at https://www.home-assistant.io/lovelace/
""" """
from functools import wraps from functools import wraps
import logging import logging
import os
from typing import Dict, List, Union from typing import Dict, List, Union
import time
import uuid import uuid
import voluptuous as vol import voluptuous as vol
@ -18,6 +20,7 @@ import homeassistant.util.ruamel_yaml as yaml
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'lovelace' DOMAIN = 'lovelace'
LOVELACE_DATA = 'lovelace'
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
@ -133,9 +136,37 @@ class DuplicateIdError(HomeAssistantError):
"""Duplicate ID's.""" """Duplicate ID's."""
def load_config(fname: str) -> JSON_TYPE: def load_config(hass) -> JSON_TYPE:
"""Load a YAML file.""" """Load a YAML file."""
return yaml.load_yaml(fname, False) 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
def migrate_config(fname: str) -> None: def migrate_config(fname: str) -> None:
@ -301,35 +332,39 @@ def get_view(fname: str, view_id: str, data_format: str = FORMAT_YAML) -> None:
"""Get view without it's cards.""" """Get view without it's cards."""
round_trip = data_format == FORMAT_YAML round_trip = data_format == FORMAT_YAML
config = yaml.load_yaml(fname, round_trip) config = yaml.load_yaml(fname, round_trip)
found = None
for view in config.get('views', []): for view in config.get('views', []):
if str(view.get('id', '')) != view_id: if str(view.get('id', '')) == view_id:
continue found = view
del view['cards'] break
if data_format == FORMAT_YAML: if found is None:
return yaml.object_to_yaml(view)
return view
raise ViewNotFoundError( raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname)) "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: def update_view(fname: str, view_id: str, view_config, data_format:
str = FORMAT_YAML) -> None: str = FORMAT_YAML) -> None:
"""Update view.""" """Update view."""
config = yaml.load_yaml(fname, True) config = yaml.load_yaml(fname, True)
found = None
for view in config.get('views', []): for view in config.get('views', []):
if str(view.get('id', '')) != view_id: if str(view.get('id', '')) == view_id:
continue found = view
if data_format == FORMAT_YAML: break
view_config = yaml.yaml_to_object(view_config) if found is None:
view_config['cards'] = view.get('cards', [])
view.clear()
view.update(view_config)
yaml.save_yaml(fname, config)
return
raise ViewNotFoundError( raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname)) "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)
def add_view(fname: str, view_config: str, def add_view(fname: str, view_config: str,
@ -350,31 +385,35 @@ def move_view(fname: str, view_id: str, position: int) -> None:
"""Move a view to a different position.""" """Move a view to a different position."""
config = yaml.load_yaml(fname, True) config = yaml.load_yaml(fname, True)
views = config.get('views', []) views = config.get('views', [])
found = None
for view in views: for view in views:
if str(view.get('id', '')) != view_id: if str(view.get('id', '')) == view_id:
continue found = view
views.insert(position, views.pop(views.index(view))) break
yaml.save_yaml(fname, config) if found is None:
return
raise ViewNotFoundError( raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname)) "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: def delete_view(fname: str, view_id: str) -> None:
"""Delete a view.""" """Delete a view."""
config = yaml.load_yaml(fname, True) config = yaml.load_yaml(fname, True)
views = config.get('views', []) views = config.get('views', [])
found = None
for view in views: for view in views:
if str(view.get('id', '')) != view_id: if str(view.get('id', '')) == view_id:
continue found = view
views.pop(views.index(view)) break
yaml.save_yaml(fname, config) if found is None:
return
raise ViewNotFoundError( raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname)) "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."""
@ -445,6 +484,8 @@ def handle_yaml_errors(func):
error = 'unsupported_error', str(err) error = 'unsupported_error', str(err)
except yaml.WriteError as err: except yaml.WriteError as err:
error = 'write_error', str(err) error = 'write_error', str(err)
except DuplicateIdError as err:
error = 'duplicate_id', str(err)
except CardNotFoundError as err: except CardNotFoundError as err:
error = 'card_not_found', str(err) error = 'card_not_found', str(err)
except ViewNotFoundError as err: except ViewNotFoundError as err:
@ -464,8 +505,7 @@ 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( return await hass.async_add_executor_job(load_config, hass)
load_config, hass.config.path(LOVELACE_CONFIG_FILE))
@websocket_api.async_response @websocket_api.async_response