소스 검색

Squashed commit of the following:

commit 033bfc741a78e6bd08571f1a2f2cccf80db40ca4
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Tue Jan 19 22:30:16 2021 +0100

    reformatted test_climate.py for eurom_600_heater

commit ff76150fe139cb66bccb8bb9dd21053e2ed6376f
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Tue Jan 19 22:13:16 2021 +0100

    Fixed an issue where I somehow managed to place the eurom tests in the geco folder and vice versa

commit 639d84b988e478afa93ddd858941dedbfc6a8980
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Tue Jan 19 22:10:56 2021 +0100

    Actually fixed the unit tests

commit 25d8fc3a1bc3f2faa6d2838602da580483c86bbf
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Tue Jan 19 21:35:52 2021 +0100

    Fixed an issue with the unit tests

commit 1d8ef1ecfa7b6629247ae77538b2daa9d5a4b5b8
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Tue Jan 12 09:25:59 2021 +0100

    Added testing for Eurom Mon Soleil IR panel

commit d30f1cd34b65b6049e7bdb996979535ca4222258
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Mon Jan 11 23:40:48 2021 +0100

    Update device.py

commit 7f437598af0d15bb821ae40ae4d1a30085480985
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Mon Jan 11 23:34:52 2021 +0100

    Update device.py

commit d5e6712f1a5bb2cadffdd9c58925907d215a655b
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Mon Jan 11 23:30:46 2021 +0100

    Fixed compile error.

    Fixed a typo and a copy pasta mistake.

commit 1504adda26345d2da6f0bf9b43db831101844ac0
Author: Feiko Joosten <feiko_joosten@hotmail.com>
Date:   Mon Jan 11 23:25:25 2021 +0100

    Added Eurom Mon Soleil 600 support

    Tried to add support for the Eurom Mon Soleil 600.
Feiko Joosten 5 년 전
부모
커밋
1d4e49a27d

+ 4 - 0
custom_components/tuya_local/climate.py

@@ -10,6 +10,7 @@ from .const import (
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_EUROM_600_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
     CONF_TYPE_GSH_HEATER,
@@ -19,6 +20,7 @@ from .const import (
 from .dehumidifier.climate import GoldairDehumidifier
 from .fan.climate import GoldairFan
 from .geco_heater.climate import GoldairGECOHeater
+from .eurom_600_heater.climate import EuromMonSoleil600Heater
 from .gpcv_heater.climate import GoldairGPCVHeater
 from .heater.climate import GoldairHeater
 from .kogan_heater.climate import KoganHeater
@@ -44,6 +46,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         data[CONF_CLIMATE] = GoldairFan(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_GECO_HEATER:
         data[CONF_CLIMATE] = GoldairGECOHeater(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_EUROM_600_HEATER:
+        data[CONF_CLIMATE] = EuromMonSoleil600Heater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
         data[CONF_CLIMATE] = GoldairGPCVHeater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:

+ 2 - 0
custom_components/tuya_local/configuration.py

@@ -13,6 +13,7 @@ from .const import (
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_EUROM_600_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
     CONF_TYPE_GSH_HEATER,
@@ -34,6 +35,7 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
                 CONF_TYPE_DEHUMIDIFIER,
                 CONF_TYPE_FAN,
                 CONF_TYPE_GECO_HEATER,
+                CONF_TYPE_EUROM_600_HEATER,
                 CONF_TYPE_GPCV_HEATER,
                 CONF_TYPE_GSH_HEATER,
                 CONF_TYPE_KOGAN_HEATER,

+ 1 - 0
custom_components/tuya_local/const.py

@@ -10,6 +10,7 @@ CONF_TYPE_GPPH_HEATER = "heater"
 CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
 CONF_TYPE_FAN = "fan"
 CONF_TYPE_GECO_HEATER = "geco_heater"
+CONF_TYPE_EUROM_600_HEATER = "eurom_heater"
 CONF_TYPE_GPCV_HEATER = "gpcv_heater"
 CONF_TYPE_KOGAN_HEATER = "kogan_heater"
 CONF_TYPE_KOGAN_SWITCH = "kogan_switch"

+ 5 - 1
custom_components/tuya_local/device.py

@@ -15,6 +15,7 @@ from .const import (
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_EUROM_600_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
     CONF_TYPE_GSH_HEATER,
@@ -98,6 +99,9 @@ class TuyaLocalDevice(object):
         if "8" in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Goldair Fan")
             return CONF_TYPE_FAN
+        if "5" in cached_state and "2" in cached_state and "4" not in cached_state:
+            _LOGGER.info(f"Detecting {self.name} as Eurom Mon Soleil 600 Heater")
+            return CONF_TYPE_EUROM_600_HEATER
         if "5" in cached_state and "3" not in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Kogan Switch")
             return CONF_TYPE_KOGAN_SWITCH
@@ -113,7 +117,7 @@ class TuyaLocalDevice(object):
         if "12" in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Andersson GSH Heter")
             return CONF_TYPE_GSH_HEATER
-        if "3" in cached_state:
+        if "3" in cached_state and "6" in cached_state:
             _LOGGER.info(f"Detecting {self.name} as Goldair GECO Heater")
             return CONF_TYPE_GECO_HEATER
         _LOGGER.warning(f"Detection for {self.name} failed")

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


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

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

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

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

+ 2 - 0
tests/const.py

@@ -23,6 +23,8 @@ GPCV_HEATER_PAYLOAD = {
     "7": "Low",
 }
 
+EUROM_600_HEATER_PAYLOAD = {"1": True, "2": 15, "5": 18}
+
 GECO_HEATER_PAYLOAD = {"1": True, "2": True, "3": 30, "4": 25, "5": 0, "6": 0}
 
 KOGAN_HEATER_PAYLOAD = {"2": 30, "3": 25, "4": "Low", "6": True, "7": True, "8": 0}

+ 0 - 0
tests/eurom_600_heater/__init__.py


+ 156 - 0
tests/eurom_600_heater/test_climate.py

@@ -0,0 +1,156 @@
+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.eurom_600_heater.climate import (
+    EuromMonSoleil600Heater,
+)
+from custom_components.tuya_local.eurom_600_heater.const import (
+    ATTR_ERROR,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    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 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()

+ 8 - 0
tests/test_device.py

@@ -10,6 +10,7 @@ from custom_components.tuya_local.const import (
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_EUROM_600_HEATER,
     CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_GPPH_HEATER,
     CONF_TYPE_GSH_HEATER,
@@ -22,6 +23,7 @@ from .const import (
     DEHUMIDIFIER_PAYLOAD,
     FAN_PAYLOAD,
     GECO_HEATER_PAYLOAD,
+    EUROM_600_HEATER_PAYLOAD,
     GPCV_HEATER_PAYLOAD,
     GPPH_HEATER_PAYLOAD,
     GSH_HEATER_PAYLOAD,
@@ -80,6 +82,12 @@ class TestDevice(IsolatedAsyncioTestCase):
 
         self.subject.async_refresh.assert_awaited()
 
+    async def test_detects_eurom_600_heater_payload(self):
+        self.subject._cached_state = EUROM_600_HEATER_PAYLOAD
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_EUROM_600_HEATER
+        )
+
     async def test_detects_geco_heater_payload(self):
         self.subject._cached_state = GECO_HEATER_PAYLOAD
         self.assertEqual(