Pārlūkot izejas kodu

Additional tests to fill out coverage more.

Remove mostly obsolete configuration.py, replace the portions used with inline
schemas creation.
Jason Rumney 4 gadi atpakaļ
vecāks
revīzija
c16cb54d7d

+ 13 - 2
custom_components/tuya_local/config_flow.py

@@ -6,7 +6,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME
 from homeassistant.core import HomeAssistant, callback
 
 from . import DOMAIN
-from .configuration import individual_config_schema
 from .device import TuyaLocalDevice
 from .const import CONF_DEVICE_ID, CONF_LOCAL_KEY, CONF_TYPE
 from .helpers.device_config import config_for_legacy_use
@@ -22,6 +21,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
     async def async_step_user(self, user_input=None):
         errors = {}
+        devid_opts = {}
+        host_opts = {}
+        key_opts = {}
 
         if user_input is not None:
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
@@ -33,10 +35,19 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
                 return await self.async_step_select_type()
             else:
                 errors["base"] = "connection"
+                devid_opts["default"] = user_input[CONF_DEVICE_ID]
+                host_opts["default"] = user_input[CONF_HOST]
+                key_opts["default"] = user_input[CONF_LOCAL_KEY]
 
         return self.async_show_form(
             step_id="user",
-            data_schema=vol.Schema(individual_config_schema(user_input or {})),
+            data_schema=vol.Schema(
+                {
+                    vol.Required(CONF_DEVICE_ID, **devid_opts): str,
+                    vol.Required(CONF_HOST, **host_opts): str,
+                    vol.Required(CONF_LOCAL_KEY, **key_opts): str,
+                }
+            ),
             errors=errors,
         )
 

+ 0 - 111
custom_components/tuya_local/configuration.py

