Kaynağa Gözat

Add support for valve entities.

Issue #1720

Modify water valve devices to use it.
Jason Rumney 1 yıl önce
ebeveyn
işleme
f79fea25e6

+ 10 - 3
custom_components/tuya_local/devices/ard100_valve.yaml

@@ -3,13 +3,20 @@ products:
   - id: nguto5atyd2xxnap
     name: ARD-100+ smart valve controller
 primary_entity:
-  entity: switch
-  icon: "mdi:valve"
+  entity: valve
   dps:
     - id: 1
       type: boolean
-      name: switch
+      name: valve
 secondary_entities:
+  - entity: switch
+    icon: "mdi:valve"
+    deprecated: valve
+    category: config
+    dps:
+      - id: 1
+        type: boolean
+        name: switch
   - entity: number
     translation_key: timer
     category: config

+ 11 - 3
custom_components/tuya_local/devices/aubess_rainpoint_irrigation_system.yaml

@@ -3,13 +3,21 @@ products:
   - id: 2ak7r2culspkc7hx
     name: Aubess RainPoint TTP106W
 primary_entity:
-  entity: switch
-  icon: "mdi:pipe-valve"
+  entity: valve
+  class: water
   dps:
     - id: 1
-      name: switch
+      name: valve
       type: boolean
 secondary_entities:
+  - entity: switch
+    icon: "mdi:pipe-valve"
+    deprecated: valve
+    category: config
+    dps:
+      - id: 1
+        name: switch
+        type: boolean
   - entity: sensor
     name: Status
     class: enum

+ 11 - 3
custom_components/tuya_local/devices/ble_hct611_watertimer.yaml

@@ -3,12 +3,12 @@ products:
   - id: tqzkwarw
     name: HCT-611
 primary_entity:
-  entity: switch
-  icon: "mdi:pipe-valve"
+  entity: valve
+  class: water
   dps:
     - id: 1
       type: boolean
-      name: switch
+      name: valve
     - id: 12
       type: string
       name: state
@@ -49,6 +49,14 @@ primary_entity:
       name: program_8
       optional: true
 secondary_entities:
+  - entity: switch
+    icon: "mdi:pipe-valve"
+    deprecated: valve
+    category: config
+    dps:
+      - id: 1
+        type: boolean
+        name: switch
   - entity: sensor
     class: battery
     category: diagnostic

+ 11 - 3
custom_components/tuya_local/devices/ble_water_valve.yaml

@@ -10,12 +10,12 @@ products:
   - id: so5ybnw9
     name: BWC-495.bt Royal Gardineer
 primary_entity:
-  entity: switch
-  icon: "mdi:pipe-valve"
+  entity: valve
+  class: water
   dps:
     - id: 1
       type: boolean
-      name: switch
+      name: valve
     - id: 13
       type: string
       name: weather
@@ -29,6 +29,14 @@ primary_entity:
       name: irrigation_schedule
       optional: true
 secondary_entities:
+  - entity: switch
+    icon: "mdi:pipe-valve"
+    deprecated: valve
+    category: config
+    dps:
+      - id: 1
+        type: boolean
+        name: switch
   - entity: sensor
     class: battery
     category: diagnostic

+ 22 - 4
custom_components/tuya_local/devices/diivoo_wt05.yaml

@@ -17,16 +17,34 @@ products:
   - id: fdrbxxbg
     name: Diivoo WT-05
 primary_entity:
-  entity: switch
-  icon: "mdi:pipe-valve"
-  name: Switch 1
+  entity: valve
+  class: water
+  name: Valve 1
   dps:
     - id: 105
       type: boolean
-      name: switch
+      name: valve
 secondary_entities:
+  - entity: valve
+    class: water
+    name: Valve 2
+    dps:
+      - id: 104
+        type: boolean
+        name: valve
+  - entity: switch
+    name: Switch 1
+    category: config
+    deprecated: valve
+    icon: "mdi:pipe-valve"
+    dps:
+      - id: 105
+        type: boolean
+        name: switch
   - entity: switch
     name: Switch 2
+    category: config
+    deprecated: valve
     icon: "mdi:pipe-valve"
     dps:
       - id: 104

+ 17 - 7
custom_components/tuya_local/devices/qoto_03_sprinkler.yaml

