Explorar o código

Add support for Owon PCT513 Thermometer

Issue #100
Jason Rumney %!s(int64=4) %!d(string=hai) anos
pai
achega
03b5866a70

+ 162 - 0
custom_components/tuya_local/devices/owon_pct513_thermostat.yaml

@@ -0,0 +1,162 @@
+name: PCT513 Themostat
+products:
+  - id: rsujjtinksnzcezy
+primary_entity:
+  entity: climate
+  dps:
+    - id: 2
+      type: string
+      name: hvac_mode
+      mapping:
+        - dps_val: "off"
+          value: "off"
+        - dps_val: cool
+          value: cool
+        - dps_val: heat
+          value: heat
+        - dps_val: auto
+          value: heat_cool
+        - dps_val: emergencyheat
+          value: heat
+    - id: 16
+      type: integer
+      name: temperature
+      range:
+        min: 1500
+        max: 4500
+      mapping:
+        - scale: 100
+          step: 50
+          constraint: temperature_unit
+          conditions:
+            - dps_val: f
+              value_redirect: temp_f
+              scale: 1
+              step: 1
+              range:
+                min: 32
+                max: 122
+    - id: 17
+      type: integer
+      name: temp_f
+      hidden: true
+      range:
+        min: 32
+        max: 122
+    - 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: 100
+          constraint: temperature_unit
+          conditions:
+            - dps_val: f
+              scale: 1
+              value_redirect: curtemp_f
+    - id: 29
+      type: integer
+      name: curtemp_f
+      hidden: true
+    - id: 34
+      type: integer
+      name: current_humidity
+    - id: 45
+      type: integer
+      name: unknown_45
+    - id: 107
+      type: string
+      name: installation
+    - id: 108
+      type: integer
+      name: temp_c
+      mapping:
+        - scale: 100
+    - id: 109
+      type: integer
+      name: unknown_109
+    - id: 110
+      type: integer
+      name: temp_f2
+    - id: 111
+      type: integer
+      name: unknown_111
+    - id: 115
+      type: string
+      name: fan_mode
+      mapping:
+        - dps_val: auto
+          value: auto
+        - dps_val: "on"
+          value: "on"
+        - dps_val: cycle
+          value: cycle
+    - id: 116
+      type: string
+      name: unknown_116
+    - id: 119
+      type: boolean
+      name: scheduling
+      hidden: true
+      mapping:
+        - dps_val: false
+          value: Manual
+    - id: 120
+      type: string
+      name: preset_mode
+      mapping:
+        - dps_val: followschedule
+          constraint: scheduling
+          conditions:
+            - dps_val: false
+              value_redirect: scheduling
+              value: Manual
+            - dps_val: true
+              value: Follow Schedule
+        - dps_val: temphold
+          constraint: scheduling
+          conditions:
+            - dps_val: false
+              value_redirect: scheduling
+            - dps_val: true
+              value: Temporary Hold
+        - dps_val: permhold
+          constraint: scheduling
+          conditions:
+            - dps_val: false
+              value_redirect: scheduling
+            - dps_val: true
+              value: Permanent Hold
+    - id: 129
+      type: string
+      name: hvac_action
+      mapping:
+        - dps_val: coolfanon
+          value: cooling
+        - dps_val: alloff
+          value: idle
+        - dps_val: fanon
+          value: fan
+        - dps_val: heatfanon
+          value: heating
+secondary_entities:
+  - entity: number
+    name: Fan Runtime
+    category: config
+    dps:
+      - id: 123
+        type: integer
+        name: value
+        unit: min
+        range:
+          min: 0
+          max: 55
+        mapping:
+          - step: 5

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

@@ -54,6 +54,7 @@
 		    "number_calibration_offset_internal": "Include calibration offset for internal sensor as a number entity",
 		    "number_calibration_swing": "Include calibration swing as a number entity",
 		    "number_continuous_heat_hours": "Include Continuous Heating Time as a number entity",
+		    "number_fan_runtime": "Include fan run time as a number entity",
 		    "number_high_temperature_limit": "Include High Temperature Limit as a number entity",
 		    "number_low_temperature_limit": "Include Low Temperature Limit as a number entity",
 		    "number_floor_temperature_limit": "Include Floor Temperature Limit as a number entity",
