Просмотр исходного кода

Implement a basic generic climate entity.

Functionality is quite limited at first, but it should cover most of the existing and pending heaters.

Implement ranges in TuyaDpsConfig, and fix the logging in map implementation.
Use the TuyaDpsConfig getter and setters in lock and switch entities (light was already accidentally committed along with another bugfix).

Remove all the simple heaters.

The generic climate class can handle the following heaters, so their specific classes can be removed:
- eurom_600_heater
- geco_heater
- gpcv_heater
- gsh_heater
- kogan_heater
Jason Rumney 4 лет назад
Родитель
Сommit
02c613b326
44 измененных файлов с 451 добавлено и 1995 удалено
  1. 9 3
      custom_components/tuya_local/climate.py
  2. 0 1
      custom_components/tuya_local/devices/andersson_gsh_heater.yaml
  3. 0 1
      custom_components/tuya_local/devices/eurom_600_heater.yaml
  4. 0 2
      custom_components/tuya_local/devices/goldair_dehumidifier.yaml
  5. 0 1
      custom_components/tuya_local/devices/goldair_fan.yaml
  6. 0 2
      custom_components/tuya_local/devices/goldair_geco_heater.yaml
  7. 0 2
      custom_components/tuya_local/devices/goldair_gpcv_heater.yaml
  8. 0 2
      custom_components/tuya_local/devices/goldair_gpph_heater.yaml
  9. 0 2
      custom_components/tuya_local/devices/kogan_heater.yaml
  10. 0 1
      custom_components/tuya_local/devices/kogan_switch.yaml
  11. 0 1
      custom_components/tuya_local/devices/kogan_switch2.yaml
  12. 0 2
      custom_components/tuya_local/devices/purline_m100_heater.yaml
  13. 0 0
      custom_components/tuya_local/eurom_600_heater/__init__.py
  14. 0 151
      custom_components/tuya_local/eurom_600_heater/climate.py
  15. 0 19
      custom_components/tuya_local/eurom_600_heater/const.py
  16. 0 0
      custom_components/tuya_local/geco_heater/__init__.py
  17. 0 151
      custom_components/tuya_local/geco_heater/climate.py
  18. 0 21
      custom_components/tuya_local/geco_heater/const.py
  19. 205 0
      custom_components/tuya_local/generic/climate.py
  20. 4 8
      custom_components/tuya_local/generic/lock.py
  21. 5 13
      custom_components/tuya_local/generic/switch.py
  22. 0 0
      custom_components/tuya_local/gpcv_heater/__init__.py
  23. 0 177
      custom_components/tuya_local/gpcv_heater/climate.py
  24. 0 26
      custom_components/tuya_local/gpcv_heater/const.py
  25. 0 0
      custom_components/tuya_local/gsh_heater/__init__.py
  26. 0 177
      custom_components/tuya_local/gsh_heater/climate.py
  27. 0 29
      custom_components/tuya_local/gsh_heater/const.py
  28. 15 3
      custom_components/tuya_local/helpers/device_config.py
  29. 0 0
      custom_components/tuya_local/kogan_heater/__init__.py
  30. 0 184
      custom_components/tuya_local/kogan_heater/climate.py
  31. 0 29
      custom_components/tuya_local/kogan_heater/const.py
  32. 1 1
      custom_components/tuya_local/manifest.json
  33. 0 0
      tests/eurom_600_heater/__init__.py
  34. 0 156
      tests/eurom_600_heater/test_climate.py
  35. 0 0
      tests/geco_heater/__init__.py
  36. 0 156
      tests/geco_heater/test_climate.py
  37. 0 0
      tests/gpcv_heater/__init__.py
  38. 0 216
      tests/gpcv_heater/test_climate.py
  39. 0 0
      tests/gsh_heater/__init__.py
  40. 0 235
      tests/gsh_heater/test_climate.py
  41. 0 0
      tests/kogan_heater/__init__.py
  42. 0 216
      tests/kogan_heater/test_climate.py
  43. 207 2
      tests/test_climate.py
  44. 5 5
      tests/test_device_config.py

+ 9 - 3
custom_components/tuya_local/climate.py

@@ -10,6 +10,7 @@ from .const import (
     CONF_TYPE,
     CONF_TYPE_AUTO,
 )
+from .generic.climate import TuyaLocalClimate
 from .helpers.device_config import config_for_legacy_use
 
 _LOGGER = logging.getLogger(__name__)
