Browse Source

fix(config_flow): allow using cloud with locally discovered devices

When devices are locally discovered, we skipped straight to local step for manual entry of
non discovered data. But this includes the local key, which the user needs to fetch from
the cloud themselves, so it is more convenient for them to use the cloud discovery flow
as well.
During the cloud flow, detect that we already have a discovered device, and just fill in
the additional data for it instead of asking the user to select one of our cloud discovered
devices and doing a local search for the IP address.

Issue #5635
Jason Rumney 1 day ago
parent
commit
9dbeb313c4
2 changed files with 57 additions and 43 deletions
  1. 54 40
      custom_components/tuya_local/config_flow.py
  2. 3 3
      tests/test_config_flow.py

+ 54 - 40
custom_components/tuya_local/config_flow.py

@@ -60,7 +60,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
 
 
     __qr_code: str | None = None
     __qr_code: str | None = None
     __cloud_devices: dict[str, Any] = {}
     __cloud_devices: dict[str, Any] = {}
-    __cloud_device: dict[str, Any] | None = None
+    __discovered_device: dict[str, Any] | None = None
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
         """Initialize the config flow."""
         """Initialize the config flow."""
@@ -82,7 +82,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
         self._abort_if_unique_id_configured()
         self._abort_if_unique_id_configured()
         # Reuse the cloud-device plumbing that async_step_local reads for its
         # Reuse the cloud-device plumbing that async_step_local reads for its
         # form defaults; the local key is not known from discovery.
         # form defaults; the local key is not known from discovery.
-        self.__cloud_device = {
+        self.__discovered_device = {
             "id": device_id,
             "id": device_id,
             "ip": discovery_info.get(CONF_HOST),
             "ip": discovery_info.get(CONF_HOST),
             "version": discovery_info.get("version"),
             "version": discovery_info.get("version"),
@@ -92,7 +92,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
         self.context["title_placeholders"] = {
         self.context["title_placeholders"] = {
             "name": discovery_info.get(CONF_HOST) or device_id
             "name": discovery_info.get(CONF_HOST) or device_id
         }
         }
-        return await self.async_step_local()
+        return await self.async_step_user()
 
 
     async def async_step_user(self, user_input=None):
     async def async_step_user(self, user_input=None):
         errors = {}
         errors = {}
@@ -218,7 +218,19 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
             )
             )
 
 
         self.__cloud_devices = await self.cloud.async_get_devices()
         self.__cloud_devices = await self.cloud.async_get_devices()
-
+        if self.__discovered_device:
+            # If local discovery already found a device, we can skip the choose device step
+            # after updating discovery_info.
+            device_choice = self.__cloud_devices.get(self.__discovered_device["id"])
+            if device_choice:
+                self.__discovered_device[CONF_LOCAL_KEY] = device_choice.get(
+                    CONF_LOCAL_KEY
+                )
+                self.__discovered_device["product_id"] = device_choice.get("product_id")
+                self.__discovered_device["product_name"] = device_choice.get(
+                    "product_name"
+                )
+            return await self.async_step_local()
         return await self.async_step_choose_device()
         return await self.async_step_choose_device()
 
 
     async def async_step_choose_device(self, user_input=None):
     async def async_step_choose_device(self, user_input=None):
@@ -230,7 +242,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
                 # This is a directly addable device.
                 # This is a directly addable device.
                 if user_input["hub_id"] == "None":
                 if user_input["hub_id"] == "None":
                     device_choice["ip"] = ""
                     device_choice["ip"] = ""
-                    self.__cloud_device = device_choice
+                    self.__discovered_device = device_choice
                     return await self.async_step_search()
                     return await self.async_step_search()
                 else:
                 else:
                     # Show error if user selected a hub.
                     # Show error if user selected a hub.
@@ -251,7 +263,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
                     # Communicate the sub device product id to help match the
                     # Communicate the sub device product id to help match the
                     # correect device config in the next step.
                     # correect device config in the next step.
                     hub_choice["product_id"] = device_choice["product_id"]
                     hub_choice["product_id"] = device_choice["product_id"]
-                    self.__cloud_device = hub_choice
+                    self.__discovered_device = hub_choice
                     return await self.async_step_search()
                     return await self.async_step_search()
                 else:
                 else:
                     # Show error if user did not select a hub.
                     # Show error if user did not select a hub.
@@ -320,11 +332,11 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
     @property
     @property
     def _device_name_placeholder(self) -> str:
     def _device_name_placeholder(self) -> str:
         """Return device name placeholder for step descriptions."""
         """Return device name placeholder for step descriptions."""
