Просмотр исходного кода

Allow explicitly setting the protocol version.

With protocol versions 3.2 and 3.4 being similar to 3.3, there may be some misdetection happening, leading to issues such as reported in #300 where the device appears to work for returned values, but does not accept set values.

Add protocol version to the config, with default of "auto" that retains the same behaviour as previous versions.  When set to anything else, the protocol does not rotate to other versions when there is an error, so the user must know what version the device is using.
Jason Rumney 3 лет назад
Родитель
Сommit
f64fcd0d02

+ 14 - 0
custom_components/tuya_local/__init__.py

@@ -16,6 +16,7 @@ from homeassistant.helpers.entity_registry import async_migrate_entries
 from .const import (
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -137,6 +138,19 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
         entry.options = {}
         entry.version = 9
 
+    if entry.version <= 9:
+        # Added protocol_version, default to auto
+        conf = {**entry.data, **entry.options}
+        entry.data = {
+            CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
+            CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
+            CONF_HOST: conf[CONF_HOST],
+            CONF_TYPE: conf[CONF_TYPE],
+            CONF_PROTOCOL_VERSION: "auto",
+        }
+        entry.options = {}
+        entry.version = 10
+
     return True
 
 

+ 23 - 3
custom_components/tuya_local/config_flow.py

@@ -7,14 +7,20 @@ from homeassistant.core import HomeAssistant, callback
 
 from . import DOMAIN
 from .device import TuyaLocalDevice
-from .const import CONF_DEVICE_ID, CONF_LOCAL_KEY, CONF_TYPE
+from .const import (
+    API_PROTOCOL_VERSIONS,
+    CONF_DEVICE_ID,
+    CONF_LOCAL_KEY,
+    CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
+)
 from .helpers.device_config import get_config
 
 _LOGGER = logging.getLogger(__name__)
 
 
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
-    VERSION = 9
+    VERSION = 10
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
     device = None
     data = {}
@@ -24,6 +30,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         devid_opts = {}
         host_opts = {}
         key_opts = {}
+        proto_opts = {"default": "auto"}
 
         if user_input is not None:
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
@@ -38,6 +45,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
                 devid_opts["default"] = user_input[CONF_DEVICE_ID]
                 host_opts["default"] = user_input[CONF_HOST]
                 key_opts["default"] = user_input[CONF_LOCAL_KEY]
+                proto_opts["default"] = user_input[CONF_PROTOCOL_VERSION]
 
         return self.async_show_form(
             step_id="user",
@@ -46,6 +54,10 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
                     vol.Required(CONF_DEVICE_ID, **devid_opts): str,
                     vol.Required(CONF_HOST, **host_opts): str,
                     vol.Required(CONF_LOCAL_KEY, **key_opts): str,
+                    vol.Required(
+                        CONF_PROTOCOL_VERSION,
+                        **proto_opts,
+                    ): vol.In(["auto"] + API_PROTOCOL_VERSIONS),
                 }
             ),
             errors=errors,
@@ -132,6 +144,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
         schema = {
             vol.Required(CONF_LOCAL_KEY, default=config.get(CONF_LOCAL_KEY, "")): str,
             vol.Required(CONF_HOST, default=config.get(CONF_HOST, "")): str,
+            vol.Required(
+                CONF_PROTOCOL_VERSION, default=config.get(CONF_PROTOCOL_VERSION, "auto")
+            ): vol.In(["auto"] + API_PROTOCOL_VERSIONS),
         }
         cfg = get_config(config[CONF_TYPE])
         if cfg is None:
@@ -146,7 +161,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
 
 async def async_test_connection(config: dict, hass: HomeAssistant):
     device = TuyaLocalDevice(
-        "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],
+        config[CONF_PROTOCOL_VERSION],
+        hass,
     )
     await device.async_refresh()
     return device if device.has_returned_state else None

+ 1 - 0
custom_components/tuya_local/const.py

@@ -5,4 +5,5 @@ DOMAIN = "tuya_local"
 CONF_DEVICE_ID = "device_id"
 CONF_LOCAL_KEY = "local_key"
 CONF_TYPE = "type"
+CONF_PROTOCOL_VERSION = "protocol_version"
 API_PROTOCOL_VERSIONS = [3.3, 3.1, 3.2, 3.4]

+ 22 - 3
custom_components/tuya_local/device.py

