Repository: XKNX/xknx
Pull request #717 (2021-06-11 16:04:45)

add `respond_to_read` option to Switch
<!--
  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.
-->
Allows a Switch device to optionally respond to GroupValueRead requests.

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)
- [x] 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/717?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#717](https://codecov.io/gh/XKNX/xknx/pull/717?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (59a65f3) into [main](https://codecov.io/gh/XKNX/xknx/commit/ed80ebd47b1f24dc8b3268ba4c29367238d16263?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (ed80ebd) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/717/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/717?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main     #717   +/-   ##
=======================================
  Coverage   94.54%   94.55%           
=======================================
  Files         103      103           
  Lines        6565     6569    +4     
=======================================
+ Hits         6207     6211    +4     
  Misses        358      358           
```


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

marvin-w: That's a nice idea, do you have a use case for this?

LGTM
farmio: I use similar functionalities in other KNX logic servers. E.g. for enable/disable heating (like a main switch overriding automatics); disable specific light modes that would be automatically triggered; enable/disable warm water extra heating etc.
Mostly things used in multiple knx devices or are part of logic blocks that I want to control manually.

Patch:
diff --git a/changelog.md b/changelog.md
index d023e5b6..22b57395 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,7 @@
 
 ### Devices
 
+- Add `respond_to_read` option to Switch. If `True` GroupValueRead telegrams to the `group_address` are answered.
 - Add NumericValue device.
 
 ### Internals
diff --git a/docs/switch.md b/docs/switch.md
index bad66e17..925ef872 100644
--- a/docs/switch.md
+++ b/docs/switch.md
@@ -17,6 +17,7 @@ Switches are simple representations of binary actors. They mainly support switch
 - `name` is the name of the object.
 - `group_address` is the KNX group address of the switch device. Used for sending.
 - `group_address_state` is the KNX group address of the switch state. Used for updating and reading state.
+- `respond_to_read` if `True` GroupValueRead requests to the `group_address` are answered. Defaults to `False`
 - `invert` inverts the payload so state "on" is represented by 0 on bus and "off" by 1. Defaults to `False`
 - `reset_after` may be used to reset the switch to `OFF` again after given time in sec. Defaults to `None`
 - `device_updated_cb` awaitable callback for each update.
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index 2c51658e..601f83a7 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -32,6 +32,7 @@ class Switch(Device):
         name: str,
         group_address: GroupAddressesType | None = None,
         group_address_state: GroupAddressesType | None = None,
+        respond_to_read: bool = False,
         invert: bool = False,
         reset_after: float | None = None,
         device_updated_cb: DeviceCallbackType | None = None,
@@ -41,7 +42,7 @@ class Switch(Device):
 
         self.reset_after = reset_after
         self._reset_task: asyncio.Task[None] | None = None
-
+        self.respond_to_read = respond_to_read
         self.switch = RemoteValueSwitch(
             xknx,
             group_address,
@@ -92,6 +93,14 @@ class Switch(Device):
                     self._reset_state(self.reset_after)
                 )
 
+    async def process_group_read(self, telegram: "Telegram") -> None:
+        """Process incoming GroupValueResponse telegrams."""
+        if (
+            self.respond_to_read
+            and telegram.destination_address == self.switch.group_address
+        ):
+            await self.switch.respond()
+
     async def _reset_state(self, wait_seconds: float) -> None:
         await asyncio.sleep(wait_seconds)
         await self.set_off()


Test patch:
diff --git a/test/devices_tests/switch_test.py b/test/devices_tests/switch_test.py
index 7466339c..6e735cba 100644
--- a/test/devices_tests/switch_test.py
+++ b/test/devices_tests/switch_test.py
@@ -223,6 +223,71 @@ class TestSwitch:
 
         after_update_callback.assert_called_with(switch)
 
+    #
+    # TEST RESPOND
+    #
+    async def test_respond_to_read(self):
+        """Test respond_to_read function."""
+        xknx = XKNX()
+        responding = Switch(
+            xknx,
+            "TestSensor1",
+            group_address="1/1/1",
+            respond_to_read=True,
+        )
+        non_responding = Switch(
+            xknx,
+            "TestSensor2",
+            group_address="1/1/1",
+            respond_to_read=False,
+        )
+        responding_multiple = Switch(
+            xknx,
+            "TestSensor3",
+            group_address=["1/1/1", "3/3/3"],
+            group_address_state="2/2/2",
+            respond_to_read=True,
+        )
+        #  set initial payload of Switch
+        responding.switch.value = True
+        non_responding.switch.value = True
+        responding_multiple.switch.value = True
+
+        read_telegram = Telegram(
+            destination_address=GroupAddress("1/1/1"), payload=GroupValueRead()
+        )
+        # verify no response when respond is False
+        await non_responding.process(read_telegram)
+        assert xknx.telegrams.qsize() == 0
+
+        # verify response when respond is True
+        await responding.process(read_telegram)
+        assert xknx.telegrams.qsize() == 1
+        response = xknx.telegrams.get_nowait()
+        assert response == Telegram(
+            destination_address=GroupAddress("1/1/1"),
+            payload=GroupValueResponse(DPTBinary(True)),
+        )
+        # verify no response when GroupValueRead request is not for group_address
+        await responding_multiple.process(read_telegram)
+        assert xknx.telegrams.qsize() == 1
+        response = xknx.telegrams.get_nowait()
+        assert response == Telegram(
+            destination_address=GroupAddress("1/1/1"),
+            payload=GroupValueResponse(DPTBinary(True)),
+        )
+        await responding_multiple.process(
+            Telegram(
+                destination_address=GroupAddress("2/2/2"), payload=GroupValueRead()
+            )
+        )
+        await responding_multiple.process(
+            Telegram(
+                destination_address=GroupAddress("3/3/3"), payload=GroupValueRead()
+            )
+        )
+        assert xknx.telegrams.qsize() == 0
+
     #
     # TEST SET ON
     #