Quellcode durchsuchen

Add support for new heater type.

Based on information from @awaismun in Issue #5. The heaters appear to match some sold online as Andersson GSH 3.2.
Jason Rumney vor 5 Jahren
Ursprung
Commit
6526211454

+ 9 - 0
README.md

@@ -55,6 +55,14 @@ Current temperature is displayed, and current humidity is available as a propert
 
 Current temperature is also displayed.
 
+**Andersson Heaters**
+
+- **power** (on/off)
+- **mode** (ANTI-FREEZE/LOW/HIGH)
+- **target temperature** (`5`-`35` in °C)
+
+Current temperature is also displayed.
+
 ### Additional features
 
 **Light** (Goldair devices)
@@ -89,6 +97,7 @@ Support for newer Kogan Smartplugs with USB sockets on them is based on feedback
 
 A number of other brands of plug seem to match the DPS indexes of either the older or newer Kogan Smartplugs, so it is likely to work with other brands of single energy monitoring smartplug also.
 
+Support for heaters visually matching Andersson GSH 3.2 was added based on information from @awaismun on Issue #5.
 
 ---
 

+ 5 - 1
custom_components/tuya_local/climate.py

@@ -12,6 +12,7 @@ from .const import (
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
+    CONF_TYPE_GSH_HEATER,
     CONF_TYPE_KOGAN_HEATER,
     CONF_CLIMATE,
 )
@@ -21,10 +22,11 @@ from .geco_heater.climate import GoldairGECOHeater
 from .gpcv_heater.climate import GoldairGPCVHeater
 from .heater.climate import GoldairHeater
 from .kogan_heater.climate import KoganHeater
+from .gsh_heater.climate import AnderssonGSHHeater
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the Goldair climate device according to its type."""
+    """Set up the Tuya device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
 
@@ -46,6 +48,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         data[CONF_CLIMATE] = GoldairGPCVHeater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
         data[CONF_CLIMATE] = KoganHeater(device)
+    elif discovery_info{CONF_TYPE] == CONF_TYPE_GSH_HEATER:
+        data[CONF_CLIMATE] = AnderssonGSHHeater(device)
     else:
         raise ValueError("This device does not support working as a climate device")
 

+ 2 - 0
custom_components/tuya_local/configuration.py

@@ -15,6 +15,7 @@ from .const import (
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
+    CONF_TYPE_GSH_HEATER,
     CONF_TYPE_KOGAN_HEATER,
     CONF_TYPE_KOGAN_SWITCH,
 )
@@ -34,6 +35,7 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
                 CONF_TYPE_FAN,
                 CONF_TYPE_GECO_HEATER,
                 CONF_TYPE_GPCV_HEATER,
+                CONF_TYPE_GSH_HEATER,
                 CONF_TYPE_KOGAN_HEATER,
                 CONF_TYPE_KOGAN_SWITCH,
             ]

+ 1 - 0
custom_components/tuya_local/const.py

@@ -13,6 +13,7 @@ CONF_TYPE_GECO_HEATER = "geco_heater"
 CONF_TYPE_GPCV_HEATER = "gpcv_heater"
 CONF_TYPE_KOGAN_HEATER = "kogan_heater"
 CONF_TYPE_KOGAN_SWITCH = "kogan_switch"
+CONF_TYPE_GSH_HEATER = "gsh_heater"
 CONF_CLIMATE = "climate"
 CONF_DISPLAY_LIGHT = "display_light"
 CONF_CHILD_LOCK = "child_lock"

+ 4 - 0
custom_components/tuya_local/device.py

