浏览代码

Fix: stale device cache (#5050)

* feat(tonepie): add Tonepie T1Pro cat litter box v3 configuration

* fix: clear stale tinytuya device cache after failed setup

* Delete tonepie_t1pro_catlitterbox_v3.yaml
Giuseppe Barchetta 1 周之前
父节点
当前提交
66acb46781
共有 2 个文件被更改,包括 68 次插入1 次删除
  1. 18 1
      custom_components/tuya_local/__init__.py
  2. 50 0
      tests/test_config_flow.py

+ 18 - 1
custom_components/tuya_local/__init__.py

@@ -74,6 +74,20 @@ def get_device_unique_id(entry: ConfigEntry):
     )
 
 
+def cleanup_failed_device(hass: HomeAssistant, device_id: str):
+    """Drop cached device objects left behind by failed setup."""
+    domain_data = hass.data.get(DOMAIN, {})
+    stale = domain_data.pop(device_id, None)
+    if not stale:
+        return
+
+    api = stale.get("tuyadevice")
+    if api:
+        api.set_socketPersistent(False)
+        if api.parent:
+            api.parent.set_socketPersistent(False)
+
+
 async def async_migrate_entry(hass, entry: ConfigEntry):
     """Migrate to latest config format."""
 
@@ -885,9 +899,10 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
 
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+    device_id = get_device_id(entry.data)
     _LOGGER.debug(
         "Setting up entry for device: %s",
-        get_device_id(entry.data),
+        device_id,
     )
     config = {**entry.data, **entry.options, "name": entry.title}
     try:
@@ -895,9 +910,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
         await device.async_refresh()
 
     except Exception as e:
+        cleanup_failed_device(hass, device_id)
         raise ConfigEntryNotReady("tuya-local device not ready") from e
 
     if not device.has_returned_state:
+        cleanup_failed_device(hass, device_id)
         raise ConfigEntryNotReady("tuya-local device offline")
 
     device_conf = await hass.async_add_executor_job(

+ 50 - 0
tests/test_config_flow.py

@@ -4,10 +4,12 @@ import pytest
 import voluptuous as vol
 from homeassistant.const import CONF_HOST, CONF_NAME
 from homeassistant.data_entry_flow import FlowResultType
+from homeassistant.exceptions import ConfigEntryNotReady
 from pytest_homeassistant_custom_component.common import MockConfigEntry
 
 from custom_components.tuya_local import (
     async_migrate_entry,
+    async_setup_entry,
     config_flow,
     get_device_unique_id,
 )
@@ -75,6 +77,54 @@ async def test_init_entry(hass, bypass_data_fetch):
     assert hass.states.get("lock.test_child_lock")
 
 
+@pytest.mark.asyncio
+@pytest.mark.parametrize("refresh_error", [RuntimeError("boom"), None])
+async def test_async_setup_entry_cleans_up_failed_device(hass, mocker, refresh_error):
+    """Failed runtime setup should not leave stale device state cached."""
+
+    mock_device = mocker.MagicMock()
+    if refresh_error is None:
+        mock_device.async_refresh = mocker.AsyncMock()
+        mock_device.has_returned_state = False
+    else:
+        mock_device.async_refresh = mocker.AsyncMock(side_effect=refresh_error)
+
+    def fake_setup_device(hass, config):
+        hass.data.setdefault(DOMAIN, {})
+        hass.data[DOMAIN]["deviceid"] = {
+            "device": mock_device,
+            "tuyadevice": mock_device._api,
+            "tuyadevicelock": mocker.MagicMock(),
+        }
+        return mock_device
+
+    mocker.patch(
+        "custom_components.tuya_local.setup_device", side_effect=fake_setup_device
+    )
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=13,
+        minor_version=18,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: 3.4,
+            CONF_TYPE: "kogan_kahtp_heater",
+        },
+        options={},
+    )
+
+    with pytest.raises(ConfigEntryNotReady):
+        await async_setup_entry(hass, entry)
+
+    assert "deviceid" not in hass.data.get(DOMAIN, {})
+    mock_device._api.set_socketPersistent.assert_called_with(False)
+
+
 @pytest.mark.asyncio
 async def test_migrate_entry(hass, mocker):
     """Test migration from old entry format."""