@@ -160,6 +161,7 @@
 		    "number_calibration_offset_internal": "Include calibration offset for internal sensor as a number entity",
 		    "number_calibration_swing": "Include calibration swing as a number entity",
 		    "number_continuous_heat_hours": "Include Continuous Heating Time as a number entity",
+		    "number_fan_runtime": "Include fan run time as a number entity",
 		    "number_high_temperature_limit": "Include High Temperature Limit as a number entity",
 		    "number_low_temperature_limit": "Include Low Temperature Limit as a number entity",
 		    "number_floor_temperature_limit": "Include Floor Temperature Limit as a number entity",

+ 22 - 0
tests/const.py

@@ -733,3 +733,25 @@ WOOX_R4028_SOCKET_PAYLOAD = {
     "103": 0,
     "105": 0,
 }
+
+OWON_PCT513_THERMOSTAT_PAYLOAD = {
+    "2": "cool",
+    "16": 2150,
+    "17": 71,
+    "23": "c",
+    "24": 2950,
+    "29": 85,
+    "34": 52,
+    "45": 0,
+    "107": "0",
+    "108": 2150,
+    "109": 1650,
+    "110": 71,
+    "111": 62,
+    "115": "auto",
+    "116": "1",
+    "119": True,
+    "120": "permhold",
+    "123": 25,
+    "129": "coolfanon",
+}

+ 264 - 0
tests/devices/test_owon_pct513_thermostat.py