@@ -36,9 +37,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
             raise ValueError(f"{device.name} does not support use as a climate device.")
 
     legacy_class = ecfg.legacy_class
-    # Instantiate it: Sonarcloud thinks this is a blocker bug, and legacy_class
-    # is not callable, but the unit tests show the object is created...
-    data[CONF_CLIMATE] = legacy_class(device)
+    # Transition: generic climate entity exists, but is not complete. More
+    # complex climate devices still need a device specific class.
+    # If legacy_class exists, use it, otherwise use the generic climate class.
+    if legacy_class is not None:
+        data[CONF_CLIMATE] = legacy_class(device)
+    else:
+        data[CONF_CLIMATE] = TuyaLocalClimate(device, ecfg)
+
     async_add_entities([data[CONF_CLIMATE]])
     _LOGGER.debug(f"Adding climate device for {discovery_info[CONF_TYPE]}")
 

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

@@ -2,7 +2,6 @@ name: Andersson GSH Heater
 legacy_type: gsh_heater
 primary_entity:
   entity: climate
-  legacy_class: ".gsh_heater.climate.AnderssonGSHHeater"
   dps:
     - id: 1
       name: hvac_mode

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

@@ -2,7 +2,6 @@ name: Eurom Mon Soleil 600 Heater
 legacy_type: eurom_heater
 primary_entity:
   entity: climate
-  legacy_class: ".eurom_600_heater.climate.EuromMonSoleil600Heater"
   dps:
     - id: 1
       type: boolean

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

@@ -73,7 +73,6 @@ primary_entity:
       readonly: true
 secondary_entities:
   - entity: light
-    legacy_class: ".dehumidifier.light.GoldairDehumidifierLedDisplayLight"
     name: Panel Light
     dps:
       - id: 102
@@ -87,7 +86,6 @@ secondary_entities:
             icon: "mdi:led-off"
         name: switch
   - entity: lock
-    legacy_class: ".dehumidifier.lock.GoldairDehumidifierChildLock"
     name: Child Lock
     dps:
       - id: 7

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

@@ -59,7 +59,6 @@ primary_entity:
       name: swing_mode
 secondary_entities:
   - entity: light
-    legacy_class: ".fan.light.GoldairFanLedDisplayLight"
     name: Panel Light
     dps:
       - id: 101

+ 0 - 2
custom_components/tuya_local/devices/goldair_geco_heater.yaml

@@ -2,7 +2,6 @@ name: Goldair GECO Heater
 legacy_type: geco_heater
 primary_entity:
   entity: climate
-  legacy_class: ".geco_heater.climate.GoldairGECOHeater"
   dps:
     - id: 1
       type: boolean
@@ -33,7 +32,6 @@ primary_entity:
       name: error
 secondary_entities:
   - entity: lock
-    legacy_class: ".geco_heater.lock.GoldairGECOHeaterChildLock"
     name: "Child Lock"
     dps:
       - id: 2

+ 0 - 2
custom_components/tuya_local/devices/goldair_gpcv_heater.yaml

@@ -2,7 +2,6 @@ name: Goldair GPCV Heater
 legacy_type: gpcv_heater
 primary_entity:
   entity: climate
-  legacy_class: ".gpcv_heater.climate.GoldairGPCVHeater"
   dps:
     - id: 1
       type: boolean
@@ -41,7 +40,6 @@ primary_entity:
       name: preset_mode
 secondary_entities:
   - entity: lock
-    legacy_class: ".gpcv_heater.lock.GoldairGPCVHeaterChildLock"
     name: "Child Lock"
     dps:
       - id: 2

+ 0 - 2
custom_components/tuya_local/devices/goldair_gpph_heater.yaml

@@ -90,7 +90,6 @@ primary_entity:
       name: eco_temperature
 secondary_entities:
   - entity: light
-    legacy_class: ".heater.light.GoldairHeaterLedDisplayLight"
     name: Panel Light
     dps:
       - id: 104
@@ -102,7 +101,6 @@ secondary_entities:
             icon: "mdi:led-off"
         name: switch
   - entity: lock
-    legacy_class: ".heater.lock.GoldairHeaterChildLock"
     name: Child Lock
     dps:
       - id: 6

+ 0 - 2
custom_components/tuya_local/devices/kogan_heater.yaml

@@ -2,7 +2,6 @@ name: Kogan Panel Heater
 legacy_type: kogan_heater
 primary_entity:
   entity: climate
-  legacy_class: ".kogan_heater.climate.KoganHeater"
   dps:
     - id: 2
       type: integer
