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:
parent
7077e19cf8
commit
24c110ad3c
1 changed files with 78 additions and 38 deletions
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue