Răsfoiți Sursa

Support for Ecostrad iQ Ceramic radiators.

From @Paul-C-S on issue #118
Jason Rumney 4 ani în urmă
părinte
comite
287e81355b

+ 1 - 1
ACKNOWLEDGEMENTS.md

@@ -68,7 +68,7 @@ Further device support has been made with the assistance of users.  Please consi
  - [miannelli516](https://github.com/miannelli516) for assistance with TR9B thermostats.
  - [edwinyoo44](https://github.com/edwinyoo44) for contributing support for JJPro JPD01 dehumidifiers and assistance with Poiema One purifiers.
  - [mpetcuRO](https://github.com/mpetcuRO) for assistance with Hysen HT08WE-2 thermometers.
- - [Paul-C-S](https://github.com/Paul-C-S) for assistance with Ecostrad Accent iQ heaters.
+ - [Paul-C-S](https://github.com/Paul-C-S) for assistance with Ecostrad Accent iQ heaters and contributing support for iQ Ceramic radiators.
  - [WildeRNS](https://github.com/WildeRNS) for assistance with Nashone MTS-700-WB thermostat smartplugs, SmartMCB Energy meter.
  - [ishioni](https://github.com/ishioni) for contributing support for Eberg Cooly C32HD air conditioner.
  - [Gekko47](https://github.com/Gekko47) for contributing support for ElectriQ CD12v2 dehumidifiers.

+ 1 - 0
README.md

@@ -44,6 +44,7 @@ the device will not work despite being listed below.
 - Kogan Flame effect heater - KAWHMFP20BA model
 - Nedis convection heater - WIFIHTPL20F models
 - Ecostrad Accent iQ heating panels
+- Ecostrad iQ Ceramic radiators
 
 ### Air Conditioners / Heatpumps
 

+ 123 - 0
custom_components/tuya_local/devices/ecostrad_iqceramic_radiator.yaml

@@ -0,0 +1,123 @@
+name: Ecostrad Radiator
+primary_entity:
+  entity: climate
+  icon: "mdi:radiator"
+  dps:
+    - id: 1
+      type: boolean
+      name: hvac_mode
+      mapping:
+        - dps_val: true
+          value: heat
+        - dps_val: false
+          value: "off"
+    - id: 2
+      type: string
+      name: preset_mode
+      mapping:
+        - dps_val: "auto"
+          value: Program
+        - dps_val: "eco"
+          value: ECO
+        - dps_val: "hot"
+          value: Comfort
+        - dps_val: "cold"
+          value: Anti-Freeze
+        - dps_val: "person_infrared_ray"
+          value: Sensor
+        - dps_val: "line_control"
+          value: Pilot Wire
+    - id: 16
+      type: integer
+      name: temperature
+      range:
+        min: 70
+        max: 300
+      mapping:
+        - scale: 10
+          step: 5
+    - id: 24
+      type: integer
+      name: current_temperature
+      mapping:
+        - scale: 10
+    # documented as unavailable on this model in the manual
+    # left as an attribute so it can be observed
+    - id: 109
+      name: limit_function
+      type: string
+secondary_entities:
+  - 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: select
+    name: Open Window Detection
+    category: config
+    dps:
+      - id: 108
+        type: string
+        name: option
+        mapping:
+          - dps_val: 0
+            value: "Off"
+            icon: "mdi:window-closed"
+          - dps_val: 60
+            value: "60 mins"
+            icon: "mdi:window-open"
+          - dps_val: 90
+            value: "90 mins"
+            icon: "mdi:window-open"
+  - entity: select
+    name: PIR Timeout
+    category: config
+    icon: "mdi:timer-settings-outline"
+    dps:
+      - id: 104
+        type: string
+        name: option
+        mapping:
+          - dps_val: "15"
+            value: "15 mins"
+          - dps_val: "30"
+            value: "30 mins"
+          - dps_val: "45"
+            value: "45 mins"
+          - dps_val: "60"
+            value: "60 mins"
+  - entity: switch
+    name: Time Sync
+    category: config
+    dps:
+      - id: 107
+        type: string
+        name: switch
+        mapping:
+          - dps_val: 1
+            value: true
+            icon: "mdi:sync"
+          - dps_val: 0
+            value: false
+            icon: "mdi:sync-off"
+  - entity: number
+    name: Calibration Offset
+    category: config
+    icon: "mdi:thermometer"
+    dps:
+      - id: 27
+        type: integer
+        name: value
+        unit: C
+        range:
+          min: -5
+          max: 5
+
+    

+ 7 - 1
custom_components/tuya_local/helpers/device_config.py

@@ -250,9 +250,15 @@ class TuyaDpsConfig:
             "string": str,
             "float": float,
             "bitfield": int,
+            "json": str,
+            "base64": str,
         }
         return types.get(t)
 
+    @property
+    def rawtype(self):
+        return self._config["type"]
+
     @property
     def name(self):
         return self._config["name"]
@@ -263,7 +269,7 @@ class TuyaDpsConfig:
 
     def _match(self, matchdata, value):
         """Return true val1 matches val2"""
-        if self._config["type"] == "bitfield" and matchdata:
+        if self.rawtype == "bitfield" and matchdata:
             try:
                 return (int(value) & int(matchdata)) != 0
             except BaseException:

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

@@ -88,6 +88,8 @@
                     "select_configuration": "Include configuration as a select entity",
                     "select_initial_state": "Include initial state as a select entity",
                     "select_installation": "Include installation as a select entity",
+		    "select_open_window_detection": "Include open window detection as a select entity",
+		    "select_pir_timeout": "Include PIR timeout as a select entity",
                     "select_schedule": "Include schedule as a select entity",
                     "select_timer": "Include timer as a select entity",
                     "select_temperature_sensor": "Include temperature sensor config as a select entity",
@@ -136,6 +138,7 @@
 		    "switch_prepay": "Include prepay as a switch entity",
                     "switch_sleep": "Include sleep mode as a switch entity",
                     "switch_sound": "Include sound mute as a switch entity",
+		    "switch_time_sync": "Include time sync as a switch entity",
                     "switch_usb_switch": "Include USB as a switch entity",
                     "switch_uv_sterilization": "Include UV sterilization as a switch"
                 }
@@ -224,6 +227,8 @@
                     "select_configuration": "Include configuration as a select entity",
                     "select_initial_state": "Include initial state as a select entity",
                     "select_installation": "Include installation as a select entity",
+		    "select_open_window_detection": "Include open window detection as a select entity",
+		    "select_pir_timeout": "Include PIR timeout as a select entity",
                     "select_schedule": "Include schedule as a select entity",
                     "select_timer": "Include timer as a select entity",
                     "select_temperature_sensor": "Include temperature sensor config as a select entity",
@@ -272,6 +277,7 @@
 		    "switch_prepay": "Include prepay as a switch entity",
                     "switch_sleep": "Include sleep mode as a switch entity",
                     "switch_sound": "Include sound mute as a switch entity",
+		    "switch_time_sync": "Include time sync as a switch entity",
                     "switch_usb_switch": "Include USB as a switch entity",
                     "switch_uv_sterilization": "Include UV sterilization as a switch"
                 }

+ 13 - 0
tests/const.py

@@ -861,6 +861,19 @@ ECOSTRAD_ACCENTIQ_HEATER_PAYLOAD = {
     "101": True,
 }
 
+ECOSTRAD_IQCERAMIC_RADIATOR_PAYLOAD = {
+    "1": True,
+    "2": "hot",
+    "16": 180,
+    "24": 90,
+    "27": 0,
+    "40": False,
+    "104": "15",
+    "107": "1",
+    "108": "0",
+    "109": "0",
+}
+
 NASHONE_MTS700WB_THERMOSTAT_PAYLOAD = {
     "1": True,
     "2": "hot",

+ 218 - 0
tests/devices/test_ecostrad_iqceramic_radiator.py

@@ -0,0 +1,218 @@
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import (
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+)
+
+from ..const import ECOSTRAD_IQCERAMIC_RADIATOR_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.select import MultiSelectTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
+
+HVACMODE_DPS = "1"
+PRESET_DPS = "2"
+TEMPERATURE_DPS = "16"
+CURRENTTEMP_DPS = "24"
+CALIB_DPS = "27"
+LOCK_DPS = "40"
+PIR_DPS = "104"
+SYNC_DPS = "107"
+WINDOW_DPS = "108"
+LIMIT_DPS = "109"
+
+
+class TestEcostradAccentIqHeater(
+    BasicLockTests,
+    BasicNumberTests,
+    MultiSelectTests,
+    BasicSwitchTests,
+    TargetTemperatureTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "ecostrad_iqceramic_radiator.yaml",
+            ECOSTRAD_IQCERAMIC_RADIATOR_PAYLOAD,
+        )
+        self.subject = self.entities.get("climate")
+        self.setUpTargetTemperature(
+            TEMPERATURE_DPS, self.subject, min=7.0, max=30.0, scale=10, step=5
+        )
+        self.setUpBasicLock(
+            LOCK_DPS,
+            self.entities.get("lock_child_lock"),
+        )
+        self.setUpBasicNumber(
+            CALIB_DPS,
+            self.entities.get("number_calibration_offset"),
+            min=-5,
+            max=5,
+            unit=TEMP_CELSIUS,
+        )
+        self.setUpBasicSwitch(
+            SYNC_DPS, self.entities.get("switch_time_sync"), testdata=("1", "0")
+        )
+        self.setUpMultiSelect(
+            [
+                {
+                    "dps": PIR_DPS,
+                    "name": "select_pir_timeout",
+                    "options": {
+                        "15": "15 mins",
+                        "30": "30 mins",
+                        "45": "45 mins",
+                        "60": "60 mins",
+                    },
+                },
+                {
+                    "dps": WINDOW_DPS,
+                    "name": "select_open_window_detection",
+                    "options": {
+                        "0": "Off",
+                        "60": "60 mins",
+                        "90": "90 mins",
+                    },
+                },
+            ]
+        )
+        self.mark_secondary(
+            [
+                "lock_child_lock",
+                "number_calibration_offset",
+                "select_open_window_detection",
+                "select_pir_timeout",
+                "switch_time_sync",
+            ]
+        )
+
+    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_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 250
+        self.assertEqual(self.subject.current_temperature, 25.0)
+
+    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_modes(self):
+        self.assertCountEqual(
+            self.subject.preset_modes,
+            [
+                "Program",
+                "ECO",
+                "Comfort",
+                "Anti-Freeze",
+                "Sensor",
+                "Pilot Wire",
+            ],
+        )
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "auto"
+        self.assertEqual(self.subject.preset_mode, "Program")
+        self.dps[PRESET_DPS] = "eco"
+        self.assertEqual(self.subject.preset_mode, "ECO")
+        self.dps[PRESET_DPS] = "hot"
+        self.assertEqual(self.subject.preset_mode, "Comfort")
+        self.dps[PRESET_DPS] = "cold"
+        self.assertEqual(self.subject.preset_mode, "Anti-Freeze")
+        self.dps[PRESET_DPS] = "person_infrared_ray"
+        self.assertEqual(self.subject.preset_mode, "Sensor")
+        self.dps[PRESET_DPS] = "line_control"
+        self.assertEqual(self.subject.preset_mode, "Pilot Wire")
+
+    async def test_set_preset_to_auto(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "auto"},
+        ):
+            await self.subject.async_set_preset_mode("Program")
+
+    async def test_set_preset_to_eco(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "eco"},
+        ):
+            await self.subject.async_set_preset_mode("ECO")
+
+    async def test_set_preset_to_hot(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "hot"},
+        ):
+            await self.subject.async_set_preset_mode("Comfort")
+
+    async def test_set_preset_to_cold(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "cold"},
+        ):
+            await self.subject.async_set_preset_mode("Anti-Freeze")
+
+    async def test_set_preset_to_pir(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "person_infrared_ray"},
+        ):
+            await self.subject.async_set_preset_mode("Sensor")
+
+    async def test_set_preset_to_line(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "line_control"},
+        ):
+            await self.subject.async_set_preset_mode("Pilot Wire")
+
+    def test_extra_state_attributes(self):
+        self.dps[LIMIT_DPS] = "3"
+
+        self.assertEqual(
+            self.subject.extra_state_attributes,
+            {"limit_function": "3"},
+        )

+ 11 - 8
tests/mixins/switch.py

@@ -53,45 +53,48 @@ class BasicSwitchTests:
         device_class=DEVICE_CLASS_SWITCH,
         power_dps=None,
         power_scale=1,
+        testdata=(True, False),
     ):
         self.basicSwitch = subject
         self.basicSwitchDps = dps
         self.basicSwitchDevClass = device_class
         self.basicSwitchPowerDps = power_dps
         self.basicSwitchPowerScale = power_scale
+        self.basicSwitchOn = testdata[0]
+        self.basicSwitchOff = testdata[1]
 
     def test_basic_switch_is_on(self):
-        self.dps[self.basicSwitchDps] = True
+        self.dps[self.basicSwitchDps] = self.basicSwitchOn
         self.assertEqual(self.basicSwitch.is_on, True)
 
-        self.dps[self.basicSwitchDps] = False
+        self.dps[self.basicSwitchDps] = self.basicSwitchOff
         self.assertEqual(self.basicSwitch.is_on, False)
 
     async def test_basic_switch_turn_on(self):
         async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: True}
+            self.basicSwitch._device, {self.basicSwitchDps: self.basicSwitchOn}
         ):
             await self.basicSwitch.async_turn_on()
 
     async def test_basic_switch_turn_off(self):
         async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: False}
+            self.basicSwitch._device, {self.basicSwitchDps: self.basicSwitchOff}
         ):
             await self.basicSwitch.async_turn_off()
 
     async def test_basic_switch_toggle_turns_on_when_it_was_off(self):
-        self.dps[self.basicSwitchDps] = False
+        self.dps[self.basicSwitchDps] = self.basicSwitchOff
 
         async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: True}
+            self.basicSwitch._device, {self.basicSwitchDps: self.basicSwitchOn}
         ):
             await self.basicSwitch.async_toggle()
 
     async def test_basic_switch_toggle_turns_off_when_it_was_on(self):
-        self.dps[self.basicSwitchDps] = True
+        self.dps[self.basicSwitchDps] = self.basicSwitchOn
 
         async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: False}
+            self.basicSwitch._device, {self.basicSwitchDps: self.basicSwitchOff}
         ):
             await self.basicSwitch.async_toggle()