2017-02-14 05:34:36 +00:00
|
|
|
"""Component to configure Home Assistant via an API."""
|
2017-02-12 01:29:05 +00:00
|
|
|
import asyncio
|
2021-09-22 18:59:52 +00:00
|
|
|
from http import HTTPStatus
|
2019-03-08 22:09:18 +00:00
|
|
|
import importlib
|
2017-02-21 05:53:55 +00:00
|
|
|
import os
|
|
|
|
|
|
|
|
import voluptuous as vol
|
2017-02-12 01:29:05 +00:00
|
|
|
|
2022-01-14 09:01:12 +00:00
|
|
|
from homeassistant.components import frontend
|
2019-09-27 15:48:48 +00:00
|
|
|
from homeassistant.components.http import HomeAssistantView
|
2021-09-22 18:59:52 +00:00
|
|
|
from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED
|
2022-03-18 12:09:10 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2019-09-27 15:48:48 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2022-01-02 16:05:18 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2019-03-08 22:09:18 +00:00
|
|
|
from homeassistant.setup import ATTR_COMPONENT
|
2021-11-15 10:19:31 +00:00
|
|
|
from homeassistant.util.file import write_utf8_file_atomic
|
2019-12-08 16:57:28 +00:00
|
|
|
from homeassistant.util.yaml import dump, load_yaml
|
2017-02-12 01:29:05 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN = "config"
|
2018-09-14 09:57:18 +00:00
|
|
|
SECTIONS = (
|
2019-07-31 19:25:30 +00:00
|
|
|
"area_registry",
|
|
|
|
"auth",
|
|
|
|
"auth_provider_homeassistant",
|
|
|
|
"automation",
|
|
|
|
"config_entries",
|
|
|
|
"core",
|
|
|
|
"device_registry",
|
|
|
|
"entity_registry",
|
|
|
|
"script",
|
2019-11-04 20:38:18 +00:00
|
|
|
"scene",
|
2018-09-14 09:57:18 +00:00
|
|
|
)
|
2020-01-27 07:01:35 +00:00
|
|
|
ACTION_CREATE_UPDATE = "create_update"
|
|
|
|
ACTION_DELETE = "delete"
|
2017-02-12 01:29:05 +00:00
|
|
|
|
|
|
|
|
2022-01-02 16:05:18 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the config component."""
|
2022-01-14 09:01:12 +00:00
|
|
|
frontend.async_register_built_in_panel(
|
|
|
|
hass, "config", "config", "hass:cog", require_admin=True
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2017-02-12 01:29:05 +00:00
|
|
|
|
2018-04-14 21:58:45 +00:00
|
|
|
async def setup_panel(panel_name):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up a panel."""
|
2019-08-23 16:53:33 +00:00
|
|
|
panel = importlib.import_module(f".{panel_name}", __name__)
|
2017-02-12 01:29:05 +00:00
|
|
|
|
|
|
|
if not panel:
|
2017-02-14 05:34:36 +00:00
|
|
|
return
|
2017-02-12 01:29:05 +00:00
|
|
|
|
2018-04-14 21:58:45 +00:00
|
|
|
success = await panel.async_setup(hass)
|
2017-02-12 01:29:05 +00:00
|
|
|
|
|
|
|
if success:
|
2019-08-23 16:53:33 +00:00
|
|
|
key = f"{DOMAIN}.{panel_name}"
|
2017-02-14 05:34:36 +00:00
|
|
|
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
|
|
|
|
2021-02-10 13:16:58 +00:00
|
|
|
tasks = [asyncio.create_task(setup_panel(panel_name)) for panel_name in SECTIONS]
|
2018-04-14 21:58:45 +00:00
|
|
|
|
|
|
|
if tasks:
|
2019-05-23 04:09:59 +00:00
|
|
|
await asyncio.wait(tasks)
|
2018-04-14 21:58:45 +00:00
|
|
|
|
2017-02-12 01:29:05 +00:00
|
|
|
return True
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
|
2017-05-10 01:44:00 +00:00
|
|
|
class BaseEditConfigView(HomeAssistantView):
|
2017-02-21 05:53:55 +00:00
|
|
|
"""Configure a Group endpoint."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
component,
|
|
|
|
config_type,
|
|
|
|
path,
|
|
|
|
key_schema,
|
|
|
|
data_schema,
|
|
|
|
*,
|
|
|
|
post_write_hook=None,
|
2019-09-27 15:48:48 +00:00
|
|
|
data_validator=None,
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2017-02-21 05:53:55 +00:00
|
|
|
"""Initialize a config view."""
|
2019-08-23 16:53:33 +00:00
|
|
|
self.url = f"/api/config/{component}/{config_type}/{{config_key}}"
|
|
|
|
self.name = f"api:config:{component}:{config_type}"
|
2017-02-21 05:53:55 +00:00
|
|
|
self.path = path
|
|
|
|
self.key_schema = key_schema
|
|
|
|
self.data_schema = data_schema
|
|
|
|
self.post_write_hook = post_write_hook
|
2019-09-27 15:48:48 +00:00
|
|
|
self.data_validator = data_validator
|
2020-02-09 01:26:58 +00:00
|
|
|
self.mutation_lock = asyncio.Lock()
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2017-05-10 01:44:00 +00:00
|
|
|
def _empty_config(self):
|
|
|
|
"""Empty config if file not found."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _get_value(self, hass, data, config_key):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Get value."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _write_value(self, hass, data, config_key, new_value):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Set value."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2019-05-14 07:16:37 +00:00
|
|
|
def _delete_value(self, hass, data, config_key):
|
|
|
|
"""Delete value."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2018-04-14 21:58:45 +00:00
|
|
|
async def get(self, request, config_key):
|
2017-02-21 05:53:55 +00:00
|
|
|
"""Fetch device specific config."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2020-02-09 01:26:58 +00:00
|
|
|
async with self.mutation_lock:
|
|
|
|
current = await self.read_config(hass)
|
|
|
|
value = self._get_value(hass, current, config_key)
|
2017-05-10 01:44:00 +00:00
|
|
|
|
|
|
|
if value is None:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message("Resource not found", HTTPStatus.NOT_FOUND)
|
2017-05-10 01:44:00 +00:00
|
|
|
|
|
|
|
return self.json(value)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2018-04-14 21:58:45 +00:00
|
|
|
async def post(self, request, config_key):
|
2017-02-21 05:53:55 +00:00
|
|
|
"""Validate config and return results."""
|
|
|
|
try:
|
2018-04-14 21:58:45 +00:00
|
|
|
data = await request.json()
|
2017-02-21 05:53:55 +00:00
|
|
|
except ValueError:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
self.key_schema(config_key)
|
|
|
|
except vol.Invalid as err:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2019-09-27 15:48:48 +00:00
|
|
|
hass = request.app["hass"]
|
|
|
|
|
2017-02-21 05:53:55 +00:00
|
|
|
try:
|
|
|
|
# We just validate, we don't store that data because
|
|
|
|
# we don't want to store the defaults.
|
2019-09-27 15:48:48 +00:00
|
|
|
if self.data_validator:
|
|
|
|
await self.data_validator(hass, data)
|
|
|
|
else:
|
|
|
|
self.data_schema(data)
|
|
|
|
except (vol.Invalid, HomeAssistantError) as err:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message(
|
|
|
|
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
|
|
|
|
)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
path = hass.config.path(self.path)
|
|
|
|
|
2020-02-09 01:26:58 +00:00
|
|
|
async with self.mutation_lock:
|
|
|
|
current = await self.read_config(hass)
|
|
|
|
self._write_value(hass, current, config_key, data)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2020-02-09 01:26:58 +00:00
|
|
|
await hass.async_add_executor_job(_write, path, current)
|
2019-05-14 07:16:37 +00:00
|
|
|
|
|
|
|
if self.post_write_hook is not None:
|
2020-01-27 07:01:35 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
self.post_write_hook(ACTION_CREATE_UPDATE, config_key)
|
|
|
|
)
|
2019-05-14 07:16:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json({"result": "ok"})
|
2019-05-14 07:16:37 +00:00
|
|
|
|
|
|
|
async def delete(self, request, config_key):
|
|
|
|
"""Remove an entry."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2020-02-09 01:26:58 +00:00
|
|
|
async with self.mutation_lock:
|
|
|
|
current = await self.read_config(hass)
|
|
|
|
value = self._get_value(hass, current, config_key)
|
|
|
|
path = hass.config.path(self.path)
|
2019-05-14 07:16:37 +00:00
|
|
|
|
2020-02-09 01:26:58 +00:00
|
|
|
if value is None:
|
2021-09-22 18:59:52 +00:00
|
|
|
return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST)
|
2019-05-14 07:16:37 +00:00
|
|
|
|
2020-02-09 01:26:58 +00:00
|
|
|
self._delete_value(hass, current, config_key)
|
|
|
|
await hass.async_add_executor_job(_write, path, current)
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
if self.post_write_hook is not None:
|
2020-01-27 07:01:35 +00:00
|
|
|
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json({"result": "ok"})
|
2017-02-21 05:53:55 +00:00
|
|
|
|
2018-04-14 21:58:45 +00:00
|
|
|
async def read_config(self, hass):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Read the config."""
|
2020-10-08 07:20:39 +00:00
|
|
|
current = await hass.async_add_executor_job(_read, hass.config.path(self.path))
|
2017-05-10 01:44:00 +00:00
|
|
|
if not current:
|
|
|
|
current = self._empty_config()
|
|
|
|
return current
|
|
|
|
|
|
|
|
|
|
|
|
class EditKeyBasedConfigView(BaseEditConfigView):
|
|
|
|
"""Configure a list of entries."""
|
|
|
|
|
|
|
|
def _empty_config(self):
|
|
|
|
"""Return an empty config."""
|
|
|
|
return {}
|
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _get_value(self, hass, data, config_key):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Get value."""
|
2018-02-21 12:16:08 +00:00
|
|
|
return data.get(config_key)
|
2017-05-10 01:44:00 +00:00
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _write_value(self, hass, data, config_key, new_value):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Set value."""
|
|
|
|
data.setdefault(config_key, {}).update(new_value)
|
|
|
|
|
2019-05-14 07:16:37 +00:00
|
|
|
def _delete_value(self, hass, data, config_key):
|
|
|
|
"""Delete value."""
|
|
|
|
return data.pop(config_key)
|
|
|
|
|
2017-05-10 01:44:00 +00:00
|
|
|
|
|
|
|
class EditIdBasedConfigView(BaseEditConfigView):
|
|
|
|
"""Configure key based config entries."""
|
|
|
|
|
|
|
|
def _empty_config(self):
|
|
|
|
"""Return an empty config."""
|
|
|
|
return []
|
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _get_value(self, hass, data, config_key):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Get value."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return next((val for val in data if val.get(CONF_ID) == config_key), None)
|
2017-05-10 01:44:00 +00:00
|
|
|
|
2017-08-26 17:02:32 +00:00
|
|
|
def _write_value(self, hass, data, config_key, new_value):
|
2017-05-10 01:44:00 +00:00
|
|
|
"""Set value."""
|
2021-10-31 17:35:27 +00:00
|
|
|
if (value := self._get_value(hass, data, config_key)) is None:
|
2017-05-10 01:44:00 +00:00
|
|
|
value = {CONF_ID: config_key}
|
|
|
|
data.append(value)
|
|
|
|
|
|
|
|
value.update(new_value)
|
|
|
|
|
2019-05-14 07:16:37 +00:00
|
|
|
def _delete_value(self, hass, data, config_key):
|
|
|
|
"""Delete value."""
|
|
|
|
index = next(
|
2019-07-31 19:25:30 +00:00
|
|
|
idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key
|
|
|
|
)
|
2019-05-14 07:16:37 +00:00
|
|
|
data.pop(index)
|
|
|
|
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
def _read(path):
|
|
|
|
"""Read YAML helper."""
|
|
|
|
if not os.path.isfile(path):
|
2017-05-10 01:44:00 +00:00
|
|
|
return None
|
2017-02-21 05:53:55 +00:00
|
|
|
|
|
|
|
return load_yaml(path)
|
|
|
|
|
|
|
|
|
|
|
|
def _write(path, data):
|
|
|
|
"""Write YAML helper."""
|
2017-02-26 23:28:12 +00:00
|
|
|
# Do it before opening file. If dump causes error it will now not
|
|
|
|
# truncate the file.
|
2021-11-11 06:19:56 +00:00
|
|
|
contents = dump(data)
|
2021-11-15 10:19:31 +00:00
|
|
|
write_utf8_file_atomic(path, contents)
|