@@ -17,6 +17,7 @@ from .const import (
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
+    CONF_TYPE_GSH_HEATER,
     CONF_TYPE_KOGAN_HEATER,
     CONF_TYPE_KOGAN_SWITCH,
     DOMAIN,
@@ -109,6 +110,9 @@ class TuyaLocalDevice(object):
         if "7" in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Goldair GPCV Heater")
             return CONF_TYPE_GPCV_HEATER
+        if "12" in cached_state:
+            _LOGGER.info(f"Detecting {self.name} as Andersson GSH Heter")
+            return CONF_TYPE_GSH_HEATER
         if "3" in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Goldair GECO Heater")
             return CONF_TYPE_GECO_HEATER

+ 0 - 0
custom_components/tuya_local/gsh_heater/__init__.py


+ 177 - 0
custom_components/tuya_local/gsh_heater/climate.py

@@ -0,0 +1,177 @@
+"""
+Andersson GSH WiFi Heater device.
+"""
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    HVAC_MODE_HEAT,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from ..device import TuyaLocalDevice
+from .const import (
+    ATTR_ERROR,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+)
+
+SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+
+
+class AnderssonGSHHeater(ClimateEntity):
+    """Representation of a Andersson GSH WiFi heater."""
+
+    def __init__(self, device):
+        """Initialize the heater.
+        Args:
+            device (TuyaLocalDevice): The device API instance."""
+        self._device = device
+
+        self._support_flags = SUPPORT_FLAGS
+
+        self._TEMPERATURE_STEP = 1
+        self._TEMPERATURE_LIMITS = {"min": 5, "max": 35}
+
+    @property
+    def supported_features(self):
+        """Return the list of supported features."""
+        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 unique_id(self):
+        """Return the unique id for this heater."""
+        return self._device.unique_id
+
+    @property
+    def device_info(self):
+        """Return device information about this heater."""
+        return self._device.device_info
+
+    @property
+    def icon(self):
+        """Return the icon to use in the frontend for this device."""
+        hvac_mode = self.hvac_mode
+
+        if hvac_mode == HVAC_MODE_HEAT:
+            return "mdi:radiator"
+        else:
+            return "mdi:radiator-disabled"
+
+    @property
+    def temperature_unit(self):
+        """Return the unit of measurement."""
+        return self._device.temperature_unit
+
+    @property
+    def target_temperature(self):
+        """Return the temperature we try to reach."""
+        return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE])
+
+    @property
+    def target_temperature_step(self):
+        """Return the supported step of target temperature."""
+        return self._TEMPERATURE_STEP
+
+    @property
+    def min_temp(self):
+        """Return the minimum temperature."""
+        return self._TEMPERATURE_LIMITS["min"]
+
+    @property
+    def max_temp(self):
+        """Return the maximum temperature."""
+        return self._TEMPERATURE_LIMITS["max"]
+
+    async def async_set_temperature(self, **kwargs):
+        """Set new target temperatures."""
+        if kwargs.get(ATTR_PRESET_MODE) is not None:
+            await self.async_set_preset_mode(kwargs.get(ATTR_PRESET_MODE))
+        if kwargs.get(ATTR_TEMPERATURE) is not None:
+            await self.async_set_target_temperature(kwargs.get(ATTR_TEMPERATURE))
+
+    async def async_set_target_temperature(self, target_temperature):
+        target_temperature = int(round(target_temperature))
+
+        limits = self._TEMPERATURE_LIMITS
+        if not limits["min"] <= target_temperature <= limits["max"]:
+            raise ValueError(
+                f"Target temperature ({target_temperature}) must be between "
+                f'{limits["min"]} and {limits["max"]}.'
+            )
+
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE], target_temperature
+        )
+
+    @property
+    def current_temperature(self):
+        """Return the current temperature."""
+        return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE])
+
+    @property
+    def hvac_mode(self):
+        """Return current HVAC mode, ie Heat or Off."""
+        dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
+
+        if dps_mode is not None:
+            return TuyaLocalDevice.get_key_for_value(HVAC_MODE_TO_DPS_MODE, dps_mode)
+        else:
+            return STATE_UNAVAILABLE
+
+    @property
+    def hvac_modes(self):
+        """Return the list of available HVAC modes."""
+        return list(HVAC_MODE_TO_DPS_MODE.keys())
+
+    async def async_set_hvac_mode(self, hvac_mode):
+        """Set new HVAC mode."""
+        dps_mode = HVAC_MODE_TO_DPS_MODE[hvac_mode]
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode
+        )
+
+    @property
+    def preset_mode(self):
+        """Return current preset mode, ie Comfort, Eco, Anti-freeze."""
+        dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE])
+        if dps_mode is not None:
+            return TuyaLocalDevice.get_key_for_value(PRESET_MODE_TO_DPS_MODE, dps_mode)
+        else:
+            return None
+
+    @property
+    def preset_modes(self):
+        """Return the list of available preset modes."""
+        return list(PRESET_MODE_TO_DPS_MODE.keys())
+
+    async def async_set_preset_mode(self, preset_mode):
+        """Set new preset mode."""
+        dps_mode = PRESET_MODE_TO_DPS_MODE[preset_mode]
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode
+        )
+
+    @property
+    def device_state_attributes(self):
+        """Get additional attributes that HA doesn't naturally support."""
+        error = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_ERROR])
+
+        return {ATTR_ERROR: error or None}
+
+    async def async_update(self):
+        await self._device.async_refresh()

+ 29 - 0
custom_components/tuya_local/gsh_heater/const.py

@@ -0,0 +1,29 @@
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+)
+from homeassistant.const import ATTR_TEMPERATURE
+
+ATTR_TARGET_TEMPERATURE = "target_temperature"
+ATTR_ERROR = "error"
+
+PRESET_LOW = "Low"
+PRESET_HIGH = "High"
+PRESET_ANTIFREEZE = "Anti-freeze"
+
+PROPERTY_TO_DPS_ID = {
+    ATTR_HVAC_MODE: "1",
+    ATTR_TARGET_TEMPERATURE: "2",
+    ATTR_TEMPERATURE: "3",
+    ATTR_PRESET_MODE: "4",
+    ATTR_ERROR: "12",
+}
+
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}
+PRESET_MODE_TO_DPS_MODE = {
+    PRESET_LOW: "low",
+    PRESET_HIGH: "high",
+    PRESET_ANTIFREEZE: "af",
+}