@@ -37,7 +36,6 @@ primary_entity:
       name: timer
 secondary_entities:
   - entity: lock
-    legacy_class: ".kogan_heater.lock.KoganHeaterChildLock"
     name: "Child Lock"
     dps:
       - id: 6

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

@@ -2,7 +2,6 @@ name: Kogan Smart Switch
 legacy_type: kogan_switch
 primary_entity:
   entity: switch
-  legacy_class: ".kogan_socket.switch.KoganSocketSwitch"
   class: outlet
   dps:
     - id: 1

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

@@ -2,7 +2,6 @@ name: Kogan Smart Switch v2
 legacy_type: kogan_switch
 primary_entity:
   entity: switch
-  legacy_class: ".kogan_socket.switch.KoganSocketSwitch"
   class: outlet
   dps:
     - id: 1

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

@@ -67,7 +67,6 @@ primary_entity:
           value: "vertical"
 secondary_entities:
   - entity: light
-    legacy_class: ".purline_m100_heater.light.PurlineM100HeaterLedDisplayLight"
     name: Panel Light
     dps:
       - id: 10
@@ -81,7 +80,6 @@ secondary_entities:
             icon: "mdi:led-on"
         name: switch
   - entity: switch
-    legacy_class: ".purline_m100_heater.switch.PurlineM100OpenWindowDetector"
     name: "Open Window Detector"
     class: switch
     dps:

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


+ 0 - 151
custom_components/tuya_local/eurom_600_heater/climate.py

@@ -1,151 +0,0 @@
-"""
-Eurom Mon Soleil 600 WiFi Heater device.
-"""
-from homeassistant.components.climate import ClimateEntity
-from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE,
-    HVAC_MODE_HEAT,
-    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,
-    PROPERTY_TO_DPS_ID,
-)
-
-SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
-
-
-class EuromMonSoleil600Heater(ClimateEntity):
-    """Representation of a Eurom Mon Soleil 600 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": 15, "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_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 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()

+ 0 - 19
custom_components/tuya_local/eurom_600_heater/const.py

@@ -1,19 +0,0 @@
-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"
-
-PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: "1",
-    ATTR_TARGET_TEMPERATURE: "2",
-    ATTR_TEMPERATURE: "5",
-    ATTR_ERROR: "6",
-}
-
-HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}

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


+ 0 - 151
custom_components/tuya_local/geco_heater/climate.py

@@ -1,151 +0,0 @@
-"""
-Goldair GECO WiFi Heater device.
-"""
-from homeassistant.components.climate import ClimateEntity
-from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE,
-    HVAC_MODE_HEAT,
-    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,
-    PROPERTY_TO_DPS_ID,
-)
-
-SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
-
-
-class GoldairGECOHeater(ClimateEntity):
-    """Representation of a Goldair GECO 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": 15, "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_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 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()

+ 0 - 21
custom_components/tuya_local/geco_heater/const.py

@@ -1,21 +0,0 @@
-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_CHILD_LOCK = "child_lock"
-ATTR_ERROR = "error"
-
-PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: "1",
-    ATTR_CHILD_LOCK: "2",
-    ATTR_TARGET_TEMPERATURE: "3",
-    ATTR_TEMPERATURE: "4",
-    ATTR_ERROR: "6",
-}
-
-HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}

+ 205 - 0
custom_components/tuya_local/generic/climate.py

