Skip to content

Commit 4bc15ab

Browse files
authored
Add support for DPT 16.001 (#910)
1 parent cf242d2 commit 4bc15ab

File tree

4 files changed

+98
-160
lines changed

4 files changed

+98
-160
lines changed

changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
- Add support for SearchRequestExtended to find interfaces that allow IP Secure
88
- 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`.
9+
- Add support for DPT 16.001 (DPT_String_8859_1) as `DPTLatin1` with value_type "latin_1".
910

1011
### Bug fixes
1112

1213
- Stop SecureSession keepalive_task when session is stopped (and don't restart it from sending STATUS_CLOSE)
14+
- Fix encoding invalid characters for DPTString (value_type "string")
1315

1416
## 0.20.0 IP Secure 2022-03-29
1517

test/dpt_tests/dpt_string_test.py

+72-148
Original file line numberDiff line numberDiff line change
@@ -1,167 +1,91 @@
11
"""Unit test for KNX string object."""
22
import pytest
33

4-
from xknx.dpt import DPTString
4+
from xknx.dpt import DPTLatin1, DPTString
55
from xknx.exceptions import ConversionError
66

77

88
class TestDPTString:
9-
"""Test class for KNX float object."""
9+
"""Test class for KNX ASCII string object."""
1010

11-
def test_value_from_documentation(self):
12-
"""Test parsing and streaming Example from documentation."""
13-
raw = (
14-
0x4B,
15-
0x4E,
16-
0x58,
17-
0x20,
18-
0x69,
19-
0x73,
20-
0x20,
21-
0x4F,
22-
0x4B,
23-
0x00,
24-
0x00,
25-
0x00,
26-
0x00,
27-
0x00,
28-
)
29-
string = "KNX is OK"
30-
assert DPTString.to_knx(string) == raw
31-
assert DPTString.from_knx(raw) == string
32-
33-
def test_value_empty_string(self):
34-
"""Test parsing and streaming empty string."""
35-
raw = (
36-
0x00,
37-
0x00,
38-
0x00,
39-
0x00,
40-
0x00,
41-
0x00,
42-
0x00,
43-
0x00,
44-
0x00,
45-
0x00,
46-
0x00,
47-
0x00,
48-
0x00,
49-
0x00,
50-
)
51-
string = ""
52-
assert DPTString.to_knx(string) == raw
53-
assert DPTString.from_knx(raw) == string
11+
@pytest.mark.parametrize(
12+
"string,raw",
13+
[
14+
(
15+
"KNX is OK",
16+
(75, 78, 88, 32, 105, 115, 32, 79, 75, 0, 0, 0, 0, 0),
17+
),
18+
(
19+
"",
20+
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
21+
),
22+
(
23+
"AbCdEfGhIjKlMn",
24+
(65, 98, 67, 100, 69, 102, 71, 104, 73, 106, 75, 108, 77, 110),
25+
),
26+
(
27+
".,:;-_!?$@&#%/",
28+
(46, 44, 58, 59, 45, 95, 33, 63, 36, 64, 38, 35, 37, 47),
29+
),
30+
],
31+
)
32+
@pytest.mark.parametrize("test_dpt", [DPTString, DPTLatin1])
33+
def test_values(self, string, raw, test_dpt):
34+
"""Test parsing and streaming strings."""
35+
assert test_dpt.to_knx(string) == raw
36+
assert test_dpt.from_knx(raw) == string
5437

55-
def test_value_max_string(self):
56-
"""Test parsing and streaming large string."""
57-
raw = (
58-
0x41,
59-
0x41,
60-
0x41,
61-
0x41,
62-
0x41,
63-
0x42,
64-
0x42,
65-
0x42,
66-
0x42,
67-
0x42,
68-
0x43,
69-
0x43,
70-
0x43,
71-
0x43,
72-
)
73-
string = "AAAAABBBBBCCCC"
74-
assert DPTString.to_knx(string) == raw
75-
assert DPTString.from_knx(raw) == string
76-
77-
def test_value_special_chars(self):
78-
"""Test parsing and streaming string with special chars."""
79-
raw = (
80-
0x48,
81-
0x65,
82-
0x79,
83-
0x21,
84-
0x3F,
85-
0x24,
86-
0x20,
87-
0xC4,
88-
0xD6,
89-
0xDC,
90-
0xE4,
91-
0xF6,
92-
0xFC,
93-
0xDF,
94-
)
95-
string = "Hey!?$ ÄÖÜäöüß"
96-
assert DPTString.to_knx(string) == raw
97-
assert DPTString.from_knx(raw) == string
98-
99-
def test_to_knx_invalid_chars(self):
100-
"""Test streaming string with invalid chars."""
101-
raw = (
102-
0x4D,
103-
0x61,
104-
0x74,
105-
0x6F,
106-
0x75,
107-
0x3F,
108-
0x00,
109-
0x00,
110-
0x00,
111-
0x00,
112-
0x00,
113-
0x00,
114-
0x00,
115-
0x00,
116-
)
117-
string = "Matouš"
118-
knx_string = "Matou?"
38+
@pytest.mark.parametrize(
39+
"string,knx_string,raw",
40+
[
41+
(
42+
"Matouš",
43+
"Matou?",
44+
(77, 97, 116, 111, 117, 63, 0, 0, 0, 0, 0, 0, 0, 0),
45+
),
46+
(
47+
"Gänsefüßchen",
48+
"G?nsef??chen",
49+
(71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0),
50+
),
51+
],
52+
)
53+
def test_to_knx_ascii_invalid_chars(self, string, knx_string, raw):
54+
"""Test streaming ASCII string with invalid chars."""
11955
assert DPTString.to_knx(string) == raw
12056
assert DPTString.from_knx(raw) == knx_string
12157

58+
@pytest.mark.parametrize(
59+
"string,raw",
60+
[
61+
(
62+
"Gänsefüßchen",
63+
(71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0),
64+
),
65+
(
66+
"àáâãåæçèéêëìíî",
67+
(224, 225, 226, 227, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238),
68+
),
69+
],
70+
)
71+
def test_to_knx_latin_1(self, string, raw):
72+
"""Test streaming Latin-1 strings."""
73+
assert DPTLatin1.to_knx(string) == raw
74+
assert DPTLatin1.from_knx(raw) == string
75+
12276
def test_to_knx_too_long(self):
12377
"""Test serializing DPTString to KNX with wrong value (to long)."""
12478
with pytest.raises(ConversionError):
12579
DPTString.to_knx("AAAAABBBBBCCCCx")
12680

127-
def test_from_knx_wrong_parameter_too_large(self):
128-
"""Test parsing of KNX string with too many elements."""
129-
raw = (
130-
0x00,
131-
0x00,
132-
0x00,
133-
0x00,
134-
0x00,
135-
0x00,
136-
0x00,
137-
0x00,
138-
0x00,
139-
0x00,
140-
0x00,
141-
0x00,
142-
0x00,
143-
0x00,
144-
0x00,
145-
)
146-
with pytest.raises(ConversionError):
147-
DPTString.from_knx(raw)
148-
149-
def test_from_knx_wrong_parameter_too_small(self):
150-
"""Test parsing of KNX string with too less elements."""
151-
raw = (
152-
0x00,
153-
0x00,
154-
0x00,
155-
0x00,
156-
0x00,
157-
0x00,
158-
0x00,
159-
0x00,
160-
0x00,
161-
0x00,
162-
0x00,
163-
0x00,
164-
0x00,
165-
)
81+
@pytest.mark.parametrize(
82+
"raw",
83+
[
84+
((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),),
85+
((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),),
86+
],
87+
)
88+
def test_from_knx_wrong_parameter_length(self, raw):
89+
"""Test parsing of KNX string with wrong elements length."""
16690
with pytest.raises(ConversionError):
16791
DPTString.from_knx(raw)

xknx/dpt/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
from .dpt_datetime import DPTDateTime
173173
from .dpt_hvac_mode import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
174174
from .dpt_scaling import DPTAngle, DPTScaling
175-
from .dpt_string import DPTString
175+
from .dpt_string import DPTLatin1, DPTString
176176
from .dpt_time import DPTTime
177177

178178
__all__ = [
@@ -249,6 +249,7 @@
249249
"DPTHVACMode",
250250
"DPTImpedance",
251251
"DPTKelvinPerPercent",
252+
"DPTLatin1",
252253
"DPTLength",
253254
"DPTLengthMm",
254255
"DPTLightQuantity",

xknx/dpt/dpt_string.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class DPTString(DPTBase):
1010
"""
11-
Abstraction for KNX 14 Octet ASCII String.
11+
Abstraction for KNX 14 Octet ASCII string.
1212
1313
DPT 16.000
1414
"""
@@ -19,15 +19,15 @@ class DPTString(DPTBase):
1919
value_type = "string"
2020
unit = ""
2121

22+
_encoding = "ascii"
23+
2224
@classmethod
2325
def from_knx(cls, raw: tuple[int, ...]) -> str:
2426
"""Parse/deserialize from KNX/IP raw data."""
2527
cls.test_bytesarray(raw)
26-
value = ""
27-
for byte in raw:
28-
if byte != 0x00:
29-
value += chr(byte)
30-
return value
28+
return bytes(byte for byte in raw if byte != 0x00).decode(
29+
cls._encoding, errors="replace"
30+
)
3131

3232
@classmethod
3333
def to_knx(cls, value: str) -> tuple[int, ...]:
@@ -36,15 +36,26 @@ def to_knx(cls, value: str) -> tuple[int, ...]:
3636
knx_value = str(value)
3737
if not cls._test_boundaries(knx_value):
3838
raise ValueError
39-
raw = [ord(character) for character in knx_value]
40-
raw.extend([0] * (cls.payload_length - len(raw)))
41-
# replace invalid characters with question marks
42-
# bytes(knx_value, 'ascii') would raise UnicodeEncodeError
43-
return tuple(map(lambda char: char if char <= 0xFF else ord("?"), raw))
4439
except ValueError:
4540
raise ConversionError(f"Could not serialize {cls.__name__}", value=value)
41+
# replace invalid characters with question marks
42+
raw_bytes = knx_value.encode(cls._encoding, errors="replace")
43+
padding = bytes(cls.payload_length - len(raw_bytes))
44+
return tuple(raw_bytes + padding)
4645

4746
@classmethod
4847
def _test_boundaries(cls, value: str) -> bool:
4948
"""Test if value is within defined range for this object."""
5049
return len(value) <= cls.payload_length
50+
51+
52+
class DPTLatin1(DPTString):
53+
"""
54+
Abstraction for KNX 14 Octet Latin-1 (ISO 8859-1) string.
55+
56+
DPT 16.001
57+
"""
58+
59+
dpt_sub_number = 1
60+
value_type = "latin_1"
61+
_encoding = "latin_1"

0 commit comments

Comments
 (0)