Ver código fonte

Add support for Fairland InverterPlus pool heatpump.

Issue #222

This is almost identical to the GardenPAC pool heatpump, but with the
following differences:
- modes are labelled Silent and Boost rather than Silent and Smart
  (control is a boolean, so this is just a UI difference).
- the hvac mode is controllable between smart, heat and cool.
  GardenPac appears to use the same DP as a read-only indicator of hvac
  action, and leaves the mode in smart always.
Jason Rumney 3 anos atrás
pai
commit
b904674c93

+ 1 - 0
ACKNOWLEDGEMENTS.md

@@ -112,3 +112,4 @@ Further device support has been made with the assistance of users.  Please consi
 - [myhomeiot](https://github.com/myhomeiot) for contributing support for Bresser 7-in-1 Weather Station.
 - [Condorello](https://github.com/Condorello) for assistance supporting Inkbird sous vide cookers.
 - [sebastiangebosz](https://github.com/sebastiangebosz) for contributing improvements to Eberg Qubo heatpumps.
+- [VoyteckPL](https://github.com/VoyteckPL) for contributing support for Fairland IPHCR15 pool heatpumps.

+ 1 - 0
README.md

@@ -94,6 +94,7 @@ If you submit a pull request, please understand that the config file naming and
 - Poolex Silverline, Q-line and Vertigo heatpumps
 - IPS Pro Pool-Systems Heatpump (seems to match Fairland Inver-X as well)
 - W'eau Pool Heatpump
+- Fairland IPHCR15 pool heatpump (matches others above, but allows control of heat/cool modes while others seem to be fixed to auto only)
 
 - these seem to use a small number of common controllers with minor variations, and many other Pool heatpumps will work using the above configurations.
   Report issues if there are any differences in presets or other features,

+ 110 - 0
custom_components/tuya_local/devices/fairland_iphcr15_heatpump.yaml

@@ -0,0 +1,110 @@
+name: Fairland InverterPlus Pool Heatpump
+primary_entity:
+  entity: climate
+  dps:
+    - id: 1
+      name: hvac_mode
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: "off"
+          icon: "mdi:hvac-off"
+          icon_priority: 1
+        - dps_val: true
+          constraint: mode
+          conditions:
+            - dps_val: warm
+              value: heat
+            - dps_val: cool
+              value: cool
+            - dps_val: smart
+              value: heat_cool
+    - id: 102
+      name: current_temperature
+      type: integer
+      readonly: true
+    - id: 103
+      name: temperature_unit
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: F
+        - dps_val: true
+          value: C
+    - id: 105
+      name: mode
+      type: string
+      readonly: true
+      icon_priority: 4
+      mapping:
+        - dps_val: warm
+          icon: "mdi:hot-tub"
+        - dps_val: cool
+          icon: "mdi:snowflake"
+        - dps_val: smart
+          icon: "mdi:refresh-auto"
+    - id: 106
+      name: temperature
+      type: integer
+      mapping:
+        - constraint: temperature_unit
+          conditions:
+            - dps_val: false
+              range:
+                min: 60
+                max: 115
+      range:
+        min: 18
+        max: 45
+    - id: 107
+      type: integer
+      name: min_temperature
+    - id: 108
+      type: integer
+      name: max_temperature
+    - id: 115
+      type: bitfield
+      name: error
+      mapping:
+        - dps_val: 0
+          value: OK
+        - dps_val: 4
+          value: Water Flow Protection
+          icon: "mdi:water-pump-off"
+          icon_priority: 2
+    - id: 116
+      type: bitfield
+      name: error_2
+    - id: 117
+      name: preset_mode
+      type: boolean
+      mapping:
+        - dps_val: false
+          value: Silent
+        - dps_val: true
+          value: Boost
+secondary_entities:
+  - entity: sensor
+    category: diagnostic
+    name: Power Level
+    icon: "mdi:signal"
+    class: power_factor
+    dps:
+      - id: 104
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
+        readonly: true
+  - entity: binary_sensor
+    name: Water Flow
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 115
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 4
+            value: true
+          - value: false

+ 193 - 0
tests/devices/test_fairland_iphcr15_heatpump.py

@@ -0,0 +1,193 @@
+from homeassistant.components.binary_sensor import BinarySensorDeviceClass
+from homeassistant.components.climate.const import (
+    ClimateEntityFeature,
+    HVACAction,
+    HVACMode,
+)
+from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.const import (
+    PERCENTAGE,
+    TEMP_CELSIUS,
+    TEMP_FAHRENHEIT,
+)
+
+from ..const import GARDENPAC_HEATPUMP_PAYLOAD
+from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.climate import TargetTemperatureTests
+from ..mixins.sensor import BasicSensorTests
+from .base_device_tests import TuyaDeviceTestCase
+
+HVACMODE_DPS = "1"
+CURRENTTEMP_DPS = "102"
+UNITS_DPS = "103"
+POWERLEVEL_DPS = "104"
+OPMODE_DPS = "105"
+TEMPERATURE_DPS = "106"
+MINTEMP_DPS = "107"
+MAXTEMP_DPS = "108"
+ERROR_DPS = "115"
+ERROR2_DPS = "116"
+PRESET_DPS = "117"
+
+
+class TestFairlandIPHCR15PoolHeatpump(
+    BasicBinarySensorTests,
+    BasicSensorTests,
+    TargetTemperatureTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig(
+            "fairland_iphcr15_heatpump.yaml", GARDENPAC_HEATPUMP_PAYLOAD
+        )
+        self.subject = self.entities.get("climate")
+        self.setUpTargetTemperature(
+            TEMPERATURE_DPS,
+            self.subject,
+            min=18,
+            max=45,
+        )
+        self.setUpBasicSensor(
+            POWERLEVEL_DPS,
+            self.entities.get("sensor_power_level"),
+            unit=PERCENTAGE,
+            device_class=SensorDeviceClass.POWER_FACTOR,
+            state_class="measurement",
+        )
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_water_flow"),
+            device_class=BinarySensorDeviceClass.PROBLEM,
+            testdata=(4, 0),
+        )
+        self.mark_secondary(["sensor_power_level", "binary_sensor_water_flow"])
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            (
+                ClimateEntityFeature.TARGET_TEMPERATURE
+                | ClimateEntityFeature.PRESET_MODE
+            ),
+        )
+
+    def test_icon(self):
+        self.dps[ERROR_DPS] = 0
+        self.dps[HVACMODE_DPS] = True
+        self.dps[OPMODE_DPS] = "warm"
+        self.assertEqual(self.subject.icon, "mdi:hot-tub")
+        self.dps[OPMODE_DPS] = "cool"
+        self.assertEqual(self.subject.icon, "mdi:snowflake")
+        self.dps[OPMODE_DPS] = "smart"
+        self.assertEqual(self.subject.icon, "mdi:refresh-auto")
+
+        self.dps[ERROR_DPS] = 4
+        self.assertEqual(self.subject.icon, "mdi:water-pump-off")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:hvac-off")
+
+    def test_temperature_unit(self):
+        self.dps[UNITS_DPS] = False
+        self.assertEqual(self.subject.temperature_unit, TEMP_FAHRENHEIT)
+        self.dps[UNITS_DPS] = True
+        self.assertEqual(self.subject.temperature_unit, TEMP_CELSIUS)
+
+    def test_minimum_fahrenheit_temperature(self):
+        self.dps[UNITS_DPS] = False
+        self.dps[MINTEMP_DPS] = 60
+        self.assertEqual(self.subject.min_temp, 60)
+
+    def test_maximum_fahrenheit_temperature(self):
+        self.dps[UNITS_DPS] = False
+        self.dps[MAXTEMP_DPS] = 115
+        self.assertEqual(self.subject.max_temp, 115)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.dps[OPMODE_DPS] = "warm"
+        self.assertEqual(self.subject.hvac_mode, HVACMode.HEAT)
+        self.dps[OPMODE_DPS] = "cool"
+        self.assertEqual(self.subject.hvac_mode, HVACMode.COOL)
+        self.dps[OPMODE_DPS] = "smart"
+        self.assertEqual(self.subject.hvac_mode, HVACMode.HEAT_COOL)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVACMode.OFF)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(
+            self.subject.hvac_modes,
+            [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL],
+        )
+
+    async def test_turn_on_heat(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True, OPMODE_DPS: "warm"}
+        ):
+            await self.subject.async_set_hvac_mode(HVACMode.HEAT)
+
+    async def test_turn_on_cool(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True, OPMODE_DPS: "cool"}
+        ):
+            await self.subject.async_set_hvac_mode(HVACMode.COOL)
+
+    async def test_turn_on_smart(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True, OPMODE_DPS: "smart"}
+        ):
+            await self.subject.async_set_hvac_mode(HVACMode.HEAT_COOL)
+
+    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(HVACMode.OFF)
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = False
+        self.assertEqual(self.subject.preset_mode, "Silent")
+
+        self.dps[PRESET_DPS] = True
+        self.assertEqual(self.subject.preset_mode, "Boost")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Silent", "Boost"])
+
+    async def test_set_preset_mode_to_silent(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: False},
+        ):
+            await self.subject.async_set_preset_mode("Silent")
+
+    async def test_set_preset_mode_to_boost(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: True},
+        ):
+            await self.subject.async_set_preset_mode("Boost")
+
+    def test_extra_state_attributes(self):
+        self.dps[OPMODE_DPS] = "smart"
+        self.dps[ERROR_DPS] = 3
+        self.dps[ERROR2_DPS] = 4
+        self.assertDictEqual(
+            self.subject.extra_state_attributes,
+            {
+                "mode": "smart",
+                "error": 3,
+                "error_2": 4,
+            },
+        )