@@ -1,18 +1,28 @@
 name: QOTO 03 sprinkler controller
 primary_entity:
-  entity: number
-  icon: "mdi:pipe-valve"
+  entity: valve
+  class: water
   dps:
     - id: 102
       type: integer
-      name: value
-      unit: "%"
-      range:
-        min: 0
-        max: 100
+      name: valve
       mapping:
         - step: 5
 secondary_entities:
+  - entity: number
+    icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
+    dps:
+      - id: 102
+        type: integer
+        name: value
+        unit: "%"
+        range:
+          min: 0
+          max: 100
+        mapping:
+          - step: 5
   - entity: sensor
     category: diagnostic
     icon: "mdi:valve"

+ 15 - 9
custom_components/tuya_local/devices/qoto_05_water_valve.yaml

@@ -3,19 +3,25 @@ products:
   - id: arge1ptm
     name: QOTO 05 water valve
 primary_entity:
-  entity: number
-  icon: "mdi:pipe-valve"
+  entity: valve
+  class: water
   dps:
     - id: 2
       type: integer
-      name: value
-      unit: "%"
-      range:
-        min: 0
-        max: 100
-      mapping:
-        - step: 1
+      name: valve
 secondary_entities:
+  - entity: number
+    icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
+    dps:
+      - id: 2
+        type: integer
+        name: value
+        unit: "%"
+        range:
+          min: 0
+          max: 100
   - entity: sensor
     category: diagnostic
     icon: "mdi:valve"

+ 75 - 3
custom_components/tuya_local/devices/sh07_sprinkler_controller.yaml

@@ -3,16 +3,76 @@ products:
   - id: e8fwsklivj87msao
     name: Smart sprinkler controller
 primary_entity:
-  entity: switch
+  entity: valve
   name: Valve 1
-  icon: "mdi:pipe-valve"
+  class: water
   dps:
     - id: 101
-      name: switch
+      name: valve
       type: boolean
 secondary_entities:
+  - entity: valve
+    name: Valve 2
+    class: water
+    dps:
+      - id: 102
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 3
+    class: water
+    dps:
+      - id: 103
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 4
+    class: water
+    dps:
+      - id: 104
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 5
+    class: water
+    dps:
+      - id: 105
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 6
+    class: water
+    dps:
+      - id: 106
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 7
+    class: water
+    dps:
+      - id: 109
+        name: valve
+        type: boolean
+  - entity: valve
+    name: Valve 8
+    class: water
+    dps:
+      - id: 110
+        name: valve
+        type: boolean
+  - entity: switch
+    name: Valve 1
+    category: config
+    deprecated: valve
+    icon: "mdi:pipe-valve"
+    dps:
+      - id: 101
+        name: switch
+        type: boolean
   - entity: switch
     name: Valve 2
+    category: config
+    deprecated: valve
     icon: "mdi:pipe-valve"
     dps:
       - id: 102
@@ -21,12 +81,16 @@ secondary_entities:
   - entity: switch
     name: Valve 3
     icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
     dps:
       - id: 103
         name: switch
         type: boolean
   - entity: switch
     name: Valve 4
+    category: config
+    deprecated: valve
     icon: "mdi:pipe-valve"
     dps:
       - id: 104
@@ -35,6 +99,8 @@ secondary_entities:
   - entity: switch
     name: Valve 5
     icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
     dps:
       - id: 105
         name: switch
@@ -43,6 +109,8 @@ secondary_entities:
   - entity: switch
     name: Valve 6
     icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
     dps:
       - id: 106
         name: switch
@@ -51,6 +119,8 @@ secondary_entities:
   - entity: switch
     name: Valve 7
     icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
     dps:
       - id: 110
         name: switch
@@ -59,6 +129,8 @@ secondary_entities:
   - entity: switch
     name: Valve 8
     icon: "mdi:pipe-valve"
+    category: config
+    deprecated: valve
     dps:
       - id: 111
         name: switch

+ 102 - 0
custom_components/tuya_local/valve.py