@@ -16,6 +16,7 @@ from .const import (
     API_PROTOCOL_VERSIONS,
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from .helpers.device_config import possible_matches
@@ -25,7 +26,15 @@ _LOGGER = logging.getLogger(__name__)
 
 
 class TuyaLocalDevice(object):
-    def __init__(self, name, dev_id, address, local_key, hass: HomeAssistant):
+    def __init__(
+        self,
+        name,
+        dev_id,
+        address,
+        local_key,
+        protocol_version,
+        hass: HomeAssistant,
+    ):
         """
         Represents a Tuya-based device.
 
@@ -33,12 +42,14 @@ class TuyaLocalDevice(object):
             dev_id (str): The device id.
             address (str): The network address.
             local_key (str): The encryption key.
+            protocol_version (str | number): The protocol version.
         """
         self._name = name
         self._api_protocol_version_index = None
         self._api_protocol_working = False
         self._api = tinytuya.Device(dev_id, address, local_key)
         self._refresh_task = None
+        self._protocol_configured = protocol_version
         self._rotate_api_protocol_version()
 
         self._reset_cached_state()
@@ -267,8 +278,15 @@ class TuyaLocalDevice(object):
 
     def _rotate_api_protocol_version(self):
         if self._api_protocol_version_index is None:
-            self._api_protocol_version_index = 0
-        else:
+            try:
+                self._api_protocol_version_index = API_PROTOCOL_VERSIONS.index(
+                    self._protocol_configured
+                )
+            except ValueError:
+                self._api_protocol_version_index = 0
+
+        # only rotate if configured as auto
+        elif self._protocol_configured == "auto":
             self._api_protocol_version_index += 1
 
         if self._api_protocol_version_index >= len(API_PROTOCOL_VERSIONS):
@@ -295,6 +313,7 @@ def setup_device(hass: HomeAssistant, config: dict):
         config[CONF_DEVICE_ID],
         config[CONF_HOST],
         config[CONF_LOCAL_KEY],
+        config[CONF_PROTOCOL_VERSION],
         hass,
     )
     hass.data[DOMAIN][config[CONF_DEVICE_ID]] = {"device": device}

+ 3 - 2
custom_components/tuya_local/diagnostics.py

@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers import device_registry as dr, entity_registry as er
 from homeassistant.helpers.device_registry import DeviceEntry
 
-from .const import DOMAIN
+from .const import CONF_PROTOCOL_VERSION, CONF_TYPE, DOMAIN
 from .device import TuyaLocalDevice
 
 
