From f79fea25e6e3d5178c5b92a326b7fca0d643a737 Mon Sep 17 00:00:00 2001 From: Jason Rumney Date: Sat, 9 Mar 2024 00:32:13 +0900 Subject: [PATCH] Add support for valve entities. Issue #1720 Modify water valve devices to use it. --- .../tuya_local/devices/ard100_valve.yaml | 13 ++- .../aubess_rainpoint_irrigation_system.yaml | 14 ++- .../devices/ble_hct611_watertimer.yaml | 14 ++- .../tuya_local/devices/ble_water_valve.yaml | 14 ++- .../tuya_local/devices/diivoo_wt05.yaml | 26 ++++- .../tuya_local/devices/qoto_03_sprinkler.yaml | 24 +++-- .../devices/qoto_05_water_valve.yaml | 24 +++-- .../devices/sh07_sprinkler_controller.yaml | 78 +++++++++++++- custom_components/tuya_local/valve.py | 102 ++++++++++++++++++ tests/const.py | 10 ++ tests/devices/base_device_tests.py | 2 + tests/devices/test_ble_water_valve.py | 83 ++++++++++++++ tests/devices/test_logicom_powerstrip.py | 6 +- tests/devices/test_qoto_03_sprinkler.py | 52 +++++++++ tests/devices/test_renpho_rp_ap001s.py | 7 +- tests/devices/test_woox_r4028_powerstrip.py | 6 +- tests/mixins/switch.py | 4 +- tests/test_device_config.py | 5 + tests/test_valve.py | 94 ++++++++++++++++ 19 files changed, 538 insertions(+), 40 deletions(-) create mode 100644 custom_components/tuya_local/valve.py create mode 100644 tests/devices/test_ble_water_valve.py create mode 100644 tests/test_valve.py diff --git a/custom_components/tuya_local/devices/ard100_valve.yaml b/custom_components/tuya_local/devices/ard100_valve.yaml index de503d532d..06c742687e 100644 --- a/custom_components/tuya_local/devices/ard100_valve.yaml +++ b/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 diff --git a/custom_components/tuya_local/devices/aubess_rainpoint_irrigation_system.yaml b/custom_components/tuya_local/devices/aubess_rainpoint_irrigation_system.yaml index a3ad9b348e..e11e418a8a 100644 --- a/custom_components/tuya_local/devices/aubess_rainpoint_irrigation_system.yaml +++ b/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 diff --git a/custom_components/tuya_local/devices/ble_hct611_watertimer.yaml b/custom_components/tuya_local/devices/ble_hct611_watertimer.yaml index 9152ed8155..5e02effba8 100644 --- a/custom_components/tuya_local/devices/ble_hct611_watertimer.yaml +++ b/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 diff --git a/custom_components/tuya_local/devices/ble_water_valve.yaml b/custom_components/tuya_local/devices/ble_water_valve.yaml index cfff8d5fac..260ec38763 100644 --- a/custom_components/tuya_local/devices/ble_water_valve.yaml +++ b/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 diff --git a/custom_components/tuya_local/devices/diivoo_wt05.yaml b/custom_components/tuya_local/devices/diivoo_wt05.yaml index a1b5f43251..013f321e3a 100644 --- a/custom_components/tuya_local/devices/diivoo_wt05.yaml +++ b/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 diff --git a/custom_components/tuya_local/devices/qoto_03_sprinkler.yaml b/custom_components/tuya_local/devices/qoto_03_sprinkler.yaml index 16329e32f3..83e657cce3 100644 --- a/custom_components/tuya_local/devices/qoto_03_sprinkler.yaml +++ b/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" diff --git a/custom_components/tuya_local/devices/qoto_05_water_valve.yaml b/custom_components/tuya_local/devices/qoto_05_water_valve.yaml index ec921c6113..cc58b34630 100644 --- a/custom_components/tuya_local/devices/qoto_05_water_valve.yaml +++ b/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" diff --git a/custom_components/tuya_local/devices/sh07_sprinkler_controller.yaml b/custom_components/tuya_local/devices/sh07_sprinkler_controller.yaml index 977d442f43..bda8020008 100644 --- a/custom_components/tuya_local/devices/sh07_sprinkler_controller.yaml +++ b/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 diff --git a/custom_components/tuya_local/valve.py b/custom_components/tuya_local/valve.py new file mode 100644 index 0000000000..ccfce86d01 --- /dev/null +++ b/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) diff --git a/tests/const.py b/tests/const.py index 052b23e0c4..ced5136b5f 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1637,3 +1637,13 @@ LORATAP_CURTAINSWITCH_PAYLOAD = { "1": "3", } + +BLE_WATERVALVE_PAYLOAD = { + "1": True, + "4": 0, + "7": 50, + "9": 3600, + "10": "cancel", + "12": "unknown", + "15": 60, +} diff --git a/tests/devices/base_device_tests.py b/tests/devices/base_device_tests.py index 90ce5cee54..4672050ba8 100644 --- a/tests/devices/base_device_tests.py +++ b/tests/devices/base_device_tests.py @@ -27,6 +27,7 @@ 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 @@ "sensor": TuyaLocalSensor, "siren": TuyaLocalSiren, "vacuum": TuyaLocalVacuum, + "valve": TuyaLocalValve, "water_heater": TuyaLocalWaterHeater, } diff --git a/tests/devices/test_ble_water_valve.py b/tests/devices/test_ble_water_valve.py new file mode 100644 index 0000000000..612d332d0c --- /dev/null +++ b/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", + }, + ) diff --git a/tests/devices/test_logicom_powerstrip.py b/tests/devices/test_logicom_powerstrip.py index 6e040ac557..5d3887daee 100644 --- a/tests/devices/test_logicom_powerstrip.py +++ b/tests/devices/test_logicom_powerstrip.py @@ -51,7 +51,11 @@ def setUp(self): "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( diff --git a/tests/devices/test_qoto_03_sprinkler.py b/tests/devices/test_qoto_03_sprinkler.py index b726449f4f..cf6a2efd22 100644 --- a/tests/devices/test_qoto_03_sprinkler.py +++ b/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 @@ def setUp(self): ) 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) diff --git a/tests/devices/test_renpho_rp_ap001s.py b/tests/devices/test_renpho_rp_ap001s.py index 22444c07cc..584c9fe05c 100644 --- a/tests/devices/test_renpho_rp_ap001s.py +++ b/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 @@ def setUp(self): 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( [ { diff --git a/tests/devices/test_woox_r4028_powerstrip.py b/tests/devices/test_woox_r4028_powerstrip.py index b1bce20a0c..9a9931afbe 100644 --- a/tests/devices/test_woox_r4028_powerstrip.py +++ b/tests/devices/test_woox_r4028_powerstrip.py @@ -44,7 +44,11 @@ def setUp(self): "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( diff --git a/tests/mixins/switch.py b/tests/mixins/switch.py index e47af267a3..be5b51fe45 100644 --- a/tests/mixins/switch.py +++ b/tests/mixins/switch.py @@ -50,7 +50,7 @@ def setUpBasicSwitch( self, dps, subject, - device_class="switch", + device_class=None, power_dps=None, power_scale=1, testdata=(True, False), @@ -126,7 +126,7 @@ def setUpMultiSwitch(self, switches): 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 diff --git a/tests/test_device_config.py b/tests/test_device_config.py index 1b3cdd7d72..a87e5abf02 100644 --- a/tests/test_device_config.py +++ b/tests/test_device_config.py @@ -132,6 +132,7 @@ "siren", "switch", "vacuum", + "valve", "water_heater", ] ), @@ -259,6 +260,10 @@ "fan_speed", ], }, + "valve": { + "required": ["valve"], + "optional": [], + }, "water_heater": { "required": [], "optional": [ diff --git a/tests/test_valve.py b/tests/test_valve.py new file mode 100644 index 0000000000..896696d417 --- /dev/null +++ b/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()