@@ -0,0 +1,102 @@
+"""
+Support for Tuya valve devices
+"""
+
+import logging
+
+from homeassistant.components.valve import (
+    ValveDeviceClass,
+    ValveEntity,
+    ValveEntityFeature,
+)
+
+from .device import TuyaLocalDevice
+from .helpers.config import async_tuya_setup_platform
+from .helpers.device_config import TuyaEntityConfig
+from .helpers.mixin import TuyaLocalEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    config = {**config_entry.data, **config_entry.options}
+    await async_tuya_setup_platform(
+        hass,
+        async_add_entities,
+        config,
+        "valve",
+        TuyaLocalValve,
+    )
+
+
+class TuyaLocalValve(TuyaLocalEntity, ValveEntity):
+    """Representation of a Tuya Valve"""
+
+    def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
+        """
+        Initialise the valve.
+        Args:
+            device (TuyaLocalDevice): The device API instance.
+        """
+        super().__init__()
+        dps_map = self._init_begin(device, config)
+        self._valve_dp = dps_map.pop("valve")
+        self._init_end(dps_map)
+        if not self._valve_dp.readonly:
+            self._attr_supported_features |= ValveEntityFeature.OPEN
+            self._attr_supported_features |= ValveEntityFeature.CLOSE
+            if self._valve_dp.type is int:
+                self._attr_supported_features |= ValveEntityFeature.SET_POSITION
+
+    @property
+    def device_class(self):
+        """Return the class of this device"""
+        dclass = self._config.device_class
+        try:
+            return ValveDeviceClass(dclass)
+        except ValueError:
+            if dclass:
+                _LOGGER.warning(
+                    "%s/%s: Unrecognised valve device class of %s ignored",
+                    self._config._device.config,
+                    self.name or "valve",
+                    dclass,
+                )
+
+    @property
+    def reports_position(self):
+        """If the valve is an integer, it reports position."""
+        return self._valve_dp.type is int
+
+    @property
+    def current_position(self):
+        """Report the position of the valve."""
+        pos = self._valve_dp.get_value(self._device)
+        if isinstance(pos, int):
+            return pos
+
+    @property
+    def is_closed(self):
+        """Report whether the valve is closed."""
+        pos = self._valve_dp.get_value(self._device)
+        return not pos
+
+    async def async_open_valve(self):
+        """Open the valve."""
+        await self._valve_dp.async_set_value(
+            self._device,
+            100 if self.reports_position else True,
+        )
+
+    async def async_close_valve(self):
+        """Close the valve"""
+        await self._valve_dp.async_set_value(
+            self._device,
+            0 if self.reports_position else False,
+        )
+
+    async def async_set_valve_position(self, position):
+        """Set the position of the valve"""
+        if not self.reports_position:
+            raise NotImplementedError()
+        await self._valve_dp.async_set_value(self._device, position)

+ 10 - 0
tests/const.py