@@ -0,0 +1,205 @@
+"""
+Platform to control tuya climate devices.
+"""
+import logging
+
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+    ATTR_PRESET_MODE,
+    DEFAULT_MIN_TEMP,
+    DEFAULT_MAX_TEMP,
+    HVAC_MODE_HEAT,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from ..device import TuyaLocalDevice
+from ..helpers.device_config import TuyaEntityConfig
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TuyaLocalClimate(ClimateEntity):
+    """Representation of a Tuya Climate entity."""
+
+    def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
+        """
+        Initialise the climate device.
+        Args:
+           device (TuyaLocalDevice): The device API instance.
+           config (TuyaEntityConfig): The entity config.
+        """
+        self._device = device
+        self._config = config
+        self._support_flags = 0
+        self._current_temperature_dps = None
+        self._temperature_dps = None
+        self._preset_mode_dps = None
+        self._hvac_mode_dps = None
+        self._attr_dps = []
+        self._temperature_step = 1
+
+        for d in config.dps():
+            if d.name == "hvac_mode":
+                self._hvac_mode_dps = d
+            elif d.name == "temperature":
+                self._temperature_dps = d
+                self._support_flags |= SUPPORT_TARGET_TEMPERATURE
+
+            elif d.name == "current_temperature":
+                self._current_temperature_dps = d
+            elif d.name == "preset_mode":
+                self._preset_mode_dps = d
+                self._support_flags |= SUPPORT_PRESET_MODE
+            else:
+                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 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 temperature_unit(self):
+        """Return the unit of measurement."""
+        return self._device.temperature_unit
+
+    @property
+    def target_temperature(self):
+        """Return the currently set target temperature."""
+        if self._temperature_dps is None:
+            raise NotImplementedError()
+        return self._temperature_dps.get_value(self._device)
+
+    @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 supported target temperature."""
+        if self._temperature_dps is None or self._temperature_dps.range is None:
+            return DEFAULT_MIN_TEMP
+        return self._temperature_dps.range["min"]
+
+    @property
+    def max_temp(self):
+        """Return the maximum supported target temperature."""
+        if self._temperature_dps is None or self._temperature_dps.range is None:
+            return DEFAULT_MIN_TEMP
+        return self._temperature_dps.range["max"]
+
+    async def async_set_temperature(self, **kwargs):
+        """Set new target temperature."""
+        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):
+        if self._temperature_dps is None:
+            raise NotImplementedError()
+
+        target_temperature = int(round(target_temperature))
+        if not self.min_temp <= target_temperature <= self.max_temp:
+            raise ValueError(
+                f"Target temperature ({target_temperature}) must be between "
+                f"{self.min_temp} and {self.max_temp}."
+            )
+
+        await self._temperature_dps.async_set_value(self._device, target_temperature)
+
+    @property
+    def current_temperature(self):
+        """Return this current temperature."""
+        if self._current_temperature_dps is None:
+            return None
+        return self._current_temperature_dps.get_value(self._device)
+
+    @property
+    def hvac_mode(self):
+        """Return current HVAC mode."""
+        if self._hvac_mode_dps is None:
+            raise NotImplementedError()
+        hvac_mode = self._hvac_mode_dps.get_value(self._device)
+        return STATE_UNAVAILABLE if hvac_mode is None else hvac_mode
+
+    @property
+    def hvac_modes(self):
+        """Return available HVAC modes."""
+        if self._hvac_mode_dps is None:
+            return None
+        else:
+            return self._hvac_mode_dps.values
+
+    async def async_set_hvac_mode(self, hvac_mode):
+        """Set new HVAC mode."""
+        if self._hvac_mode_dps is None:
+            raise NotImplementedError()
+        await self._hvac_mode_dps.async_set_value(self._device, hvac_mode)
+
+    @property
+    def preset_mode(self):
+        """Return the current preset mode."""
+        if self._preset_mode_dps is None:
+            raise NotImplementedError()
+        return self._preset_mode_dps.get_value(self._device)
+
+    @property
+    def preset_modes(self):
+        """Return the list of presets that this device supports."""
+        if self._preset_mode_dps is None:
+            return None
+        return self._preset_mode_dps.values
+
+    async def async_set_preset_mode(self, preset_mode):
+        """Set the preset mode."""
+        if self._preset_mode_dps is None:
+            raise NotImplementedError()
+        await self._preset_mode_dps.async_set_value(self._device, preset_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()

+ 4 - 8
custom_components/tuya_local/generic/lock.py

@@ -57,7 +57,7 @@ class TuyaLocalLock(LockEntity):
     @property
     def state(self):
         """Return the current state."""
-        lock = self._lock_dps.map_from_dps(self._device.get_property(self._lock_dps.id))
+        lock = self._lock_dps.get_value(self._device)
 
         if lock is None:
             return STATE_UNAVAILABLE
@@ -74,20 +74,16 @@ class TuyaLocalLock(LockEntity):
         """Get additional attributes that the integration itself does not support."""
         attr = {}
         for a in self._attr_dps:
-            attr[a.name] = a.map_from_dps(self._device.get_property(a.id))
+            attr[a.name] = a.get_value(self._device)
         return attr
 
     async def async_lock(self, **kwargs):
         """Lock the lock."""
-        await self._device.async_set_property(
-            self._lock_dps.id, self._lock_dps.map_to_dps(True)
-        )
+        await self._lock_dps.async_set_value(self._device, True)
 
     async def async_unlock(self, **kwargs):
         """Unlock the lock."""
-        await self._device.async_set_property(
-            self._lock_dps.id, self._lock_dps.map_to_dps(False)
-        )
+        await self._lock_dps.async_set_value(self._device, False)
 
     async def async_update(self):
         await self._device.async_refresh()

+ 5 - 13
custom_components/tuya_local/generic/switch.py

@@ -73,9 +73,7 @@ class TuyaLocalSwitch(SwitchEntity):
     @property
     def is_on(self):
         """Return whether the switch is on or not."""
-        is_switched_on = self._switch_dps.map_from_dps(
-            self._device.get_property(self._switch_dps.id)
-        )
+        is_switched_on = self._switch_dps.get_value(self._device)
 
         if is_switched_on is None:
             return STATE_UNAVAILABLE
@@ -88,9 +86,7 @@ class TuyaLocalSwitch(SwitchEntity):
         if self._power_dps is None:
             return None
 
-        pwr = self._power_dps.map_from_dps(
-            self._device.get_property(self._power_dps.id)
-        )
+        pwr = self._power_dps.get_value(self._device)
         if pwr is None:
             return STATE_UNAVAILABLE
 
@@ -101,20 +97,16 @@ class TuyaLocalSwitch(SwitchEntity):
         """Get additional attributes that HA doesn't naturally support."""
         attr = {}
         for a in self._attr_dps:
