Parcourir la source

Add a generic humidifier entity.

Humidifiers and dehumidifiers are supposed to be represented in HomeAssistant by
the humidifier entity type, available since 0.102.

Add an implementation of this, and add config for Goldair Dehumidifier and Eanons Humidifier as a secondary device for testing.  Later this will be made
the primary device (or maybe I'll get rid of the concept of primary and secondary altogether).  I'm not sure what happens if both are configured together. If there are no ill effects, I'll probably leave both configurations as it comes down to personal choice which to use.
Jason Rumney il y a 4 ans
Parent
commit
93db615115

+ 8 - 0
custom_components/tuya_local/__init__.py

@@ -19,6 +19,7 @@ from .const import (
     CONF_CLIMATE,
     CONF_DEVICE_ID,
     CONF_DISPLAY_LIGHT,
+    CONF_HUMIDIFIER,
     CONF_SWITCH,
     DOMAIN,
 )
@@ -64,6 +65,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
         hass.async_create_task(
             hass.config_entries.async_forward_entry_setup(entry, "switch")
         )
+    if config[CONF_HUMIDIFIER] is True:
+        hass.async_create_task(
+            hass.config_entries.async_forward_entry_setup(entry, "humidifier")
+        )
+
     entry.add_update_listener(async_update_entry)
 
     return True
@@ -85,6 +91,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
         await hass.config_entries.async_forward_entry_unload(entry, "lock")
     if CONF_SWITCH in data:
         await hass.config_entries.async_forward_entry_unload(entry, "switch")
+    if CONF_HUMIDIFIER in data:
+        await hass.config_entries.async_forward_entry_unload(entry, "humidifier")
 
     delete_device(hass, config)
     del hass.data[DOMAIN][config[CONF_DEVICE_ID]]

+ 3 - 1
custom_components/tuya_local/const.py

@@ -17,10 +17,12 @@ CONF_TYPE_KOGAN_SWITCH = "kogan_switch"
 CONF_TYPE_GSH_HEATER = "gsh_heater"
 CONF_TYPE_GARDENPAC_HEATPUMP = "gardenpac_heatpump"
 CONF_TYPE_PURLINE_M100_HEATER = "purline_m100_heater"
+CONF_TYPE_EANONS_HUMIDIFIER = "eanons_humidifier"
+CONF_TYPE_REMORA_HEATPUMP = "remora_heatpump"
 CONF_CLIMATE = "climate"
 CONF_DISPLAY_LIGHT = "display_light"
 CONF_CHILD_LOCK = "child_lock"
 CONF_SWITCH = "switch"
-
+CONF_HUMIDIFIER = "humidifier"
 API_PROTOCOL_VERSIONS = [3.3, 3.1]
 SCAN_INTERVAL = timedelta(seconds=30)

+ 50 - 1
custom_components/tuya_local/devices/eanons_humidifier.yaml

@@ -57,10 +57,59 @@ primary_entity:
       name: current_humidity
       type: integer
 secondary_entities:
+  - entity: humidifier
+    name: Humidifier
+    class: humidifier
+    dps:
+      - id: 2
+        name: fan_mode
+        type: string
+        mapping:
+          - dps_val: small
+            value: low
+          - dps_val: middle
+            value: medium
+          - dps_val: large
+            value: high
+      - id: 3
+        name: timer_hr
+        type: string
+      - id: 4
+        name: timer_min
+        type: integer
+      - id: 9
+        name: error
+        type: integer
+        mapping:
+          - dps_val: 0
+            value: OK
+          - dps_val: 1
+            value: Water Level Low
+      - id: 10
+        name: switch
+        type: boolean
+      - id: 12
+        name: mode
+        type: string
+        mapping:
+          - dps_val: sleep
+            value: sleep
+          - dps_val: humidity
+            value: normal
+          - dps_val: work
+            value: boost
+      - id: 15
+        name: humidity
+        type: integer
+        range:
+          min: 40
+          max: 90
+      - id: 16
+        name: current_humidity
+        type: integer
   - entity: switch
     name: "UV Sterilization"
     dps:
       - id: 22
         name: switch
         type: boolean
-          

+ 74 - 0
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -84,6 +84,80 @@ primary_entity:
           icon_priority: 2
           readonly: true
 secondary_entities:
+  - entity: humidifier
+    name: Dehumidifier
+    class: dehumidifier
+    dps:
+      - id: 1
+        name: switch
+        type: boolean
+      - id: 2
+        name: mode
+        type: string
+        mapping:
+          - dps_val: "0"
+            value: "Normal"
+          - dps_val: "1"
+            value: "Low"
+          - dps_val: "2"
+            value: "High"
+          - dps_val: "3"
+            value: "Dry clothes"
+            icon: "mdi:tshirt-crew-outline"
+            icon_priority: 4
+      - id: 4
+        type: integer
+        name: humidity
+        range:
+          min: 30
+          max: 80
+        mapping:
+          - step: 5
+      - id: 5
+        type: boolean
+        name: air_clean_on
+      - id: 6
+        type: string
+        mapping:
+          - dps_val: "1"
+            value: "low"
+          - dps_val: "3"
+            value: "high"
+        name: fan_mode
+      - id: 11
+        type: bitfield
+        mapping:
+          - dps_val: 8
+            value: "Tank full or missing"
+            icon: "mdi:cup-water"
+            icon_priority: 1
+          - dps_val: 0
+            value: "OK"
+        name: error
+        readonly: true
+      - id: 12
+        type: string
+        name: unknown_12
+      - id: 101
+        type: boolean
+        name: unknown_101
+        readonly: true
+      - id: 103
+        type: integer
+        name: current_temperature
+        readonly: true
+      - id: 104
+        type: integer
+        name: current_humidity
+        readonly: true
+      - id: 105
+        type: boolean
+        name: defrosting
+        mapping:
+          - dps_val: true
+            icon: "mdi:snowflake-melt"
+            icon_priority: 2
+        readonly: true
   - entity: light
     name: Panel Light
     dps:

+ 0 - 1
custom_components/tuya_local/devices/purline_m100_heater.yaml

@@ -2,7 +2,6 @@ name: Purline Hoti M100 Heater
 legacy_type: purline_m100_heater
 primary_entity:
   entity: climate
-  legacy_class: ".purline_m100_heater.climate.PurlineM100Heater"
   dps:
     - id: 1
       name: hvac_mode

+ 177 - 0
custom_components/tuya_local/generic/humidifier.py

@@ -0,0 +1,177 @@
+"""
+Platform to control tuya humidifier and dehumidifier devices.
+"""
+import logging
+
+from homeassistant.components.humidifier import HumidifierEntity
+from homeassistant.components.humidifier.const import (
+    DEFAULT_MAX_HUMIDITY,
+    DEFAULT_MIN_HUMIDITY,
+    DEVICE_CLASS_DEHUMIDIFIER,
+    DEVICE_CLASS_HUMIDIFIER,
+    SUPPORT_MODES,
+)
+from homeassistant.const import (
+    STATE_UNAVAILABLE,
+)
+
+from ..device import TuyaLocalDevice
+from ..helpers.device_config import TuyaEntityConfig
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TuyaLocalHumidifier(HumidifierEntity):
+    """Representation of a Tuya Humidifier entity."""
+
+    def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
+        """
+        Initialise the humidifier device.
+        Args:
+           device (TuyaLocalDevice): The device API instance.
+           config (TuyaEntityConfig): The entity config.
+        """
+        self._device = device
+        self._config = config
+        self._support_flags = 0
+        self._humidity_dps = None
+        self._mode_dps = None
+        self._switch_dps = None
+        self._attr_dps = []
+        for d in config.dps():
+            if d.name == "switch":
+                self._switch_dps = d
+            elif d.name == "humidity":
+                self._humidity_dps = d
+            elif d.name == "mode":
+                self._mode_dps = d
+                self._support_flags |= SUPPORT_MODES
+            elif not d.hidden:
+                self._attr_dps.append(d)
+
+    @property
+    def supported_features(self):
+        """Return the features supported by this climate device."""
+        return self._support_flags
+
+    @property
+    def should_poll(self):
+        """Return the polling state."""
+        return True
+
+    @property
+    def name(self):
+        """Return the name of the climate device."""
+        return self._device.name
+
+    @property
+    def friendly_name(self):
+        """Return the friendly name of the climate entity for the UI."""
+        return self._config.name
+
+    @property
+    def unique_id(self):
+        """Return the unique id for this climate device."""
+        return self._device.unique_id
+
+    @property
+    def device_info(self):
+        """Return device information about this heater."""
+        return self._device.device_info
+
+    @property
+    def device_class(self):
+        """Return the class of this device"""
+        return (
+            DEVICE_CLASS_DEHUMIDIFIER
+            if self._config.device_class == "dehumidifier"
+            else DEVICE_CLASS_HUMIDIFIER
+        )
+
+    @property
+    def icon(self):
+        """Return the icon to use in the frontend for this device."""
+        if self.hvac_mode == HVAC_MODE_HEAT:
+            return "mdi:radiator"
+        else:
+            return "mdi:radiator-disabled"
+
+    @property
+    def is_on(self):
+        """Return whether the switch is on or not."""
+        is_switched_on = self._switch_dps.get_value(self._device)
+
+        if is_switched_on is None:
+            return STATE_UNAVAILABLE
+        else:
+            return is_switched_on
+
+    async def async_turn_on(self, **kwargs):
+        """Turn the switch on"""
+        await self._switch_dps.async_set_value(self._device, True)
+
+    async def async_turn_off(self, **kwargs):
+        """Turn the switch off"""
+        await self._switch_dps.async_set_value(self._device, False)
+
+    @property
+    def target_humidity(self):
+        """Return the currently set target humidity."""
+        if self._humidity_dps is None:
+            raise NotImplementedError()
+        return self._humidity_dps.get_value(self._device)
+
+    @property
+    def min_humidity(self):
+        """Return the minimum supported target humidity."""
+        if self._humidity_dps is None:
+            return None
+        if self._humidity_dps.range is None:
+            return DEFAULT_MIN_HUMIDITY
+        return self._humidity_dps.range["min"]
+
+    @property
+    def max_humidity(self):
+        """Return the maximum supported target humidity."""
+        if self._humidity_dps is None:
+            return None
+        if self._humidity_dps.range is None:
+            return DEFAULT_MAX_HUMIDITY
+        return self._humidity_dps.range["max"]
+
+    async def async_set_humidity(self, target_humidity):
+        if self._humidity_dps is None:
+            raise NotImplementedError()
+
+        await self._humidity_dps.async_set_value(self._device, target_humidity)
+
+    @property
+    def mode(self):
+        """Return the current preset mode."""
+        if self._mode_dps is None:
+            raise NotImplementedError()
+        return self._mode_dps.get_value(self._device)
+
+    @property
+    def available_modes(self):
+        """Return the list of presets that this device supports."""
+        if self._mode_dps is None:
+            return None
+        return self._mode_dps.values
+
+    async def async_set_mode(self, mode):
+        """Set the preset mode."""
+        if self._mode_dps is None:
+            raise NotImplementedError()
+        await self._mode_dps.async_set_value(self._device, mode)
+
+    @property
+    def device_state_attributes(self):
+        """Get additional attributes that the integration itself does not support."""
+        attr = {}
+        for a in self._attr_dps:
+            attr[a.name] = a.get_value(self._device)
+        return attr
+
+    async def async_update(self):
+        await self._device.async_refresh()

+ 53 - 0
custom_components/tuya_local/humidifier.py

@@ -0,0 +1,53 @@
+"""
+Setup for different kinds of Tuya humidifier devices
+"""
+import logging
+
+from . import DOMAIN
+from .const import (
+    CONF_HUMIDIFIER,
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
+)
+from .generic.humidifier import TuyaLocalHumidifier
+from .helpers.device_config import config_for_legacy_use
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+    """Set up the Tuya device according to its type."""
+    data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
+    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}")
+
+    cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
+    ecfg = cfg.primary_entity
+    if ecfg.entity != "humidifier":
+        for ecfg in cfg.secondary_entities():
+            if ecfg.entity == "humidifier":
+                break
+        if ecfg.entity != "humidifier":
+            raise ValueError(
+                f"{device.name} does not support use as a humidifier device."
+            )
+
+    data[CONF_HUMIDIFIER] = TuyaLocalHumidifier(device, ecfg)
+
+    async_add_entities([data[CONF_HUMIDIFIER]])
+    _LOGGER.debug(f"Adding humidifier device for {discovery_info[CONF_TYPE]}")
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    config = {**config_entry.data, **config_entry.options}
+    discovery_info = {
+        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
+        CONF_TYPE: config[CONF_TYPE],
+    }
+    await async_setup_platform(hass, {}, async_add_entities, discovery_info)