@@ -38,10 +38,11 @@ def _async_get_diagnostics(
 
     data = {
         "name": entry.title,
-        "type": entry.data["type"],
+        "type": entry.data[CONF_TYPE],
         "device_id": REDACTED,
         "local_key": REDACTED,
         "host": REDACTED,
+        "protocol_version": entry.data[CONF_PROTOCOL_VERSION],
     }
 
     # TODO: investigate what device entry holds

+ 4 - 0
tests/test_binary_sensor.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "goldair_dehumidifier",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -41,6 +43,7 @@ async def test_init_entry_fails_if_device_has_no_binary_sensor(hass):
         data={
             CONF_TYPE: "mirabella_genio_usb",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -64,6 +67,7 @@ async def test_init_entry_fails_if_config_is_missing(hass):
         data={
             CONF_TYPE: "non_existing",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()

+ 16 - 3
tests/test_climate.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -15,7 +16,11 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -64,7 +69,11 @@ async def test_init_entry_fails_if_device_has_no_climate(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -87,7 +96,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 21 - 7
tests/test_config_flow.py

@@ -15,6 +15,7 @@ from custom_components.tuya_local import (
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -39,12 +40,13 @@ async def test_init_entry(hass):
     """Test initialisation of the config flow."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         title="test",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "kogan_kahtp_heater",
         },
         options={},
@@ -240,6 +242,7 @@ async def test_async_test_connection_valid(mock_device, hass):
             CONF_DEVICE_ID: "deviceid",
             CONF_LOCAL_KEY: "localkey",
             CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
         },
         hass,
     )
@@ -257,6 +260,7 @@ async def test_async_test_connection_invalid(mock_device, hass):
             CONF_DEVICE_ID: "deviceid",
             CONF_LOCAL_KEY: "localkey",
             CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
         },
         hass,
     )
@@ -274,6 +278,7 @@ async def test_flow_user_init_invalid_config(mock_test, hass):
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "badkey",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     assert {"base": "connection"} == result["errors"]
@@ -411,6 +416,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
             CONF_DEVICE_ID: "deviceid",
             CONF_LOCAL_KEY: "localkey",
             CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "kogan_kahtp_heater",
         },
     ):
@@ -424,7 +430,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
             },
         )
         expected = {
-            "version": 9,
+            "version": 10,
             "context": {"source": "choose_entities"},
             "type": "create_entry",
             "flow_id": ANY,
@@ -438,6 +444,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
                 CONF_DEVICE_ID: "deviceid",
                 CONF_HOST: "hostname",
                 CONF_LOCAL_KEY: "localkey",
+                CONF_PROTOCOL_VERSION: "auto",
                 CONF_TYPE: "kogan_kahtp_heater",
             },
         }
@@ -448,13 +455,14 @@ async def test_options_flow_init(hass):
     """Test config flow options."""
     config_entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         unique_id="uniqueid",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             CONF_NAME: "test",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "smartplugv1",
         },
     )
@@ -483,13 +491,14 @@ async def test_options_flow_modifies_config(mock_test, hass):
 
     config_entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         unique_id="uniqueid",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             CONF_NAME: "test",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "kogan_kahtp_heater",
         },
     )
@@ -505,11 +514,13 @@ async def test_options_flow_modifies_config(mock_test, hass):
         user_input={
             CONF_HOST: "new_hostname",
             CONF_LOCAL_KEY: "new_key",
+            CONF_PROTOCOL_VERSION: 3.3,
         },
     )
     expected = {
         CONF_HOST: "new_hostname",
         CONF_LOCAL_KEY: "new_key",
+        CONF_PROTOCOL_VERSION: 3.3,
     }
     assert "create_entry" == result["type"]
     assert "" == result["title"]
@@ -523,13 +534,14 @@ async def test_options_flow_fails_when_connection_fails(mock_test, hass):
 
     config_entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         unique_id="uniqueid",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             CONF_NAME: "test",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "smartplugv1",
         },
     )
@@ -559,13 +571,14 @@ async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
 
     config_entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         unique_id="uniqueid",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             CONF_NAME: "test",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "non_existing",
         },
     )
@@ -584,13 +597,14 @@ async def test_async_setup_entry_for_switch(mock_device, hass):
     """Test setting up based on a config entry.  Repeats test_init_entry."""
     config_entry = MockConfigEntry(
         domain=DOMAIN,
-        version=9,
+        version=10,
         unique_id="uniqueid",
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             CONF_NAME: "test",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "smartplugv2",
         },
     )

+ 12 - 2
tests/test_cover.py

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.cover import TuyaLocalCover
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "garage_door_opener",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -37,7 +39,11 @@ async def test_init_entry_fails_if_device_has_no_cover(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()
@@ -59,7 +65,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 36 - 1
tests/test_device.py

@@ -24,7 +24,12 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.hass = hass_patcher.start()
 
         self.subject = TuyaLocalDevice(
-            "Some name", "some_dev_id", "some.ip.address", "some_local_key", self.hass()
+            "Some name",
+            "some_dev_id",
+            "some.ip.address",
+            "some_local_key",
+            "auto",
+            self.hass(),
         )
 
     def test_configures_tinytuya_correctly(self):
@@ -208,6 +213,36 @@ class TestDevice(IsolatedAsyncioTestCase):
             [call(3.1), call(3.2), call(3.4)]
         )
 
+    def test_api_protocol_version_is_not_rotated_when_not_auto(self):
+        self.subject._protocol_configured = 3.4
+        self.subject._api_protocol_version_index = None
+        self.subject._api.set_version.reset_mock()
+        self.subject._rotate_api_protocol_version()
+        self.subject._api.set_version.assert_called_once_with(3.4)
+        self.subject._api.set_version.reset_mock()
+
+        self.subject._api.status.side_effect = [
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            {"dps": {"1": False}},
+            {"dps": {"1": False}},
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            Exception("Error"),
+            {"dps": {"1": False}},
+        ]
+        self.subject.refresh()
+        self.assertEqual(self.subject._api_protocol_version_index, 3)
+        self.subject.refresh()
+        self.assertEqual(self.subject._api_protocol_version_index, 3)
+        self.subject.refresh()
+        self.assertEqual(self.subject._api_protocol_version_index, 3)
+
     def test_reset_cached_state_clears_cached_state_and_pending_updates(self):
         self.subject._cached_state = {"1": True, "updated_at": time()}
         self.subject._pending_updates = {"1": False}

+ 3 - 0
tests/test_diagnostics.py

@@ -6,6 +6,7 @@ from custom_components.tuya_local.const import (
     DOMAIN,
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
 )
 
@@ -21,6 +22,7 @@ async def test_config_entry_diagnostics(hass):
         data={
             CONF_DEVICE_ID: "test_device",
             CONF_LOCAL_KEY: "test_key",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "simple_switch",
         },
     )
@@ -36,6 +38,7 @@ async def test_device_diagnostics(hass):
         data={
             CONF_DEVICE_ID: "test_device",
             CONF_LOCAL_KEY: "test_key",
+            CONF_PROTOCOL_VERSION: "auto",
             CONF_TYPE: "simple_switch",
         },
     )

+ 17 - 3
tests/test_fan.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -15,7 +16,11 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "goldair_fan", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "goldair_fan",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -39,6 +44,7 @@ async def test_init_entry_as_secondary(hass):
         data={
             CONF_TYPE: "goldair_dehumidifier",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to
@@ -60,7 +66,11 @@ async def test_init_entry_fails_if_device_has_no_fan(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -83,7 +93,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 4 - 0
tests/test_humidifier.py

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "dehumidifier",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to
@@ -42,6 +44,7 @@ async def test_init_entry_fails_if_device_has_no_humidifier(hass):
         data={
             CONF_TYPE: "kogan_heater",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to
@@ -68,6 +71,7 @@ async def test_init_entry_fails_if_config_is_missing(hass):
         data={
             CONF_TYPE: "non_existing",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to

+ 12 - 2
tests/test_light.py

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.light import TuyaLocalLight
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "goldair_gpph_heater",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to
@@ -39,7 +41,11 @@ async def test_init_entry_fails_if_device_has_no_light(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -62,7 +68,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 12 - 2
tests/test_lock.py

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "goldair_gpph_heater",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     # although async, the async_add_entities function passed to
@@ -39,7 +41,11 @@ async def test_init_entry_fails_if_device_has_no_lock(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -62,7 +68,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 12 - 2
tests/test_number.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "goldair_gpph_heater",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -36,7 +38,11 @@ async def test_init_entry_fails_if_device_has_no_number(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "simple_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "simple_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()
@@ -56,7 +62,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()

+ 12 - 2
tests/test_select.py

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
     CONF_TYPE,
+    CONF_PROTOCOL_VERSION,
     DOMAIN,
 )
 from custom_components.tuya_local.generic.select import TuyaLocalSelect
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "arlec_fan",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -36,7 +38,11 @@ async def test_init_entry_fails_if_device_has_no_select(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "simple_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "simple_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()
@@ -56,7 +62,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()

+ 8 - 1
tests/test_sensor.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "goldair_dehumidifier",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -42,6 +44,7 @@ async def test_init_entry_fails_if_device_has_no_sensor(hass):
         data={
             CONF_TYPE: "mirabella_genio_usb",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -62,7 +65,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()

+ 12 - 2
tests/test_siren.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -18,6 +19,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "orion_outdoor_siren",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -34,7 +36,11 @@ async def test_init_entry_fails_if_device_has_no_siren(hass):
     """Test initialisation when device as no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "simple_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "simple_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()
@@ -52,7 +58,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when config does not exist"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()

+ 21 - 4
tests/test_switch.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -15,7 +16,11 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -36,7 +41,11 @@ async def test_init_entry_as_secondary(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "deta_fan", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "deta_fan",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -57,7 +66,11 @@ async def test_init_entry_fails_if_device_has_no_switch(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -80,7 +93,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 12 - 2
tests/test_vacuum.py

@@ -3,6 +3,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
 from unittest.mock import AsyncMock, Mock
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -17,6 +18,7 @@ async def test_init_entry(hass):
         data={
             CONF_TYPE: "lefant_m213_vacuum",
             CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
         },
     )
     m_add_entities = Mock()
@@ -37,7 +39,11 @@ async def test_init_entry_fails_if_device_has_no_vacuum(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     m_add_entities = Mock()
     m_device = AsyncMock()
@@ -59,7 +65,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 16 - 3
tests/test_water_heater.py

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
     CONF_TYPE,
     DOMAIN,
 )
@@ -15,7 +16,11 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "hydrotherm_dynamic_x8_water_heater", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "hydrotherm_dynamic_x8_water_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -36,7 +41,11 @@ async def test_init_entry_fails_if_device_has_no_water_heater(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "kogan_switch",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -59,7 +68,11 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use