Repository: XKNX/xknx
Pull request #789 (2021-11-01 08:56:10)

`always_callback` for NumericValue and RawValue
<!--
  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.
-->
`always_callback` for NumericValue and RawValue

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/789?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#789](https://codecov.io/gh/XKNX/xknx/pull/789?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (6c30492) into [main](https://codecov.io/gh/XKNX/xknx/commit/bde3007b4aefefbf1c3819b090e5e124c9e5f3b5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (bde3007) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@           Coverage Diff           @@
##             main     #789   +/-   ##
=======================================
  Coverage   94.78%   94.78%           
=======================================
  Files         109      109           
  Lines        6791     6793    +2     
=======================================
+ Hits         6437     6439    +2     
  Misses        354      354           
```


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


Patch:
diff --git a/changelog.md b/changelog.md
index 79af2f21..a8ad004e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,7 +2,9 @@
 
 ## Unreleased changes
 
-### Internals
+### Devices
+
+- Added `always_callback` option to NumericValue and RawValue
 
 ## 0.18.11 Task Registry 2021-10-16
 
diff --git a/docs/numeric_value.md b/docs/numeric_value.md
index 472fc41e..0c15ab7d 100644
--- a/docs/numeric_value.md
+++ b/docs/numeric_value.md
@@ -20,6 +20,7 @@ NumericValue devices send values to the KNX bus. Received values update the devi
 - `respond_to_read` if `True` GroupValueRead requests to the `group_address` are answered. Defaults to `False`
 - `sync_state` defines if and how often the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
 - `value_type` controls how the value should be encoded / decoded. The attribut may have may have parseable value types representing numeric values.
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
 - `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
diff --git a/docs/raw_value.md b/docs/raw_value.md
index a0854dd9..95491904 100644
--- a/docs/raw_value.md
+++ b/docs/raw_value.md
@@ -20,6 +20,7 @@ RawValue devices send uint values to the KNX bus. Received values update the dev
 - `group_address_state` is the KNX group address of the raw value device.
 - `respond_to_read` if `True` GroupValueRead requests to the `group_address` are answered. Defaults to `False`
 - `sync_state` defines if and how often the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
 - `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
diff --git a/docs/sensor.md b/docs/sensor.md
index 93788d8c..e97c46d2 100644
--- a/docs/sensor.md
+++ b/docs/sensor.md
@@ -13,13 +13,13 @@ Sensors are monitoring temperature, air humidity, pressure etc. from KNX bus.
 
 ## [](#header-2)Interface
 
-* `xknx` is the XKNX object.
-* `name` is the name of the object.
-* `group_address_state` is the KNX group address of the sensor device.
-* `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
-* `always_callback` defines if a callback/update should always be triggered no matter if the previous and the new state are identical.
-* `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
-* `device_updated_cb` awaitable callback for each update.
+- `xknx` is the XKNX object.
+- `name` is the name of the object.
+- `group_address_state` is the KNX group address of the sensor device.
+- `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
+- `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
+- `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
 
diff --git a/xknx/devices/numeric_value.py b/xknx/devices/numeric_value.py
index c4d9d41d..2d5fbb6d 100644
--- a/xknx/devices/numeric_value.py
+++ b/xknx/devices/numeric_value.py
@@ -32,10 +32,12 @@ class NumericValue(Device):
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
         value_type: int | str | None = None,
+        always_callback: bool = False,
         device_updated_cb: DeviceCallbackType | None = None,
     ):
         """Initialize Sensor class."""
         super().__init__(xknx, name, device_updated_cb)
+        self.always_callback = always_callback
         self.respond_to_read = respond_to_read
         self.sensor_value = RemoteValueNumeric(
             xknx,
@@ -58,7 +60,7 @@ class NumericValue(Device):
 
     async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
-        await self.sensor_value.process(telegram)
+        await self.sensor_value.process(telegram, always_callback=self.always_callback)
 
     async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""
diff --git a/xknx/devices/raw_value.py b/xknx/devices/raw_value.py
index 4825139e..bfd66013 100644
--- a/xknx/devices/raw_value.py
+++ b/xknx/devices/raw_value.py
@@ -32,10 +32,12 @@ class RawValue(Device):
         group_address_state: GroupAddressesType | None = None,
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
+        always_callback: bool = False,
         device_updated_cb: DeviceCallbackType | None = None,
     ):
         """Initialize Sensor class."""
         super().__init__(xknx, name, device_updated_cb)
+        self.always_callback = always_callback
         self.respond_to_read = respond_to_read
         self.remote_value = RemoteValueRaw(
             xknx,
@@ -58,7 +60,7 @@ class RawValue(Device):
 
     async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
-        await self.remote_value.process(telegram)
+        await self.remote_value.process(telegram, always_callback=self.always_callback)
 
     async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 986738c7..e32168eb 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -8,6 +8,6 @@ pylint==2.11.1
 pytest==6.2.5
 pytest-asyncio==0.16.0
 pytest-cov==3.0.0
-setuptools==58.3.0
+setuptools==58.4.0
 tox==3.24.4
 mypy==0.910
diff --git a/test/devices_tests/numeric_value_test.py b/test/devices_tests/numeric_value_test.py
index 8fb45f64..c635bece 100644
--- a/test/devices_tests/numeric_value_test.py
+++ b/test/devices_tests/numeric_value_test.py
@@ -256,6 +256,36 @@ class TestNumericValue:
         await sensor.process(telegram)
         after_update_callback.assert_called_with(sensor)
         assert sensor.last_telegram == telegram
+        # consecutive telegrams with same payload shall only trigger one callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_not_called()
+
+    async def test_process_callback_always(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = NumericValue(
+            xknx,
+            "TestSensor",
+            group_address="1/2/3",
+            value_type="temperature",
+            always_callback=True,
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # every telegram shall trigger callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
 
     async def test_process_callback_set(self):
         """Test setting value. Test if callback is called."""
diff --git a/test/devices_tests/raw_value_test.py b/test/devices_tests/raw_value_test.py
index 469fdd59..c7b42ef2 100644
--- a/test/devices_tests/raw_value_test.py
+++ b/test/devices_tests/raw_value_test.py
@@ -1,4 +1,6 @@
 """Unit test for RawValue objects."""
+from unittest.mock import AsyncMock
+
 import pytest
 from xknx import XKNX
 from xknx.devices import RawValue
@@ -137,6 +139,61 @@ class TestRawValue:
         )
         assert xknx.telegrams.qsize() == 0
 
+    #
+    # TEST PROCESS CALLBACK
+    #
+
+    async def test_process_callback(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = RawValue(
+            xknx,
+            "TestSensor",
+            2,
+            group_address="1/2/3",
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # consecutive telegrams with same payload shall only trigger one callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_not_called()
+
+    async def test_process_callback_always(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = RawValue(
+            xknx,
+            "TestSensor",
+            2,
+            group_address="1/2/3",
+            always_callback=True,
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # every telegram shall trigger callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+
     #
     # TEST SET
     #