@@ -1,111 +0,0 @@
-import voluptuous as vol
-from homeassistant.const import CONF_HOST, CONF_NAME
-
-from .const import (
-    CONF_CLIMATE,
-    CONF_DEVICE_ID,
-    CONF_FAN,
-    CONF_HUMIDIFIER,
-    CONF_LIGHT,
-    CONF_LOCAL_KEY,
-    CONF_LOCK,
-    CONF_SWITCH,
-    CONF_TYPE,
-)
-from .helpers.device_config import available_configs, TuyaDeviceConfig
-
-
-def conf_types():
-    types = []
-    for cfg in available_configs():
-        parsed = TuyaDeviceConfig(cfg)
-        types.append(parsed.legacy_type)
-    return types
-
-
-INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
-    {"key": CONF_HOST, "type": str, "required": True, "option": True},
-    {"key": CONF_DEVICE_ID, "type": str, "required": True, "option": False},
-    {"key": CONF_LOCAL_KEY, "type": str, "required": True, "option": True},
-]
-
-STAGE2_CONFIG_SCHEMA_TEMPLATE = [
-    {"key": CONF_NAME, "type": str, "required": True, "option": False},
-    {
-        "key": CONF_TYPE,
-        "type": vol.In(conf_types()),
-        "required": True,
-        "option": True,
-    },
-    {
-        "key": CONF_CLIMATE,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-    {
-        "key": CONF_LIGHT,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-    {
-        "key": CONF_LOCK,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-    {
-        "key": CONF_SWITCH,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-    {
-        "key": CONF_HUMIDIFIER,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-    {
-        "key": CONF_FAN,
-        "type": bool,
-        "required": False,
-        "default": False,
-        "option": True,
-    },
-]
-
-
-def individual_config_schema(defaults={}, options_only=False, stage=1):
-    output = {}
-    if options_only:
-        schema = [*INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE, *STAGE2_CONFIG_SCHEMA_TEMPLATE]
-    elif stage == 1:
-        schema = INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE
-    else:
-        schema = STAGE2_CONFIG_SCHEMA_TEMPLATE
-
-    for prop in schema:
-        if options_only and not prop.get("option"):
-            continue
-
-        options = {}
-
-        default = defaults.get(prop["key"], prop.get("default"))
-        if default is not None:
-            options["default"] = default
-
-        key = (
-            vol.Required(prop["key"], **options)
-            if prop["required"]
-            else vol.Optional(prop["key"], **options)
-        )
-        output[key] = prop["type"]
-
-    return output

+ 68 - 0
tests/test_climate.py

@@ -8,6 +8,7 @@ from custom_components.tuya_local.const import (
     CONF_TYPE,
     DOMAIN,
 )
+from custom_components.tuya_local.dehumidifier.climate import GoldairDehumidifier
 from custom_components.tuya_local.heater.climate import GoldairHeater
 from custom_components.tuya_local.climate import async_setup_entry
 
@@ -31,3 +32,70 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_CLIMATE]) == GoldairHeater
     m_add_entities.assert_called_once()
+
+
+async def test_init_entry_as_secondary(hass):
+    """Test initialisation when fan is a secondary entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"][CONF_CLIMATE]) == GoldairDehumidifier
+    m_add_entities.assert_called_once()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()

+ 39 - 2
tests/test_config_flow.py

@@ -7,7 +7,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
 
 import voluptuous as vol
 
-from custom_components.tuya_local import config_flow
+from custom_components.tuya_local import config_flow, async_migrate_entry
 from custom_components.tuya_local.const import (
     CONF_CLIMATE,
     CONF_DEVICE_ID,
@@ -56,13 +56,37 @@ async def test_init_entry(hass):
     assert state
 
 
+@patch("custom_components.tuya_local.setup_device")
+async def test_migrate_entry(mock_setup, hass):
+    """Test migration from old entry format."""
+    mock_device = MagicMock()
+    mock_device.async_inferred_type = AsyncMock(return_value="heater")
+    mock_setup.return_value = mock_device
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=1,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_TYPE: "auto",
+            CONF_CLIMATE: True,
+            "child_lock": True,
+            "display_light": True,
+        },
+    )
+    assert await async_migrate_entry(hass, entry)
+
+
 async def test_flow_user_init(hass):
     """Test the initialisation of the form in the first step of the config flow."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": "user"}
     )
     expected = {
-        "data_schema": vol.Schema(config_flow.individual_config_schema()),
+        "data_schema": ANY,
         "description_placeholders": None,
         "errors": {},
         "flow_id": ANY,
@@ -72,6 +96,19 @@ async def test_flow_user_init(hass):
         "last_step": ANY,
     }
     assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"](
+            {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: "test", CONF_HOST: "test"}
+        )
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
+        assert False
+    except vol.MultipleInvalid:
+        pass
 
 
 @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")

+ 67 - 0
tests/test_fan.py

@@ -31,3 +31,70 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_FAN]) == TuyaLocalFan
     m_add_entities.assert_called_once()
+
+
+async def test_init_entry_as_secondary(hass):
+    """Test initialisation when fan is a secondary entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"][CONF_FAN]) == TuyaLocalFan
+    m_add_entities.assert_called_once()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()

+ 46 - 0
tests/test_humidifier.py

@@ -31,3 +31,49 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_HUMIDIFIER]) == TuyaLocalHumidifier
     m_add_entities.assert_called_once()
+
+
+async def test_init_entry_fails_if_device_has_no_humidifier(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: "kogan_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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()

+ 46 - 0
tests/test_light.py

@@ -31,3 +31,49 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_LIGHT]) == TuyaLocalLight
     m_add_entities.assert_called_once()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()

+ 46 - 0
tests/test_lock.py

@@ -31,3 +31,49 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_LOCK]) == TuyaLocalLock
     m_add_entities.assert_called_once()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()

+ 67 - 0
tests/test_switch.py

@@ -31,3 +31,70 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_SWITCH]) == TuyaLocalSwitch
     m_add_entities.assert_called_once()
+
+
+async def test_init_entry_as_secondary(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: "deta_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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"][CONF_SWITCH]) == TuyaLocalSwitch
+    m_add_entities.assert_called_once()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+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"},
+    )
+    # 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()
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()