Kaynağa Gözat

Add Madimack heatpump (similar to GardenPAC) and Simple Switch as a fallback for unsupported devices.

Jason Rumney 4 yıl önce
ebeveyn
işleme
9245bb3406

+ 114 - 0
custom_components/tuya_local/devices/madimack_heatpump.yaml

@@ -0,0 +1,114 @@
+name: Madimack Pool Heatpump
+primary_entity:
+  entity: climate
+  dps:
+    - id: 1
+      name: hvac_mode
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: "off"
+          icon: "mdi:hvac-off"
+          icon_priority: 1
+        - dps_val: true
+          value: "heat"
+          icon: "mdi:hot-tub"
+          icon_priority: 3
+    - id: 102
+      name: current_temperature
+      type: integer
+      readonly: true
+    - id: 103
+      name: temperature_unit
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: F
+        - dps_val: true
+          value: C
+    - id: 104
+      name: power_level
+      type: integer
+      readonly: true
+    - id: 105
+      name: operating_mode
+      type: string
+      readonly: true
+    - id: 106
+      name: temperature
+      type: integer
+      mapping:
+        - constraint: temperature_unit
+          conditions:
+            - dps_val: false
+              range:
+                min: 60
+                max: 115
+      range:
+        min: 18
+        max: 45
+    - id: 107
+      type: integer
+      name: unknown_107
+    - id: 108
+      type: integer
+      name: unknown_108
+    - id: 115
+      type: integer
+      name: unknown_115
+    - id: 116
+      type: integer
+      name: unknown_116
+    - id: 117
+      name: preset_mode
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: Silent
+        - dps_val: true
+          value: Boost
+    - id: 118
+      name: unknown_118
+      type: boolean
+    - id: 120
+      name: unknown_120
+      type: integer
+    - id: 122
+      name: unknown_122
+      type: integer
+    - id: 124
+      name: unknown_124
+      type: integer
+    - id: 125
+      name: unknown_125
+      type: integer
+    - id: 126
+      name: unknown_126
+      type: integer
+    - id: 127
+      name: unknown_127
+      type: integer
+    - id: 128
+      name: unknown_128
+      type: integer
+    - id: 129
+      name: unknown_129
+      type: integer
+    - id: 130
+      name: unknown_130
+      type: boolean
+    - id: 134
+      name: unknown_134
+      type: boolean
+    - id: 135
+      name: unknown_135
+      type: boolean
+    - id: 136
+      name: unknown_136
+      type: boolean
+    - id: 139
+      name: unknown_139
+      type: boolean
+    - id: 140
+      name: unknown_140
+      type: string

+ 7 - 0
custom_components/tuya_local/devices/simple_switch.yaml

@@ -0,0 +1,7 @@
+name: Simple Switch
+primary_entity:
+  entity: switch
+  dps:
+    - id: 1
+      name: switch
+      type: boolean

+ 29 - 0
tests/const.py

@@ -96,6 +96,35 @@ GARDENPAC_HEATPUMP_PAYLOAD = {
     "117": True,
 }
 
