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
|
|
|
|
import time
|
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-12-10 07:57:17 +00:00
|
|
|
from homeassistant.util.yaml import load_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-12-10 07:57:17 +00:00
|
|
|
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)
|
2018-09-25 06:39:35 +00:00
|
|
|
|
2018-10-22 12:45:13 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
|
2018-10-23 19:48:35 +00:00
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'
|
2018-11-23 21:56:58 +00:00
|
|
|
WS_TYPE_SAVE_CONFIG = 'lovelace/config/save'
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-09-25 06:39:35 +00:00
|
|
|
SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
2018-12-10 07:57:17 +00:00
|
|
|
vol.Required('type'): WS_TYPE_GET_LOVELACE_UI,
|
2018-12-07 06:09:05 +00:00
|
|
|
vol.Optional('force', default=False): bool,
|
2018-09-25 06:39:35 +00:00
|
|
|
})
|
|
|
|
|
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,
|
2018-11-24 09:02:06 +00:00
|
|
|
vol.Required('config'): vol.Any(str, dict),
|
2018-11-23 21:56:58 +00:00
|
|
|
})
|
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
class ConfigNotFound(HomeAssistantError):
|
|
|
|
"""When no config available."""
|
2018-10-17 14:31:06 +00:00
|
|
|
|
2018-10-26 10:56:14 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
async def async_setup(hass, config):
|
|
|
|
"""Set up the Lovelace commands."""
|
|
|
|
# Pass in default to `get` because defaults not set if loaded as dep
|
|
|
|
mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
await hass.components.frontend.async_register_built_in_panel(
|
|
|
|
DOMAIN, config={
|
|
|
|
'mode': mode
|
|
|
|
})
|
2018-10-17 14:31:06 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
if mode == MODE_YAML:
|
|
|
|
hass.data[DOMAIN] = LovelaceYAML(hass)
|
2018-10-26 10:56:14 +00:00
|
|
|
else:
|
2018-12-10 07:57:17 +00:00
|
|
|
hass.data[DOMAIN] = LovelaceStorage(hass)
|
2018-09-25 06:39:35 +00:00
|
|
|
|
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-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-12-10 07:57:17 +00:00
|
|
|
return True
|
2018-10-22 12:45:13 +00:00
|
|
|
|
2018-10-23 19:48:35 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
class LovelaceStorage:
|
|
|
|
"""Class to handle Storage based Lovelace config."""
|
2018-10-22 12:45:13 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
def __init__(self, hass):
|
|
|
|
"""Initialize Lovelace config based on storage helper."""
|
|
|
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
|
|
|
self._data = None
|
2018-10-26 10:56:14 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
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}
|
2018-10-26 15:29:33 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
config = self._data['config']
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
if config is None:
|
|
|
|
raise ConfigNotFound
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
return config
|
2018-11-01 08:44:38 +00:00
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
async def async_save(self, config):
|
|
|
|
"""Save config."""
|
2019-01-24 05:13:55 +00:00
|
|
|
if self._data is None:
|
|
|
|
self._data = {'config': None}
|
2018-12-10 11:25:08 +00:00
|
|
|
self._data['config'] = config
|
|
|
|
await self._store.async_save(self._data)
|
2018-11-01 08:44:38 +00:00
|
|
|
|
|
|
|
|
2018-12-10 07:57:17 +00:00
|
|
|
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')
|
2018-09-25 06:39:35 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
2018-12-10 07:57:17 +00:00
|
|
|
except ConfigNotFound:
|
|
|
|
error = 'config_not_found', 'No config found.'
|
2018-10-31 12:49:54 +00:00
|
|
|
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-12-10 07:57:17 +00:00
|
|
|
return await hass.data[DOMAIN].async_load(msg['force'])
|
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."""
|
2018-12-10 07:57:17 +00:00
|
|
|
await hass.data[DOMAIN].async_save(msg['config'])
|