mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Fix config modules being imported in the event loop (#112462)
* Fix config modules being imported in the event loop There was a late import in this integration because of the circular import. The code has been rearranged to avoid the circular imports * fixes * fixes * fix patching * make eager * remove unrelated change from this branch
This commit is contained in:
parent
f03be2fd9e
commit
3f9dbd3e25
@ -1,48 +1,44 @@
|
|||||||
"""Component to configure Home Assistant via an API."""
|
"""Component to configure Home Assistant via an API."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Callable, Coroutine
|
|
||||||
from http import HTTPStatus
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
from typing import Any, Generic, TypeVar, cast
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
from homeassistant.components.http import HomeAssistantView, require_admin
|
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||||
from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.setup import ATTR_COMPONENT
|
from homeassistant.setup import ATTR_COMPONENT
|
||||||
from homeassistant.util.file import write_utf8_file_atomic
|
|
||||||
from homeassistant.util.yaml import dump, load_yaml
|
|
||||||
from homeassistant.util.yaml.loader import JSON_TYPE
|
|
||||||
|
|
||||||
_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]])
|
from . import (
|
||||||
|
area_registry,
|
||||||
DOMAIN = "config"
|
auth,
|
||||||
|
auth_provider_homeassistant,
|
||||||
|
automation,
|
||||||
|
config_entries,
|
||||||
|
core,
|
||||||
|
device_registry,
|
||||||
|
entity_registry,
|
||||||
|
floor_registry,
|
||||||
|
label_registry,
|
||||||
|
scene,
|
||||||
|
script,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
SECTIONS = (
|
SECTIONS = (
|
||||||
"area_registry",
|
area_registry,
|
||||||
"auth",
|
auth,
|
||||||
"auth_provider_homeassistant",
|
auth_provider_homeassistant,
|
||||||
"automation",
|
automation,
|
||||||
"config_entries",
|
config_entries,
|
||||||
"core",
|
core,
|
||||||
"device_registry",
|
device_registry,
|
||||||
"entity_registry",
|
entity_registry,
|
||||||
"floor_registry",
|
floor_registry,
|
||||||
"label_registry",
|
label_registry,
|
||||||
"script",
|
script,
|
||||||
"scene",
|
scene,
|
||||||
)
|
)
|
||||||
ACTION_CREATE_UPDATE = "create_update"
|
|
||||||
ACTION_DELETE = "delete"
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
@ -53,231 +49,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
hass, "config", "config", "hass:cog", require_admin=True
|
hass, "config", "config", "hass:cog", require_admin=True
|
||||||
)
|
)
|
||||||
|
|
||||||
for panel_name in SECTIONS:
|
for panel in SECTIONS:
|
||||||
panel = importlib.import_module(f".{panel_name}", __name__)
|
|
||||||
|
|
||||||
if panel.async_setup(hass):
|
if panel.async_setup(hass):
|
||||||
key = f"{DOMAIN}.{panel_name}"
|
name = panel.__name__.split(".")[-1]
|
||||||
|
key = f"{DOMAIN}.{name}"
|
||||||
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BaseEditConfigView(HomeAssistantView, Generic[_DataT]):
|
|
||||||
"""Configure a Group endpoint."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
component: str,
|
|
||||||
config_type: str,
|
|
||||||
path: str,
|
|
||||||
key_schema: Callable[[Any], str],
|
|
||||||
data_schema: Callable[[dict[str, Any]], Any],
|
|
||||||
*,
|
|
||||||
post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None,
|
|
||||||
data_validator: Callable[
|
|
||||||
[HomeAssistant, str, dict[str, Any]],
|
|
||||||
Coroutine[Any, Any, dict[str, Any] | None],
|
|
||||||
]
|
|
||||||
| None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize a config view."""
|
|
||||||
self.url = f"/api/config/{component}/{config_type}/{{config_key}}"
|
|
||||||
self.name = f"api:config:{component}:{config_type}"
|
|
||||||
self.path = path
|
|
||||||
self.key_schema = key_schema
|
|
||||||
self.data_schema = data_schema
|
|
||||||
self.post_write_hook = post_write_hook
|
|
||||||
self.data_validator = data_validator
|
|
||||||
self.mutation_lock = asyncio.Lock()
|
|
||||||
|
|
||||||
def _empty_config(self) -> _DataT:
|
|
||||||
"""Empty config if file not found."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _get_value(
|
|
||||||
self, hass: HomeAssistant, data: _DataT, config_key: str
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Get value."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _write_value(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
data: _DataT,
|
|
||||||
config_key: str,
|
|
||||||
new_value: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Set value."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _delete_value(
|
|
||||||
self, hass: HomeAssistant, data: _DataT, config_key: str
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Delete value."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@require_admin
|
|
||||||
async def get(self, request: web.Request, config_key: str) -> web.Response:
|
|
||||||
"""Fetch device specific config."""
|
|
||||||
hass: HomeAssistant = request.app["hass"]
|
|
||||||
async with self.mutation_lock:
|
|
||||||
current = await self.read_config(hass)
|
|
||||||
value = self._get_value(hass, current, config_key)
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return self.json_message("Resource not found", HTTPStatus.NOT_FOUND)
|
|
||||||
|
|
||||||
return self.json(value)
|
|
||||||
|
|
||||||
@require_admin
|
|
||||||
async def post(self, request: web.Request, config_key: str) -> web.Response:
|
|
||||||
"""Validate config and return results."""
|
|
||||||
try:
|
|
||||||
data = await request.json()
|
|
||||||
except ValueError:
|
|
||||||
return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.key_schema(config_key)
|
|
||||||
except vol.Invalid as err:
|
|
||||||
return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
hass: HomeAssistant = request.app["hass"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# We just validate, we don't store that data because
|
|
||||||
# we don't want to store the defaults.
|
|
||||||
if self.data_validator:
|
|
||||||
await self.data_validator(hass, config_key, data)
|
|
||||||
else:
|
|
||||||
self.data_schema(data)
|
|
||||||
except (vol.Invalid, HomeAssistantError) as err:
|
|
||||||
return self.json_message(
|
|
||||||
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
path = hass.config.path(self.path)
|
|
||||||
|
|
||||||
async with self.mutation_lock:
|
|
||||||
current = await self.read_config(hass)
|
|
||||||
self._write_value(hass, current, config_key, data)
|
|
||||||
|
|
||||||
await hass.async_add_executor_job(_write, path, current)
|
|
||||||
|
|
||||||
if self.post_write_hook is not None:
|
|
||||||
hass.async_create_task(
|
|
||||||
self.post_write_hook(ACTION_CREATE_UPDATE, config_key)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.json({"result": "ok"})
|
|
||||||
|
|
||||||
@require_admin
|
|
||||||
async def delete(self, request: web.Request, config_key: str) -> web.Response:
|
|
||||||
"""Remove an entry."""
|
|
||||||
hass: HomeAssistant = request.app["hass"]
|
|
||||||
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)
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
self._delete_value(hass, current, config_key)
|
|
||||||
await hass.async_add_executor_job(_write, path, current)
|
|
||||||
|
|
||||||
if self.post_write_hook is not None:
|
|
||||||
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
|
|
||||||
|
|
||||||
return self.json({"result": "ok"})
|
|
||||||
|
|
||||||
async def read_config(self, hass: HomeAssistant) -> _DataT:
|
|
||||||
"""Read the config."""
|
|
||||||
current = await hass.async_add_executor_job(_read, hass.config.path(self.path))
|
|
||||||
if not current:
|
|
||||||
current = self._empty_config()
|
|
||||||
return cast(_DataT, current)
|
|
||||||
|
|
||||||
|
|
||||||
class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]):
|
|
||||||
"""Configure a list of entries."""
|
|
||||||
|
|
||||||
def _empty_config(self) -> dict[str, Any]:
|
|
||||||
"""Return an empty config."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _get_value(
|
|
||||||
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Get value."""
|
|
||||||
return data.get(config_key)
|
|
||||||
|
|
||||||
def _write_value(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
data: dict[str, dict[str, Any]],
|
|
||||||
config_key: str,
|
|
||||||
new_value: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Set value."""
|
|
||||||
data.setdefault(config_key, {}).update(new_value)
|
|
||||||
|
|
||||||
def _delete_value(
|
|
||||||
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Delete value."""
|
|
||||||
return data.pop(config_key)
|
|
||||||
|
|
||||||
|
|
||||||
class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]):
|
|
||||||
"""Configure key based config entries."""
|
|
||||||
|
|
||||||
def _empty_config(self) -> list[Any]:
|
|
||||||
"""Return an empty config."""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _get_value(
|
|
||||||
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Get value."""
|
|
||||||
return next((val for val in data if val.get(CONF_ID) == config_key), None)
|
|
||||||
|
|
||||||
def _write_value(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
data: list[dict[str, Any]],
|
|
||||||
config_key: str,
|
|
||||||
new_value: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Set value."""
|
|
||||||
if (value := self._get_value(hass, data, config_key)) is None:
|
|
||||||
value = {CONF_ID: config_key}
|
|
||||||
data.append(value)
|
|
||||||
|
|
||||||
value.update(new_value)
|
|
||||||
|
|
||||||
def _delete_value(
|
|
||||||
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
|
|
||||||
) -> None:
|
|
||||||
"""Delete value."""
|
|
||||||
index = next(
|
|
||||||
idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key
|
|
||||||
)
|
|
||||||
data.pop(index)
|
|
||||||
|
|
||||||
|
|
||||||
def _read(path: str) -> JSON_TYPE | None:
|
|
||||||
"""Read YAML helper."""
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return load_yaml(path)
|
|
||||||
|
|
||||||
|
|
||||||
def _write(path: str, data: dict | list) -> None:
|
|
||||||
"""Write YAML helper."""
|
|
||||||
# Do it before opening file. If dump causes error it will now not
|
|
||||||
# truncate the file.
|
|
||||||
contents = dump(data)
|
|
||||||
write_utf8_file_atomic(path, contents)
|
|
||||||
|
@ -14,7 +14,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
|
||||||
from . import ACTION_DELETE, EditIdBasedConfigView
|
from .const import ACTION_DELETE
|
||||||
|
from .view import EditIdBasedConfigView
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
5
homeassistant/components/config/const.py
Normal file
5
homeassistant/components/config/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for config."""
|
||||||
|
|
||||||
|
ACTION_CREATE_UPDATE = "create_update"
|
||||||
|
ACTION_DELETE = "delete"
|
||||||
|
DOMAIN = "config"
|
@ -10,7 +10,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD
|
|||||||
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
|
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
|
||||||
from . import ACTION_DELETE, EditIdBasedConfigView
|
from .const import ACTION_DELETE
|
||||||
|
from .view import EditIdBasedConfigView
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -13,7 +13,8 @@ from homeassistant.const import SERVICE_RELOAD
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
|
||||||
from . import ACTION_DELETE, EditKeyBasedConfigView
|
from .const import ACTION_DELETE
|
||||||
|
from .view import EditKeyBasedConfigView
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
243
homeassistant/components/config/view.py
Normal file
243
homeassistant/components/config/view.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
"""Component to configure Home Assistant via an API."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from http import HTTPStatus
|
||||||
|
import os
|
||||||
|
from typing import Any, Generic, TypeVar, cast
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.http import HomeAssistantView, require_admin
|
||||||
|
from homeassistant.const import CONF_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.util.file import write_utf8_file_atomic
|
||||||
|
from homeassistant.util.yaml import dump, load_yaml
|
||||||
|
from homeassistant.util.yaml.loader import JSON_TYPE
|
||||||
|
|
||||||
|
from .const import ACTION_CREATE_UPDATE, ACTION_DELETE
|
||||||
|
|
||||||
|
_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEditConfigView(HomeAssistantView, Generic[_DataT]):
|
||||||
|
"""Configure a Group endpoint."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
component: str,
|
||||||
|
config_type: str,
|
||||||
|
path: str,
|
||||||
|
key_schema: Callable[[Any], str],
|
||||||
|
data_schema: Callable[[dict[str, Any]], Any],
|
||||||
|
*,
|
||||||
|
post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None,
|
||||||
|
data_validator: Callable[
|
||||||
|
[HomeAssistant, str, dict[str, Any]],
|
||||||
|
Coroutine[Any, Any, dict[str, Any] | None],
|
||||||
|
]
|
||||||
|
| None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a config view."""
|
||||||
|
self.url = f"/api/config/{component}/{config_type}/{{config_key}}"
|
||||||
|
self.name = f"api:config:{component}:{config_type}"
|
||||||
|
self.path = path
|
||||||
|
self.key_schema = key_schema
|
||||||
|
self.data_schema = data_schema
|
||||||
|
self.post_write_hook = post_write_hook
|
||||||
|
self.data_validator = data_validator
|
||||||
|
self.mutation_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def _empty_config(self) -> _DataT:
|
||||||
|
"""Empty config if file not found."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _get_value(
|
||||||
|
self, hass: HomeAssistant, data: _DataT, config_key: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Get value."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _write_value(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: _DataT,
|
||||||
|
config_key: str,
|
||||||
|
new_value: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Set value."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _delete_value(
|
||||||
|
self, hass: HomeAssistant, data: _DataT, config_key: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Delete value."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
async def get(self, request: web.Request, config_key: str) -> web.Response:
|
||||||
|
"""Fetch device specific config."""
|
||||||
|
hass: HomeAssistant = request.app["hass"]
|
||||||
|
async with self.mutation_lock:
|
||||||
|
current = await self.read_config(hass)
|
||||||
|
value = self._get_value(hass, current, config_key)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return self.json_message("Resource not found", HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
return self.json(value)
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
async def post(self, request: web.Request, config_key: str) -> web.Response:
|
||||||
|
"""Validate config and return results."""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
except ValueError:
|
||||||
|
return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.key_schema(config_key)
|
||||||
|
except vol.Invalid as err:
|
||||||
|
return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
hass: HomeAssistant = request.app["hass"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We just validate, we don't store that data because
|
||||||
|
# we don't want to store the defaults.
|
||||||
|
if self.data_validator:
|
||||||
|
await self.data_validator(hass, config_key, data)
|
||||||
|
else:
|
||||||
|
self.data_schema(data)
|
||||||
|
except (vol.Invalid, HomeAssistantError) as err:
|
||||||
|
return self.json_message(
|
||||||
|
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
path = hass.config.path(self.path)
|
||||||
|
|
||||||
|
async with self.mutation_lock:
|
||||||
|
current = await self.read_config(hass)
|
||||||
|
self._write_value(hass, current, config_key, data)
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(_write, path, current)
|
||||||
|
|
||||||
|
if self.post_write_hook is not None:
|
||||||
|
hass.async_create_task(
|
||||||
|
self.post_write_hook(ACTION_CREATE_UPDATE, config_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.json({"result": "ok"})
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
async def delete(self, request: web.Request, config_key: str) -> web.Response:
|
||||||
|
"""Remove an entry."""
|
||||||
|
hass: HomeAssistant = request.app["hass"]
|
||||||
|
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)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
self._delete_value(hass, current, config_key)
|
||||||
|
await hass.async_add_executor_job(_write, path, current)
|
||||||
|
|
||||||
|
if self.post_write_hook is not None:
|
||||||
|
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
|
||||||
|
|
||||||
|
return self.json({"result": "ok"})
|
||||||
|
|
||||||
|
async def read_config(self, hass: HomeAssistant) -> _DataT:
|
||||||
|
"""Read the config."""
|
||||||
|
current = await hass.async_add_executor_job(_read, hass.config.path(self.path))
|
||||||
|
if not current:
|
||||||
|
current = self._empty_config()
|
||||||
|
return cast(_DataT, current)
|
||||||
|
|
||||||
|
|
||||||
|
class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]):
|
||||||
|
"""Configure a list of entries."""
|
||||||
|
|
||||||
|
def _empty_config(self) -> dict[str, Any]:
|
||||||
|
"""Return an empty config."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _get_value(
|
||||||
|
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Get value."""
|
||||||
|
return data.get(config_key)
|
||||||
|
|
||||||
|
def _write_value(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: dict[str, dict[str, Any]],
|
||||||
|
config_key: str,
|
||||||
|
new_value: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Set value."""
|
||||||
|
data.setdefault(config_key, {}).update(new_value)
|
||||||
|
|
||||||
|
def _delete_value(
|
||||||
|
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Delete value."""
|
||||||
|
return data.pop(config_key)
|
||||||
|
|
||||||
|
|
||||||
|
class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]):
|
||||||
|
"""Configure key based config entries."""
|
||||||
|
|
||||||
|
def _empty_config(self) -> list[Any]:
|
||||||
|
"""Return an empty config."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_value(
|
||||||
|
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Get value."""
|
||||||
|
return next((val for val in data if val.get(CONF_ID) == config_key), None)
|
||||||
|
|
||||||
|
def _write_value(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: list[dict[str, Any]],
|
||||||
|
config_key: str,
|
||||||
|
new_value: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Set value."""
|
||||||
|
if (value := self._get_value(hass, data, config_key)) is None:
|
||||||
|
value = {CONF_ID: config_key}
|
||||||
|
data.append(value)
|
||||||
|
|
||||||
|
value.update(new_value)
|
||||||
|
|
||||||
|
def _delete_value(
|
||||||
|
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
|
||||||
|
) -> None:
|
||||||
|
"""Delete value."""
|
||||||
|
index = next(
|
||||||
|
idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key
|
||||||
|
)
|
||||||
|
data.pop(index)
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path: str) -> JSON_TYPE | None:
|
||||||
|
"""Read YAML helper."""
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return load_yaml(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _write(path: str, data: dict | list) -> None:
|
||||||
|
"""Write YAML helper."""
|
||||||
|
# Do it before opening file. If dump causes error it will now not
|
||||||
|
# truncate the file.
|
||||||
|
contents = dump(data)
|
||||||
|
write_utf8_file_atomic(path, contents)
|
@ -51,11 +51,11 @@ def mock_config_store(data=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.config._read",
|
"homeassistant.components.config.view._read",
|
||||||
side_effect=mock_read,
|
side_effect=mock_read,
|
||||||
autospec=True,
|
autospec=True,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.config._write",
|
"homeassistant.components.config.view._write",
|
||||||
side_effect=mock_write,
|
side_effect=mock_write,
|
||||||
autospec=True,
|
autospec=True,
|
||||||
), patch(
|
), patch(
|
||||||
|
@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components import config
|
from homeassistant.components import config
|
||||||
|
from homeassistant.components.config import automation
|
||||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
|
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -41,7 +42,7 @@ async def test_get_automation_config(
|
|||||||
setup_automation,
|
setup_automation,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test getting automation config."""
|
"""Test getting automation config."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -64,7 +65,7 @@ async def test_update_automation_config(
|
|||||||
setup_automation,
|
setup_automation,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating automation config."""
|
"""Test updating automation config."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == []
|
assert sorted(hass.states.async_entity_ids("automation")) == []
|
||||||
@ -153,7 +154,7 @@ async def test_update_automation_config_with_error(
|
|||||||
validation_error: str,
|
validation_error: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating automation config with errors."""
|
"""Test updating automation config with errors."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == []
|
assert sorted(hass.states.async_entity_ids("automation")) == []
|
||||||
@ -206,7 +207,7 @@ async def test_update_automation_config_with_blueprint_substitution_error(
|
|||||||
validation_error: str,
|
validation_error: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating automation config with errors."""
|
"""Test updating automation config with errors."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == []
|
assert sorted(hass.states.async_entity_ids("automation")) == []
|
||||||
@ -242,7 +243,7 @@ async def test_update_remove_key_automation_config(
|
|||||||
setup_automation,
|
setup_automation,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating automation config while removing a key."""
|
"""Test updating automation config while removing a key."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == []
|
assert sorted(hass.states.async_entity_ids("automation")) == []
|
||||||
@ -281,7 +282,7 @@ async def test_bad_formatted_automations(
|
|||||||
setup_automation,
|
setup_automation,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we handle automations without ID."""
|
"""Test that we handle automations without ID."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == []
|
assert sorted(hass.states.async_entity_ids("automation")) == []
|
||||||
@ -347,7 +348,7 @@ async def test_delete_automation(
|
|||||||
|
|
||||||
assert len(entity_registry.entities) == 2
|
assert len(entity_registry.entities) == 2
|
||||||
|
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("automation")) == [
|
assert sorted(hass.states.async_entity_ids("automation")) == [
|
||||||
@ -385,7 +386,7 @@ async def test_api_calls_require_admin(
|
|||||||
setup_automation,
|
setup_automation,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test cloud APIs endpoints do not work as a normal user."""
|
"""Test cloud APIs endpoints do not work as a normal user."""
|
||||||
with patch.object(config, "SECTIONS", ["automation"]):
|
with patch.object(config, "SECTIONS", [automation]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}]
|
hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}]
|
||||||
|
@ -6,6 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components import config
|
from homeassistant.components import config
|
||||||
|
from homeassistant.components.config import core
|
||||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_UNIT_SYSTEM,
|
CONF_UNIT_SYSTEM,
|
||||||
@ -23,7 +24,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def client(hass, hass_ws_client):
|
async def client(hass, hass_ws_client):
|
||||||
"""Fixture that can interact with the config manager API."""
|
"""Fixture that can interact with the config manager API."""
|
||||||
with patch.object(config, "SECTIONS", ["core"]):
|
with patch.object(config, "SECTIONS", [core]):
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
return await hass_ws_client(hass)
|
return await hass_ws_client(hass)
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ async def test_validate_config_ok(
|
|||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test checking config."""
|
"""Test checking config."""
|
||||||
with patch.object(config, "SECTIONS", ["core"]):
|
with patch.object(config, "SECTIONS", [core]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -95,7 +96,7 @@ async def test_validate_config_requires_admin(
|
|||||||
hass_read_only_access_token: str,
|
hass_read_only_access_token: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test checking configuration does not work as a normal user."""
|
"""Test checking configuration does not work as a normal user."""
|
||||||
with patch.object(config, "SECTIONS", ["core"]):
|
with patch.object(config, "SECTIONS", [core]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
client = await hass_client(hass_read_only_access_token)
|
client = await hass_client(hass_read_only_access_token)
|
||||||
@ -180,7 +181,7 @@ async def test_websocket_core_update_not_admin(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test core config fails for non admin."""
|
"""Test core config fails for non admin."""
|
||||||
hass_admin_user.groups = []
|
hass_admin_user.groups = []
|
||||||
with patch.object(config, "SECTIONS", ["core"]):
|
with patch.object(config, "SECTIONS", [core]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components import config
|
from homeassistant.components import config
|
||||||
|
from homeassistant.components.config import scene
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ async def test_create_scene(
|
|||||||
setup_scene,
|
setup_scene,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test creating a scene."""
|
"""Test creating a scene."""
|
||||||
with patch.object(config, "SECTIONS", ["scene"]):
|
with patch.object(config, "SECTIONS", [scene]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("scene")) == []
|
assert sorted(hass.states.async_entity_ids("scene")) == []
|
||||||
@ -74,7 +75,7 @@ async def test_update_scene(
|
|||||||
setup_scene,
|
setup_scene,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating a scene."""
|
"""Test updating a scene."""
|
||||||
with patch.object(config, "SECTIONS", ["scene"]):
|
with patch.object(config, "SECTIONS", [scene]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("scene")) == []
|
assert sorted(hass.states.async_entity_ids("scene")) == []
|
||||||
@ -122,7 +123,7 @@ async def test_bad_formatted_scene(
|
|||||||
setup_scene,
|
setup_scene,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we handle scene without ID."""
|
"""Test that we handle scene without ID."""
|
||||||
with patch.object(config, "SECTIONS", ["scene"]):
|
with patch.object(config, "SECTIONS", [scene]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("scene")) == []
|
assert sorted(hass.states.async_entity_ids("scene")) == []
|
||||||
@ -192,7 +193,7 @@ async def test_delete_scene(
|
|||||||
|
|
||||||
assert len(entity_registry.entities) == 2
|
assert len(entity_registry.entities) == 2
|
||||||
|
|
||||||
with patch.object(config, "SECTIONS", ["scene"]):
|
with patch.object(config, "SECTIONS", [scene]):
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("scene")) == [
|
assert sorted(hass.states.async_entity_ids("scene")) == [
|
||||||
@ -232,7 +233,7 @@ async def test_api_calls_require_admin(
|
|||||||
setup_scene,
|
setup_scene,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test scene APIs endpoints do not work as a normal user."""
|
"""Test scene APIs endpoints do not work as a normal user."""
|
||||||
with patch.object(config, "SECTIONS", ["scene"]):
|
with patch.object(config, "SECTIONS", [scene]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
hass_config_store["scenes.yaml"] = [
|
hass_config_store["scenes.yaml"] = [
|
||||||
|
@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components import config
|
from homeassistant.components import config
|
||||||
|
from homeassistant.components.config import script
|
||||||
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
|
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -32,7 +33,7 @@ async def test_get_script_config(
|
|||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test getting script config."""
|
"""Test getting script config."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -55,7 +56,7 @@ async def test_update_script_config(
|
|||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating script config."""
|
"""Test updating script config."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == []
|
assert sorted(hass.states.async_entity_ids("script")) == []
|
||||||
@ -91,7 +92,7 @@ async def test_invalid_object_id(
|
|||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test creating a script with an invalid object_id."""
|
"""Test creating a script with an invalid object_id."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == []
|
assert sorted(hass.states.async_entity_ids("script")) == []
|
||||||
@ -156,7 +157,7 @@ async def test_update_script_config_with_error(
|
|||||||
validation_error: str,
|
validation_error: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating script config with errors."""
|
"""Test updating script config with errors."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == []
|
assert sorted(hass.states.async_entity_ids("script")) == []
|
||||||
@ -207,7 +208,7 @@ async def test_update_script_config_with_blueprint_substitution_error(
|
|||||||
validation_error: str,
|
validation_error: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating script config with errors."""
|
"""Test updating script config with errors."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == []
|
assert sorted(hass.states.async_entity_ids("script")) == []
|
||||||
@ -240,7 +241,7 @@ async def test_update_remove_key_script_config(
|
|||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating script config while removing a key."""
|
"""Test updating script config while removing a key."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == []
|
assert sorted(hass.states.async_entity_ids("script")) == []
|
||||||
@ -287,7 +288,7 @@ async def test_delete_script(
|
|||||||
hass_config_store,
|
hass_config_store,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test deleting a script."""
|
"""Test deleting a script."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
assert sorted(hass.states.async_entity_ids("script")) == [
|
assert sorted(hass.states.async_entity_ids("script")) == [
|
||||||
@ -326,7 +327,7 @@ async def test_api_calls_require_admin(
|
|||||||
hass_config_store,
|
hass_config_store,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test script APIs endpoints do not work as a normal user."""
|
"""Test script APIs endpoints do not work as a normal user."""
|
||||||
with patch.object(config, "SECTIONS", ["script"]):
|
with patch.object(config, "SECTIONS", [script]):
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
|
||||||
hass_config_store["scripts.yaml"] = {
|
hass_config_store["scripts.yaml"] = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user