+ 1 - 1
custom_components/tuya_local/light.py

@@ -1,5 +1,5 @@
 """
-Setup for different kinds of Tuya climate devices
+Setup for different kinds of Tuya light devices
 """
 import logging
 

+ 1 - 1
custom_components/tuya_local/lock.py

@@ -1,5 +1,5 @@
 """
-Setup for different kinds of Tuya climate devices
+Setup for different kinds of Tuya lock devices
 """
 import logging
 

+ 61 - 0
tests/devices/test_goldair_dehumidifier.py

@@ -10,10 +10,12 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_HUMIDITY,
 )
+from homeassistant.components.humidifier.const import SUPPORT_MODES
 from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
 
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
 from custom_components.tuya_local.generic.light import TuyaLocalLight
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
 from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
@@ -52,18 +54,23 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         climate = cfg.primary_entity
         light = None
         lock = None
+        humidifier = None
         for e in cfg.secondary_entities():
             if e.entity == "light":
                 light = e
             elif e.entity == "lock":
                 lock = e
+            elif e.entity == "humidifier":
+                humidifier = e
         self.climate_name = climate.name
         self.light_name = "missing" if light is None else light.name
         self.lock_name = "missing" if lock is None else lock.name
+        self.humidifier_name = "missing" if humidifier is None else humidifier.name
 
         self.subject = TuyaLocalClimate(self.mock_device(), climate)
         self.light = TuyaLocalLight(self.mock_device(), light)
         self.lock = TuyaLocalLock(self.mock_device(), lock)
