Bladeren bron

Add support for Beok TR9B thermostats.

Issue #103
Jason Rumney 4 jaren geleden
bovenliggende
commit
bef404e45a

+ 3 - 2
ACKNOWLEDGEMENTS.md

@@ -30,7 +30,7 @@ Further device support has been made with the assistance of users.  Please consi
  - [jorgenDK](https://github.com/jorgenDK) for assistance in supporting TroniTechnik Air Conditioner, and thanks for the coffee!
  - [Fannangir](https://github.com/Fannangir) for assistance in supporting Tadiran Wind Air Conditioner.
  - [marrold](https://github.com/marrold) for contributing support for ElectriQ CD20PRO dehumidifiers.
- - [Uaeguy](https://github.com/Uaeguy) for assistance in supporting Beca BHP-6000 and Siswell T29UTK thermostats, and thanks for the coffee!
+ - [Uaeguy](https://github.com/Uaeguy) for assistance in supporting Beca BHP-6000, Siswell T29UTK and Owon PCT513 thermostats, and thanks for the coffee!
  - [Johnnybyzhang](https://github.com/Johnnybyzhang) for assistance in supporting Lexy F501 fans.
  - [domgrimm](https://github.com/domgrimm) for assistance in supporting newer models of Kogan heater.
  - [EKCJ](https://github.com/EKCJ) for contributing support for ElectriQ DESD9LW dehumidifiers.
@@ -64,4 +64,5 @@ Further device support has been made with the assistance of users.  Please consi
  - [bob-tm](https://github.com/bob-tm) for contributing support from Wetair WAW-H1210LW humidifiers.
  - [shakin89](https://github.com/shakin89) for assistance in supporting Beca BAC-002 thermostats.
  - [PaulJoosten](https://github.com/PaulJoosten) for assistance in figuring out the similarities and capabilities of different Eurom heaters.
- 
+ - [jdavidr17](https://github.com/jdavidr17) for assistance with discovering timer parameters for switches.
+ - [miannelli516](https://github.com/miannelli516) for assistance with TR9B thermostats.

+ 2 - 0
README.md

@@ -77,6 +77,8 @@ the device will not work despite being listed below.
 - Siswell T29UTW thermostat
 - Siswell C16 thermostat (rebadged as Warmme, Klima and others)
 - Minco MH-1823D thermostat
+- Owon PCT513 thermostat
+- Beok TR9B thermostat (rebadged as Vancoo and perhaps others)
 
 ### Kettles
 - Kogan Glass 1.7L Smart Kettle (not reliably detected)

+ 175 - 0
custom_components/tuya_local/devices/beok_tr9b_thermostat.yaml

@@ -0,0 +1,175 @@
+name: BHT-002 Thermostat (C)
+primary_entity:
+  entity: climate
+  dps:
+    - id: 1
+      type: boolean
+      name: power
+      hidden: true
+      mapping:
+        - dps_val: false
+          value: "off"
+    - id: 2
+      type: string
+      name: hvac_mode
+      mapping:
+        - dps_val: "auto"
+          constraint: power
+          conditions:
+            - dps_val: false
+              value_redirect: power
+              value: "off"
+            - dps_val: true
+              value: auto
+        - dps_val: manual
+          constraint: power
+          conditions:
+            - dps_val: false
+              value_redirect: power
+            - dps_val: true
+              value: heat
+    - id: 16
+      type: integer
+      name: temperature
+      range:
+        min: 50
+        max: 10000
+      mapping:
+        - scale: 10
+          step: 5
+    - id: 19
+      type: integer
+      name: max_temperature
+      mapping:
+        - scale: 10
+    - id: 23
+      type: string
+      name: temperature_unit
+      mapping:
+        - dps_val: c
+          value: C
+        - dps_val: f
+          value: F
+    - id: 24
+      type: integer
+      name: current_temperature
+      mapping:
+        - scale: 10
+    - id: 26
+      type: integer
+      name: min_temperature
+      mapping:
+        - scale: 10
+    - id: 45
+      type: integer
+      name: Error Code
+    - id: 101
+      type: integer
+      name: unknown_101
+    - id: 102
+      type: integer
+      name: unknown_102
+secondary_entities:
+  - entity: switch
+    name: Anti-frost
+    icon: "mdi:snowflake"
+    category: config
+    dps:
+      - id: 10
+        type: boolean
+        name: switch
+  - entity: select
+    name: Temperature Unit
+    category: config
+    icon: "mdi:temperature-celsius"
+    dps:
+      - id: 23
+        type: string
+        name: option
+        mapping:
+          - dps_val: c
+            value: Celsius
+          - dps_val: f
+            value: Fahrenheit
+  - entity: select
+    name: Schedule
+    category: config
+    icon: "mdi:calendar-clock"
+    dps:
+      - id: 31
+        type: string
+        name: option
+        mapping:
+          - dps_val: "5_2"
+            value: "Weekday+Weekend"
+          - dps_val: "6_1"
+            value: "Mon-Sat+Sun"
+          - dps_val: "7"
+            value: "Daily"
+  - entity: binary_sensor
+    name: Valve
+    class: opening
+    category: diagnostic
+    dps:
+      - id: 36
+        type: string
+        name: sensor
+        mapping:
+          - dps_val: open
+            value: True
+          - dps_val: close
+            value: False
+  - entity: lock
+    name: "Child Lock"
+    category: config
+    dps:
+      - id: 40
+        type: boolean
+        name: lock
+        mapping:
+          - dps_val: true
+            icon: "mdi:hand-back-right-off"
+          - dps_val: false
+            icon: "mdi:hand-back-right"
+  - entity: binary_sensor
+    name: Error
+    category: diagnostic
+    class: problem
+    dps:
+      - id: 45
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: False
+          - value: True
+  - entity: number
+    name: High Temperature Limit
+    category: config
+    icon: "mdi:thermometer"
+    dps:
+      - id: 19
+        type: integer
+        name: value
+        unit: C
+        range:
+          min: 50
+          max: 10000
+        mapping:
+          - scale: 10
+            step: 10
+  - entity: number
+    name: Low Temperature Limit
+    category: config
+    dps:
+      - id: 26
+        name: value
+        type: integer
+        unit: C
+        icon: "mdi:thermometer"
+        range:
+          min: 50
+          max: 10000
+        mapping:
+          - scale: 10
+            step: 10

+ 2 - 0
custom_components/tuya_local/translations/en.json

@@ -40,6 +40,7 @@
 		    "binary_sensor_high_temperature": "Include high temperature alarm as a binary_sensor entity",
 		    "binary_sensor_low_temperature": "Include low temperature alarm as a binary_sensor entity",
 		    "binary_sensor_tank": "Include tank as a binary_sensor entity",
+		    "binary_sensor_valve": "Include valve as a binary_sensor entity",
 		    "binary_sensor_water_flow": "Include water flow warning as a binary_sensor entity",
 		    "climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		    "fan_intensity": "Include intensity as a fan entity",
@@ -147,6 +148,7 @@
 		    "binary_sensor_high_temperature": "Include high temperature alarm as a binary_sensor entity",
 		    "binary_sensor_low_temperature": "Include low temperature alarm as a binary_sensor entity",
 		    "binary_sensor_tank": "Include tank as a binary_sensor entity",
+		    "binary_sensor_valve": "Include valve as a binary_sensor entity",
 		    "binary_sensor_water_flow": "Include water flow warning as a binary_sensor entity",
 		    "climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		    "fan_intensity": "Include intensity as a fan entity",

+ 17 - 0
tests/const.py

@@ -425,6 +425,23 @@ MOES_BHT002_PAYLOAD = {
     "104": True,
 }
 
+BEOK_TR9B_PAYLOAD = {
+    "1": True,
+    "2": "manual",
+    "10": True,
+    "16": 590,
+    "19": 990,
+    "23": "f",
+    "24": 666,
+    "26": 410,
+    "31": "5_2",
+    "36": "close",
+    "40": False,
+    "45": 0,
+    "101": 1313,
+    "102": 10,
+}
+
 BECA_BAC002_PAYLOAD = {
     "1": True,
     "2": 39,

+ 212 - 0
tests/devices/test_beok_tr9b_thermostat.py

@@ -0,0 +1,212 @@
+from homeassistant.components.climate.const import (
+    HVAC_MODE_AUTO,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+from ..const import BEOK_TR9B_PAYLOAD
+from ..helpers import assert_device_properties_set
+from ..mixins.climate import TargetTemperatureTests
+from ..mixins.binary_sensor import MultiBinarySensorTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import MultiNumberTests
+from ..mixins.select import MultiSelectTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
+
+POWER_DPS = "1"
+HVACMODE_DPS = "2"
+ANTIFROST_DPS = "10"
+TEMPERATURE_DPS = "16"
+MAXTEMP_DPS = "19"
+UNIT_DPS = "23"
+CURRENTTEMP_DPS = "24"
+MINTEMP_DPS = "26"
+SCHED_DPS = "31"
+VALVE_DPS = "36"
+LOCK_DPS = "40"
+ERROR_DPS = "45"
+UNKNOWN101_DPS = "101"
+UNKNOWN102_DPS = "102"
+
+
+class TestBeokTR9BThermostat(
+    MultiBinarySensorTests,
+    BasicLockTests,
+    MultiNumberTests,
+    MultiSelectTests,
+    BasicSwitchTests,
+    TargetTemperatureTests,
+    TuyaDeviceTestCase
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "beok_tr9b_thermostat.yaml",
+            BEOK_TR9B_PAYLOAD,
+        )
+        self.subject = self.entities.get("climate")
+        self.setUpTargetTemperature(
+            TEMPERATURE_DPS,
+            self.subject,
+            min=41.0,
+            max=99.0,
+            scale=10,
+            step=5,
+        )
+        self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpMultiSelect(
+            [
+                {
+                    "dps": SCHED_DPS,
+                    "name": "select_schedule",
+                    "options": {
+                        "5_2": "Weekday+Weekend",
+                        "6_1": "Mon-Sat+Sun",
+                        "7": "Daily",
+                    },
+                },
+                {
+                    "dps": UNIT_DPS,
+                    "name": "select_temperature_unit",
+                    "options": {
+                        "c": "Celsius",
+                        "f": "Fahrenheit",
+                    },
+                },
+            ],
+        )
+        self.setUpBasicSwitch(
+            ANTIFROST_DPS,
+            self.entities.get("switch_anti_frost"),
+        )
+        self.setUpMultiBinarySensors(
+            [
+                {
+                    "dps": ERROR_DPS,
+                    "name": "binary_sensor_error",
+                    "device_class": "problem",
+                    "testdata": (1, 0),
+                },
+                {
+                    "dps": VALVE_DPS,
+                    "name": "binary_sensor_valve",
+                    "device_class": "opening",
+                    "testdata": ("open", "close"),
+                },
+            ],
+        )
+        self.setUpMultiNumber(
+            [
+                {
+                    "dps": MINTEMP_DPS,
+                    "name": "number_low_temperature_limit",
+                    "min": 5.0,
+                    "max": 1000.0,
+                    "step": 1.0,
+                    "scale": 10,
+                    "unit": TEMP_CELSIUS,
+                },
+                {
+                    "dps": MAXTEMP_DPS,
+                    "name": "number_high_temperature_limit",
+                    "min": 5.0,
+                    "max": 1000.0,
+                    "step": 1.0,
+                    "scale": 10,
+                    "unit": TEMP_CELSIUS,
+                },
+            ],
+        )
+        self.mark_secondary(
+            [
+                "binary_sensor_error",
+                "binary_sensor_valve",
+                "lock_child_lock",
+                "number_low_temperature_limit",
+                "number_high_temperature_limit",
+                "select_schedule",
+                "select_temperature_unit",
+                "switch_anti_frost",
+            ],
+        )
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_TARGET_TEMPERATURE,
+        )
+
+    def test_temperature_unit(self):
+        self.dps[UNIT_DPS] = "c"
+        self.assertEqual(
+            self.subject.temperature_unit,
+            TEMP_CELSIUS,
+        )
+        self.dps[UNIT_DPS] = "f"
+        self.assertEqual(
+            self.subject.temperature_unit,
+            TEMP_FAHRENHEIT,
+        )
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 685
+        self.assertEqual(self.subject.current_temperature, 68.5)
+
+    def test_hvac_mode(self):
+        self.dps[POWER_DPS] = False
+        self.dps[HVACMODE_DPS] = "auto"
+        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] = "manual"
+        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,
+            ],
+        )
+
+    # Override - since min and max are set by attributes, the range
+    # allowed when setting is wider than normal.  The thermostat seems
+    # to be configurable as at least a water heater (to 212F), as tuya
+    # doc says max 1000.0 (after scaling)
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            ValueError,
+            f"temperature \\(4.5\\) must be between 5.0 and 1000.0",
+        ):
+            await self.subject.async_set_target_temperature(4.5)
+        with self.assertRaisesRegex(
+            ValueError,
+            f"temperature \\(1000.5\\) must be between 5.0 and 1000.0",
+        ):
+            await self.subject.async_set_target_temperature(1000.5)
+
+    def test_extra_state_attributes(self):
+        self.dps[ERROR_DPS] = 8
+        self.dps[UNKNOWN101_DPS] = 101
+        self.dps[UNKNOWN102_DPS] = 102
+        self.assertDictEqual(
+            self.subject.extra_state_attributes,
+            {"Error Code": 8, "unknown_101": 101, "unknown_102": 102},
+        )
+
+    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")