+ 8 - 0
tests/const.py

@@ -68,3 +68,11 @@ KOGAN_SOCKET_PAYLOAD2 = {
     "19": 460,
     "20": 2300,
 }
+
+GSH_HEATER_PAYLOAD = {
+    "1": True,
+    "2": 22,
+    "3": 24,
+    "4": "low",
+    "12": 0,
+}

+ 0 - 0
tests/gsh_heater/__init__.py


+ 234 - 0
tests/gsh_heater/test_climate.py

@@ -0,0 +1,234 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from custom_components.tuya_local.gsh_heater.climate import AnderssonGSHHeater
+from custom_components.tuya_local.gsh_heater.const import (
+    ATTR_ERROR,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_ANTIFREEZE,
+    PRESET_HIGH,
+    PRESET_LOW,
+    PRESET_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+)
+
+from ..const import GSH_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+
+class TestAnderssonGSHHeater(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+
+        self.subject = AnderssonGSHHeater(self.mock_device())
+
+        self.dps = GSH_HEATER_PAYLOAD.copy()
+        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE,
+        )
+
+    def test_should_poll(self):
+        self.assertTrue(self.subject.should_poll)
+
+    def test_name_returns_device_name(self):
+        self.assertEqual(self.subject.name, self.subject._device.name)
+
+    def test_unique_id_returns_device_unique_id(self):
+        self.assertEqual(self.subject.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)
+
+    def test_icon(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 5)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 35)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]: 25}
+        ):
+            await self.subject.async_set_temperature(temperature=25)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[PRESET_LOW]},
+        ):
+            await self.subject.async_set_temperature(preset_mode=PRESET_LOW)
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]: 25,
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_LOW
+                ],
+            },
+        ):
+            await self.subject.async_set_temperature(
+                temperature=25, preset_mode=PRESET_LOW
+            )
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]: 25}
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]: 25},
+        ):
+            await self.subject.async_set_target_temperature(24.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            ValueError, "Target temperature \\(4\\) must be between 5 and 35"
+        ):
+            await self.subject.async_set_target_temperature(4)
+
+        with self.assertRaisesRegex(
+            ValueError, "Target temperature \\(36\\) must be between 5 and 35"
+        ):
+            await self.subject.async_set_target_temperature(36)
+
+    def test_current_temperature(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE]] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_LOW)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_ANTIFREEZE
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_ANTIFREEZE)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertEqual(
+            self.subject.preset_modes, [PRESET_LOW, PRESET_HIGH, PRESET_ANTIFREEZE]
+        )
+
+    async def test_set_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[PRESET_LOW]},
+        ):
+            await self.subject.async_set_preset_mode(PRESET_LOW)
+
+    async def test_set_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_HIGH
+                ]
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_HIGH)
+
+    async def test_set_preset_mode_to_af(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_ANTIFREEZE
+                ]
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_ANTIFREEZE)
+
+    def test_error_state(self):
+        # There are currently no known error states; update this as they're discovered
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = "something"
+        self.assertEqual(
+            self.subject.device_state_attributes, {ATTR_ERROR: "something"}
+        )
+
+    async def test_update(self):
+        result = AsyncMock()
+        self.subject._device.async_refresh.return_value = result()
+
+        await self.subject.async_update()
+
+        self.subject._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 6 - 0
tests/test_device.py

@@ -12,6 +12,7 @@ from custom_components.tuya_local.const import (
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
+    CONF_TYPE_GSH_HEATER,
     CONF_TYPE_KOGAN_HEATER,
     CONF_TYPE_KOGAN_SWITCH,
 )
@@ -23,6 +24,7 @@ from .const import (
     GECO_HEATER_PAYLOAD,
     GPCV_HEATER_PAYLOAD,
     GPPH_HEATER_PAYLOAD,
+    GSH_HEATER_PAYLOAD,
     KOGAN_HEATER_PAYLOAD,
     KOGAN_SOCKET_PAYLOAD,
 )
@@ -118,6 +120,10 @@ class TestDevice(IsolatedAsyncioTestCase):
             await self.subject.async_inferred_type(), CONF_TYPE_KOGAN_SWITCH
         )
 
+    async def test_detects_gsh_heater_payload(self):
+        self.subject._cached_state = GSH_HEATER_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), CONF_TYPE_GSH_HEATER)
+
     async def test_detection_returns_none_when_device_type_could_not_be_detected(self):
         self.subject._cached_state = {"1": False}
         self.assertEqual(await self.subject.async_inferred_type(), None)