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

Add support for Moes BHT-002 thermostat without external temperature sensor.

Issue #64
Jason Rumney 4 лет назад
Родитель
Сommit
b2d29ade1d

+ 1 - 1
ACKNOWLEDGEMENTS.md

@@ -48,4 +48,4 @@ Further device support has been made with the assistance of users.  Please consi
  - [SatarisGIT](https://github.com/SatarisGIT) for assistance in supporting Eberg Qubo Q40HD portable heatpump.
  - [lucaxxaa](https://github.com/lucaxxaa) for assistance in supporting Beca BHT-002 thermostat.
  - [nickdos](https://github.com/nickdos) for assistance in supporting Stirling FS1-40DC fan.
- 
+ - [Skro11-ru](https://github.com/Skro11-ru) for assistance in supporting Moes BHT-002 variant without external temperature sensor.

+ 3 - 2
README.md

@@ -65,8 +65,9 @@ the device will not work despite being listed below.
 ### Thermostats
 - Inkbird ITC306A thermostat smartplug (not fully functional)
 - Beca BHP-6000 Room Heat Pump control thermostat
-- Beca BHT-6000 Floor Heating thermostat
-- Beca BHT-002 Floor Heating thermostat
+- Beca BHT-6000/8000 Floor Heating thermostat
+- Beca BHT-002 Floor Heating thermostat (with external temp sensor)
+- Moes BHT-002 thermostat (without external temp sensor)
 - Awow/Mi-heat TH213 thermostat
 - Siswell T29UTW thermostat
 - Siswell C16 thermostat (rebadged as Warmme, Klima and others)

+ 3 - 2
custom_components/tuya_local/devices/README.md

@@ -86,13 +86,14 @@ by the generic implementations. It should not be used for new devices.
 
 ### `deprecated`
 
-//Optional, deprecated//
+//Optional//
 
-This boolean flag is used to mark an entity as deprecated.  This is mainly
+This is used to mark an entity as deprecated.  This is mainly
 for older devices that were implemented when only climate devices were
 supported, but are better represented in HA as fan or humidifier devices.
 An entity should be moved to `secondary_entities` before being marked as
 deprecated, and the preferred device type moved to the `primary_entity`.
+The value of this should indicated what to use instead.
 
 ### `class`
 

+ 22 - 3
custom_components/tuya_local/devices/beca_bht002_thermostat_c.yaml

@@ -1,7 +1,14 @@
-name: Beca BHT-002 Thermostat (C)
+name: BHT-002 Thermostat with external sensor (C)
 primary_entity:
   entity: climate
   dps:
+    - id: 1
+      type: boolean
+      name: power
+      mapping:
+        - dps_val: false
+          value: "off"
+      hidden: true
     - id: 2
       type: integer
       name: temperature
@@ -20,9 +27,20 @@ primary_entity:
       name: hvac_mode
       mapping:
         - dps_val: "0"
-          value: auto
+          constraint: power
+          conditions:
+            - dps_val: true
+              value: auto
+            - dps_val: false
+              value_redirect: power
+              value: "off"
         - dps_val: "1"
-          value: heat
+          constraint: power
+          conditions:
+            - dps_val: true
+              value: heat
+            - dps_val: false
+              value_redirect: power
     - id: 5
       type: boolean
       name: preset_mode
@@ -42,6 +60,7 @@ primary_entity:
 secondary_entities:
   - entity: light
     name: Display
+    deprecated: climate hvac_mode
     dps:
       - id: 1
         type: boolean

+ 66 - 0
custom_components/tuya_local/devices/moes_bht002_thermostat_c.yaml

@@ -0,0 +1,66 @@
+name: BHT-002 Thermostat without external sensor (C)
+primary_entity:
+  entity: climate
+  dps:
+    - id: 1
+      type: boolean
+      name: power
+      mapping:
+        - dps_val: false
+          value: "off"
+      hidden: true
+    - id: 2
+      type: integer
+      name: temperature
+      range:
+        min: 10
+        max: 70
+      mapping:
+        - scale: 2
+    - id: 3
+      type: integer
+      name: current_temperature
+      mapping:
+        - scale: 2
+    - id: 4
+      type: string
+      name: hvac_mode
+      mapping:
+        - dps_val: "0"
+          constraint: power
+          conditions:
+            - dps_val: true
+              value: auto
+            - dps_val: false
+              value_redirect: power
+              value: "off"
+        - dps_val: "1"
+          constraint: power
+          conditions:
+            - dps_val: true
+              value: heat
+            - dps_val: false
+              value_redirect: power
+    - id: 5
+      type: boolean
+      name: preset_mode
+      mapping:
+        - dps_val: true
+          value: eco 
+        - dps_val: false
+          value: comfort
+    - id: 104
+      type: boolean
+      name: unknown_104
+secondary_entities:
+  - entity: lock
+    name: Child Lock
+    dps:
+      - id: 6
+        type: boolean
+        name: lock
+        mapping:
+          - dps_val: true
+            icon: "mdi:hand-back-right-off"
+          - dps_val: false
+            icon: "mdi:hand-back-right"

+ 10 - 0
tests/const.py

@@ -353,6 +353,16 @@ BECA_BHT002_PAYLOAD = {
     "104": True,
 }
 
+MOES_BHT002_PAYLOAD = {
+    "1": False,
+    "2": 40,
+    "3": 42,
+    "4": "0",
+    "5": False,
+    "6": False,
+    "104": True,
+}
+
 LEXY_F501_PAYLOAD = {
     "1": True,
     "2": "forestwindhigh",

+ 10 - 4
tests/devices/test_beca_bht002_thermostat.py

@@ -1,6 +1,7 @@
 from homeassistant.components.climate.const import (
     HVAC_MODE_AUTO,
     HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
     PRESET_ECO,
     PRESET_COMFORT,
     SUPPORT_PRESET_MODE,
@@ -12,7 +13,7 @@ from ..const import BECA_BHT002_PAYLOAD
 from ..helpers import assert_device_properties_set
 from .base_device_tests import BasicLightTests, BasicLockTests, TuyaDeviceTestCase
 
-LIGHT_DPS = "1"
+POWER_DPS = "1"
 TEMPERATURE_DPS = "2"
 CURRENTTEMP_DPS = "3"
 HVACMODE_DPS = "4"
@@ -31,7 +32,7 @@ class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCa
             BECA_BHT002_PAYLOAD,
         )
         self.subject = self.entities.get("climate")
-        self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_display"))
+        self.setUpBasicLight(POWER_DPS, self.entities.get("light_display"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
 
     def test_supported_features(self):
@@ -112,7 +113,11 @@ class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCa
         self.assertEqual(self.subject.current_temperature, 22)
 
     def test_hvac_mode(self):
+        self.dps[POWER_DPS] = False
         self.dps[HVACMODE_DPS] = "0"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[POWER_DPS] = True
         self.assertEqual(self.subject.hvac_mode, HVAC_MODE_AUTO)
 
         self.dps[HVACMODE_DPS] = "1"
@@ -127,6 +132,7 @@ class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCa
             [
                 HVAC_MODE_HEAT,
                 HVAC_MODE_AUTO,
+                HVAC_MODE_OFF,
             ],
         )
 
@@ -140,9 +146,9 @@ class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCa
         )
 
     def test_icons(self):
-        self.dps[LIGHT_DPS] = True
+        self.dps[POWER_DPS] = True
         self.assertEqual(self.basicLight.icon, "mdi:led-on")
-        self.dps[LIGHT_DPS] = False
+        self.dps[POWER_DPS] = False
         self.assertEqual(self.basicLight.icon, "mdi:led-off")
 
         self.dps[LOCK_DPS] = True

+ 149 - 0
tests/devices/test_moes_bht002_thermostat.py

@@ -0,0 +1,149 @@
+from homeassistant.components.climate.const import (
+    HVAC_MODE_AUTO,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    PRESET_ECO,
+    PRESET_COMFORT,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..const import MOES_BHT002_PAYLOAD
+from ..helpers import assert_device_properties_set
+from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+
+POWER_DPS = "1"
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "3"
+HVACMODE_DPS = "4"
+PRESET_DPS = "5"
+LOCK_DPS = "6"
+UNKNOWN104_DPS = "104"
+
+
+class TestMoesBHT002Thermostat(BasicLockTests, TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "moes_bht002_thermostat_c.yaml",
+            MOES_BHT002_PAYLOAD,
+        )
+        self.subject = self.entities.get("climate")
+        self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE,
+        )
+
+    def test_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit,
+            self.subject._device.temperature_unit,
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 50
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 0.5)
+
+    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, {TEMPERATURE_DPS: 41}
+        ):
+            await self.subject.async_set_temperature(temperature=20.5)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PRESET_DPS: True}
+        ):
+            await self.subject.async_set_temperature(preset_mode=PRESET_ECO)
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                TEMPERATURE_DPS: 44,
+                PRESET_DPS: False,
+            },
+        ):
+            await self.subject.async_set_temperature(
+                temperature=22, preset_mode=PRESET_COMFORT
+            )
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 45},
+        ):
+            await self.subject.async_set_target_temperature(22.5)
+
+    async def test_set_target_temperature_rounds_value_to_closest_half(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 35}
+        ):
+            await self.subject.async_set_target_temperature(17.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            ValueError, "temperature \\(4.5\\) must be between 5.0 and 35.0"
+        ):
+            await self.subject.async_set_target_temperature(4.5)
+
+        with self.assertRaisesRegex(
+            ValueError, "temperature \\(35.5\\) must be between 5.0 and 35.0"
+        ):
+            await self.subject.async_set_target_temperature(35.5)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 44
+        self.assertEqual(self.subject.current_temperature, 22)
+
+    def test_hvac_mode(self):
+        self.dps[POWER_DPS] = False
+        self.dps[HVACMODE_DPS] = "0"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[POWER_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_AUTO)
+
+        self.dps[HVACMODE_DPS] = "1"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        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_HEAT,
+                HVAC_MODE_AUTO,
+                HVAC_MODE_OFF,
+            ],
+        )
+
+    def test_device_state_attribures(self):
+        self.dps[UNKNOWN104_DPS] = False
+
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {"unknown_104": False},
+        )
+
+    def test_icons(self):
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.basicLock.icon, "mdi:hand-back-right-off")
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.basicLock.icon, "mdi:hand-back-right")