Repository: XKNX/xknx (python)

Issue:
AttributeError: 'Keyring' object has no attribute 'backbone'
Hi.

My ETS generates a .knxkeys file that doesn't have a "backbone" parameter:

```
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="Apartment" CreatedBy="ETS 5.7.4 (Build 1093)" Created="2022-08-09T15:53:32" Signature="yaIT..." xmlns="http://knx.org/xml/keyring/1">
  <Interface Type="Tunneling" Host="1.1.251" UserID="2" Password="HIDDEN" IndividualAddress="HIDDEN" Authentication="HIDDEN" />
  <Interface Type="Tunneling" Host="1.1.251" UserID="3" Password="HIDDEN" IndividualAddress="HIDDEN" Authentication="HIDDEN" />
  <Interface Type="Tunneling" Host="1.1.251" UserID="4" Password="HIDDEN" IndividualAddress="HIDDEN" Authentication="HIDDEN" />
  <Interface Type="Tunneling" Host="1.1.251" UserID="5" Password="HIDDEN" IndividualAddress="HIDDEN" Authentication="HIDDEN" />
  <Devices>
    <Device IndividualAddress="1.1.251" SequenceNumber="24" />
  </Devices>
</Keyring>
```

Because of that i get the following error:

```
There was an error during loading the knxkeys file.
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/secure/keyring.py", line 316, in load_key_ring
    keyring.decrypt(password)
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/secure/keyring.py", line 299, in decrypt
    if self.backbone is not None:
AttributeError: 'Keyring' object has no attribute 'backbone'
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/secure/keyring.py", line 316, in load_key_ring
    keyring.decrypt(password)
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/secure/keyring.py", line 299, in decrypt
    if self.backbone is not None:
AttributeError: 'Keyring' object has no attribute 'backbone'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/max/Projects/KNX/example_telegram_monitor.py", line 69, in <module>
    asyncio.run(main(sys.argv[1:]))
  File "/opt/homebrew/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/opt/homebrew/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
  File "/Users/max/Projects/KNX/example_telegram_monitor.py", line 65, in main
    await monitor(address_filters)
  File "/Users/max/Projects/KNX/example_telegram_monitor.py", line 47, in monitor
    await xknx.start()
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/xknx.py", line 128, in start
    await self.knxip_interface.start()
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/io/knxip_interface.py", line 69, in start
    await self._start()
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/io/knxip_interface.py", line 105, in _start
    keyring: Keyring = load_key_ring(
  File "/opt/homebrew/lib/python3.9/site-packages/xknx/secure/keyring.py", line 321, in load_key_ring
    raise InvalidSecureConfiguration() from exception
xknx.exceptions.exception.InvalidSecureConfiguration
```

I get the same error in the standalone version of XKNX and in Home Assistant when using the "TCP with IP Secure" connection mode.

My code:

```
    connection_config = ConnectionConfig(
        connection_type=ConnectionType.TUNNELING_TCP_SECURE,
        gateway_ip="192.168.11.24",
        secure_config=SecureConfig(
            user_id=3,
            knxkeys_file_path="/Users/max/KNX/Deployment/Apartment.knxkeys",
            knxkeys_password="HIDDEN",
        ),
    )

    xknx = XKNX(connection_config=connection_config, daemon_mode=True)
```

As a temporary fix i've applied the following patch:

```
--- xknx/secure/keyring.py	2022-08-09 19:49:28.000000000 +0300
+++ xknx/secure/keyring_fixed.py	2022-08-09 20:27:51.000000000 +0300
@@ -233,6 +233,7 @@
 
     def __init__(self) -> None:
         """Initialize the Keyring."""
+        self.backbone = None
         self.interfaces = []
         self.devices = []
         self.group_addresses = []
```

- [X] using xknx standalone
- [X] using Home-Assistant knx integration

**Version information:**
- Standalone xknx: 0.22.1
- xknx / Home-Assistant release with the issue: 2022.7.7

**KNX installation:**
ETS 5.7.4
KNX IP Interface: MDT SCN-IP000.03

Pull request description:
Fix: 'Keyring' object has no attribute 'backbone'
<!--
  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.
-->

Fixes #1006 

## 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)


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)
 
diff --git a/xknx/secure/keyring.py b/xknx/secure/keyring.py
index e23bc7e7..1727e5e9 100644
--- a/xknx/secure/keyring.py
+++ b/xknx/secure/keyring.py
@@ -222,7 +222,7 @@ class XMLDevice(AttributeReader):
 class Keyring(AttributeReader):
     """Class for loading and decrypting knxkeys XML files."""
 
-    backbone: XMLBackbone
+    backbone: XMLBackbone | None = None
     interfaces: list[XMLInterface]
     group_addresses: list[XMLGroupAddress]
     devices: list[XMLDevice]


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 76aac084..ee9c39d4 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -8,6 +8,6 @@ pylint==2.14.5
 pytest==7.1.2
 pytest-asyncio==0.19.0
 pytest-cov==3.0.0
-setuptools==63.4.2
+setuptools==64.0.1
 tox==3.25.1
 mypy==0.971
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)