1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
|
"""Unit test for KNX binary/integer objects."""
from inspect import isabstract
from typing import Any
import pytest
from xknx.dpt import (
DPT2ByteFloat,
DPT2ByteUnsigned,
DPTActiveEnergy,
DPTArray,
DPTBase,
DPTBinary,
DPTColorRGBW,
DPTComplex,
DPTConsumerProducer,
DPTEnum,
DPTHVACContrMode,
DPTNumeric,
DPTScaling,
DPTString,
DPTTemperature,
)
from xknx.exceptions import CouldNotParseTelegram
class TestDPTBase:
"""Test class for transcoder base object."""
def test_dpt_abstract_subclasses_ignored(self) -> None:
"""Test if abstract base classes are ignored by dpt_class_tree and __recursive_subclasses__."""
for dpt in DPTBase.dpt_class_tree():
assert dpt not in (DPTBase, DPTNumeric, DPTEnum, DPTComplex)
assert not isabstract(dpt)
assert dpt() # test for abstract class - to be removed if instantiation is not allowed anymore
def test_dpt_concrete_subclasses_included(self) -> None:
"""Test if concrete subclasses are included by dpt_class_tree."""
for dpt in (
DPT2ByteFloat,
DPTString,
DPTTemperature,
DPTScaling,
DPTHVACContrMode,
DPTColorRGBW,
DPTConsumerProducer,
):
assert dpt in DPTBase.dpt_class_tree()
@pytest.mark.parametrize("dpt_class", [DPTString, DPT2ByteFloat])
def test_dpt_non_abstract_baseclass_included(
self, dpt_class: type[DPTBase]
) -> None:
"""Test if non-abstract base classes is included by dpt_class_tree."""
assert dpt_class in dpt_class.dpt_class_tree()
def test_dpt_subclasses_definition_types(self) -> None:
"""Test value_type and dpt_*_number values for correct type in subclasses of DPTBase."""
for dpt in DPTBase.dpt_class_tree():
assert isinstance(dpt.value_type, str), (
f"Wrong type for value_type in {dpt} : {type(dpt.value_type)} - str expected"
)
assert dpt.value_type, f"Empty string for value_type in {dpt} not allowed"
assert isinstance(dpt.dpt_main_number, int), (
f"Wrong type for dpt_main_number in {dpt} : {type(dpt.dpt_main_number)} - int expected"
)
assert dpt.dpt_main_number, (
f"Zero value for dpt_main_number in {dpt} not allowed"
)
assert isinstance(dpt.dpt_sub_number, int | type(None)), (
f"Wrong type for dpt_sub_number in {dpt} : {type(dpt.dpt_sub_number)} - int or `None` expected"
)
def test_dpt_subclasses_no_duplicate_value_types(self) -> None:
"""Test for duplicate value_type values in subclasses of DPTBase."""
value_types = [
dpt.value_type
for dpt in DPTBase.dpt_class_tree()
if dpt.value_type is not None
]
assert len(value_types) == len(set(value_types)), (
f"Duplicate DPT value_types found: { {item for item in value_types if value_types.count(item) > 1} }"
)
def test_dpt_subclasses_no_duplicate_dpt_number(self) -> None:
"""Test for duplicate value_type values in subclasses of DPTBase."""
dpt_tuples = [
(dpt.dpt_main_number, dpt.dpt_sub_number)
for dpt in DPTBase.dpt_class_tree()
]
assert len(dpt_tuples) == len(set(dpt_tuples)), (
f"Duplicate DPT numbers found: { {item for item in dpt_tuples if dpt_tuples.count(item) > 1} }"
)
@pytest.mark.parametrize(
"equal_dpts",
[
# strings in dictionaries would fail type checking, but should work nevertheless
["2byte_unsigned", 7, "DPT-7", {"main": 7}, {"main": "7", "sub": None}],
["temperature", "9.001", {"main": 9, "sub": 1}, {"main": "9", "sub": "1"}],
["active_energy", "13.010", {"main": 13, "sub": 10}],
["consumer_producer", "1.1200", {"main": 1, "sub": 1200}],
],
)
def test_dpt_alternative_notations(self, equal_dpts: list[Any]) -> None:
"""Test the parser for accepting alternative notations for the same DPT class."""
parsed = [DPTBase.parse_transcoder(dpt) for dpt in equal_dpts]
assert issubclass(parsed[0], DPTBase)
assert all(parsed[0] == dpt for dpt in parsed)
@pytest.mark.parametrize(
"equal_dpts",
[
# strings in dictionaries would fail type checking, but should work nevertheless
[
"2byte_unsigned",
7,
"DPT-7",
{"main": 7},
{"main": "7", "sub": None},
DPT2ByteUnsigned,
],
[
"temperature",
"9.001",
{"main": 9, "sub": 1},
{"main": "9", "sub": "1"},
DPTTemperature,
],
["active_energy", "13.010", {"main": 13, "sub": 10}, DPTActiveEnergy],
],
)
def test_get_dpt_alternative_notations(self, equal_dpts: list[Any]) -> None:
"""Test the parser for accepting alternative notations for the same DPT class."""
parsed = [DPTBase.get_dpt(dpt) for dpt in equal_dpts]
assert issubclass(parsed[0], DPTBase)
assert all(parsed[0] == dpt for dpt in parsed)
INVALID_DPT_IDENTIFIERS = [
None,
0,
999999999,
9.001, # float is not valid
"invalid_string",
{"sub": 1},
{"main": None, "sub": None},
{"main": "invalid"},
{"main": 9, "sub": "invalid"},
[9, 1],
(9,),
]
@pytest.mark.parametrize("value", INVALID_DPT_IDENTIFIERS)
def test_parse_transcoder_invalid_data(self, value: Any) -> None:
"""Test parsing invalid data."""
assert DPTBase.parse_transcoder(value) is None
@pytest.mark.parametrize("value", INVALID_DPT_IDENTIFIERS)
def test_get_dpt_invalid_data(self, value: Any) -> None:
"""Test parsing invalid data."""
with pytest.raises(ValueError):
DPTBase.get_dpt(value)
def test_parse_transcoder_from_subclass(self) -> None:
"""Test parsing only subclasses of a DPT class."""
assert DPTBase.parse_transcoder("string") == DPTString
assert DPTNumeric.parse_transcoder("string") is None
assert DPT2ByteFloat.parse_transcoder("string") is None
assert DPTBase.parse_transcoder("percent") == DPTScaling
assert DPTNumeric.parse_transcoder("percent") == DPTScaling
assert DPT2ByteFloat.parse_transcoder("percent") is None
assert DPTBase.parse_transcoder("temperature") == DPTTemperature
assert DPTNumeric.parse_transcoder("temperature") == DPTTemperature
assert DPT2ByteFloat.parse_transcoder("temperature") == DPTTemperature
def test_get_dpt_from_subclass(self) -> None:
"""Test parsing only subclasses of a DPT class."""
assert DPTBase.get_dpt("string") == DPTString
with pytest.raises(ValueError):
DPTNumeric.get_dpt("string")
assert DPTBase.get_dpt("percent") == DPTScaling
assert DPTNumeric.get_dpt("percent") == DPTScaling
assert DPTBase.get_dpt("temperature") == DPTTemperature
assert DPTNumeric.get_dpt("temperature") == DPTTemperature
assert DPT2ByteFloat.get_dpt("temperature") == DPTTemperature
def test_dpt_name(self) -> None:
"""Test DPT name."""
assert DPTBase.dpt_name() == "DPTBase (abstract)"
assert DPTNumeric.dpt_name() == "DPTNumeric (abstract)"
assert DPT2ByteFloat.dpt_name() == "DPT2ByteFloat (9)"
assert DPTString.dpt_name() == "DPTString (16.000)"
assert DPTColorRGBW.dpt_name() == "DPTColorRGBW (251.600)"
assert DPTConsumerProducer.dpt_name() == "DPTConsumerProducer (1.1200)"
class TestDPTBaseSubclass:
"""Test subclass of transcoder base object."""
@pytest.mark.parametrize("dpt_class", DPTBase.dpt_class_tree())
def test_required_values(self, dpt_class: type[DPTBase]) -> None:
"""Test required class variables are set for definitions."""
assert dpt_class.payload_type in (DPTArray, DPTBinary)
assert isinstance(dpt_class.payload_length, int)
assert dpt_class.payload_length, "Payload_length 0 is invalid"
assert isinstance(dpt_class.dpt_main_number, int)
assert dpt_class.dpt_main_number, "DPT main number 0 is invalid"
assert isinstance(dpt_class.dpt_sub_number, int | type(None))
assert isinstance(dpt_class.value_type, str)
assert dpt_class.value_type, "Empty string for value_type is invalid"
def test_validate_payload_array(self) -> None:
"""Test validate_payload method."""
class DPTArrayTest(DPTBase):
"""Mock class for testing array payloads."""
payload_type = DPTArray
payload_length = 2
with pytest.raises(CouldNotParseTelegram):
DPTArrayTest.validate_payload(DPTArray((1,)))
with pytest.raises(CouldNotParseTelegram):
DPTArrayTest.validate_payload(DPTArray((1, 1, 1)))
with pytest.raises(CouldNotParseTelegram):
DPTArrayTest.validate_payload(DPTBinary(1))
with pytest.raises(CouldNotParseTelegram):
DPTArrayTest.validate_payload("why?")
assert DPTArrayTest.validate_payload(DPTArray((1, 1))) == (1, 1)
def test_validate_payload_binary(self) -> None:
"""Test validate_payload method."""
class DPTBinaryTest(DPTBase):
"""Mock class for testing binary payloads."""
payload_type = DPTBinary
payload_length = 1
with pytest.raises(CouldNotParseTelegram):
DPTBinaryTest.validate_payload(DPTArray(1))
with pytest.raises(CouldNotParseTelegram):
DPTBinaryTest.validate_payload(DPTArray((1, 1)))
with pytest.raises(CouldNotParseTelegram):
DPTBinaryTest.validate_payload("why?")
assert DPTBinaryTest.validate_payload(DPTBinary(1)) == (1,)
class TestDPTNumeric:
"""Test class for numeric transcoder base object."""
@pytest.mark.parametrize("dpt_class", DPTNumeric.dpt_class_tree())
def test_values(self, dpt_class: type[DPTNumeric]) -> None:
"""Test boundary values are set for numeric definitions (because mypy doesn't)."""
assert isinstance(dpt_class.value_min, int | float)
assert isinstance(dpt_class.value_max, int | float)
assert isinstance(dpt_class.resolution, int | float)
|