+        self.humidifier = TuyaLocalHumidifier(self.mock_device(), humidifier)
 
         self.dps = DEHUMIDIFIER_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
@@ -78,26 +85,31 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.assertTrue(self.subject.should_poll)
         self.assertTrue(self.light.should_poll)
         self.assertTrue(self.lock.should_poll)
+        self.assertTrue(self.humidifier.should_poll)
 
     def test_name_returns_device_name(self):
         self.assertEqual(self.subject.name, self.subject._device.name)
         self.assertEqual(self.light.name, self.subject._device.name)
         self.assertEqual(self.lock.name, self.subject._device.name)
+        self.assertEqual(self.humidifier.name, self.subject._device.name)
 
     def test_friendly_name_returns_config_name(self):
         self.assertEqual(self.subject.friendly_name, self.climate_name)
         self.assertEqual(self.light.friendly_name, self.light_name)
         self.assertEqual(self.lock.friendly_name, self.lock_name)
+        self.assertEqual(self.humidifier.friendly_name, self.humidifier_name)
 
     def test_unique_id_returns_device_unique_id(self):
         self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
         self.assertEqual(self.light.unique_id, self.subject._device.unique_id)
         self.assertEqual(self.lock.unique_id, self.subject._device.unique_id)
+        self.assertEqual(self.humidifier.unique_id, self.subject._device.unique_id)
 
     def test_device_info_returns_device_info_from_device(self):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
         self.assertEqual(self.light.device_info, self.subject._device.device_info)
         self.assertEqual(self.lock.device_info, self.subject._device.device_info)