-            attr[a.name] = a.map_from_dps(self._device.get_property(a.id))
+            attr[a.name] = a.get_value(self._device)
         return attr
 
     async def async_turn_on(self, **kwargs):
         """Turn the switch on"""
-        await self._device.async_set_property(
-            self._switch_dps.id, self._switch_dps.map_to_dps(True)
-        )
+        await self._switch_dps.async_set_value(self._device, True)
 
     async def async_turn_off(self, **kwargs):
         """Turn the switch off"""
-        await self._device.async_set_property(
-            self._switch_dps.id, self._switch_dps.map_to_dps(False)
-        )
+        await self._switch_dps.async_set_value(self._device, False)
 
     async def async_update(self):
         await self._device.async_refresh()

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


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

@@ -1,177 +0,0 @@
-"""
-Goldair GPCV 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 GoldairGPCVHeater(ClimateEntity):
-    """Representation of a Goldair GPCV 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": 15, "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()

+ 0 - 26
custom_components/tuya_local/gpcv_heater/const.py

@@ -1,26 +0,0 @@
-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_CHILD_LOCK = "child_lock"
-ATTR_ERROR = "error"
-
-PRESET_LOW = "Low"
-PRESET_HIGH = "High"
-
-PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: "1",
-    ATTR_CHILD_LOCK: "2",
-    ATTR_TARGET_TEMPERATURE: "3",
-    ATTR_TEMPERATURE: "4",
-    ATTR_ERROR: "6",
-    ATTR_PRESET_MODE: "7",
-}
-
-HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}
-PRESET_MODE_TO_DPS_MODE = {PRESET_LOW: "Low", PRESET_HIGH: "High"}

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


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

