python-duco-connectivity 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/PKG-INFO +1 -1
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/pyproject.toml +1 -1
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/client.py +74 -8
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/models.py +7 -3
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/python_duco_connectivity.egg-info/PKG-INFO +1 -1
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/tests/test_client.py +193 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/tests/test_models.py +5 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/LICENSE +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/README.md +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/setup.cfg +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/__init__.py +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/exceptions.py +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/py.typed +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/python_duco_connectivity.egg-info/SOURCES.txt +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/python_duco_connectivity.egg-info/dependency_links.txt +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/python_duco_connectivity.egg-info/requires.txt +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/python_duco_connectivity.egg-info/top_level.txt +0 -0
- {python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/tests/test_exceptions.py +0 -0
{python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/client.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any
|
|
5
|
+
from urllib.parse import urlsplit
|
|
5
6
|
|
|
6
7
|
import aiohttp
|
|
7
8
|
|
|
@@ -37,17 +38,61 @@ class DucoClient:
|
|
|
37
38
|
) -> None:
|
|
38
39
|
self._session = session
|
|
39
40
|
self._timeout = aiohttp.ClientTimeout(total=request_timeout)
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
raw_host = host.rstrip("/")
|
|
43
|
+
authority = raw_host.split("://", 1)[1] if "://" in raw_host else raw_host
|
|
44
|
+
authority = authority.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0]
|
|
45
|
+
|
|
46
|
+
if "@" not in authority and authority.count(":") >= 2 and not authority.startswith("["):
|
|
47
|
+
msg = "Unbracketed IPv6 host values are not supported; use [addr] or [addr]:port"
|
|
48
|
+
raise ValueError(msg)
|
|
49
|
+
|
|
50
|
+
parsed_host = urlsplit(raw_host if "://" in host else f"//{raw_host}")
|
|
51
|
+
scheme = parsed_host.scheme.lower()
|
|
52
|
+
|
|
53
|
+
if scheme == "https":
|
|
41
54
|
msg = "HTTPS is not supported by this client"
|
|
42
55
|
raise ValueError(msg)
|
|
43
|
-
|
|
44
|
-
if
|
|
56
|
+
|
|
57
|
+
if scheme not in ("", "http"):
|
|
45
58
|
msg = f"Unsupported scheme in host value: {host}"
|
|
46
59
|
raise ValueError(msg)
|
|
47
|
-
|
|
60
|
+
|
|
61
|
+
if parsed_host.username is not None or parsed_host.password is not None:
|
|
62
|
+
msg = f"Host value must not include user credentials: {host}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
|
|
65
|
+
if parsed_host.path not in ("", "/") or parsed_host.query or parsed_host.fragment:
|
|
66
|
+
msg = f"Host value must not include a path, query, or fragment: {host}"
|
|
67
|
+
raise ValueError(msg)
|
|
68
|
+
|
|
69
|
+
normalized_host = parsed_host.hostname
|
|
70
|
+
if normalized_host is None:
|
|
71
|
+
msg = f"Invalid host value: {host}"
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
embedded_port = parsed_host.port
|
|
76
|
+
except ValueError as err:
|
|
77
|
+
msg = f"Invalid port in host value: {host}"
|
|
78
|
+
raise ValueError(msg) from err
|
|
79
|
+
|
|
80
|
+
if port is not None and not 0 <= port <= 65535:
|
|
81
|
+
msg = f"Invalid port argument: {port}"
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
if embedded_port is not None and port is not None:
|
|
85
|
+
msg = "Port specified both in host and port argument"
|
|
86
|
+
raise ValueError(msg)
|
|
87
|
+
|
|
88
|
+
if ":" in normalized_host:
|
|
89
|
+
normalized_host = f"[{normalized_host}]"
|
|
90
|
+
|
|
91
|
+
resolved_port = port if port is not None else embedded_port
|
|
92
|
+
if resolved_port is None:
|
|
48
93
|
self._base_url = f"http://{normalized_host}"
|
|
49
94
|
else:
|
|
50
|
-
self._base_url = f"http://{normalized_host}:{
|
|
95
|
+
self._base_url = f"http://{normalized_host}:{resolved_port}"
|
|
51
96
|
|
|
52
97
|
@property
|
|
53
98
|
def base_url(self) -> str:
|
|
@@ -106,6 +151,27 @@ class DucoClient:
|
|
|
106
151
|
except ValueError:
|
|
107
152
|
return NetworkType.UNKNOWN
|
|
108
153
|
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _to_diag_status(raw_value: str) -> DiagStatus:
|
|
156
|
+
try:
|
|
157
|
+
return DiagStatus(raw_value)
|
|
158
|
+
except ValueError:
|
|
159
|
+
return DiagStatus.UNKNOWN
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _to_ventilation_state(raw_value: str) -> VentilationState:
|
|
163
|
+
try:
|
|
164
|
+
return VentilationState(raw_value)
|
|
165
|
+
except ValueError:
|
|
166
|
+
return VentilationState.UNKNOWN
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _to_ventilation_mode(raw_value: str) -> VentilationMode:
|
|
170
|
+
try:
|
|
171
|
+
return VentilationMode(raw_value)
|
|
172
|
+
except ValueError:
|
|
173
|
+
return VentilationMode.UNKNOWN
|
|
174
|
+
|
|
109
175
|
async def async_get_api_info(self) -> ApiInfo:
|
|
110
176
|
"""Return API metadata advertised by the box."""
|
|
111
177
|
payload = await self._request_json("GET", "/api")
|
|
@@ -177,7 +243,7 @@ class DucoClient:
|
|
|
177
243
|
return [
|
|
178
244
|
DiagComponent(
|
|
179
245
|
component=item["Component"],
|
|
180
|
-
status=
|
|
246
|
+
status=self._to_diag_status(item["Status"]),
|
|
181
247
|
)
|
|
182
248
|
for item in payload["Diag"]["SubSystems"]
|
|
183
249
|
]
|
|
@@ -223,8 +289,8 @@ class DucoClient:
|
|
|
223
289
|
if "Ventilation" in payload:
|
|
224
290
|
vent = payload["Ventilation"]
|
|
225
291
|
ventilation = NodeVentilationInfo(
|
|
226
|
-
state=
|
|
227
|
-
mode=
|
|
292
|
+
state=self._to_ventilation_state(self._read_wrapped_value(vent, "State")),
|
|
293
|
+
mode=self._to_ventilation_mode(self._read_wrapped_value(vent, "Mode")),
|
|
228
294
|
time_state_remain=self._read_wrapped_value(vent, "TimeStateRemain"),
|
|
229
295
|
time_state_end=self._read_wrapped_value(vent, "TimeStateEnd"),
|
|
230
296
|
flow_lvl_tgt=self._read_wrapped_value(vent, "FlowLvlTgt")
|
{python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/models.py
RENAMED
|
@@ -23,19 +23,21 @@ class NetworkType(StrEnum):
|
|
|
23
23
|
VIRT = "VIRT"
|
|
24
24
|
RF = "RF"
|
|
25
25
|
WI = "WI"
|
|
26
|
+
MB = "MB"
|
|
26
27
|
UNKNOWN = "UNKNOWN"
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class VentilationMode(StrEnum):
|
|
30
|
-
"""Control modes reported for node ventilation."""
|
|
31
|
+
"""Control modes reported for node ventilation, plus a client-side fallback."""
|
|
31
32
|
|
|
32
33
|
AUTO = "AUTO"
|
|
33
34
|
MANU = "MANU"
|
|
34
35
|
NONE = "-"
|
|
36
|
+
UNKNOWN = "UNKNOWN"
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class VentilationState(StrEnum):
|
|
38
|
-
"""
|
|
40
|
+
"""Ventilation states reported by the API, plus a client-side fallback."""
|
|
39
41
|
|
|
40
42
|
AUTO = "AUTO"
|
|
41
43
|
AUT1 = "AUT1"
|
|
@@ -54,14 +56,16 @@ class VentilationState(StrEnum):
|
|
|
54
56
|
MAN1x3 = "MAN1x3"
|
|
55
57
|
MAN2x3 = "MAN2x3"
|
|
56
58
|
MAN3x3 = "MAN3x3"
|
|
59
|
+
UNKNOWN = "UNKNOWN"
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class DiagStatus(StrEnum):
|
|
60
|
-
"""Health states returned by the diagnostics API."""
|
|
63
|
+
"""Health states returned by the diagnostics API, plus a client-side fallback."""
|
|
61
64
|
|
|
62
65
|
OK = "Ok"
|
|
63
66
|
DISABLE = "Disable"
|
|
64
67
|
ERROR = "Error"
|
|
68
|
+
UNKNOWN = "UNKNOWN"
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
@dataclass(frozen=True, slots=True)
|
|
@@ -13,6 +13,7 @@ from duco_connectivity import (
|
|
|
13
13
|
DucoWriteLimitError,
|
|
14
14
|
NetworkType,
|
|
15
15
|
NodeType,
|
|
16
|
+
VentilationMode,
|
|
16
17
|
VentilationState,
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -62,6 +63,69 @@ async def test_base_url_defaults_to_http() -> None:
|
|
|
62
63
|
assert client.base_url == "http://192.0.2.94"
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
async def test_base_url_uses_embedded_port_from_host() -> None:
|
|
67
|
+
"""Hosts with an embedded port should preserve that port in the base URL."""
|
|
68
|
+
async with aiohttp.ClientSession() as session:
|
|
69
|
+
client = DucoClient(session=session, host="http://192.0.2.94:8080")
|
|
70
|
+
assert client.base_url == "http://192.0.2.94:8080"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def test_bracketed_ipv6_host_is_accepted() -> None:
|
|
74
|
+
"""Bracketed IPv6 hosts should be accepted and normalized."""
|
|
75
|
+
async with aiohttp.ClientSession() as session:
|
|
76
|
+
client = DucoClient(session=session, host="[fe80::1]")
|
|
77
|
+
assert client.base_url == "http://[fe80::1]"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def test_uppercase_http_scheme_is_accepted() -> None:
|
|
81
|
+
"""HTTP hosts should be accepted regardless of scheme casing."""
|
|
82
|
+
async with aiohttp.ClientSession() as session:
|
|
83
|
+
client = DucoClient(session=session, host="HTTP://192.0.2.94:8080")
|
|
84
|
+
assert client.base_url == "http://192.0.2.94:8080"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def test_conflicting_port_sources_are_rejected() -> None:
|
|
88
|
+
"""The client should reject a separate port when the host already includes one."""
|
|
89
|
+
async with aiohttp.ClientSession() as session:
|
|
90
|
+
with pytest.raises(ValueError, match="Port specified both"):
|
|
91
|
+
DucoClient(session=session, host="192.0.2.94:8080", port=8081)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def test_userinfo_in_host_is_rejected() -> None:
|
|
95
|
+
"""The unauthenticated client should reject credentials embedded in the host value."""
|
|
96
|
+
async with aiohttp.ClientSession() as session:
|
|
97
|
+
with pytest.raises(ValueError, match="must not include user credentials"):
|
|
98
|
+
DucoClient(session=session, host="user:pass@192.0.2.94")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def test_invalid_embedded_port_is_rejected() -> None:
|
|
102
|
+
"""Malformed embedded ports should raise a consistent client ValueError."""
|
|
103
|
+
async with aiohttp.ClientSession() as session:
|
|
104
|
+
with pytest.raises(ValueError, match="Invalid port in host value"):
|
|
105
|
+
DucoClient(session=session, host="192.0.2.94:abc")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def test_unbracketed_ipv6_host_is_rejected() -> None:
|
|
109
|
+
"""Bare IPv6 hosts should require brackets to avoid ambiguous parsing."""
|
|
110
|
+
async with aiohttp.ClientSession() as session:
|
|
111
|
+
with pytest.raises(ValueError, match="Unbracketed IPv6 host values"):
|
|
112
|
+
DucoClient(session=session, host="fe80::1")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def test_negative_port_argument_is_rejected() -> None:
|
|
116
|
+
"""Explicit negative ports should be rejected during client construction."""
|
|
117
|
+
async with aiohttp.ClientSession() as session:
|
|
118
|
+
with pytest.raises(ValueError, match="Invalid port argument"):
|
|
119
|
+
DucoClient(session=session, host="192.0.2.94", port=-1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def test_out_of_range_port_argument_is_rejected() -> None:
|
|
123
|
+
"""Explicit ports above 65535 should be rejected during client construction."""
|
|
124
|
+
async with aiohttp.ClientSession() as session:
|
|
125
|
+
with pytest.raises(ValueError, match="Invalid port argument"):
|
|
126
|
+
DucoClient(session=session, host="192.0.2.94", port=99999)
|
|
127
|
+
|
|
128
|
+
|
|
65
129
|
async def test_api_info_is_parsed(api_info_full_data: dict[str, object]) -> None:
|
|
66
130
|
"""Test that the API info model follows the public API payload."""
|
|
67
131
|
mock_response = _response(json_payload=api_info_full_data)
|
|
@@ -154,6 +218,26 @@ async def test_get_diagnostics(diag_data: dict[str, object]) -> None:
|
|
|
154
218
|
assert diags[0].status == DiagStatus.OK
|
|
155
219
|
|
|
156
220
|
|
|
221
|
+
async def test_get_diagnostics_unknown_status_falls_back_to_unknown() -> None:
|
|
222
|
+
"""Unknown diagnostic statuses should not break parsing."""
|
|
223
|
+
payload: dict[str, object] = {
|
|
224
|
+
"Diag": {
|
|
225
|
+
"SubSystems": [
|
|
226
|
+
{"Component": "Ventilation", "Status": "FutureState"},
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
mock_response = _response(json_payload=payload)
|
|
231
|
+
|
|
232
|
+
async with aiohttp.ClientSession() as session:
|
|
233
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
234
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
235
|
+
diags = await client.async_get_diagnostics()
|
|
236
|
+
|
|
237
|
+
assert len(diags) == 1
|
|
238
|
+
assert diags[0].status == DiagStatus.UNKNOWN
|
|
239
|
+
|
|
240
|
+
|
|
157
241
|
async def test_get_nodes_parses_full_payload(nodes_data: dict[str, object]) -> None:
|
|
158
242
|
"""Test node parsing across box and sensor nodes."""
|
|
159
243
|
mock_response = _response(json_payload=nodes_data)
|
|
@@ -221,6 +305,41 @@ async def test_get_nodes_unknown_network_type_falls_back_to_unknown() -> None:
|
|
|
221
305
|
assert nodes[0].general.network_type == NetworkType.UNKNOWN
|
|
222
306
|
|
|
223
307
|
|
|
308
|
+
async def test_get_nodes_mb_network_type_is_parsed() -> None:
|
|
309
|
+
"""Known MB network types should be parsed explicitly."""
|
|
310
|
+
payload: dict[str, object] = {
|
|
311
|
+
"Nodes": [
|
|
312
|
+
{
|
|
313
|
+
"Node": 4,
|
|
314
|
+
"General": {
|
|
315
|
+
"Type": {"Val": "UCCO2"},
|
|
316
|
+
"SubType": {"Val": 0},
|
|
317
|
+
"NetworkType": {"Val": "MB"},
|
|
318
|
+
"Parent": {"Val": 1},
|
|
319
|
+
"Asso": {"Val": 1},
|
|
320
|
+
"Name": {"Val": ""},
|
|
321
|
+
"Identify": {"Val": 0},
|
|
322
|
+
},
|
|
323
|
+
"Ventilation": {
|
|
324
|
+
"State": {"Val": "AUTO"},
|
|
325
|
+
"TimeStateRemain": {"Val": 0},
|
|
326
|
+
"TimeStateEnd": {"Val": 0},
|
|
327
|
+
"Mode": {"Val": "-"},
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
mock_response = _response(json_payload=payload)
|
|
333
|
+
|
|
334
|
+
async with aiohttp.ClientSession() as session:
|
|
335
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
336
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
337
|
+
nodes = await client.async_get_nodes()
|
|
338
|
+
|
|
339
|
+
assert len(nodes) == 1
|
|
340
|
+
assert nodes[0].general.network_type == NetworkType.MB
|
|
341
|
+
|
|
342
|
+
|
|
224
343
|
async def test_get_nodes_unknown_node_type_falls_back_to_unknown() -> None:
|
|
225
344
|
"""Test that unknown node types do not crash parsing."""
|
|
226
345
|
payload: dict[str, object] = {
|
|
@@ -301,6 +420,80 @@ async def test_timed_manual_state_is_parsed() -> None:
|
|
|
301
420
|
assert nodes[0].ventilation.state is VentilationState.MAN3x2
|
|
302
421
|
|
|
303
422
|
|
|
423
|
+
async def test_get_nodes_unknown_ventilation_state_falls_back_to_unknown() -> None:
|
|
424
|
+
"""Unknown ventilation states should not break node parsing."""
|
|
425
|
+
payload: dict[str, object] = {
|
|
426
|
+
"Nodes": [
|
|
427
|
+
{
|
|
428
|
+
"Node": 6,
|
|
429
|
+
"General": {
|
|
430
|
+
"Type": {"Val": "BOX"},
|
|
431
|
+
"SubType": {"Val": 1},
|
|
432
|
+
"NetworkType": {"Val": "VIRT"},
|
|
433
|
+
"Parent": {"Val": 0},
|
|
434
|
+
"Asso": {"Val": 0},
|
|
435
|
+
"Name": {"Val": "Living"},
|
|
436
|
+
"Identify": {"Val": 0},
|
|
437
|
+
},
|
|
438
|
+
"Ventilation": {
|
|
439
|
+
"State": {"Val": "FUTURE_STATE"},
|
|
440
|
+
"Mode": {"Val": "AUTO"},
|
|
441
|
+
"TimeStateRemain": {"Val": 0},
|
|
442
|
+
"TimeStateEnd": {"Val": 0},
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
mock_response = _response(json_payload=payload)
|
|
449
|
+
|
|
450
|
+
async with aiohttp.ClientSession() as session:
|
|
451
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
452
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
453
|
+
nodes = await client.async_get_nodes()
|
|
454
|
+
|
|
455
|
+
assert len(nodes) == 1
|
|
456
|
+
assert nodes[0].ventilation is not None
|
|
457
|
+
assert nodes[0].ventilation.state is VentilationState.UNKNOWN
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
async def test_get_nodes_unknown_ventilation_mode_falls_back_to_unknown() -> None:
|
|
461
|
+
"""Unknown ventilation modes should not break node parsing."""
|
|
462
|
+
payload: dict[str, object] = {
|
|
463
|
+
"Nodes": [
|
|
464
|
+
{
|
|
465
|
+
"Node": 7,
|
|
466
|
+
"General": {
|
|
467
|
+
"Type": {"Val": "BOX"},
|
|
468
|
+
"SubType": {"Val": 1},
|
|
469
|
+
"NetworkType": {"Val": "VIRT"},
|
|
470
|
+
"Parent": {"Val": 0},
|
|
471
|
+
"Asso": {"Val": 0},
|
|
472
|
+
"Name": {"Val": "Bedroom"},
|
|
473
|
+
"Identify": {"Val": 0},
|
|
474
|
+
},
|
|
475
|
+
"Ventilation": {
|
|
476
|
+
"State": {"Val": "AUTO"},
|
|
477
|
+
"Mode": {"Val": "FUTURE_MODE"},
|
|
478
|
+
"TimeStateRemain": {"Val": 0},
|
|
479
|
+
"TimeStateEnd": {"Val": 0},
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
mock_response = _response(json_payload=payload)
|
|
486
|
+
|
|
487
|
+
async with aiohttp.ClientSession() as session:
|
|
488
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
489
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
490
|
+
nodes = await client.async_get_nodes()
|
|
491
|
+
|
|
492
|
+
assert len(nodes) == 1
|
|
493
|
+
assert nodes[0].ventilation is not None
|
|
494
|
+
assert nodes[0].ventilation.mode is VentilationMode.UNKNOWN
|
|
495
|
+
|
|
496
|
+
|
|
304
497
|
async def test_connection_error_raises_duco_connection_error() -> None:
|
|
305
498
|
"""Transport errors should raise DucoConnectionError."""
|
|
306
499
|
async with aiohttp.ClientSession() as session:
|
|
@@ -84,3 +84,8 @@ def test_node_is_frozen() -> None:
|
|
|
84
84
|
)
|
|
85
85
|
with pytest.raises(AttributeError):
|
|
86
86
|
node.node_id = 2 # type: ignore[misc]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_network_type_includes_mb() -> None:
|
|
90
|
+
"""NetworkType should expose MB as an explicit known value."""
|
|
91
|
+
assert NetworkType.MB.value == "MB"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.1.0 → python_duco_connectivity-0.1.1}/src/duco_connectivity/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|