+        self.assertEqual(self.humidifier.device_info, self.subject._device.device_info)
 
     @skip("Icon customisation not supported yet")
     def test_icon_is_always_standard_when_off_without_error(self):
@@ -169,9 +181,11 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
 
     def test_min_target_humidity(self):
         self.assertEqual(self.subject.min_humidity, 30)
+        self.assertEqual(self.humidifier.min_humidity, 30)
 
     def test_max_target_humidity(self):
         self.assertEqual(self.subject.max_humidity, 80)
+        self.assertEqual(self.humidifier.max_humidity, 80)
 
     def test_target_humidity_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
@@ -179,6 +193,12 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
 
         self.assertEqual(self.subject.target_humidity, 55)
 
+    def test_target_humidity_in_humidifier(self):
+        self.dps[PRESET_DPS] = PRESET_NORMAL
+        self.dps[HUMIDITY_DPS] = 45
+
+        self.assertEqual(self.humidifier.target_humidity, 45)
+
     @skip("Conditions not supported yet")
     def test_target_humidity_outside_normal_preset(self):
         self.dps[HUMIDITY_DPS] = 55
@@ -213,6 +233,22 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             await self.subject.async_set_humidity(52)
 
+    async def test_set_humidity_in_humidifier_rounds_up_to_5_percent(self):
+        self.dps[PRESET_DPS] = PRESET_NORMAL
+        async with assert_device_properties_set(
+            self.humidifier._device,
+            {HUMIDITY_DPS: 45},
+        ):
+            await self.humidifier.async_set_humidity(43)
+
+    async def test_set_humidity_in_humidifier_rounds_down_to_5_percent(self):
+        self.dps[PRESET_DPS] = PRESET_NORMAL
+        async with assert_device_properties_set(
+            self.humidifier._device,
+            {HUMIDITY_DPS: 40},
+        ):
+            await self.humidifier.async_set_humidity(42)
+
     @skip("Conditions not supported yet")
     async def test_set_target_humidity_raises_error_outside_of_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_LOW
