Ver código fonte

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 4 anos atrás
pai
commit
3678ee3782

+ 1 - 0
README.md

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

+ 10 - 4
custom_components/tuya_local/__init__.py

@@ -22,6 +22,8 @@ from .const import (
     CONF_FAN,
     CONF_FAN,
     CONF_HUMIDIFIER,
     CONF_HUMIDIFIER,
     CONF_SWITCH,
     CONF_SWITCH,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
     DOMAIN,
     DOMAIN,
 )
 )
 from .device import setup_device, delete_device
 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):
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
     _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
     _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
     config = {**entry.data, **entry.options, "name": entry.title}
     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:
     if config.get(CONF_CLIMATE, False) is True:
         hass.async_create_task(
         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):
 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]}")
     _LOGGER.debug(f"Updating entry for device: {entry.data[CONF_DEVICE_ID]}")
     await async_unload_entry(hass, entry)
     await async_unload_entry(hass, entry)
     await async_setup_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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     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])
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
             self._abort_if_unique_id_configured()
             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]
                 title = user_input[CONF_NAME]
                 del user_input[CONF_NAME]
                 del user_input[CONF_NAME]
                 return self.async_create_entry(title=title, data=user_input)
                 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
         "Test", config[CONF_DEVICE_ID], config[CONF_HOST], config[CONF_LOCAL_KEY], hass
     )
     )
     await device.async_refresh()
     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
                 break
             except Exception as e:
             except Exception as e:
                 _LOGGER.debug(f"Retrying after exception {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._reset_cached_state()
                     self._api_protocol_working = False
                     self._api_protocol_working = False
                     _LOGGER.error(error_message)
                     _LOGGER.error(error_message)
-                elif self._api_protocol_working is False:
-                    self._rotate_api_protocol_version()
 
 
     def _get_cached_state(self):
     def _get_cached_state(self):
         cached_state = self._cached_state.copy()
         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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     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"]
     device = data["device"]
 
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
     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])
     cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
     ecfg = cfg.primary_entity
     ecfg = cfg.primary_entity

+ 1 - 3
tests/test_climate.py

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

+ 1 - 3
tests/test_fan.py

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

+ 1 - 3
tests/test_humidifier.py

@@ -6,7 +6,6 @@ from custom_components.tuya_local.const import (
     CONF_HUMIDIFIER,
     CONF_HUMIDIFIER,
     CONF_DEVICE_ID,
     CONF_DEVICE_ID,
     CONF_TYPE,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
     DOMAIN,
 )
 )
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     """Test the initialisation."""
     entry = MockConfigEntry(
     entry = MockConfigEntry(
         domain=DOMAIN,
         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
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_add_entities = Mock()
     m_device = AsyncMock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="dehumidifier")
 
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}
     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_DISPLAY_LIGHT,
     CONF_DEVICE_ID,
     CONF_DEVICE_ID,
     CONF_TYPE,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
     DOMAIN,
 )
 )
 from custom_components.tuya_local.generic.light import TuyaLocalLight
 from custom_components.tuya_local.generic.light import TuyaLocalLight
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     """Test the initialisation."""
     entry = MockConfigEntry(
     entry = MockConfigEntry(
         domain=DOMAIN,
         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
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_add_entities = Mock()
     m_device = AsyncMock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="heater")
 
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}
     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_CHILD_LOCK,
     CONF_DEVICE_ID,
     CONF_DEVICE_ID,
     CONF_TYPE,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
     DOMAIN,
 )
 )
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     """Test the initialisation."""
     entry = MockConfigEntry(
     entry = MockConfigEntry(
         domain=DOMAIN,
         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
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_add_entities = Mock()
     m_device = AsyncMock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="heater")
 
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}
     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_DEVICE_ID,
     CONF_SWITCH,
     CONF_SWITCH,
     CONF_TYPE,
     CONF_TYPE,
-    CONF_TYPE_AUTO,
     DOMAIN,
     DOMAIN,
 )
 )
 from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
 from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
@@ -17,14 +16,13 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     """Test the initialisation."""
     entry = MockConfigEntry(
     entry = MockConfigEntry(
         domain=DOMAIN,
         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
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
     # async_setup_entry is called truly asynchronously. If we use
     # AsyncMock, it expects us to await the result.
     # AsyncMock, it expects us to await the result.
     m_add_entities = Mock()
     m_add_entities = Mock()
     m_device = AsyncMock()
     m_device = AsyncMock()
-    m_device.async_inferred_type = AsyncMock(return_value="kogan_switch")
 
 
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN] = {}
     hass.data[DOMAIN]["dummy"] = {}
     hass.data[DOMAIN]["dummy"] = {}