From dff5e457614513eca71a016e36e136a5f76d14e9 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 25 Jan 2024 20:20:19 -1000
Subject: [PATCH] Small speed up to listing config entries in the websocket api
 (#108892)

---
 .../components/config/config_entries.py       | 29 +++++++++----------
 homeassistant/config_entries.py               | 13 +++++++++
 tests/test_config_entries.py                  |  7 +++++
 3 files changed, 33 insertions(+), 16 deletions(-)

diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index c289459a2af..b19c0101232 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -516,7 +516,7 @@ async def async_matching_config_entries(
     if not type_filter:
         return [entry_json(entry) for entry in entries]
 
-    integrations = {}
+    integrations: dict[str, Integration] = {}
     # Fetch all the integrations so we can check their type
     domains = {entry.domain for entry in entries}
     for domain_key, integration_or_exc in (
@@ -531,35 +531,32 @@ async def async_matching_config_entries(
     # when only helpers are requested, also filter out entries
     # from unknown integrations. This prevent them from showing
     # up in the helpers UI.
-    entries = [
-        entry
+    filter_is_not_helper = type_filter != ["helper"]
+    filter_set = set(type_filter)
+    return [
+        entry_json(entry)
         for entry in entries
-        if (type_filter != ["helper"] and entry.domain not in integrations)
-        or (
-            entry.domain in integrations
-            and integrations[entry.domain].integration_type in type_filter
+        # If the filter is not 'helper', we still include the integration
+        # even if its not returned from async_get_integrations for backwards
+        # compatibility.
+        if (
+            (integration := integrations.get(entry.domain))
+            and integration.integration_type in filter_set
         )
+        or (filter_is_not_helper and entry.domain not in integrations)
     ]
 
-    return [entry_json(entry) for entry in entries]
-
 
 @callback
 def entry_json(entry: config_entries.ConfigEntry) -> dict[str, Any]:
     """Return JSON value of a config entry."""
-    handler = config_entries.HANDLERS.get(entry.domain)
-    # work out if handler has support for options flow
-    supports_options = handler is not None and handler.async_supports_options_flow(
-        entry
-    )
-
     return {
         "entry_id": entry.entry_id,
         "domain": entry.domain,
         "title": entry.title,
         "source": entry.source,
         "state": entry.state.value,
-        "supports_options": supports_options,
+        "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,
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 7eee83953a7..d9cfbd08886 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -54,6 +54,7 @@ if TYPE_CHECKING:
     from .components.zeroconf import ZeroconfServiceInfo
     from .helpers.service_info.mqtt import MqttServiceInfo
 
+
 _LOGGER = logging.getLogger(__name__)
 
 SOURCE_BLUETOOTH = "bluetooth"
@@ -238,6 +239,7 @@ class ConfigEntry:
         "_integration_for_domain",
         "_tries",
         "_setup_again_job",
+        "_supports_options",
     )
 
     def __init__(
@@ -318,6 +320,9 @@ class ConfigEntry:
         # Supports remove device
         self.supports_remove_device: bool | None = None
 
+        # Supports options
+        self._supports_options: bool | None = None
+
         # Listeners to call on update
         self.update_listeners: list[UpdateListenerType] = []
 
@@ -351,6 +356,14 @@ class ConfigEntry:
             f"title={self.title} state={self.state} unique_id={self.unique_id}>"
         )
 
+    @property
+    def supports_options(self) -> bool:
+        """Return if entry supports config options."""
+        if self._supports_options is None and (handler := HANDLERS.get(self.domain)):
+            # work out if handler has support for options flow
+            self._supports_options = handler.async_supports_options_flow(self)
+        return self._supports_options or False
+
     async def async_setup(
         self,
         hass: HomeAssistant,
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index db382ac35f4..e9e1437c06c 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -734,6 +734,7 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None:
         "_integration_for_domain",
         "_tries",
         "_setup_again_job",
+        "_supports_options",
     }
 
     entry = MockConfigEntry(entry_id="mock-entry")
@@ -1176,6 +1177,7 @@ async def test_create_entry_options(
 
         entries = hass.config_entries.async_entries("comp")
         assert len(entries) == 1
+        assert entries[0].supports_options is False
         assert entries[0].data == {"example": "data"}
         assert entries[0].options == {"example": "option"}
 
@@ -1202,6 +1204,10 @@ async def test_entry_options(
 
             return OptionsFlowHandler()
 
+        def async_supports_options_flow(self, entry: MockConfigEntry) -> bool:
+            """Test options flow."""
+            return True
+
     config_entries.HANDLERS["test"] = TestFlow()
     flow = await manager.options.async_create_flow(
         entry.entry_id, context={"source": "test"}, data=None
@@ -1216,6 +1222,7 @@ async def test_entry_options(
 
     assert entry.data == {"first": True}
     assert entry.options == {"second": True}
+    assert entry.supports_options is True
 
 
 async def test_entry_options_abort(
-- 
GitLab