Repository: XKNX/xknx
Pull request #909 (2022-04-05 10:12:16)

Bump setuptools from 61.3.1 to 62.0.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 61.3.1 to 62.0.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/pypa/setuptools/blob/main/CHANGES.rst">setuptools's changelog</a>.</em></p>
<blockquote>
<h2>v62.0.0</h2>
<p>Breaking Changes
^^^^^^^^^^^^^^^^</p>
<ul>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3151">#3151</a>: Made <code>setup.py develop --user</code> install to the user site packages directory even if it is disabled in the current interpreter.</li>
</ul>
<p>Changes
^^^^^^^</p>
<ul>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3153">#3153</a>: When resolving requirements use both canonical and normalized names -- by :user:<code>ldaniluk</code></li>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3167">#3167</a>: Honor unix file mode in ZipFile when installing wheel via <code>install_as_egg</code> -- by :user:<code>delijati</code></li>
</ul>
<p>Misc
^^^^</p>
<ul>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3088">#3088</a>: Fixed duplicated tag with the <code>dist-info</code> command.</li>
<li><a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3247">#3247</a>: Fixed problem preventing <code>readme</code> specified as dynamic in <code>pyproject.toml</code>
from being dynamically specified in <code>setup.py</code>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pypa/setuptools/commit/b686a319a2938019039c73aecba714970f9d6f74"><code>b686a31</code></a> Bump version: 61.3.1 → 62.0.0</li>
<li><a href="https://github.com/pypa/setuptools/commit/58f0fb2165ff0ed93b7ca6a4666fe7acb9586a00"><code>58f0fb2</code></a> Fix problem with dynamic readme (<a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3247">#3247</a>)</li>
<li><a href="https://github.com/pypa/setuptools/commit/3465caca4fb3b9fe2c41c378cb693655a87c1f33"><code>3465cac</code></a> Fix version produced by dist_info (<a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3230">#3230</a>)</li>
<li><a href="https://github.com/pypa/setuptools/commit/cde42d6303f170eafd4e925665701e88a4d65e0d"><code>cde42d6</code></a> Honor file mode for wheel install_as_egg (<a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3167">#3167</a>)</li>
<li><a href="https://github.com/pypa/setuptools/commit/1b4d0c2ee5e7690fc212764d502c2aed07480406"><code>1b4d0c2</code></a> Match normalized requirement names (<a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3153">#3153</a>)</li>
<li><a href="https://github.com/pypa/setuptools/commit/760255d17c68937c6ff7e98169d9dc61f29cadf9"><code>760255d</code></a> Rename news fragment file</li>
<li><a href="https://github.com/pypa/setuptools/commit/5c33096f68828bc2afe8490174fa6ccf8a70931d"><code>5c33096</code></a> Add news fragment</li>
<li><a href="https://github.com/pypa/setuptools/commit/0a836a3c2c8cb2f3a7b418f2c476b4b499cabdd1"><code>0a836a3</code></a> Fix dynamic readme</li>
<li><a href="https://github.com/pypa/setuptools/commit/ba7dc9eaa88131e7eab502a43c8e552de18b5319"><code>ba7dc9e</code></a> Add test for dynamic readme from setup.py args</li>
<li><a href="https://github.com/pypa/setuptools/commit/dad91613f06d8c916b8bf30cce596ac2fe00fe4e"><code>dad9161</code></a> Fix error with <code>test_easy_install</code> (<a href="https://github-redirect.dependabot.com/pypa/setuptools/issues/3245">#3245</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/pypa/setuptools/compare/v61.3.1...v62.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=pip&previous-version=61.3.1&new-version=62.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/909?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#909](https://codecov.io/gh/XKNX/xknx/pull/909?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (e81c0b9) into [main](https://codecov.io/gh/XKNX/xknx/commit/cf242d29979d21f5433b3e9e36b35ea8b1d14356?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (cf242d2) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##             main     #909   +/-   ##
=======================================
  Coverage   95.61%   95.61%           
=======================================
  Files         131      131           
  Lines        7802     7802           
=======================================
  Hits         7460     7460           
  Misses        342      342           
```




Patch:
diff --git a/changelog.md b/changelog.md
index aef5dcc2..10751dbb 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,15 +1,17 @@
 # Changelog
 
-## Unreleased changes
+## 0.20.1 Add support for DPT 16.001 and SearchRequestExtended
 
 ### Features
 
 - Add support for SearchRequestExtended to find interfaces that allow IP Secure
 - Use XKNX `state_updater` argument to set default method for StateUpdater. StateUpdater is always started - Device / RemoteValue can always opt in to use it, even if default is `False`.
+- Add support for DPT 16.001 (DPT_String_8859_1) as `DPTLatin1` with value_type "latin_1".
 
 ### Bug fixes
 
 - Stop SecureSession keepalive_task when session is stopped (and don't restart it from sending STATUS_CLOSE)
+- Fix encoding invalid characters for DPTString (value_type "string")
 
 ## 0.20.0 IP Secure 2022-03-29
 
diff --git a/xknx/__version__.py b/xknx/__version__.py
index 2a151c0b..62b86236 100644
--- a/xknx/__version__.py
+++ b/xknx/__version__.py
@@ -1,3 +1,3 @@
 """XKNX version."""
 
-__version__ = "0.20.0"
+__version__ = "0.20.1"
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index 669c6993..cea33e90 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -172,7 +172,7 @@ from .dpt_date import DPTDate
 from .dpt_datetime import DPTDateTime
 from .dpt_hvac_mode import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
 from .dpt_scaling import DPTAngle, DPTScaling
-from .dpt_string import DPTString
+from .dpt_string import DPTLatin1, DPTString
 from .dpt_time import DPTTime
 
 __all__ = [
@@ -249,6 +249,7 @@ __all__ = [
     "DPTHVACMode",
     "DPTImpedance",
     "DPTKelvinPerPercent",
+    "DPTLatin1",
     "DPTLength",
     "DPTLengthMm",
     "DPTLightQuantity",
diff --git a/xknx/dpt/dpt_string.py b/xknx/dpt/dpt_string.py
index 0788bc33..faf33207 100644
--- a/xknx/dpt/dpt_string.py
+++ b/xknx/dpt/dpt_string.py
@@ -8,7 +8,7 @@ from .dpt import DPTBase
 
 class DPTString(DPTBase):
     """
-    Abstraction for KNX 14 Octet ASCII String.
+    Abstraction for KNX 14 Octet ASCII string.
 
     DPT 16.000
     """
@@ -19,15 +19,15 @@ class DPTString(DPTBase):
     value_type = "string"
     unit = ""
 
+    _encoding = "ascii"
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> str:
         """Parse/deserialize from KNX/IP raw data."""
         cls.test_bytesarray(raw)
-        value = ""
-        for byte in raw:
-            if byte != 0x00:
-                value += chr(byte)
-        return value
+        return bytes(byte for byte in raw if byte != 0x00).decode(
+            cls._encoding, errors="replace"
+        )
 
     @classmethod
     def to_knx(cls, value: str) -> tuple[int, ...]:
@@ -36,15 +36,26 @@ class DPTString(DPTBase):
             knx_value = str(value)
             if not cls._test_boundaries(knx_value):
                 raise ValueError
-            raw = [ord(character) for character in knx_value]
-            raw.extend([0] * (cls.payload_length - len(raw)))
-            # replace invalid characters with question marks
-            # bytes(knx_value, 'ascii') would raise UnicodeEncodeError
-            return tuple(map(lambda char: char if char <= 0xFF else ord("?"), raw))
         except ValueError:
             raise ConversionError(f"Could not serialize {cls.__name__}", value=value)
+        # replace invalid characters with question marks
+        raw_bytes = knx_value.encode(cls._encoding, errors="replace")
+        padding = bytes(cls.payload_length - len(raw_bytes))
+        return tuple(raw_bytes + padding)
 
     @classmethod
     def _test_boundaries(cls, value: str) -> bool:
         """Test if value is within defined range for this object."""
         return len(value) <= cls.payload_length
+
+
+class DPTLatin1(DPTString):
+    """
+    Abstraction for KNX 14 Octet Latin-1 (ISO 8859-1) string.
+
+    DPT 16.001
+    """
+
+    dpt_sub_number = 1
+    value_type = "latin_1"
+    _encoding = "latin_1"


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index a76b8107..431445a3 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -8,6 +8,6 @@ pylint==2.13.4
 pytest==7.1.1
 pytest-asyncio==0.18.3
 pytest-cov==3.0.0
-setuptools==61.3.1
+setuptools==62.0.0
 tox==3.24.5
 mypy==0.942
diff --git a/test/dpt_tests/dpt_string_test.py b/test/dpt_tests/dpt_string_test.py
index b73065ae..0cf820e5 100644
--- a/test/dpt_tests/dpt_string_test.py
+++ b/test/dpt_tests/dpt_string_test.py
@@ -1,167 +1,91 @@
 """Unit test for KNX string object."""
 import pytest
 
-from xknx.dpt import DPTString
+from xknx.dpt import DPTLatin1, DPTString
 from xknx.exceptions import ConversionError
 
 
 class TestDPTString:
-    """Test class for KNX float object."""
+    """Test class for KNX ASCII string object."""
 
-    def test_value_from_documentation(self):
-        """Test parsing and streaming Example from documentation."""
-        raw = (
-            0x4B,
-            0x4E,
-            0x58,
-            0x20,
-            0x69,
-            0x73,
-            0x20,
-            0x4F,
-            0x4B,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-        )
-        string = "KNX is OK"
-        assert DPTString.to_knx(string) == raw
-        assert DPTString.from_knx(raw) == string
-
-    def test_value_empty_string(self):
-        """Test parsing and streaming empty string."""
-        raw = (
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-        )
-        string = ""
-        assert DPTString.to_knx(string) == raw
-        assert DPTString.from_knx(raw) == string
+    @pytest.mark.parametrize(
+        "string,raw",
+        [
+            (
+                "KNX is OK",
+                (75, 78, 88, 32, 105, 115, 32, 79, 75, 0, 0, 0, 0, 0),
+            ),
+            (
+                "",
+                (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+            ),
+            (
+                "AbCdEfGhIjKlMn",
+                (65, 98, 67, 100, 69, 102, 71, 104, 73, 106, 75, 108, 77, 110),
+            ),
+            (
+                ".,:;-_!?$@&#%/",
+                (46, 44, 58, 59, 45, 95, 33, 63, 36, 64, 38, 35, 37, 47),
+            ),
+        ],
+    )
+    @pytest.mark.parametrize("test_dpt", [DPTString, DPTLatin1])
+    def test_values(self, string, raw, test_dpt):
+        """Test parsing and streaming strings."""
+        assert test_dpt.to_knx(string) == raw
+        assert test_dpt.from_knx(raw) == string
 
-    def test_value_max_string(self):
-        """Test parsing and streaming large string."""
-        raw = (
-            0x41,
-            0x41,
-            0x41,
-            0x41,
-            0x41,
-            0x42,
-            0x42,
-            0x42,
-            0x42,
-            0x42,
-            0x43,
-            0x43,
-            0x43,
-            0x43,
-        )
-        string = "AAAAABBBBBCCCC"
-        assert DPTString.to_knx(string) == raw
-        assert DPTString.from_knx(raw) == string
-
-    def test_value_special_chars(self):
-        """Test parsing and streaming string with special chars."""
-        raw = (
-            0x48,
-            0x65,
-            0x79,
-            0x21,
-            0x3F,
-            0x24,
-            0x20,
-            0xC4,
-            0xD6,
-            0xDC,
-            0xE4,
-            0xF6,
-            0xFC,
-            0xDF,
-        )
-        string = "Hey!?$ ÄÖÜäöüß"
-        assert DPTString.to_knx(string) == raw
-        assert DPTString.from_knx(raw) == string
-
-    def test_to_knx_invalid_chars(self):
-        """Test streaming string with invalid chars."""
-        raw = (
-            0x4D,
-            0x61,
-            0x74,
-            0x6F,
-            0x75,
-            0x3F,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-        )
-        string = "Matouš"
-        knx_string = "Matou?"
+    @pytest.mark.parametrize(
+        "string,knx_string,raw",
+        [
+            (
+                "Matouš",
+                "Matou?",
+                (77, 97, 116, 111, 117, 63, 0, 0, 0, 0, 0, 0, 0, 0),
+            ),
+            (
+                "Gänsefüßchen",
+                "G?nsef??chen",
+                (71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0),
+            ),
+        ],
+    )
+    def test_to_knx_ascii_invalid_chars(self, string, knx_string, raw):
+        """Test streaming ASCII string with invalid chars."""
         assert DPTString.to_knx(string) == raw
         assert DPTString.from_knx(raw) == knx_string
 
+    @pytest.mark.parametrize(
+        "string,raw",
+        [
+            (
+                "Gänsefüßchen",
+                (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0),
+            ),
+            (
+                "àáâãåæçèéêëìíî",
+                (224, 225, 226, 227, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238),
+            ),
+        ],
+    )
+    def test_to_knx_latin_1(self, string, raw):
+        """Test streaming Latin-1 strings."""
+        assert DPTLatin1.to_knx(string) == raw
+        assert DPTLatin1.from_knx(raw) == string
+
     def test_to_knx_too_long(self):
         """Test serializing DPTString to KNX with wrong value (to long)."""
         with pytest.raises(ConversionError):
             DPTString.to_knx("AAAAABBBBBCCCCx")
 
-    def test_from_knx_wrong_parameter_too_large(self):
-        """Test parsing of KNX string with too many elements."""
-        raw = (
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-        )
-        with pytest.raises(ConversionError):
-            DPTString.from_knx(raw)
-
-    def test_from_knx_wrong_parameter_too_small(self):
-        """Test parsing of KNX string with too less elements."""
-        raw = (
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-            0x00,
-        )
+    @pytest.mark.parametrize(
+        "raw",
+        [
+            ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),),
+            ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),),
+        ],
+    )
+    def test_from_knx_wrong_parameter_length(self, raw):
+        """Test parsing of KNX string with wrong elements length."""
         with pytest.raises(ConversionError):
             DPTString.from_knx(raw)