@@ -1637,3 +1637,13 @@ IR_REMOTE_SENSORS_PAYLOAD = {
 LORATAP_CURTAINSWITCH_PAYLOAD = {
     "1": "3",
 }
+
+BLE_WATERVALVE_PAYLOAD = {
+    "1": True,
+    "4": 0,
+    "7": 50,
+    "9": 3600,
+    "10": "cancel",
+    "12": "unknown",
+    "15": 60,
+}

+ 2 - 0
tests/devices/base_device_tests.py

@@ -27,6 +27,7 @@ from custom_components.tuya_local.sensor import TuyaLocalSensor
 from custom_components.tuya_local.siren import TuyaLocalSiren
 from custom_components.tuya_local.switch import TuyaLocalSwitch
 from custom_components.tuya_local.vacuum import TuyaLocalVacuum
+from custom_components.tuya_local.valve import TuyaLocalValve
 from custom_components.tuya_local.water_heater import TuyaLocalWaterHeater
 
 DEVICE_TYPES = {
@@ -49,6 +50,7 @@ DEVICE_TYPES = {
     "sensor": TuyaLocalSensor,
     "siren": TuyaLocalSiren,
     "vacuum": TuyaLocalVacuum,
+    "valve": TuyaLocalValve,
     "water_heater": TuyaLocalWaterHeater,
 }
 

+ 83 - 0
tests/devices/test_ble_water_valve.py

@@ -0,0 +1,83 @@
+"""Tests for the BLE water valve."""
+
+from homeassistant.components.valve import ValveDeviceClass, ValveEntityFeature
+
+from ..const import BLE_WATERVALVE_PAYLOAD
+from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
+
+VALVE_DP = "1"
+PROBLEM_DP = "4"
+BATTERY_DP = "7"
+TOTALUSE_DP = "9"
+WEATHERDELAY_DP = "10"
+IRRIGTIME = "11"
+OPERATION_DP = "12"
+WEATHER_DP = "13"
+WEATHERSW_DP = "14"
+LASTUSE_DP = "15"
+SOAKSCHED_DP = "16"
+IRRIGSCHED_DP = "17"
+
+
+class TestBLEValve(TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("ble_water_valve.yaml", BLE_WATERVALVE_PAYLOAD)
+        self.subject = self.entities["valve_water"]
+        self.mark_secondary(
+            [
+                "switch",
+                "sensor_battery",
+                "binary_sensor_problem",
+                "sensor_operation",
+                "sensor_accumulated_use_time",
+                "sensor_last_use_time",
+                "select_weather_delay",
+                "number_irrigation_time",
+                "switch_smart_weather_switch",
+            ]
+        )
+
+    def test_device_class_is_water(self):
+        self.assertEqual(self.subject.device_class, ValveDeviceClass.WATER)
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE,
+        )
+
+    def test_is_closed(self):
+        self.dps[VALVE_DP] = True
+        self.assertFalse(self.subject.is_closed)
+        self.dps[VALVE_DP] = False
+        self.assertTrue(self.subject.is_closed)
+
+    async def test_open_valve(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {VALVE_DP: True},
+        ):
+            await self.subject.async_open_valve()
+
+    async def test_close_valve(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {VALVE_DP: False},
+        ):
+            await self.subject.async_close_valve()
+
+    def test_extra_state_attributes(self):
+        self.dps[WEATHER_DP] = "Sunny"
+        self.dps[SOAKSCHED_DP] = "soaktest"
+        self.dps[IRRIGSCHED_DP] = "irrigationtest"
+        self.assertDictEqual(
+            self.subject.extra_state_attributes,
+            {
+                "weather": "Sunny",
+                "soak_schedule": "soaktest",
+                "irrigation_schedule": "irrigationtest",
+            },
+        )

+ 5 - 1
tests/devices/test_logicom_powerstrip.py

@@ -51,7 +51,11 @@ class TestLogicomPowerstrip(
                     "name": "switch_outlet_4",
                     "device_class": SwitchDeviceClass.OUTLET,
                 },
-                {"dps": SWITCHUSB_DPS, "name": "switch_usb_switch"},
+                {
+                    "dps": SWITCHUSB_DPS,
+                    "name": "switch_usb_switch",
+                    "device_class": SwitchDeviceClass.SWITCH,
+                },
             ]
         )
         self.setUpMultiNumber(

+ 52 - 0
tests/devices/test_qoto_03_sprinkler.py

@@ -1,9 +1,11 @@
 """Tests for the Quto 03 Sprinkler."""
 
 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
+from homeassistant.components.valve import ValveDeviceClass, ValveEntityFeature
 from homeassistant.const import PERCENTAGE, UnitOfTime
 
 from ..const import QOTO_SPRINKLER_PAYLOAD
+from ..helpers import assert_device_properties_set
 from ..mixins.binary_sensor import BasicBinarySensorTests
 from ..mixins.number import MultiNumberTests
 from ..mixins.sensor import MultiSensorTests
@@ -26,6 +28,7 @@ class TestQotoSprinkler(
 
     def setUp(self):
         self.setUpForConfig("qoto_03_sprinkler.yaml", QOTO_SPRINKLER_PAYLOAD)
+        self.subject = self.entities.get("valve_water")
         self.setUpBasicBinarySensor(
             ERROR_DPS,
             self.entities.get("binary_sensor_problem"),
@@ -66,9 +69,58 @@ class TestQotoSprinkler(
         )
         self.mark_secondary(
             [
+                "number",
                 "binary_sensor_problem",
                 "number_timer",
                 "sensor_open",
                 "sensor_timer",
             ]
         )
+
+    def test_device_class_is_water(self):
+        self.assertEqual(self.subject.device_class, ValveDeviceClass.WATER)
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            ValveEntityFeature.OPEN
+            | ValveEntityFeature.CLOSE
+            | ValveEntityFeature.SET_POSITION,
+        )
+
+    def test_is_closed(self):
+        self.dps[TARGET_DPS] = 100
+        self.assertFalse(self.subject.is_closed)
+        self.dps[TARGET_DPS] = 50
+        self.assertFalse(self.subject.is_closed)
+        self.dps[TARGET_DPS] = 0
+        self.assertTrue(self.subject.is_closed)
+
+    def test_current_position(self):
+        self.dps[TARGET_DPS] = 100
+        self.assertEqual(self.subject.current_position, 100)
+        self.dps[TARGET_DPS] = 50
+        self.assertEqual(self.subject.current_position, 50)
+        self.dps[TARGET_DPS] = 0
+        self.assertEqual(self.subject.current_position, 0)
+
+    async def test_open_valve(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TARGET_DPS: 100},
+        ):
+            await self.subject.async_open_valve()
+
+    async def test_close_valve(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TARGET_DPS: 0},
+        ):
+            await self.subject.async_close_valve()
+
+    async def test_set_valve_position(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TARGET_DPS: 50},
+        ):
+            await self.subject.async_set_valve_position(50)

