Преглед изворни кода

Add support for Devola WiFi Patio heater.

Issue #163

- added tests for smartplug_encoded and product id for Japan version.
- tweaks to Wilfa config.
Jason Rumney пре 3 година
родитељ
комит
3a2f738aa8

+ 121 - 0
custom_components/tuya_local/devices/devola_patio_heater.yaml

@@ -0,0 +1,121 @@
+name: Devola Patio Heater
+primary_entity:
+  entity: climate
+  dps:
+    - id: 1
+      type: boolean
+      name: hvac_mode
+      mapping:
+        - dps_val: false
+          value: "off"
+          icon: "mdi:fire-off"
+        - dps_val: true
+          value: heat
+          icon: "mdi:fire"
+    - id: 2
+      type: integer
+      name: temperature
+      range:
+        min: 5
+        max: 45
+      mapping:
+        - constraint: temperature_unit
+          conditions:
+            - dps_val: f
+              value_redirect: temp_f
+              range:
+                min: 44
+                max: 99
+    - id: 3
+      type: integer
+      name: current_temperature
+      mapping:
+        - constraint: temperature_unit
+          conditions:
+            - dps_val: f
+              value_redirect: current_temp_f
+    - id: 6
+      type: boolean
+      name: preset_mode
+      mapping:
+        - dps_val: true
+          value: Eco
+        - dps_val: false
+          value: Normal
+    - id: 14
+      type: string
+      name: hvac_action
+      mapping:
+        - dps_val: heating
+          value: heating
+        - dps_val: warming
+          value: idle
+    - id: 19
+      type: string
+      name: temperature_unit
+      mapping:
+        - dps_val: c
+          value: C
+        - dps_val: f
+          value: F
+    - id: 20
+      type: integer
+      name: temp_f
+      range:
+        min: 44
+        max: 99
+      hidden: true
+    - id: 21
+      type: integer
+      name: current_temp_f
+      hidden: true
+secondary_entities:
+  - entity: lock
+    name: Child Lock
+    category: config
+    dps:
+      - id: 7
+        type: boolean
+        name: lock
+        mapping:
+          - dps_val: true
+            icon: "mdi:hand-back-right-off"
+          - dps_val: false
+            icon: "mdi:hand-back-right"
+  - entity: sensor
+    name: Power Level
+    category: diagnostic
+    class: power_factor
+    dps:
+      - id: 5
+        type: string
+        name: sensor
+        unit: "%"
+        mapping:
+          - dps_val: "1"
+            value: 25
+          - dps_val: "2"
+            value: 50
+          - dps_val: "3"
+            value: 75
+          - dps_val: "4"
+            value: 100
+  - entity: sensor
+    name: Mode
+    category: diagnostic
+    dps:
+      - id: 4
+        type: string
+        name: sensor
+  - entity: number
+    name: Timer
+    category: config
+    icon: "mdi:timer"
+    dps:
+      - id: 12
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 1440
+        unit: min

+ 3 - 0
custom_components/tuya_local/devices/smartplug_encoded.yaml

@@ -1,4 +1,7 @@
 name: Smartplug with encoded info
+product:
+  - id: A6bBfm2fmKKRfIxU
+    name: スマートプラグ
 primary_entity:
   entity: switch
   class: outlet

+ 1 - 1
custom_components/tuya_local/devices/wilfa_haze_hu400bc_humidifier.yaml

@@ -81,7 +81,7 @@ secondary_entities:
     category: config
     dps:
       - id: 8
-        name: "switch"
+        name: switch
         type: boolean
         mapping:
             - dps_val: true

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

@@ -141,6 +141,7 @@
                     "sensor_hepa_filter_life": "Include HEPA filter life as a sensor entity",
 		    "sensor_max_current": "Include max current as a sensor entity",
 		    "sensor_max_temperature_count": "Include max temperature count as a sensor entity",
+		    "sensor_mode": "Include mode as a sensor entity",
 		    "sensor_name": "Include name as a sensor entity",
                     "sensor_open": "Include open as a sensor entity",
                     "sensor_power": "Include power as a sensor entity",
@@ -326,6 +327,7 @@
                     "sensor_hepa_filter_life": "Include HEPA filter life as a sensor entity",
 		    "sensor_max_current": "Include max current as a sensor entity",
 		    "sensor_max_temperature_count": "Include max temperature count as a sensor entity",
+		    "sensor_mode": "Include mode as a sensor entity",
 		    "sensor_name": "Include name as a sensor entity",
                     "sensor_open": "Include open as a sensor entity",
                     "sensor_power": "Include power as a sensor entity",

+ 23 - 0
tests/const.py

@@ -1189,3 +1189,26 @@ WEAU_POOL_HEATPUMP_PAYLOAD = {
     "4": "auto",
     "6": 0,
 }