-        if self.__cloud_device and self.__cloud_device.get("product_name"):
+        if self.__discovered_device and self.__discovered_device.get("product_name"):
             parts = []
             parts = []
-            if self.__cloud_device.get("name"):
-                parts.append(self.__cloud_device["name"])
-            parts.append(self.__cloud_device["product_name"])
+            if self.__discovered_device.get("name"):
+                parts.append(self.__discovered_device["name"])
+            parts.append(self.__discovered_device["product_name"])
             return "**" + " — ".join(parts) + "**\n\n"
             return "**" + " — ".join(parts) + "**\n\n"
         return ""
         return ""
 
 
@@ -335,27 +347,27 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
             # will just leave the IP address blank and hope the user can discover the IP by other
             # will just leave the IP address blank and hope the user can discover the IP by other
             # means such as router device IP assignments.
             # means such as router device IP assignments.
             _LOGGER.debug(
             _LOGGER.debug(
-                f"Scanning network to get IP address for {self.__cloud_device.get('id', 'DEVICE_KEY_UNAVAILABLE')}."
+                f"Scanning network to get IP address for {self.__discovered_device.get('id', 'DEVICE_KEY_UNAVAILABLE')}."
             )
             )
-            self.__cloud_device["ip"] = ""
+            self.__discovered_device["ip"] = ""
             try:
             try:
                 local_device = await self.hass.async_add_executor_job(
                 local_device = await self.hass.async_add_executor_job(
-                    scan_for_device, self.__cloud_device.get("id")
+                    scan_for_device, self.__discovered_device.get("id")
                 )
                 )
             except OSError:
             except OSError:
                 local_device = {"ip": None, "version": ""}
                 local_device = {"ip": None, "version": ""}
 
 
             if local_device.get("ip"):
             if local_device.get("ip"):
                 _LOGGER.debug(f"Found: {local_device}")
                 _LOGGER.debug(f"Found: {local_device}")
-                self.__cloud_device["ip"] = local_device.get("ip")
-                self.__cloud_device["version"] = local_device.get("version")
-                if not self.__cloud_device.get(CONF_DEVICE_CID):
-                    self.__cloud_device["local_product_id"] = local_device.get(
+                self.__discovered_device["ip"] = local_device.get("ip")
+                self.__discovered_device["version"] = local_device.get("version")
+                if not self.__discovered_device.get(CONF_DEVICE_CID):
+                    self.__discovered_device["local_product_id"] = local_device.get(
                         "productKey"
                         "productKey"
                     )
                     )
             else:
             else:
                 _LOGGER.warning(
                 _LOGGER.warning(
-                    f"Could not find device: {self.__cloud_device.get('id', 'DEVICE_KEY_UNAVAILABLE')}"
+                    f"Could not find device: {self.__discovered_device.get('id', 'DEVICE_KEY_UNAVAILABLE')}"
                 )
                 )
             return await self.async_step_local()
             return await self.async_step_local()
 
 
@@ -378,15 +390,15 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
         polling_opts = {"default": False}
         polling_opts = {"default": False}
         devcid_opts = {}
         devcid_opts = {}
 
 
-        if self.__cloud_device is not None:
+        if self.__discovered_device is not None:
             # We already have some or all of the device settings from the cloud flow. Set them into the defaults.
             # We already have some or all of the device settings from the cloud flow. Set them into the defaults.
-            devid_opts = {"default": self.__cloud_device.get("id")}
-            host_opts = {"default": self.__cloud_device.get("ip")}
-            key_opts = {"default": self.__cloud_device.get(CONF_LOCAL_KEY)}
-            if self.__cloud_device.get("version"):
-                proto_opts = {"default": str(self.__cloud_device.get("version"))}
-            if self.__cloud_device.get(CONF_DEVICE_CID):
-                devcid_opts = {"default": self.__cloud_device.get(CONF_DEVICE_CID)}
+            devid_opts = {"default": self.__discovered_device.get("id")}
+            host_opts = {"default": self.__discovered_device.get("ip")}
+            key_opts = {"default": self.__discovered_device.get(CONF_LOCAL_KEY)}
+            if self.__discovered_device.get("version"):
+                proto_opts = {"default": str(self.__discovered_device.get("version"))}
+            if self.__discovered_device.get(CONF_DEVICE_CID):
+                devcid_opts = {"default": self.__discovered_device.get(CONF_DEVICE_CID)}
 
 
         if user_input is not None:
         if user_input is not None:
             proto = user_input.get(CONF_PROTOCOL_VERSION)
             proto = user_input.get(CONF_PROTOCOL_VERSION)