+ 6 - 1
tests/devices/test_renpho_rp_ap001s.py

@@ -1,5 +1,6 @@
 from homeassistant.components.fan import FanEntityFeature
 from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.components.switch import SwitchDeviceClass
 
 from ..const import RENPHO_PURIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -38,7 +39,11 @@ class TestRenphoPurifier(
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_aq_indicator"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.setUpBasicSwitch(SLEEP_DPS, self.entities.get("switch_sleep"))
+        self.setUpBasicSwitch(
+            SLEEP_DPS,
+            self.entities.get("switch_sleep"),
+            device_class=SwitchDeviceClass.SWITCH,
+        )
         self.setUpMultiSensors(
             [
                 {

+ 5 - 1
tests/devices/test_woox_r4028_powerstrip.py

@@ -44,7 +44,11 @@ class TestWooxR4028Powerstrip(
                     "name": "switch_outlet_3",
                     "device_class": SwitchDeviceClass.OUTLET,
                 },
-                {"dps": SWITCHUSB_DPS, "name": "switch_usb_switch"},
+                {
+                    "dps": SWITCHUSB_DPS,
+                    "name": "switch_usb_switch",
+                    "device_class": SwitchDeviceClass.SWITCH,
+                },
             ]
         )
         self.setUpMultiNumber(

+ 2 - 2
tests/mixins/switch.py

@@ -50,7 +50,7 @@ class BasicSwitchTests:
         self,
         dps,
         subject,
-        device_class="switch",
+        device_class=None,
         power_dps=None,
         power_scale=1,
         testdata=(True, False),
@@ -126,7 +126,7 @@ class MultiSwitchTests:
             self.multiSwitchDps[name] = s.get("dps")
             try:
                 self.multiSwitchDevClass[name] = SwitchDeviceClass(
-                    s.get("device_class", "switch")
+                    s.get("device_class")
                 )
             except ValueError:
                 self.multiSwitchDevClass[name] = None

+ 5 - 0
tests/test_device_config.py

@@ -132,6 +132,7 @@ ENTITY_SCHEMA = vol.Schema(
                 "siren",
                 "switch",
                 "vacuum",
+                "valve",
                 "water_heater",
             ]
         ),
@@ -259,6 +260,10 @@ KNOWN_DPS = {
             "fan_speed",
         ],
     },
+    "valve": {
+        "required": ["valve"],
+        "optional": [],
+    },
     "water_heater": {
         "required": [],
         "optional": [

+ 94 - 0
tests/test_valve.py

@@ -0,0 +1,94 @@
+""" Tests for the valve entity"""
+
+from unittest.mock import AsyncMock, Mock
+
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.tuya_local.const import (
+    CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
+    CONF_TYPE,
+    DOMAIN,
+)
+from custom_components.tuya_local.valve import TuyaLocalValve, async_setup_entry
+
+
+@pytest.mark.asyncio
+async def test_init_entry(hass):
+    """Test initialisation"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "ble_water_valve",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {
+            "device": m_device,
+        },
+    }
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"]["valve_water"]) == TuyaLocalValve
+    m_add_entities.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_init_entry_fails_if_device_has_no_valve(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "kogan_heater",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    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()
+
+
+@pytest.mark.asyncio
+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",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+    # 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()