Explorar o código

Resolve CONF_TYPE_AUTO once per device.

During init, when device type is set to auto, the device needs to be polled once before the device type can be resolved.  But when creating multiple entities per device, the reply from the earlier entities still hasn't come back, so we may end up creating multiple refresh requests.
Resolve the CONF_TYPE before setting up platforms so that the platforms can
be setup more quickly.

Also, reverse the logic for deciding when to rotate the protocol version, so that devices that have never communicated rotate immediately for faster detection.
Jason Rumney %!s(int64=4) %!d(string=hai) anos
pai
achega
3678ee3782

+ 1 - 0
README.md

@@ -32,6 +32,7 @@ Note that devices sometimes get firmware upgrades, or incompatible versions are
 #### Fans
 - Goldair GCPF315 fans
 - Anko HEGSM40 fans
+- Deta fan controllers
 
 #### Dehumidifiers
 - Goldair GPDH420 dehumidifiers

+ 10 - 4
custom_components/tuya_local/__init__.py

@@ -22,6 +22,8 @@ from .const import (
     CONF_FAN,
     CONF_HUMIDIFIER,
     CONF_SWITCH,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from .device import setup_device, delete_device
@@ -51,7 +53,14 @@ async def async_setup(hass: HomeAssistant, config: dict):
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
     _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
     config = {**entry.data, **entry.options, "name": entry.title}
-    setup_device(hass, config)
+    device = setup_device(hass, config)
+
+    if config[CONF_TYPE] == CONF_TYPE_AUTO:
+        config[CONF_TYPE] = await device.async_inferred_type
+        if config[CONF_TYPE] is None:
+            raise ValueError(f"Unable to detect type for device {device.name}")
+        entry.data[CONF_TYPE] = config[CONF_TYPE]
+        entry.options[CONF_TYPE] = config[CONF_TYPE]
 
     if config.get(CONF_CLIMATE, False) is True:
         hass.async_create_task(
@@ -110,9 +119,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
 
 
 async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
-    if entry.data.get(SOURCE_IMPORT):
-        raise ValueError("Devices configured via yaml cannot be updated from the UI.")
-
     _LOGGER.debug(f"Updating entry for device: {entry.data[CONF_DEVICE_ID]}")
     await async_unload_entry(hass, entry)
     await async_setup_entry(hass, entry)

+ 3 - 4
custom_components/tuya_local/climate.py

@@ -22,10 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(
+            f"Device type for {device.name} not resolved before climate init"
+        )
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 3 - 3
custom_components/tuya_local/config_flow.py

@@ -23,8 +23,8 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
             self._abort_if_unique_id_configured()
 
-            connect_success = await async_test_connection(user_input, self.hass)
-            if connect_success:
+            device = await async_test_connection(user_input, self.hass)
+            if device:
                 title = user_input[CONF_NAME]
                 del user_input[CONF_NAME]
                 return self.async_create_entry(title=title, data=user_input)
@@ -98,4 +98,4 @@ async def async_test_connection(config: dict, hass: HomeAssistant):
         "Test", config[CONF_DEVICE_ID], config[CONF_HOST], config[CONF_LOCAL_KEY], hass
     )
     await device.async_refresh()
-    return device.has_returned_state
+    return device if device.has_returned_state else None

+ 3 - 3
custom_components/tuya_local/device.py

@@ -230,12 +230,12 @@ class TuyaLocalDevice(object):
                 break
             except Exception as e:
                 _LOGGER.debug(f"Retrying after exception {e}")
-                if i + 1 == self._CONNECTION_ATTEMPTS:
+                if not self._api_protocol_working:
+                    self._rotate_api_protocol_version()
+                elif i + 1 == self._CONNECTION_ATTEMPTS:
                     self._reset_cached_state()
                     self._api_protocol_working = False
                     _LOGGER.error(error_message)
-                elif self._api_protocol_working is False:
-                    self._rotate_api_protocol_version()
 
     def _get_cached_state(self):
         cached_state = self._cached_state.copy()

+ 1 - 4
custom_components/tuya_local/fan.py

@@ -22,10 +22,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(f"Device type for {device.name} not resolved before fan init")
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 3 - 4
custom_components/tuya_local/humidifier.py

@@ -22,10 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(
+            f"Device type for {device.name} not resolved before humidifier init"
+        )
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 3 - 4
custom_components/tuya_local/light.py

@@ -22,10 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(
+            f"Device type for {device.name} not resolved before light init"
+        )
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 1 - 4
custom_components/tuya_local/lock.py

@@ -23,10 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(f"Device type for {device.name} not resolved before lock init")
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 3 - 4
custom_components/tuya_local/switch.py

@@ -22,10 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     device = data["device"]
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
-        discovery_info[CONF_TYPE] = await device.async_inferred_type()
-
-        if discovery_info[CONF_TYPE] is None:
-            raise ValueError(f"Unable to detect type for device {device.name}")
+        raise ValueError(
+            f"Device type for {device.name} not resolved before switch init"
+        )
 
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity

+ 1 - 3
tests/test_climate.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_CLIMATE,
     CONF_DEVICE_ID,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.heater.climate import GoldairHeater
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="heater")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}

+ 1 - 3
tests/test_fan.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_FAN,
     CONF_DEVICE_ID,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.fan import TuyaLocalFan
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "fan", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="fan")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}

+ 1 - 3
tests/test_humidifier.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_HUMIDIFIER,
     CONF_DEVICE_ID,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="dehumidifier")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}

+ 1 - 3
tests/test_light.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_DISPLAY_LIGHT,
     CONF_DEVICE_ID,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.light import TuyaLocalLight
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="heater")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}

+ 1 - 3
tests/test_lock.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_CHILD_LOCK,
     CONF_DEVICE_ID,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="heater")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}

+ 1 - 3
tests/test_switch.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_SWITCH,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="kogan_switch")
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}