@@ -0,0 +1,264 @@
+from homeassistant.components.climate.const import (
+    CURRENT_HVAC_COOL,
+    CURRENT_HVAC_FAN,
+    CURRENT_HVAC_HEAT,
+    CURRENT_HVAC_IDLE,
+    HVAC_MODE_COOL,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_HEAT_COOL,
+    HVAC_MODE_OFF,
+    SUPPORT_FAN_MODE,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+from ..const import OWON_PCT513_THERMOSTAT_PAYLOAD
+from ..helpers import assert_device_properties_set
+from ..mixins.climate import TargetTemperatureTests
+from ..mixins.number import BasicNumberTests
+from .base_device_tests import TuyaDeviceTestCase
+
+HVACMODE_DPS = "2"
+TEMPERATURE_DPS = "16"
+TEMPF_DPS = "17"
+UNIT_DPS = "23"
+CURRENTTEMP_DPS = "24"
+CURRTEMPF_DPS = "29"
+CURRENTHUMID_DPS = "34"
+UNKNOWN45_DPS = "45"
+INSTALL_DPS = "107"
+TEMPC_DPS = "108"
+UNKNOWN109_DPS = "109"
+TEMPF2_DPS = "110"
+UNKNOWN111_DPS = "111"
+FAN_DPS = "115"
+UNKNOWN116_DPS = "116"
+SCHED_DPS = "119"
+PRESET_DPS = "120"
+DUTYCYCLE_DPS = "123"
+HVACACTION_DPS = "129"
+
+class TestOwonPCT513Thermostat(
+    BasicNumberTests,
+    TargetTemperatureTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "owon_pct513_thermostat.yaml", OWON_PCT513_THERMOSTAT_PAYLOAD
+        )
+        self.subject = self.entities.get("climate")
+        self.setUpTargetTemperature(
+            TEMPERATURE_DPS,
+            self.subject,
+            min=15.0,
+            max=45.0,
+            scale=100,
+            step=50,
+        )
+        self.setUpBasicNumber(
+            DUTYCYCLE_DPS,
+            self.entities.get("number_fan_runtime"),
+            max=55,
+            step=5,
+            unit="min",
+        )
+
+        self.mark_secondary(["number_fan_runtime"])
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE | 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[UNIT_DPS] = "c"
+        self.dps[CURRENTTEMP_DPS] = 2100
+        self.assertEqual(self.subject.current_temperature, 21.00)
+        self.dps[UNIT_DPS] = "f"
+        self.dps[CURRTEMPF_DPS] = 82
+        self.assertEqual(self.subject.current_temperature, 82)
+
+    def test_current_humidity(self):
+        self.dps[CURRENTHUMID_DPS] = 50
+        self.assertEqual(self.subject.current_humidity, 50)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(
+            self.subject.hvac_modes,
+            [
+                HVAC_MODE_COOL,
+                HVAC_MODE_HEAT,
+                HVAC_MODE_HEAT_COOL,
+                HVAC_MODE_OFF,
+            ],
+        )
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = "off"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+        self.dps[HVACMODE_DPS] = "cool"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_COOL)
+        self.dps[HVACMODE_DPS] = "heat"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+        self.dps[HVACMODE_DPS] = "auto"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT_COOL)
+        self.dps[HVACMODE_DPS] = "emergencyheat"
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+        
+    async def test_set_hvac_mode_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: "off"}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    async def test_set_hvac_mode_cool(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: "cool"}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_COOL)
+
+    async def test_set_hvac_mode_heat(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: "heat"}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_set_hvac_mode_heatcool(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: "auto"}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT_COOL)
+
+    def test_hvac_action(self):
+        self.dps[HVACACTION_DPS] = "coolfanon"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_COOL)
+        self.dps[HVACACTION_DPS] = "alloff"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_IDLE)
+        self.dps[HVACACTION_DPS] = "fanon"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_FAN)
+        self.dps[HVACACTION_DPS] = "heatfanon"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_HEAT)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(
+            self.subject.preset_modes,
+            ["Manual", "Follow Schedule", "Temporary Hold", "Permanent Hold"],
+        )
+
+    def test_preset_mode(self):
+        self.dps[SCHED_DPS] = True
+        self.dps[PRESET_DPS] = "followschedule"
+        self.assertEqual(self.subject.preset_mode, "Follow Schedule")
+        self.dps[PRESET_DPS] = "temphold"
+        self.assertEqual(self.subject.preset_mode, "Temporary Hold")
+        self.dps[PRESET_DPS] = "permhold"
+        self.assertEqual(self.subject.preset_mode, "Permanent Hold")
+        self.dps[SCHED_DPS] = False
+        self.assertEqual(self.subject.preset_mode, "Manual")
+
+    async def test_set_preset_to_schedule(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                SCHED_DPS: True,
+                PRESET_DPS: "followschedule",
+            },
+        ):
+            await self.subject.async_set_preset_mode("Follow Schedule")
+
+    async def test_set_preset_to_temp_hold(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                SCHED_DPS: True,
+                PRESET_DPS: "temphold",
+            },
+        ):
+            await self.subject.async_set_preset_mode("Temporary Hold")
+
+    async def test_set_preset_to_perm_hold(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                SCHED_DPS: True,
+                PRESET_DPS: "permhold",
+            },
+        ):
+            await self.subject.async_set_preset_mode("Permanent Hold")
+
+    async def test_set_preset_to_manual(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                SCHED_DPS: False,
+            },
+        ):
+            await self.subject.async_set_preset_mode("Manual")
+
+    def test_fan_modes(self):
+        self.assertCountEqual(
+            self.subject.fan_modes,
+            ["on", "auto", "cycle"],
+        )
+
+    def test_fan_mode(self):
+        self.dps[FAN_DPS] = "on"
+        self.assertEqual(self.subject.fan_mode, "on")
+        self.dps[FAN_DPS] = "auto"
+        self.assertEqual(self.subject.fan_mode, "auto")
+        self.dps[FAN_DPS] = "cycle"
+        self.assertEqual(self.subject.fan_mode, "cycle")
+
+    async def test_set_fan_on(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {FAN_DPS: "on"},
+        ):
+            await self.subject.async_set_fan_mode("on")
+
+    async def test_set_fan_auto(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {FAN_DPS: "auto"},
+        ):
+            await self.subject.async_set_fan_mode("auto")
+
+    async def test_set_fan_cycle(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {FAN_DPS: "cycle"},
+        ):
+            await self.subject.async_set_fan_mode("cycle")
+
+    def test_extra_state_attributes(self):
+        self.dps[UNKNOWN45_DPS] = 45
+        self.dps[INSTALL_DPS] = "107"
+        self.dps[TEMPC_DPS] = 1080
+        self.dps[UNKNOWN109_DPS] = 109
+        self.dps[TEMPF2_DPS] = 110
+        self.dps[UNKNOWN111_DPS] = 111
+        self.dps[UNKNOWN116_DPS] = "116"
+        self.assertDictEqual(
+            self.subject.extra_state_attributes,
+            {
+                "unknown_45": 45,
+                "installation": "107",
+                "temp_c": 10.8,
+                "unknown_109": 109,
+                "temp_f2": 110,
+                "unknown_111": 111,
+                "unknown_116": "116",
+             },
+        )