@@ -281,26 +317,51 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             await self.subject.async_set_hvac_mode(HVAC_MODE_DRY)
 
     async def test_turn_off(self):
+
         async with assert_device_properties_set(
             self.subject._device, {HVACMODE_DPS: False}
         ):
             await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
 
+    def test_humidifier_is_on(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertTrue(self.humidifier.is_on)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertFalse(self.humidifier.is_on)
+
+    async def test_dehumidifier_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.humidifier.async_turn_on()
+
+    async def test_dehumidifier_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.humidifier.async_turn_off()
+
     def test_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.assertEqual(self.subject.preset_mode, "Normal")
+        self.assertEqual(self.humidifier.mode, "Normal")
 
         self.dps[PRESET_DPS] = PRESET_LOW
         self.assertEqual(self.subject.preset_mode, "Low")
+        self.assertEqual(self.humidifier.mode, "Low")
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.assertEqual(self.subject.preset_mode, "High")
+        self.assertEqual(self.humidifier.mode, "High")
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.assertEqual(self.subject.preset_mode, "Dry clothes")
+        self.assertEqual(self.humidifier.mode, "Dry clothes")
 
         self.dps[PRESET_DPS] = None
         self.assertEqual(self.subject.preset_mode, None)
+        self.assertEqual(self.humidifier.mode, None)
 
     @skip("Conditions not supported yet")
     def test_air_clean_is_surfaced_in_preset_mode(self):

+ 9 - 2
tests/test_device_config.py

@@ -15,6 +15,8 @@ from custom_components.tuya_local.const import (
     CONF_TYPE_KOGAN_HEATER,
     CONF_TYPE_KOGAN_SWITCH,
     CONF_TYPE_PURLINE_M100_HEATER,
+    CONF_TYPE_REMORA_HEATPUMP,
+    CONF_TYPE_EANONS_HUMIDIFIER,
 )
 
 from custom_components.tuya_local.helpers.device_config import (
@@ -38,6 +40,7 @@ from .const import (
     KOGAN_SOCKET_PAYLOAD2,
     PURLINE_M100_HEATER_PAYLOAD,
     REMORA_HEATPUMP_PAYLOAD,
+    EANONS_HUMIDIFIER_PAYLOAD,
 )
 
 
@@ -196,10 +199,14 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             PURLINE_M100_HEATER_PAYLOAD,
             CONF_TYPE_PURLINE_M100_HEATER,
-            "PurlineM100Heater",
+            None,
         )
 
     # Non-legacy devices start here.
     def test_remora_heatpump_detection(self):
         """Test that Remora heatpump can be detected from its sample payload."""
-        self._test_detect(REMORA_HEATPUMP_PAYLOAD, "remora_heatpump", None)
+        self._test_detect(REMORA_HEATPUMP_PAYLOAD, CONF_TYPE_REMORA_HEATPUMP, None)
+
+    def test_eanons_humidifier(self):
+        """Test that Eanons humidifier can be detected from its sample payload."""
+        self._test_detect(EANONS_HUMIDIFIER_PAYLOAD, CONF_TYPE_EANONS_HUMIDIFIER, None)

+ 36 - 0
tests/test_humidifier.py

@@ -0,0 +1,36 @@
+"""Tests for the humidifier entity."""
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from unittest.mock import AsyncMock, Mock
+
+from custom_components.tuya_local.const import (
+    CONF_HUMIDIFIER,
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
+    CONF_TYPE_DEHUMIDIFIER,
+    DOMAIN,
+)
+from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
+from custom_components.tuya_local.humidifier import async_setup_entry
+
+
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: CONF_TYPE_AUTO, 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=CONF_TYPE_DEHUMIDIFIER)
+
+    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_HUMIDIFIER]) == TuyaLocalHumidifier
+    m_add_entities.assert_called_once()