+MADIMACK_HEATPUMP_PAYLOAD = {
+    "1": True,
+    "102": 9,
+    "103": True,
+    "104": 0,
+    "105": "warm",
+    "106": 30,
+    "107": 18,
+    "108": 40,
+    "115": 4,
+    "116": 0,
+    "117": True,
+    "118": False,
+    "120": 8,
+    "122": 11,
+    "124": 9,
+    "125": 0,
+    "126": 0,
+    "127": 17,
+    "128": 480,
+    "129": 0,
+    "130": False,
+    "134": False,
+    "135": False,
+    "136": False,
+    "139": False,
+    "140": "LowSpeed",
+}
+
 PURLINE_M100_HEATER_PAYLOAD = {
     "1": True,
     "2": 23,

+ 263 - 0
tests/devices/test_madimack_heatpump.py

@@ -0,0 +1,263 @@
+from unittest import IsolatedAsyncioTestCase, skip
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import (
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+    TEMP_FAHRENHEIT,
+)
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import MADIMACK_HEATPUMP_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+HVACMODE_DPS = "1"
+CURRENTTEMP_DPS = "102"
+UNITS_DPS = "103"
+POWERLEVEL_DPS = "104"
+OPMODE_DPS = "105"
+TEMPERATURE_DPS = "106"
+UNKNOWN107_DPS = "107"
+UNKNOWN108_DPS = "108"
+UNKNOWN115_DPS = "115"
+UNKNOWN116_DPS = "116"
+UNKNOWN118_DPS = "118"
+UNKNOWN120_DPS = "120"
+UNKNOWN122_DPS = "122"
+UNKNOWN124_DPS = "124"
+UNKNOWN125_DPS = "125"
+UNKNOWN126_DPS = "126"
+UNKNOWN127_DPS = "127"
+UNKNOWN128_DPS = "128"
+UNKNOWN129_DPS = "129"
+UNKNOWN130_DPS = "130"
+UNKNOWN134_DPS = "134"
+UNKNOWN135_DPS = "135"
+UNKNOWN136_DPS = "136"
+UNKNOWN139_DPS = "139"
+UNKNOWN140_DPS = "140"
+PRESET_DPS = "117"
+
+
+class TestMadimackPoolHeatpump(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("madimack_heatpump.yaml")
+        climate = cfg.primary_entity
+        self.climate_name = climate.name
+
+        self.subject = TuyaLocalClimate(self.mock_device(), climate)
+
+        self.dps = MADIMACK_HEATPUMP_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)
+
+    @skip("Icon customisation not supported yet")
+    def test_icon(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:hot-tub")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit(self):
+        self.dps[UNITS_DPS] = False
+        self.assertEqual(self.subject.temperature_unit, TEMP_FAHRENHEIT)
+        self.dps[UNITS_DPS] = True
+        self.assertEqual(self.subject.temperature_unit, TEMP_CELSIUS)
+
+    def test_target_temperature(self):
+        self.dps[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, 18)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 45)
+
+    def test_minimum_fahrenheit_temperature(self):
+        self.dps[UNITS_DPS] = False
+        self.assertEqual(self.subject.min_temp, 60)
+
+    def test_maximum_fahrenheit_temperature(self):
+        self.dps[UNITS_DPS] = False
+        self.assertEqual(self.subject.max_temp, 115)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 25}
+        ):
+            await self.subject.async_set_temperature(temperature=25)
+
+    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, {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,
+            {TEMPERATURE_DPS: 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, "temperature \\(14\\) must be between 18 and 45"
+        ):
+            await self.subject.async_set_target_temperature(14)
+
+        with self.assertRaisesRegex(
+            ValueError, "temperature \\(46\\) must be between 18 and 45"
+        ):
+            await self.subject.async_set_target_temperature(46)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[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, {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, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = False
+        self.assertEqual(self.subject.preset_mode, "Silent")
+
+        self.dps[PRESET_DPS] = True
+        self.assertEqual(self.subject.preset_mode, "Boost")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Silent", "Boost"])
+
+    async def test_set_preset_mode_to_silent(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: False},
+        ):
+            await self.subject.async_set_preset_mode("Silent")
+
+    async def test_set_preset_mode_to_boost(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: True},
+        ):
+            await self.subject.async_set_preset_mode("Boost")
+
+    def test_device_state_attributes(self):
+        self.dps[POWERLEVEL_DPS] = 50
+        self.dps[OPMODE_DPS] = "cool"
+        self.dps[UNKNOWN107_DPS] = 1
+        self.dps[UNKNOWN108_DPS] = 2
+        self.dps[UNKNOWN115_DPS] = 3
+        self.dps[UNKNOWN116_DPS] = 4
+        self.dps[UNKNOWN118_DPS] = 5
+        self.dps[UNKNOWN120_DPS] = 6
+        self.dps[UNKNOWN122_DPS] = 7
+        self.dps[UNKNOWN124_DPS] = 8
+        self.dps[UNKNOWN125_DPS] = 9
+        self.dps[UNKNOWN126_DPS] = 10
+        self.dps[UNKNOWN127_DPS] = 11
+        self.dps[UNKNOWN128_DPS] = 12
+        self.dps[UNKNOWN129_DPS] = 13
+        self.dps[UNKNOWN130_DPS] = True
+        self.dps[UNKNOWN134_DPS] = False
+        self.dps[UNKNOWN135_DPS] = True
+        self.dps[UNKNOWN136_DPS] = False
+        self.dps[UNKNOWN139_DPS] = True
+        self.dps[UNKNOWN140_DPS] = "test"
+        self.assertCountEqual(
+            self.subject.device_state_attributes,
+            {
+                "power_level": 50,
+                "operating_mode": "cool",
+                "unknown_107": 1,
+                "unknown_108": 2,
+                "unknown_115": 3,
+                "unknown_116": 4,
+                "unknown_118": 5,
+                "unknown_120": 6,
+                "unknown_122": 7,
+                "unknown_124": 8,
+                "unknown_125": 9,
+                "unknown_126": 10,
+                "unknown_127": 11,
+                "unknown_128": 12,
+                "unknown_129": 13,
+                "unknown_130": True,
+                "unknown_134": False,
+                "unknown_135": True,
+                "unknown_136": False,
+                "unknown_139": True,
+                "unknown_140": "test",
+            },
+        )
+
+    async def test_update(self):
+        result = AsyncMock()
+        self.subject._device.async_refresh.return_value = result()
+
+        await self.subject.async_update()
+
+        self.subject._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 6 - 1
tests/test_device.py

@@ -21,6 +21,7 @@ from .const import (
     KOGAN_SOCKET_PAYLOAD,
     KOGAN_SOCKET_PAYLOAD2,
     GARDENPAC_HEATPUMP_PAYLOAD,
+    MADIMACK_HEATPUMP_PAYLOAD,
     PURLINE_M100_HEATER_PAYLOAD,
     REMORA_HEATPUMP_PAYLOAD,
     BWT_HEATPUMP_PAYLOAD,
@@ -132,6 +133,10 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.subject._cached_state = GARDENPAC_HEATPUMP_PAYLOAD
         self.assertEqual(await self.subject.async_inferred_type(), "gardenpac_heatpump")
 
+    async def test_detects_madimack_heatpump_payload(self):
+        self.subject._cached_state = MADIMACK_HEATPUMP_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), "madimack_heatpump")
+
     async def test_detects_purline_m100_heater_payload(self):
         self.subject._cached_state = PURLINE_M100_HEATER_PAYLOAD
         self.assertEqual(
@@ -165,7 +170,7 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.assertEqual(await self.subject.async_inferred_type(), "anko_fan")
 
     async def test_detection_returns_none_when_device_type_could_not_be_detected(self):
-        self.subject._cached_state = {"1": False, "updated_at": datetime.now()}
+        self.subject._cached_state = {"2": False, "updated_at": datetime.now()}
         self.assertEqual(await self.subject.async_inferred_type(), None)
 
     async def test_does_not_refresh_more_often_than_cache_timeout(self):