+
+SMARTPLUG_ENCODED_PAYLOAD = {
+    "1": True,
+    "11": 0,
+    "101": "QVA=",
+    "102": "QVA=",
+    "103": "QVA=",
+}
+
+DEVOLA_PATIO_HEATER_PAYLOAD = {
+    "1": True,
+    "2": 20,
+    "3": 15,
+    "4": "smart",
+    "5": "4",
+    "6": False,
+    "7": False,
+    "12": 0,
+    "14": "heating",
+    "19": "c",
+    "20": 68,
+    "21": 59,  
+}

+ 190 - 0
tests/devices/test_devola_patio_heater.py

@@ -0,0 +1,190 @@
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.const import (
+    PERCENTAGE,
+    STATE_UNAVAILABLE,
+    TIME_MINUTES,
+    TEMP_CELSIUS,
+    TEMP_FAHRENHEIT,
+)
+
+from ..const import DEVOLA_PATIO_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+from ..mixins.climate import TargetTemperatureTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from ..mixins.sensor import MultiSensorTests
+from .base_device_tests import TuyaDeviceTestCase
+
+HVACMODE_DPS = "1"
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "3"
+MODE_DPS = "4"
+POWERLEVEL_DPS = "5"
+PRESET_DPS = "6"
+LOCK_DPS = "7"
+TIMER_DPS = "12"
+HVACACTION_DPS = "14"
+UNIT_DPS = "19"
+TEMPF_DPS = "20"
+CURTEMPF_DPS = "21"
+
+
+class TestDevolaPatioHeater(
+    BasicLockTests,
+    BasicNumberTests,
+    MultiSensorTests,
+    TargetTemperatureTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("devola_patio_heater.yaml", DEVOLA_PATIO_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.setUpTargetTemperature(
+            TEMPERATURE_DPS,
+            self.subject,
+            min=5,
+            max=45,
+        )
+        self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            step=1.0,
+            unit=TIME_MINUTES,
+        )
+        self.setUpMultiSensors(
+            [
+                {
+                    "dps": POWERLEVEL_DPS,
+                    "name": "sensor_power_level",
+                    "unit": PERCENTAGE,
+                    "device_class": SensorDeviceClass.POWER_FACTOR,
+                    "testdata": ("2", 50),
+                },
+                {
+                    "dps": MODE_DPS,
+                    "name": "sensor_mode",
+                    "testdata": ("test", "test"),
+                },
+            ],
+        )
+        self.mark_secondary(
+            [
+                "lock_child_lock",
+                "number_timer",
+                "sensor_power_level",
+                "sensor_mode",
+            ]
+        )
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE,
+        )
+
+    def test_icon(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:fire")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:fire-off")
+
+    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,
+        )
+
+    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="Eco")
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                TEMPERATURE_DPS: 22,
+                PRESET_DPS: True,
+            },
+        ):
+            await self.subject.async_set_temperature(temperature=22, preset_mode="Eco")
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_current_temperature_redirects_in_f(self):
+        self.dps[CURRENTTEMP_DPS] = 24
+        self.dps[CURTEMPF_DPS] = 75
+        self.dps[UNIT_DPS] = "f"
+        self.assertEqual(self.subject.current_temperature, 75)
+
+    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, "Normal")
+
+        self.dps[PRESET_DPS] = True
+        self.assertEqual(self.subject.preset_mode, "Eco")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Normal", "Eco"])
+
+    async def test_set_preset_mode_to_normal(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: False},
+        ):
+            await self.subject.async_set_preset_mode("Normal")
+
+    async def test_set_preset_mode_to_eco(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: True},
+        ):
+            await self.subject.async_set_preset_mode("Eco")

+ 51 - 0
tests/devices/test_smartplug_encoded.py

@@ -0,0 +1,51 @@
+"""Tests for the switch entity."""
+from homeassistant.components.switch import SwitchDeviceClass
+from homeassistant.const import (
+    TIME_MINUTES,
+)
+
+from ..const import SMARTPLUG_ENCODED_PAYLOAD
+from ..mixins.number import BasicNumberTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
+
+SWITCH_DPS = "1"
+TIMER_DPS = "11"
+UNKNOWN101_DPS = "101"
+UNKNOWN102_DPS = "102"
+UNKNOWN103_DPS = "103"
+
+
+class TestSwitchEncoded(
+    BasicNumberTests, SwitchableTests, TuyaDeviceTestCase
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("smartplug_encoded.yaml", SMARTPLUG_ENCODED_PAYLOAD)
+        self.subject = self.entities.get("switch")
+        self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440.0,
+            unit=TIME_MINUTES,
+            scale=60,
+        )
+        self.mark_secondary(["number_timer"])
+
+    def test_device_class_is_outlet(self):
+        self.assertEqual(self.subject.device_class, SwitchDeviceClass.OUTLET)
+
+    def test_extra_state_attributes_set(self):
+        self.dps[UNKNOWN101_DPS] = "101"
+        self.dps[UNKNOWN102_DPS] = "102"
+        self.dps[UNKNOWN103_DPS] = "103"
+        self.assertDictEqual(
+            self.subject.extra_state_attributes,
+            {
+                "unknown_101": "101",
+                "unknown_102": "102",
+                "unknown_103": "103",
+            },
+        )