Repository: XKNX/xknx
Pull request #1011 (2022-08-13 10:20:40)

Fix DPT 2 Byte numeric limits and refactor logic
<!--
  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.
-->
Fix numeric limits for DPT 2 Byte Float and refactor the logic to be better readable according to the spec.

Fixes https://github.com/home-assistant/core/issues/76686

## 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.
-->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] 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] The documentation has been adjusted accordingly
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog (docs/changelog.md)

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

> :exclamation: Current head 65801d3 differs from pull request most recent head b91b3dc. Consider uploading reports for the commit b91b3dc to get more accurate results

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

```diff
@@            Coverage Diff             @@
##             main    #1011      +/-   ##
==========================================
- Coverage   96.09%   96.09%   -0.01%     
==========================================
  Files         133      133              
  Lines        8143     8140       -3     
==========================================
- Hits         7825     7822       -3     
  Misses        318      318              
```


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


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 98a4c42b..9a9e0700 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,12 @@ nav_order: 2
 
 # Changelog
 
+## Unreleased changes
+
+### Internal
+
+- Fix DPT2ByteFloat numeric range issues
+
 ## 0.22.1 Wrong delivery 2022-07-29
 
 ### Management
diff --git a/xknx/dpt/dpt_2byte_float.py b/xknx/dpt/dpt_2byte_float.py
index ead6abdc..c5858011 100644
--- a/xknx/dpt/dpt_2byte_float.py
+++ b/xknx/dpt/dpt_2byte_float.py
@@ -49,33 +49,23 @@ class DPT2ByteFloat(DPTNumeric):
     @classmethod
     def to_knx(cls, value: float) -> tuple[int, int]:
         """Serialize to KNX/IP raw data."""
-
-        def calc_exponent(float_value: float, sign: bool) -> tuple[int, int]:
-            """Return float exponent."""
-            exponent = 0
-            significand = abs(int(float_value * 100))
-
-            while significand < -2048 or significand > 2048:
-                exponent += 1
-                significand >>= 1
-
-            if sign:
-                significand ^= 0x7FF  # invert
-                significand += 1  # and add 1
-
-            return exponent, significand
-
         try:
             knx_value = float(value)
             if not cls._test_boundaries(knx_value):
                 raise ValueError
 
-            sign = knx_value < 0
-            exponent, significand = calc_exponent(knx_value, sign)
+            value = knx_value * 100
+            exponent = 0
+            while value < -2048 or value > 2047:
+                exponent += 1
+                value /= 2
+
+            mantisse = int(round(value)) & 0x7FF
+            msb = exponent << 3 | mantisse >> 8
+            if value < 0:
+                msb |= 0x80
 
-            return (sign << 7) | (exponent << 3) | (
-                significand >> 8
-            ), significand & 0xFF
+            return msb, mantisse & 0xFF
         except ValueError:
             raise ConversionError(f"Could not serialize {cls.__name__}", value=value)
 


Test patch:
diff --git a/test/dpt_tests/dpt_float_test.py b/test/dpt_tests/dpt_float_test.py
index 0a9dc336..083fa7c1 100644
--- a/test/dpt_tests/dpt_float_test.py
+++ b/test/dpt_tests/dpt_float_test.py
@@ -79,6 +79,13 @@ class TestDPTFloat:
         assert DPT2ByteFloat.to_knx(DPT2ByteFloat.value_max) == (0x7F, 0xFF)
         assert DPT2ByteFloat.from_knx((0x7F, 0xFF)) == DPT2ByteFloat.value_max
 
+    def test_close_to_limit(self):
+        """Test parsing and streaming of DPT2ByteFloat with numeric limit."""
+        assert DPT2ByteFloat.to_knx(20.48) == (0x0C, 0x00)
+        assert DPT2ByteFloat.from_knx((0x0C, 0x00)) == 20.48
+        assert DPT2ByteFloat.to_knx(-20.48) == (0x80, 0x00)
+        assert DPT2ByteFloat.from_knx((0x80, 0x00)) == -20.48
+
     def test_min(self):
         """Test parsing and streaming of DPT2ByteFloat with minimum value."""
         assert DPT2ByteFloat.to_knx(DPT2ByteFloat.value_min) == (0xF8, 0x00)