Repository: XKNX/xknx
Pull request #718 (2021-06-16 20:09:58)

Climate: is_active property
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
add Climate.is_active property to signal active / idle state. This can be used in HAs hvac_action property.

Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [x] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/718?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#718](https://codecov.io/gh/XKNX/xknx/pull/718?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (8c50c53) into [main](https://codecov.io/gh/XKNX/xknx/commit/ebb1045fcd09bcc8da5963c3263055d7e1912bb5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (ebb1045) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/718/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/718?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main     #718   +/-   ##
=======================================
  Coverage   94.63%   94.63%           
=======================================
  Files         104      104           
  Lines        6612     6623   +11     
=======================================
+ Hits         6257     6268   +11     
  Misses        355      355           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/718?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/devices/climate.py](https://codecov.io/gh/XKNX/xknx/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NsaW1hdGUucHk=) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/changelog.md b/changelog.md
index 07c8771d..5ce89715 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased changes
+
+### Devices
+
+- Climate: add `group_address_active_state`, `group_address_command_value_state` and a `is_active` property.
+
 ## 0.18.6 NumericValue 2021-06-11
 
 ### Devices
diff --git a/docs/climate.md b/docs/climate.md
index 0084631f..84856490 100644
--- a/docs/climate.md
+++ b/docs/climate.md
@@ -27,6 +27,8 @@ Climate are representations of KNX HVAC/Climate controls.
 - `group_address_on_off` KNX address for turning climate device on or off. *DPT 1*
 - `group_address_on_off_state` KNX address for reading the on/off state. *DPT 1*
 - `on_off_invert` Invert on/off. Default: `False`
+- `group_address_active_state` KNX address for reading if the climate device is currently active. *DPT 1*
+- `group_address_command_value_state` KNX address for reading the current command value / valve position in %. *DPT 5.001*
 - `max_temp` Maximum value for target temperature.
 - `min_temp` Minimum value for target temperature.
 - `mode` ClimateMode instance for this climate device
@@ -112,4 +114,4 @@ climate_target_temp = Climate(
     group_address_target_temperature='2/2/3',
     group_address_target_temperature_state='2/2/4'
 )
-```
\ No newline at end of file
+```
diff --git a/xknx/devices/climate.py b/xknx/devices/climate.py
index d5a2ff95..a14674dd 100644
--- a/xknx/devices/climate.py
+++ b/xknx/devices/climate.py
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Iterator
 from xknx.remote_value import (
     GroupAddressesType,
     RemoteValue,
+    RemoteValueScaling,
     RemoteValueSetpointShift,
     RemoteValueSwitch,
     RemoteValueTemp,
@@ -54,6 +55,8 @@ class Climate(Device):
         group_address_on_off: GroupAddressesType | None = None,
         group_address_on_off_state: GroupAddressesType | None = None,
         on_off_invert: bool = False,
+        group_address_active_state: GroupAddressesType | None = None,
+        group_address_command_value_state: GroupAddressesType | None = None,
         min_temp: float | None = None,
         max_temp: float | None = None,
         mode: ClimateMode | None = None,
@@ -109,6 +112,22 @@ class Climate(Device):
             invert=on_off_invert,
         )
 
+        self.active = RemoteValueSwitch(
+            xknx,
+            group_address_state=group_address_active_state,
+            device_name=self.name,
+            feature_name="Active",
+            after_update_cb=self.after_update,
+        )
+
+        self.command_value = RemoteValueScaling(
+            xknx,
+            group_address_state=group_address_command_value_state,
+            device_name=self.name,
+            feature_name="Command value",
+            after_update_cb=self.after_update,
+        )
+
         self.mode = mode
 
         if create_temperature_sensors:
@@ -120,6 +139,8 @@ class Climate(Device):
         yield self.target_temperature
         yield self._setpoint_shift
         yield self.on
+        yield self.active
+        yield self.command_value
 
     def create_temperature_sensors(self) -> None:
         """Create temperature sensors."""
@@ -160,6 +181,15 @@ class Climate(Device):
         # None will return False
         return bool(self.on.value)
 
+    @property
+    def is_active(self) -> bool | None:
+        """Return if currently active. None if unknown."""
+        if self.active.value is not None:
+            return self.active.value
+        if self.command_value.value is not None:
+            return bool(self.command_value.value)
+        return None
+
     async def turn_on(self) -> None:
         """Set power status to on."""
         await self.on.on()


Test patch:
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 9acb6be0..21678ca3 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -1380,6 +1380,68 @@ class TestClimate:
             payload=GroupValueWrite(DPTBinary(True)),
         )
 
+    async def test_is_active(self):
+        """Test is_active property."""
+        xknx = XKNX()
+        climate_active = Climate(
+            xknx, "TestClimate1", group_address_active_state="1/1/1"
+        )
+        climate_command = Climate(
+            xknx, "TestClimate2", group_address_command_value_state="2/2/2"
+        )
+        climate_active_command = Climate(
+            xknx,
+            "TestClimate3",
+            group_address_active_state="1/1/1",
+            group_address_command_value_state="2/2/2",
+        )
+        assert climate_active.is_active is None
+        assert climate_command.is_active is None
+        assert climate_active_command.is_active is None
+        # set active to False
+        await xknx.devices.process(
+            Telegram(
+                destination_address=GroupAddress("1/1/1"),
+                payload=GroupValueWrite(DPTBinary(False)),
+            )
+        )
+        assert climate_active.is_active is False
+        assert climate_command.is_active is None
+        assert climate_active_command.is_active is False
+        # set command to 50%
+        await xknx.devices.process(
+            Telegram(
+                destination_address=GroupAddress("2/2/2"),
+                payload=GroupValueWrite(DPTArray((128,))),
+            )
+        )
+        assert climate_active.is_active is False
+        assert climate_command.is_active is True
+        assert climate_active_command.is_active is False
+        # set active to True
+        await xknx.devices.process(
+            Telegram(
+                destination_address=GroupAddress("1/1/1"),
+                payload=GroupValueWrite(DPTBinary(True)),
+            )
+        )
+        assert climate_active.is_active is True
+        assert climate_command.is_active is True
+        assert climate_active_command.is_active is True
+        # set command to 0%
+        await xknx.devices.process(
+            Telegram(
+                destination_address=GroupAddress("2/2/2"),
+                payload=GroupValueWrite(DPTArray((0,))),
+            )
+        )
+        assert climate_active.is_active is True
+        assert climate_command.is_active is False
+        assert climate_active_command.is_active is True
+        # only command initialized
+        climate_active_command.active.value = None
+        assert climate_active_command.is_active is False
+
     #
     # Create temperature sensor tests
     #