mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Cache JSON representation of ConfigEntry objects (#110823)
* Cache JSON representation of ConfigEntry objects * fix recursive set * tweak * adjust * order
This commit is contained in:
parent
0d4c82b54d
commit
0a01161cdd
@ -21,6 +21,7 @@ from homeassistant.helpers.data_entry_flow import (
|
|||||||
FlowManagerResourceView,
|
FlowManagerResourceView,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.json import json_fragment
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationNotFound,
|
IntegrationNotFound,
|
||||||
@ -69,7 +70,10 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
|
|||||||
type_filter = None
|
type_filter = None
|
||||||
if "type" in request.query:
|
if "type" in request.query:
|
||||||
type_filter = [request.query["type"]]
|
type_filter = [request.query["type"]]
|
||||||
return self.json(await async_matching_config_entries(hass, type_filter, domain))
|
fragments = await _async_matching_config_entries_json_fragments(
|
||||||
|
hass, type_filter, domain
|
||||||
|
)
|
||||||
|
return self.json(fragments)
|
||||||
|
|
||||||
|
|
||||||
class ConfigManagerEntryResourceView(HomeAssistantView):
|
class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||||
@ -129,7 +133,8 @@ def _prepare_config_flow_result_json(
|
|||||||
return prepare_result_json(result)
|
return prepare_result_json(result)
|
||||||
|
|
||||||
data = result.copy()
|
data = result.copy()
|
||||||
data["result"] = entry_json(result["result"])
|
entry: config_entries.ConfigEntry = data["result"]
|
||||||
|
data["result"] = entry.as_json_fragment
|
||||||
data.pop("data")
|
data.pop("data")
|
||||||
data.pop("context")
|
data.pop("context")
|
||||||
return data
|
return data
|
||||||
@ -312,7 +317,7 @@ async def config_entry_get_single(
|
|||||||
if entry is None:
|
if entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
result = {"config_entry": entry_json(entry)}
|
result = {"config_entry": entry.as_json_fragment}
|
||||||
connection.send_result(msg["id"], result)
|
connection.send_result(msg["id"], result)
|
||||||
|
|
||||||
|
|
||||||
@ -347,7 +352,7 @@ async def config_entry_update(
|
|||||||
hass.config_entries.async_update_entry(entry, **changes)
|
hass.config_entries.async_update_entry(entry, **changes)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"config_entry": entry_json(entry),
|
"config_entry": entry.as_json_fragment,
|
||||||
"require_restart": False,
|
"require_restart": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,12 +458,10 @@ async def config_entries_get(
|
|||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Return matching config entries by type and/or domain."""
|
"""Return matching config entries by type and/or domain."""
|
||||||
connection.send_result(
|
fragments = await _async_matching_config_entries_json_fragments(
|
||||||
msg["id"],
|
|
||||||
await async_matching_config_entries(
|
|
||||||
hass, msg.get("type_filter"), msg.get("domain")
|
hass, msg.get("type_filter"), msg.get("domain")
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
connection.send_result(msg["id"], fragments)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@ -491,13 +494,15 @@ async def config_entries_subscribe(
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": change,
|
"type": change,
|
||||||
"entry": entry_json(entry),
|
"entry": entry.as_json_fragment,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
current_entries = await async_matching_config_entries(hass, type_filter, None)
|
current_entries = await _async_matching_config_entries_json_fragments(
|
||||||
|
hass, type_filter, None
|
||||||
|
)
|
||||||
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
|
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
|
||||||
hass,
|
hass,
|
||||||
config_entries.SIGNAL_CONFIG_ENTRY_CHANGED,
|
config_entries.SIGNAL_CONFIG_ENTRY_CHANGED,
|
||||||
@ -511,9 +516,9 @@ async def config_entries_subscribe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_matching_config_entries(
|
async def _async_matching_config_entries_json_fragments(
|
||||||
hass: HomeAssistant, type_filter: list[str] | None, domain: str | None
|
hass: HomeAssistant, type_filter: list[str] | None, domain: str | None
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[json_fragment]:
|
||||||
"""Return matching config entries by type and/or domain."""
|
"""Return matching config entries by type and/or domain."""
|
||||||
if domain:
|
if domain:
|
||||||
entries = hass.config_entries.async_entries(domain)
|
entries = hass.config_entries.async_entries(domain)
|
||||||
@ -521,7 +526,7 @@ async def async_matching_config_entries(
|
|||||||
entries = hass.config_entries.async_entries()
|
entries = hass.config_entries.async_entries()
|
||||||
|
|
||||||
if not type_filter:
|
if not type_filter:
|
||||||
return [entry_json(entry) for entry in entries]
|
return [entry.as_json_fragment for entry in entries]
|
||||||
|
|
||||||
integrations: dict[str, Integration] = {}
|
integrations: dict[str, Integration] = {}
|
||||||
# Fetch all the integrations so we can check their type
|
# Fetch all the integrations so we can check their type
|
||||||
@ -541,7 +546,7 @@ async def async_matching_config_entries(
|
|||||||
filter_is_not_helper = type_filter != ["helper"]
|
filter_is_not_helper = type_filter != ["helper"]
|
||||||
filter_set = set(type_filter)
|
filter_set = set(type_filter)
|
||||||
return [
|
return [
|
||||||
entry_json(entry)
|
entry.as_json_fragment
|
||||||
for entry in entries
|
for entry in entries
|
||||||
# If the filter is not 'helper', we still include the integration
|
# If the filter is not 'helper', we still include the integration
|
||||||
# even if its not returned from async_get_integrations for backwards
|
# even if its not returned from async_get_integrations for backwards
|
||||||
@ -552,22 +557,3 @@ async def async_matching_config_entries(
|
|||||||
)
|
)
|
||||||
or (filter_is_not_helper and entry.domain not in integrations)
|
or (filter_is_not_helper and entry.domain not in integrations)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def entry_json(entry: config_entries.ConfigEntry) -> dict[str, Any]:
|
|
||||||
"""Return JSON value of a config entry."""
|
|
||||||
return {
|
|
||||||
"entry_id": entry.entry_id,
|
|
||||||
"domain": entry.domain,
|
|
||||||
"title": entry.title,
|
|
||||||
"source": entry.source,
|
|
||||||
"state": entry.state.value,
|
|
||||||
"supports_options": entry.supports_options,
|
|
||||||
"supports_remove_device": entry.supports_remove_device or False,
|
|
||||||
"supports_unload": entry.supports_unload or False,
|
|
||||||
"pref_disable_new_entities": entry.pref_disable_new_entities,
|
|
||||||
"pref_disable_polling": entry.pref_disable_polling,
|
|
||||||
"disabled_by": entry.disabled_by,
|
|
||||||
"reason": entry.reason,
|
|
||||||
}
|
|
||||||
|
@ -12,6 +12,7 @@ from collections.abc import (
|
|||||||
Mapping,
|
Mapping,
|
||||||
ValuesView,
|
ValuesView,
|
||||||
)
|
)
|
||||||
|
import contextlib
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from enum import Enum, StrEnum
|
from enum import Enum, StrEnum
|
||||||
@ -49,6 +50,7 @@ from .helpers.event import (
|
|||||||
async_call_later,
|
async_call_later,
|
||||||
)
|
)
|
||||||
from .helpers.frame import report
|
from .helpers.frame import report
|
||||||
|
from .helpers.json import json_bytes, json_fragment
|
||||||
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
||||||
from .loader import async_suggest_report_issue
|
from .loader import async_suggest_report_issue
|
||||||
from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component
|
from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component
|
||||||
@ -56,6 +58,8 @@ from .util import uuid as uuid_util
|
|||||||
from .util.decorator import Registry
|
from .util.decorator import Registry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from .components.bluetooth import BluetoothServiceInfoBleak
|
from .components.bluetooth import BluetoothServiceInfoBleak
|
||||||
from .components.dhcp import DhcpServiceInfo
|
from .components.dhcp import DhcpServiceInfo
|
||||||
from .components.hassio import HassioServiceInfo
|
from .components.hassio import HassioServiceInfo
|
||||||
@ -63,6 +67,8 @@ if TYPE_CHECKING:
|
|||||||
from .components.usb import UsbServiceInfo
|
from .components.usb import UsbServiceInfo
|
||||||
from .components.zeroconf import ZeroconfServiceInfo
|
from .components.zeroconf import ZeroconfServiceInfo
|
||||||
from .helpers.service_info.mqtt import MqttServiceInfo
|
from .helpers.service_info.mqtt import MqttServiceInfo
|
||||||
|
else:
|
||||||
|
from .backports.functools import cached_property
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -233,37 +239,6 @@ UPDATE_ENTRY_CONFIG_ENTRY_ATTRS = {
|
|||||||
class ConfigEntry:
|
class ConfigEntry:
|
||||||
"""Hold a configuration entry."""
|
"""Hold a configuration entry."""
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"entry_id",
|
|
||||||
"version",
|
|
||||||
"minor_version",
|
|
||||||
"domain",
|
|
||||||
"title",
|
|
||||||
"data",
|
|
||||||
"options",
|
|
||||||
"unique_id",
|
|
||||||
"supports_unload",
|
|
||||||
"supports_remove_device",
|
|
||||||
"pref_disable_new_entities",
|
|
||||||
"pref_disable_polling",
|
|
||||||
"source",
|
|
||||||
"state",
|
|
||||||
"disabled_by",
|
|
||||||
"_setup_lock",
|
|
||||||
"update_listeners",
|
|
||||||
"reason",
|
|
||||||
"_async_cancel_retry_setup",
|
|
||||||
"_on_unload",
|
|
||||||
"reload_lock",
|
|
||||||
"_reauth_lock",
|
|
||||||
"_tasks",
|
|
||||||
"_background_tasks",
|
|
||||||
"_integration_for_domain",
|
|
||||||
"_tries",
|
|
||||||
"_setup_again_job",
|
|
||||||
"_supports_options",
|
|
||||||
)
|
|
||||||
|
|
||||||
entry_id: str
|
entry_id: str
|
||||||
domain: str
|
domain: str
|
||||||
title: str
|
title: str
|
||||||
@ -418,15 +393,42 @@ class ConfigEntry:
|
|||||||
raise AttributeError(f"{key} cannot be changed")
|
raise AttributeError(f"{key} cannot be changed")
|
||||||
|
|
||||||
super().__setattr__(key, value)
|
super().__setattr__(key, value)
|
||||||
|
self.clear_cache()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_options(self) -> bool:
|
def supports_options(self) -> bool:
|
||||||
"""Return if entry supports config options."""
|
"""Return if entry supports config options."""
|
||||||
if self._supports_options is None and (handler := HANDLERS.get(self.domain)):
|
if self._supports_options is None and (handler := HANDLERS.get(self.domain)):
|
||||||
# work out if handler has support for options flow
|
# work out if handler has support for options flow
|
||||||
self._supports_options = handler.async_supports_options_flow(self)
|
object.__setattr__(
|
||||||
|
self, "_supports_options", handler.async_supports_options_flow(self)
|
||||||
|
)
|
||||||
return self._supports_options or False
|
return self._supports_options or False
|
||||||
|
|
||||||
|
def clear_cache(self) -> None:
|
||||||
|
"""Clear cached properties."""
|
||||||
|
with contextlib.suppress(AttributeError):
|
||||||
|
delattr(self, "as_json_fragment")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def as_json_fragment(self) -> json_fragment:
|
||||||
|
"""Return JSON fragment of a config entry."""
|
||||||
|
json_repr = {
|
||||||
|
"entry_id": self.entry_id,
|
||||||
|
"domain": self.domain,
|
||||||
|
"title": self.title,
|
||||||
|
"source": self.source,
|
||||||
|
"state": self.state.value,
|
||||||
|
"supports_options": self.supports_options,
|
||||||
|
"supports_remove_device": self.supports_remove_device or False,
|
||||||
|
"supports_unload": self.supports_unload or False,
|
||||||
|
"pref_disable_new_entities": self.pref_disable_new_entities,
|
||||||
|
"pref_disable_polling": self.pref_disable_polling,
|
||||||
|
"disabled_by": self.disabled_by,
|
||||||
|
"reason": self.reason,
|
||||||
|
}
|
||||||
|
return json_fragment(json_bytes(json_repr))
|
||||||
|
|
||||||
async def async_setup(
|
async def async_setup(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -716,6 +718,7 @@ class ConfigEntry:
|
|||||||
_setter = object.__setattr__
|
_setter = object.__setattr__
|
||||||
_setter(self, "state", state)
|
_setter(self, "state", state)
|
||||||
_setter(self, "reason", reason)
|
_setter(self, "reason", reason)
|
||||||
|
self.clear_cache()
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self
|
hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self
|
||||||
)
|
)
|
||||||
@ -1261,6 +1264,7 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
|||||||
self._unindex_entry(entry_id)
|
self._unindex_entry(entry_id)
|
||||||
object.__setattr__(entry, "unique_id", new_unique_id)
|
object.__setattr__(entry, "unique_id", new_unique_id)
|
||||||
self._index_entry(entry)
|
self._index_entry(entry)
|
||||||
|
entry.clear_cache()
|
||||||
|
|
||||||
def get_entries_for_domain(self, domain: str) -> list[ConfigEntry]:
|
def get_entries_for_domain(self, domain: str) -> list[ConfigEntry]:
|
||||||
"""Get entries for a domain."""
|
"""Get entries for a domain."""
|
||||||
@ -1642,6 +1646,7 @@ class ConfigEntries:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
entry.clear_cache()
|
||||||
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
|
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import pytest
|
|||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow, loader
|
from homeassistant import config_entries, data_entry_flow, loader
|
||||||
|
from homeassistant.backports.functools import cached_property
|
||||||
from homeassistant.components import dhcp
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
from homeassistant.components.hassio import HassioServiceInfo
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -834,7 +835,14 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None:
|
|||||||
|
|
||||||
# Make sure the expected keys are present
|
# Make sure the expected keys are present
|
||||||
dict_repr = entry.as_dict()
|
dict_repr = entry.as_dict()
|
||||||
for key in config_entries.ConfigEntry.__slots__:
|
for key in config_entries.ConfigEntry.__dict__:
|
||||||
|
func = getattr(config_entries.ConfigEntry, key)
|
||||||
|
if (
|
||||||
|
key.startswith("__")
|
||||||
|
or callable(func)
|
||||||
|
or type(func) in (cached_property, property)
|
||||||
|
):
|
||||||
|
continue
|
||||||
assert key in dict_repr or key in excluded_from_dict
|
assert key in dict_repr or key in excluded_from_dict
|
||||||
assert not (key in dict_repr and key in excluded_from_dict)
|
assert not (key in dict_repr and key in excluded_from_dict)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user