@@ -407,14 +419,14 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
                         **self.data,
                         **self.data,
                         CONF_PROTOCOL_VERSION: self._auto_detected_protocol,
                         CONF_PROTOCOL_VERSION: self._auto_detected_protocol,
                     }
                     }
-                if self.__cloud_device:
-                    if self.__cloud_device.get("product_id"):
+                if self.__discovered_device:
+                    if self.__discovered_device.get("product_id"):
                         self.device.set_detected_product_id(
                         self.device.set_detected_product_id(
-                            self.__cloud_device.get("product_id")
+                            self.__discovered_device.get("product_id")
                         )
                         )
-                    if self.__cloud_device.get("local_product_id"):
+                    if self.__discovered_device.get("local_product_id"):
                         self.device.set_detected_product_id(
                         self.device.set_detected_product_id(
-                            self.__cloud_device.get("local_product_id")
+                            self.__discovered_device.get("local_product_id")
                         )
                         )
                 await self.async_set_unique_id(get_device_id(user_input))
                 await self.async_set_unique_id(get_device_id(user_input))
                 self._abort_if_unique_id_configured()
                 self._abort_if_unique_id_configured()
@@ -492,23 +504,25 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
 
 
         best_match = int(best_match)
         best_match = int(best_match)
         dps = self.device._get_cached_state()
         dps = self.device._get_cached_state()
-        if self.__cloud_device:
+        if self.__discovered_device:
             _LOGGER.warning(
             _LOGGER.warning(
                 "Adding %s device with product id %s",
                 "Adding %s device with product id %s",
-                self.__cloud_device.get("product_name", "UNKNOWN"),
-                self.__cloud_device.get("product_id", "UNKNOWN"),
+                self.__discovered_device.get("product_name", "UNKNOWN"),
+                self.__discovered_device.get("product_id", "UNKNOWN"),
             )
             )
-            if self.__cloud_device.get("local_product_id") and self.__cloud_device.get(
+            if self.__discovered_device.get(
+                "local_product_id"
+            ) and self.__discovered_device.get(
                 "local_product_id"
                 "local_product_id"
-            ) != self.__cloud_device.get("product_id"):
+            ) != self.__discovered_device.get("product_id"):
                 _LOGGER.warning(
                 _LOGGER.warning(
                     "Local product id differs from cloud: %s",
                     "Local product id differs from cloud: %s",
-                    self.__cloud_device.get("local_product_id"),
+                    self.__discovered_device.get("local_product_id"),
                 )
                 )
             try:
             try:
                 self.init_cloud()
                 self.init_cloud()
                 model = await self.cloud.async_get_datamodel(
                 model = await self.cloud.async_get_datamodel(
-                    self.__cloud_device.get("id"),
+                    self.__discovered_device.get("id"),
                 )
                 )
                 if model:
                 if model:
                     _LOGGER.warning(
                     _LOGGER.warning(
@@ -574,8 +588,8 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
                 title=title, data={**self.data, **user_input}
                 title=title, data={**self.data, **user_input}
             )
             )
         default_name = config.name
         default_name = config.name
-        if self.__cloud_device and self.__cloud_device.get("name"):
-            default_name = self.__cloud_device["name"]
+        if self.__discovered_device and self.__discovered_device.get("name"):
+            default_name = self.__discovered_device["name"]
         schema = {vol.Required(CONF_NAME, default=default_name): str}
         schema = {vol.Required(CONF_NAME, default=default_name): str}
 
 
         return self.async_show_form(
         return self.async_show_form(

+ 3 - 3
tests/test_config_flow.py

@@ -1682,8 +1682,8 @@ async def test_flow_choose_entities_uses_cloud_name_as_default(
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio
-async def test_flow_integration_discovery_shows_local_form(hass):
-    """A device found by background discovery advances to the local setup form."""
+async def test_flow_integration_discovery_shows_user_form(hass):
+    """A device found by background discovery advances to the user setup form."""
     result = await hass.config_entries.flow.async_init(
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         DOMAIN,
         context={"source": "integration_discovery"},
         context={"source": "integration_discovery"},
@@ -1695,7 +1695,7 @@ async def test_flow_integration_discovery_shows_local_form(hass):
         },
         },
     )
     )
     assert result["type"] == "form"
     assert result["type"] == "form"
-    assert result["step_id"] == "local"
+    assert result["step_id"] == "user"
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio