Преглед изворни кода

Add support for Inkbird Thermostat.

Reported in #19 by @woolmonkey
Jason Rumney пре 4 година
родитељ
комит
364e31b3ef

+ 1 - 0
custom_components/tuya_local/const.py

@@ -19,6 +19,7 @@ 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_TYPE_INKBIRD_THERMOSTAT = "inkbird_thermostat"
 CONF_CLIMATE = "climate"
 CONF_DISPLAY_LIGHT = "display_light"
 CONF_CHILD_LOCK = "child_lock"

+ 105 - 0
custom_components/tuya_local/devices/inkbird_ITC306A_thermostat.yaml

@@ -0,0 +1,105 @@
+name: Inkbird ITC-306A Thermostat
+legacy_type: inkbird_thermostat
+primary_entity:
+  entity: climate
+  dps:
+    - id: 12
+      type: bitfield
+      name: error
+      mapping:
+        - dps_val: 0
+          value: OK
+    - id: 101
+      type: string
+      name: temperature_unit
+    - id: 102
+      type: integer
+      name: temperature_calibration_offset
+      mapping:
+        - scale: 10
+    - id: 103
+      type: string
+      name: preset_mode
+      mapping:
+        - dps_val: 'on'
+          value: 'On'
+        - dps_val: 'pause'
+          value: 'Pause'
+        - dps_val: 'off'
+          value: 'Off'
+    - id: 104
+      type: integer
+      name: current_temperature
+      mapping:
+        - scale: 10
+    - id: 106
+      type: integer
+      name: target_temp_low
+      mapping:
+        - scale: 10
+    - id: 108
+      type: integer
+      name: heat_time_alarm_threshold_hours
+    - id: 109
+      type: integer
+      name: high_temp_alarm_threshold
+      mapping:
+        - scale: 10
+    - id: 110
+      type: integer
+      name: low_temp_alarm_threshold
+      mapping:
+        - scale: 10
+    - id: 111
+      type: boolean
+      name: high_temp_alarm
+      mapping:
+        - dps_val: true
+          icon: "mdi:thermometer-alert"
+          icon_priority: 1
+    - id: 112
+      type: boolean
+      name: low_temp_alarm
+      mapping:
+        - dps_val: true
+          icon: "mdi:thermometer-alert"
+          icon_priority: 2
+    - id: 113
+      type: boolean
+      name: heat_time_alarm
+      mapping:
+        - dps_val: true
+          icon: "mdi:thermometer-alert"
+          icon_priority: 3
+    - id: 114
+      type: integer
+      name: target_temp_high
+      mapping:
+        - scale: 10
+    - id: 115
+      type: boolean
+      name: switch_state
+      mapping:
+        - dps_val: true
+          icon: "mdi:thermometer"
+          icon_priority: 5
+        - dps_val: false
+          icon: "mdi:thermometer-off"
+          icon_priority: 4
+    - id: 116
+      type: integer
+      name: current_temperature_f
+      mapping:
+        - scale: 10
+    - id: 117
+      type: boolean
+      name: unknown_117
+    - id: 118
+      type: boolean
+      name: unknown_118
+    - id: 119
+      type: boolean
+      name: unknown_119
+    - id: 120
+      type: boolean
+      name: unknown_120

+ 58 - 16
custom_components/tuya_local/generic/climate.py

@@ -5,17 +5,26 @@ import logging
 
 from homeassistant.components.climate import ClimateEntity
 from homeassistant.components.climate.const import (
+    ATTR_CURRENT_HUMIDITY,
+    ATTR_CURRENT_TEMPERATURE,
+    ATTR_FAN_MODE,
+    ATTR_HUMIDITY,
     ATTR_PRESET_MODE,
+    ATTR_SWING_MODE,
+    ATTR_TARGET_TEMP_HIGH,
+    ATTR_TARGET_TEMP_LOW,
     DEFAULT_MAX_HUMIDITY,
     DEFAULT_MAX_TEMP,
     DEFAULT_MIN_HUMIDITY,
     DEFAULT_MIN_TEMP,
-    HVAC_MODE_HEAT,
+    HVAC_MODE_AUTO,
+    HVAC_MODE_OFF,
     SUPPORT_FAN_MODE,
     SUPPORT_PRESET_MODE,
     SUPPORT_SWING_MODE,
     SUPPORT_TARGET_HUMIDITY,
     SUPPORT_TARGET_TEMPERATURE,
+    SUPPORT_TARGET_TEMPERATURE_RANGE,
 )
 from homeassistant.const import (
     ATTR_TEMPERATURE,
@@ -46,6 +55,8 @@ class TuyaLocalClimate(ClimateEntity):
         self._support_flags = 0
         self._current_temperature_dps = None
         self._temperature_dps = None
+        self._temp_high_dps = None
+        self._temp_low_dps = None
         self._current_humidity_dps = None
         self._humidity_dps = None
         self._preset_mode_dps = None
@@ -59,23 +70,26 @@ class TuyaLocalClimate(ClimateEntity):
         for d in config.dps():
             if d.name == "hvac_mode":
                 self._hvac_mode_dps = d
-            elif d.name == "temperature":
+            elif d.name == ATTR_TEMPERATURE:
                 self._temperature_dps = d
-                self._support_flags |= SUPPORT_TARGET_TEMPERATURE
-            elif d.name == "current_temperature":
+            elif d.name == ATTR_TARGET_TEMP_HIGH:
+                self._temp_high_dps = d
+            elif d.name == ATTR_TARGET_TEMP_LOW:
+                self._temp_low_dps = d
+            elif d.name == ATTR_CURRENT_TEMPERATURE:
                 self._current_temperature_dps = d
-            elif d.name == "humidity":
+            elif d.name == ATTR_HUMIDITY:
                 self._humidity_dps = d
                 self._support_flags |= SUPPORT_TARGET_HUMIDITY
-            elif d.name == "current_humidity":
+            elif d.name == ATTR_CURRENT_HUMIDITY:
                 self._current_humidity_dps = d
-            elif d.name == "preset_mode":
+            elif d.name == ATTR_PRESET_MODE:
                 self._preset_mode_dps = d
                 self._support_flags |= SUPPORT_PRESET_MODE
-            elif d.name == "swing_mode":
+            elif d.name == ATTR_SWING_MODE:
                 self._swing_mode_dps = d
                 self._support_flags |= SUPPORT_SWING_MODE
-            elif d.name == "fan_mode":
+            elif d.name == ATTR_FAN_MODE:
                 self._fan_mode_dps = d
                 self._support_flags |= SUPPORT_FAN_MODE
             elif d.name == "temperature_unit":
@@ -83,6 +97,11 @@ class TuyaLocalClimate(ClimateEntity):
             elif not d.hidden:
                 self._attr_dps.append(d)
 
+        if self._temp_high_dps is not None and self._temp_low_dps is not None:
+            self._support_flags |= SUPPORT_TARGET_TEMPERATURE_RANGE
+        elif self._temperature_dps is not None:
+            self._support_flags |= SUPPORT_TARGET_TEMPERATURE
+
     @property
     def supported_features(self):
         """Return the features supported by this climate device."""
@@ -116,10 +135,10 @@ class TuyaLocalClimate(ClimateEntity):
     @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"
+        if self.hvac_mode == HVAC_MODE_OFF:
+            return "mdi:hvac-off"
         else:
-            return "mdi:radiator-disabled"
+            return "mdi:hvac"
 
     @property
     def temperature_unit(self):
@@ -142,6 +161,20 @@ class TuyaLocalClimate(ClimateEntity):
             raise NotImplementedError()
         return self._temperature_dps.get_value(self._device)
 
+    @property
+    def target_temperature_high(self):
+        """Return the currently set high target temperature."""
+        if self._temp_high_dps is None:
+            raise NotImplementedError()
+        return self._temp_high_dps.get_value(self._device)
+
+    @property
+    def target_temperature_low(self):
+        """Return the currently set low target temperature."""
+        if self._temp_low_dps is None:
+            raise NotImplementedError()
+        return self._temp_low_dps.get_value(self._device)
+
     @property
     def target_temperature_step(self):
         """Return the supported step of target temperature."""
@@ -171,15 +204,24 @@ class TuyaLocalClimate(ClimateEntity):
             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))
+        high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
+        low = kwargs.get(ATTR_TARGET_TEMP_LOW)
+        if high is not None or low is not None:
+            await self.async_set_target_temperature_range(low, high)
 
     async def async_set_target_temperature(self, target_temperature):
         if self._temperature_dps is None:
             raise NotImplementedError()
 
-        target_temperature = int(round(target_temperature))
-
         await self._temperature_dps.async_set_value(self._device, target_temperature)
 
+    async def async_set_target_temperature_range(self, low, high):
+        """Set the target temperature range."""
+        if low is not None and self._temp_low_dps is not None:
+            await self._temp_low_dps.async_set_value(self._device, low)
+        if high is not None and self._temp_high_dps is not None:
+            await self._temp_high_dps.async_set_value(self._device, high)
+
     @property
     def current_temperature(self):
         """Return the current measured temperature."""
@@ -229,7 +271,7 @@ class TuyaLocalClimate(ClimateEntity):
     def hvac_mode(self):
         """Return current HVAC mode."""
         if self._hvac_mode_dps is None:
-            raise NotImplementedError()
+            return HVAC_MODE_AUTO
         hvac_mode = self._hvac_mode_dps.get_value(self._device)
         return STATE_UNAVAILABLE if hvac_mode is None else hvac_mode
 
@@ -237,7 +279,7 @@ class TuyaLocalClimate(ClimateEntity):
     def hvac_modes(self):
         """Return available HVAC modes."""
         if self._hvac_mode_dps is None:
-            return None
+            return []
         else:
             return self._hvac_mode_dps.values
 

+ 22 - 0
tests/const.py

@@ -121,3 +121,25 @@ EANONS_HUMIDIFIER_PAYLOAD = {
     "16": 65,
     "22": True,
 }
+
+INKBIRD_THERMOSTAT_PAYLOAD = {
+    "12": 0,
+    "101": "C",
+    "102": 0,
+    "103": "on",
+    "104": 257,
+    "106": 252,
+    "108": 6,
+    "109": 1000,
+    "110": 0,
+    "111": False,
+    "112": False,
+    "113": False,
+    "114": 260,
+    "115": True,
+    "116": 783,
+    "117": False,
+    "118": False,
+    "119": False,
+    "120": False,
+}

+ 2 - 1
tests/devices/test_andersson_gsh_heater.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -53,6 +53,7 @@ class TestAnderssonGSHHeater(IsolatedAsyncioTestCase):
     def test_device_info_returns_device_info_from_device(self):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")

+ 2 - 1
tests/devices/test_eurom_600_heater.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -52,6 +52,7 @@ class TestEurom600Heater(IsolatedAsyncioTestCase):
     def test_device_info_returns_device_info_from_device(self):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")

+ 2 - 1
tests/devices/test_goldair_geco_heater.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -72,6 +72,7 @@ class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
         self.assertEqual(self.lock.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")

+ 2 - 1
tests/devices/test_goldair_gpcv_heater.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -74,6 +74,7 @@ class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
         self.assertEqual(self.lock.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")

+ 228 - 0
tests/devices/test_inkbird_thermostat.py

@@ -0,0 +1,228 @@
+from unittest import IsolatedAsyncioTestCase, skip
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE_RANGE,
+)
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import INKBIRD_THERMOSTAT_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+ERROR_DPS = "12"
+UNIT_DPS = "101"
+CALIBRATE_DPS = "102"
+PRESET_DPS = "103"
+CURRENTTEMP_DPS = "104"
+TEMPLOW_DPS = "106"
+TIME_THRES_DPS = "108"
+HIGH_THRES_DPS = "109"
+LOW_THRES_DPS = "110"
+ALARM_HIGH_DPS = "111"
+ALARM_LOW_DPS = "112"
+ALARM_TIME_DPS = "113"
+TEMPHIGH_DPS = "114"
+SWITCH_DPS = "115"
+TEMPF_DPS = "116"
+UNKNOWN117_DPS = "117"
+UNKNOWN118_DPS = "118"
+UNKNOWN119_DPS = "119"
+UNKNOWN120_DPS = "120"
+
+
+class TestInkbirdThermostat(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("inkbird_ITC306A_thermostat.yaml")
+        entities = {}
+        entities[cfg.primary_entity.entity] = cfg.primary_entity
+        for e in cfg.secondary_entities():
+            entities[e.entity] = e
+
+        self.climate_name = (
+            "missing" if "climate" not in entities else entities["climate"].name
+        )
+
+        self.subject = TuyaLocalClimate(self.mock_device(), entities.get("climate"))
+        self.dps = INKBIRD_THERMOSTAT_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_RANGE | SUPPORT_PRESET_MODE,
+        )
+
+    def test_shouldPoll(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_friendly_name_returns_config_name(self):
+        self.assertEqual(self.subject.friendly_name, self.climate_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)
+
+    @skip("Icon customisation not supported yet")
+    def test_icon(self):
+        """Test that the icon is as expected."""
+        self.dps[ALARM_HIGH_DPS] = False
+        self.dps[ALARM_LOW_DPS] = False
+        self.dps[ALARM_TIME_DPS] = False
+        self.dps[SWITCH_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:thermometer")
+
+        self.dps[SWITCH_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:thermometer-off")
+
+        self.dps[ALARM_HIGH_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:thermometer-alert")
+
+        self.dps[SWITCH_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:thermometer-alert")
+
+        self.dps[ALARM_HIGH_DPS] = False
+        self.dps[ALARM_LOW_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:thermometer-alert")
+
+        self.dps[ALARM_LOW_DPS] = False
+        self.dps[ALARM_TIME_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:thermometer-alert")
+
+    def test_climate_hvac_modes(self):
+        self.assertEqual(self.subject.hvac_modes, [])
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "on"
+        self.assertEqual(self.subject.preset_mode, "On")
+
+        self.dps[PRESET_DPS] = "pause"
+        self.assertEqual(self.subject.preset_mode, "Pause")
+
+        self.dps[PRESET_DPS] = "off"
+        self.assertEqual(self.subject.preset_mode, "Off")
+
+        self.dps[PRESET_DPS] = None
+        self.assertEqual(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(
+            self.subject.preset_modes,
+            {"On", "Pause", "Off"},
+        )
+
+    async def test_set_preset_to_on(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PRESET_DPS: "on",
+            },
+        ):
+            await self.subject.async_set_preset_mode("On")
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    async def test_set_preset_to_pause(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PRESET_DPS: "pause",
+            },
+        ):
+            await self.subject.async_set_preset_mode("Pause")
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    async def test_set_preset_to_off(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PRESET_DPS: "off",
+            },
+        ):
+            await self.subject.async_set_preset_mode("Off")
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 289
+        self.assertEqual(self.subject.current_temperature, 28.9)
+
+    def test_temperature_unit(self):
+        self.dps[UNIT_DPS] = "F"
+        self.assertEqual(self.subject.temperature_unit, TEMP_FAHRENHEIT)
+
+        self.dps[UNIT_DPS] = "C"
+        self.assertEqual(self.subject.temperature_unit, TEMP_CELSIUS)
+
+    def test_temperature_range(self):
+        self.dps[TEMPHIGH_DPS] = 301
+        self.dps[TEMPLOW_DPS] = 255
+        self.assertEqual(self.subject.target_temperature_high, 30.1)
+        self.assertEqual(self.subject.target_temperature_low, 25.5)
+
+    async def test_set_temperature_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                TEMPHIGH_DPS: 322,
+                TEMPLOW_DPS: 266,
+            },
+        ):
+            await self.subject.async_set_temperature(
+                target_temp_high=32.2, target_temp_low=26.6
+            )
+
+    def test_device_state_attributes(self):
+        self.dps[ERROR_DPS] = 1
+        self.dps[CALIBRATE_DPS] = 1
+        self.dps[TIME_THRES_DPS] = 5
+        self.dps[HIGH_THRES_DPS] = 400
+        self.dps[LOW_THRES_DPS] = 300
+        self.dps[ALARM_HIGH_DPS] = True
+        self.dps[ALARM_LOW_DPS] = False
+        self.dps[ALARM_TIME_DPS] = True
+        self.dps[SWITCH_DPS] = False
+        self.dps[TEMPF_DPS] = 999
+        self.dps[UNKNOWN117_DPS] = True
+        self.dps[UNKNOWN118_DPS] = False
+        self.dps[UNKNOWN119_DPS] = True
+        self.dps[UNKNOWN120_DPS] = False
+
+        self.assertCountEqual(
+            self.subject.device_state_attributes,
+            {
+                "error": 1,
+                "temperature_calibration_offset": 0.1,
+                "heat_time_alarm_threshold_hours": 5,
+                "high_temp_alarm_threshold": 40.0,
+                "low_temp_alarm_threshold": 30.0,
+                "high_temp_alarm": True,
+                "low_temp_alarm": False,
+                "heat_time_alarm": True,
+                "switch_state": False,
+                "current_temperature_f": 99.9,
+                "unknown_117": True,
+                "unknown_118": False,
+                "unknown_119": True,
+                "unknown_120": False,
+            },
+        )
+
+    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()

+ 2 - 1
tests/devices/test_kogan_heater.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -73,6 +73,7 @@ class TestGoldairKoganHeater(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
         self.assertEqual(self.lock.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")

+ 4 - 7
tests/devices/test_remora_heatpump.py

@@ -1,4 +1,4 @@
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, skip
 from unittest.mock import AsyncMock, patch
 
 from homeassistant.components.climate.const import (
@@ -53,15 +53,12 @@ class TestRemoraHeatpump(IsolatedAsyncioTestCase):
     def test_device_info_returns_device_info_from_device(self):
         self.assertEqual(self.subject.device_info, self.subject._device.device_info)
 
+    @skip("Icon customisation not supported yet")
     def test_icon(self):
-        # Temporary: since proper icon parsing from config files is not yet
-        # implemented, the icons are fixed to defaults
         self.dps[HVACMODE_DPS] = True
-        self.assertEqual(self.subject.icon, "mdi:radiator")
-        # self.assertEqual(self.subject.icon, "mdi:hot-tub")
+        self.assertEqual(self.subject.icon, "mdi:hot-tub")
         self.dps[HVACMODE_DPS] = False
-        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
-        # selt.assertEqual(self.subject.icon, "mdi:hvac_off")
+        selt.assertEqual(self.subject.icon, "mdi:hvac_off")
 
     def test_temperature_unit_returns_device_temperature_unit(self):
         self.assertEqual(

+ 8 - 0
tests/test_device_config.py

@@ -17,6 +17,7 @@ from custom_components.tuya_local.const import (
     CONF_TYPE_PURLINE_M100_HEATER,
     CONF_TYPE_REMORA_HEATPUMP,
     CONF_TYPE_EANONS_HUMIDIFIER,
+    CONF_TYPE_INKBIRD_THERMOSTAT,
 )
 
 from custom_components.tuya_local.helpers.device_config import (
@@ -41,6 +42,7 @@ from .const import (
     PURLINE_M100_HEATER_PAYLOAD,
     REMORA_HEATPUMP_PAYLOAD,
     EANONS_HUMIDIFIER_PAYLOAD,
+    INKBIRD_THERMOSTAT_PAYLOAD,
 )
 
 
@@ -223,3 +225,9 @@ class TestDeviceConfig(unittest.TestCase):
     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)
+
+    def test_inkbird_thermostat(self):
+        """Test that Inkbird thermostat can be detected from its sample payload."""
+        self._test_detect(
+            INKBIRD_THERMOSTAT_PAYLOAD, CONF_TYPE_INKBIRD_THERMOSTAT, None
+        )