Przeglądaj źródła

Add support for Beca BHT-6000 thermostat.

From issue #50
Some info based on values from https://github.com/klausahrenberg/WThermostatBeca
(assuming direct mapping from DPS to internal MCU protocol values).
Jason Rumney 4 lat temu
rodzic
commit
f392ad3ca5

+ 2 - 0
ACKNOWLEDGEMENTS.md

@@ -41,3 +41,5 @@ Further device support has been made with the assistance of users.  Please consi
  - [ThomasADavis](https://github.com/ThomasADavis) for contributing support for Renpho RP-AP001S air purifiers.
  - [ThomasADavis](https://github.com/ThomasADavis) for contributing support for Renpho RP-AP001S air purifiers.
  - [darek-margas](https://github.com/darek-margas) for contributing support for Arlec fans and Carson portable air conditioners.
  - [darek-margas](https://github.com/darek-margas) for contributing support for Arlec fans and Carson portable air conditioners.
  - [SamJongenelen](https://github.com/SamJongenelen) for assistance in supporting Siswell C16 Thermostats
  - [SamJongenelen](https://github.com/SamJongenelen) for assistance in supporting Siswell C16 Thermostats
+ - [antoweb](https://github.com/antoweb) for assistance in supporting Beca BHT-6000 thermostats.
+ - [klausahrenberg](https://github.com/klausahrenberg) for figuring out the BHT-6000 and other thermostats' internal MCU protocol for his alternate MQQT firmware, which helped with finding some of the details.

+ 5 - 4
README.md

@@ -62,10 +62,11 @@ the device will not work despite being listed below.
 
 
 ### Thermostats
 ### Thermostats
 - Inkbird ITC306A thermostat smartplug (not fully functional)
 - Inkbird ITC306A thermostat smartplug (not fully functional)
-- Beca BHP-6000 Room Heat Pump control Thermostat
-- Awow/Mi-heat TH213 Thermostat
-- Siswell T29UTW Thermostat
-- Siswell C16 Thermostat (rebadged as Warmme, Klima and others)
+- Beca BHP-6000 Room Heat Pump control thermostat
+- Beca BHT-6000 Floor Heating thermostat
+- Awow/Mi-heat TH213 thermostat
+- Siswell T29UTW thermostat
+- Siswell C16 thermostat (rebadged as Warmme, Klima and others)
 
 
 ### Kettles
 ### Kettles
 - Kogan Glass 1.7L Smart Kettle (not reliably detected)
 - Kogan Glass 1.7L Smart Kettle (not reliably detected)

+ 67 - 0
custom_components/tuya_local/devices/beca_bht6000_thermostat_c.yaml

@@ -0,0 +1,67 @@
+name: Beca BHT-6000 Thermostat (C)
+primary_entity:
+  entity: climate
+  dps:
+    - 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"
+          value: auto
+        - dps_val: "1"
+          value: heat
+    - id: 5
+      type: boolean
+      name: preset_mode
+      mapping:
+        - dps_val: true
+          value: eco 
+        - dps_val: false
+          value: comfort
+    - id: 102
+      type: integer
+      name: floor_temperature
+      mapping:
+        - scale: 2
+    - id: 103
+      type: string
+      name: unknown_103
+    - id: 104
+      type: boolean
+      name: unknown_104
+secondary_entities:
+  - entity: light
+    name: Display
+    dps:
+      - id: 1
+        type: boolean
+        name: switch
+        mapping:
+          - dps_val: true
+            icon: "mdi:led-on"
+          - dps_val: false
+            icon: "mdi:led-off"
+  - 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"

+ 12 - 0
tests/const.py

@@ -321,6 +321,18 @@ BECA_BHP6000_PAYLOAD = {
     "7": False,
     "7": False,
 }
 }
 
 
+BECA_BHT6000_PAYLOAD = {
+    "1": False,
+    "2": 40,
+    "3": 42,
+    "4": "0",
+    "5": False,
+    "6": False,
+    "102": 0,
+    "103": "1",
+    "104": True,
+}
+
 LEXY_F501_PAYLOAD = {
 LEXY_F501_PAYLOAD = {
     "1": True,
     "1": True,
     "2": "forestwindhigh",
     "2": "forestwindhigh",

+ 155 - 0
tests/devices/test_beca_bht6000_thermostat.py

@@ -0,0 +1,155 @@
+from homeassistant.components.climate.const import (
+    HVAC_MODE_AUTO,
+    HVAC_MODE_HEAT,
+    PRESET_ECO,
+    PRESET_COMFORT,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..const import BECA_BHT6000_PAYLOAD
+from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
+
+LIGHT_DPS = "1"
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "3"
+HVACMODE_DPS = "4"
+PRESET_DPS = "5"
+LOCK_DPS = "6"
+FLOOR_DPS = "102"
+UNKNOWN103_DPS = "103"
+UNKNOWN104_DPS = "104"
+
+
+class TestBecaBHT6000Thermostat(TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "beca_bht6000_thermostat_c.yaml",
+            BECA_BHT6000_PAYLOAD,
+        )
+        self.subject = self.entities.get("climate")
+        self.light = self.entities.get("light")
+        self.lock = self.entities.get("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[HVACMODE_DPS] = "0"
+        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,
+            ],
+        )
+
+    def test_device_state_attribures(self):
+        self.dps[FLOOR_DPS] = 45
+        self.dps[UNKNOWN103_DPS] = "103"
+        self.dps[UNKNOWN104_DPS] = False
+
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {"floor_temperature": 22.5, "unknown_103": "103", "unknown_104": False},
+        )
+        self.assertDictEqual(self.light.device_state_attributes, {})
+        self.assertDictEqual(self.lock.device_state_attributes, {})
+
+    def test_icons(self):
+        self.dps[LIGHT_DPS] = True
+        self.assertEqual(self.light.icon, "mdi:led-on")
+        self.dps[LIGHT_DPS] = False
+        self.assertEqual(self.light.icon, "mdi:led-off")
+
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.lock.icon, "mdi:hand-back-right-off")
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.lock.icon, "mdi:hand-back-right")