@@ -1,177 +0,0 @@
-"""
-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()

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

@@ -1,29 +0,0 @@
-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",
-}

+ 15 - 3
custom_components/tuya_local/helpers/device_config.py

@@ -181,7 +181,19 @@ class TuyaDpsConfig:
         for map in self._config["mapping"]:
             if "value" in map:
                 v.append(map["value"])
-        return v
+        return v if len(v) > 0 else None
+
+    @property
+    def range(self):
+        """Return the range for this dps if configured."""
+        if (
+            "range" in self._config.keys()
+            and "min" in self._config["range"].keys()
+            and "max" in self._config["range"].keys()
+        ):
+            return self._config["range"]
+        else:
+            return None
 
     @property
     def isreadonly(self):
@@ -195,7 +207,7 @@ class TuyaDpsConfig:
                 if "value" in map and ("dps_val" not in map or map["dps_val"] == value):
                     result = map["value"]
                     _LOGGER.debug(
-                        "%s: Mapped dps %d value from %s to %s",
+                        "%s: Mapped dps %s value from %s to %s",
                         self._entity._device.name,
                         self.id,
                         value,
@@ -218,7 +230,7 @@ class TuyaDpsConfig:
                 if "value" in map and "dps_val" in map and map["value"] == value:
                     result = map["dps_val"]
                     _LOGGER.debug(
-                        "%s: Mapped dps %d to %s from %s",
+                        "%s: Mapped dps %s to %s from %s",
                         self._entity._device.name,
                         self.id,
                         result,

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


+ 0 - 184
custom_components/tuya_local/kogan_heater/climate.py

@@ -1,184 +0,0 @@
-"""
-Kogan WiFi Heater device.
-
-dps:
-  2 = target temperature (integer)
-  3 = current temperature (integer)
-  4 = preset_mode (string Low/High)
-  6 = child_lock (boolean)
-  7 = hvac_mode (boolean)
-  8 = timer (integer) [supported for read only - use HA based timers]
-"""
-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_TARGET_TEMPERATURE,
-    ATTR_TIMER,
-    HVAC_MODE_TO_DPS_MODE,
-    PRESET_MODE_TO_DPS_MODE,
-    PROPERTY_TO_DPS_ID,
-)
-
-SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
-
-
-class KoganHeater(ClimateEntity):
-    """Representation of a Kogan WiFi heater."""
-
-    def __init__(self, device):
-        """Initialize the heater.
-        Args:
-            name (str): The device's name.
-            device (TuyaLocalDevice): The device API instance."""
-        self._device = device
-
-        self._support_flags = SUPPORT_FLAGS
-
-        self._TEMPERATURE_STEP = 1
-        self._TEMPERATURE_LIMITS = {"min": 15, "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 Low or High."""
-        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."""
-        timer = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_TIMER])
-        return {ATTR_TIMER: timer}
-
-    async def async_update(self):
-        await self._device.async_refresh()

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

@@ -1,29 +0,0 @@
-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_CHILD_LOCK = "child_lock"
-ATTR_TIMER = "timer"
-
-PRESET_LOW = "LOW"
-PRESET_HIGH = "HIGH"
-
-PROPERTY_TO_DPS_ID = {
-    ATTR_TARGET_TEMPERATURE: "2",
-    ATTR_TEMPERATURE: "3",
-    ATTR_PRESET_MODE: "4",
-    ATTR_CHILD_LOCK: "6",
-    ATTR_HVAC_MODE: "7",
-    ATTR_TIMER: "8",
-}
-
-HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}
-PRESET_MODE_TO_DPS_MODE = {
-    PRESET_LOW: "Low",
-    PRESET_HIGH: "High",
-}

+ 1 - 1
custom_components/tuya_local/manifest.json

@@ -2,7 +2,7 @@
     "domain": "tuya_local",
     "iot_class": "local_polling",
     "name": "Tuya based devices local control",
-    "version": "0.4.2", 
+    "version": "0.4.3", 
     "documentation": "https://github.com/make-all/tuya-local",
     "issue_tracker": "https://github.com/make-all/tuya-local/issues",
     "dependencies": [],

+ 0 - 0
tests/eurom_600_heater/__init__.py


+ 0 - 156
tests/eurom_600_heater/test_climate.py

@@ -1,156 +0,0 @@
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, patch
-
-from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE,
-    HVAC_MODE_HEAT,
-    HVAC_MODE_OFF,
-    SUPPORT_TARGET_TEMPERATURE,
-)
-from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
-
-from custom_components.tuya_local.eurom_600_heater.climate import (
-    EuromMonSoleil600Heater,
-)
-from custom_components.tuya_local.eurom_600_heater.const import (
-    ATTR_ERROR,
-    ATTR_TARGET_TEMPERATURE,
-    PROPERTY_TO_DPS_ID,
-)
-
-from ..const import EUROM_600_HEATER_PAYLOAD
-from ..helpers import assert_device_properties_set
-
-
-class TestEuromMonSoleil600Heater(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 = EuromMonSoleil600Heater(self.mock_device())
-
-        self.dps = EUROM_600_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,
-        )
-
-    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, 15)
-
-    def test_maximum_target_temperature(self):
-        self.assertEqual(self.subject.max_temp, 35)
-
-    async def test_legacy_set_temperature_method(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_does_nothing_without_temperature_value(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 \\(14\\) must be between 15 and 35"
-        ):
-            await self.subject.async_set_target_temperature(14)
-
-        with self.assertRaisesRegex(
-            ValueError, "Target temperature \\(36\\) must be between 15 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_error_state(self):
-        # There are currently no known error states; update if 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()

+ 0 - 0
tests/geco_heater/__init__.py


+ 0 - 156
tests/geco_heater/test_climate.py

@@ -1,156 +0,0 @@
-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_TARGET_TEMPERATURE,
-)
-from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
-
-from custom_components.tuya_local.geco_heater.climate import GoldairGECOHeater
-from custom_components.tuya_local.geco_heater.const import (
-    ATTR_ERROR,
-    ATTR_TARGET_TEMPERATURE,
-    HVAC_MODE_TO_DPS_MODE,
-    PROPERTY_TO_DPS_ID,
-)
-
-from ..const import GECO_HEATER_PAYLOAD
-from ..helpers import assert_device_properties_set
-
-
-class TestGoldairGECOHeater(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 = GoldairGECOHeater(self.mock_device())
-
-        self.dps = GECO_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,
-        )
-
-    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, 15)
-
-    def test_maximum_target_temperature(self):
-        self.assertEqual(self.subject.max_temp, 35)
-
-    async def test_legacy_set_temperature_method(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_does_nothing_without_temperature_value(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 \\(14\\) must be between 15 and 35"
-        ):
-            await self.subject.async_set_target_temperature(14)
-
-        with self.assertRaisesRegex(
-            ValueError, "Target temperature \\(36\\) must be between 15 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_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()

+ 0 - 0
tests/gpcv_heater/__init__.py


+ 0 - 216
tests/gpcv_heater/test_climate.py

@@ -1,216 +0,0 @@
-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.gpcv_heater.climate import GoldairGPCVHeater
-from custom_components.tuya_local.gpcv_heater.const import (
-    ATTR_ERROR,
-    ATTR_TARGET_TEMPERATURE,
-    HVAC_MODE_TO_DPS_MODE,
-    PRESET_HIGH,
-    PRESET_LOW,
-    PRESET_MODE_TO_DPS_MODE,
-    PROPERTY_TO_DPS_ID,
-)
-
-from ..const import GPCV_HEATER_PAYLOAD
-from ..helpers import assert_device_properties_set
-
-
-class TestGoldairGPCVHeater(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 = GoldairGPCVHeater(self.mock_device())
-
-        self.dps = GPCV_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, 15)
-
-    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 \\(14\\) must be between 15 and 35"
-        ):
-            await self.subject.async_set_target_temperature(14)
-
-        with self.assertRaisesRegex(
-            ValueError, "Target temperature \\(36\\) must be between 15 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]] = None
-        self.assertIs(self.subject.preset_mode, None)
-
-    def test_preset_modes(self):
-        self.assertEqual(self.subject.preset_modes, [PRESET_LOW, PRESET_HIGH])
-
-    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)
-
-    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()

+ 0 - 0
tests/gsh_heater/__init__.py


+ 0 - 235
tests/gsh_heater/test_climate.py

@@ -1,235 +0,0 @@
-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()

+ 0 - 0
tests/kogan_heater/__init__.py


+ 0 - 216
tests/kogan_heater/test_climate.py

@@ -1,216 +0,0 @@
-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.kogan_heater.climate import KoganHeater
-from custom_components.tuya_local.kogan_heater.const import (
-    ATTR_TARGET_TEMPERATURE,
-    ATTR_TIMER,
-    HVAC_MODE_TO_DPS_MODE,
-    PRESET_HIGH,
-    PRESET_LOW,
-    PRESET_MODE_TO_DPS_MODE,
-    PROPERTY_TO_DPS_ID,
-)
-
-from ..const import KOGAN_HEATER_PAYLOAD
-from ..helpers import assert_device_properties_set
-
-
-class TestKoganHeater(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 = KoganHeater(self.mock_device())
-
-        self.dps = KOGAN_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, 15)
-
-    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 \\(14\\) must be between 15 and 35"
-        ):
-            await self.subject.async_set_target_temperature(14)
-
-        with self.assertRaisesRegex(
-            ValueError, "Target temperature \\(36\\) must be between 15 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]] = None
-        self.assertIs(self.subject.preset_mode, None)
-
-    def test_preset_modes(self):
-        self.assertEqual(self.subject.preset_modes, [PRESET_LOW, PRESET_HIGH])
-
-    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)
-
-    def test_device_state_attributes(self):
-        self.dps[PROPERTY_TO_DPS_ID[ATTR_TIMER]] = 1
-        self.assertEqual(
-            self.subject.device_state_attributes,
-            {ATTR_TIMER: 1},
-        )
-
-    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()

+ 207 - 2
tests/test_climate.py

@@ -1,7 +1,17 @@
 """Tests for the light entity."""
-import pytest
 from pytest_homeassistant_custom_component.common import MockConfigEntry
-from unittest.mock import AsyncMock, Mock
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, Mock, 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.const import (
     CONF_CLIMATE,
@@ -9,11 +19,23 @@ from custom_components.tuya_local.const import (
     CONF_TYPE,
     CONF_TYPE_AUTO,
     CONF_TYPE_GPPH_HEATER,
+    CONF_TYPE_GSH_HEATER,
     DOMAIN,
 )
 from custom_components.tuya_local.heater.climate import GoldairHeater
+from custom_components.tuya_local.helpers.device_config import config_for_legacy_use
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
 from custom_components.tuya_local.climate import async_setup_entry
 
+from .const import GSH_HEATER_PAYLOAD
+from .helpers import assert_device_properties_set
+
+GSH_HVACMODE_DPS = "1"
+GSH_TEMPERATURE_DPS = "2"
+GSH_CURRENTTEMP_DPS = "3"
+GSH_PRESET_DPS = "4"
+GSH_ERROR_DPS = "12"
+
 
 async def test_init_entry(hass):
     """Test the initialisation."""
@@ -35,3 +57,186 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_CLIMATE]) == GoldairHeater
     m_add_entities.assert_called_once()
+
+
+class TestTuyaLocalClimate(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        gsh_heater_config = config_for_legacy_use(CONF_TYPE_GSH_HEATER)
+        climate = gsh_heater_config.primary_entity
+        self.climate_name = climate.name
+        self.subject = TuyaLocalClimate(self.mock_device(), climate)
+        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[GSH_HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[GSH_HVACMODE_DPS] = 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[GSH_TEMPERATURE_DPS] = 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, {GSH_TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device, {GSH_PRESET_DPS: "low"}
+        ):
+            await self.subject.async_set_temperature(preset_mode="Low")
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device, {GSH_TEMPERATURE_DPS: 26, GSH_PRESET_DPS: "high"}
+        ):
+            await self.subject.async_set_temperature(temperature=26, preset_mode="High")
+
+    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,
+            {GSH_TEMPERATURE_DPS: 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, {GSH_TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.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[GSH_CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[GSH_HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[GSH_HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[GSH_HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(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, {GSH_HVACMODE_DPS: 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, {GSH_HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[GSH_PRESET_DPS] = "low"
+        self.assertEqual(self.subject.preset_mode, "Low")
+
+        self.dps[GSH_PRESET_DPS] = "high"
+        self.assertEqual(self.subject.preset_mode, "High")
+
+        self.dps[GSH_PRESET_DPS] = "af"
+        self.assertEqual(self.subject.preset_mode, "Anti-freeze")
+
+        self.dps[GSH_PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Low", "High", "Anti-freeze"])
+
+    async def test_set_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {GSH_PRESET_DPS: "low"},
+        ):
+            await self.subject.async_set_preset_mode("Low")
+
+    async def test_set_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {GSH_PRESET_DPS: "high"},
+        ):
+            await self.subject.async_set_preset_mode("High")
+
+    async def test_set_preset_mode_to_af(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {GSH_PRESET_DPS: "af"},
+        ):
+            await self.subject.async_set_preset_mode("Anti-freeze")
+
+    def test_error_state(self):
+        # There are currently no known error states; update this as they're discovered
+        self.dps[GSH_ERROR_DPS] = "something"
+        self.assertEqual(self.subject.device_state_attributes, {"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()

+ 5 - 5
tests/test_device_config.py

@@ -117,7 +117,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GPCV_HEATER_PAYLOAD,
             CONF_TYPE_GPCV_HEATER,
-            "GoldairGPCVHeater",
+            None,
         )
 
     def test_eurom_heater_detection(self):
@@ -125,7 +125,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             EUROM_600_HEATER_PAYLOAD,
             CONF_TYPE_EUROM_600_HEATER,
-            "EuromMonSoleil600Heater",
+            None,
         )
 
     def test_geco_heater_detection(self):
@@ -133,7 +133,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GECO_HEATER_PAYLOAD,
             CONF_TYPE_GECO_HEATER,
-            "GoldairGECOHeater",
+            None,
         )
 
     def test_kogan_heater_detection(self):
@@ -141,7 +141,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             KOGAN_HEATER_PAYLOAD,
             CONF_TYPE_KOGAN_HEATER,
-            "KoganHeater",
+            None,
         )
 
     def test_goldair_dehumidifier_detection(self):
@@ -177,7 +177,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GSH_HEATER_PAYLOAD,
             CONF_TYPE_GSH_HEATER,
-            "AnderssonGSHHeater",
+            None,
         )
 
     def test_gardenpac_heatpump_detection(self):