python-duco-connectivity 0.6.0__tar.gz → 0.7.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.6.0/src/python_duco_connectivity.egg-info → python_duco_connectivity-0.7.1}/PKG-INFO +3 -1
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/README.md +2 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/pyproject.toml +1 -1
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/client.py +74 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1/src/python_duco_connectivity.egg-info}/PKG-INFO +3 -1
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_client.py +97 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/LICENSE +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/setup.cfg +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/__init__.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/__main__.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/cli.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/exceptions.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/models.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/py.typed +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/python_duco_connectivity.egg-info/SOURCES.txt +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/python_duco_connectivity.egg-info/dependency_links.txt +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/python_duco_connectivity.egg-info/entry_points.txt +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/python_duco_connectivity.egg-info/requires.txt +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/python_duco_connectivity.egg-info/top_level.txt +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_api_reference.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_cli.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_exceptions.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_local_sample_validation.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_models.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_pytest_live_support.py +0 -0
- {python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_replay_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-duco-connectivity
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Async HTTP client for the local Duco Connectivity API
|
|
5
5
|
Author: Ronald van der Meer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,6 +63,7 @@ in the development examples below.
|
|
|
63
63
|
- HTTP only
|
|
64
64
|
- asynchronous communication via `aiohttp`
|
|
65
65
|
- typed stable config families for the documented `/config` branches
|
|
66
|
+
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
66
67
|
- typed models that stay close to the API response shape
|
|
67
68
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
68
69
|
|
|
@@ -192,6 +193,7 @@ development pass, covering:
|
|
|
192
193
|
- `PATCH /config` with a no-op `TimeZone` write against the current value
|
|
193
194
|
- `GET /info?module=General&submodule=Board`
|
|
194
195
|
- `GET /info?module=General&submodule=Lan`
|
|
196
|
+
- `GET /info?module=HeatRecovery`
|
|
195
197
|
- `GET /info/nodes`
|
|
196
198
|
- `GET /info?module=General&submodule=PublicApi`
|
|
197
199
|
- `POST /action/nodes/{node}` with a no-op `SetVentilationState`
|
|
@@ -31,6 +31,7 @@ in the development examples below.
|
|
|
31
31
|
- HTTP only
|
|
32
32
|
- asynchronous communication via `aiohttp`
|
|
33
33
|
- typed stable config families for the documented `/config` branches
|
|
34
|
+
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
34
35
|
- typed models that stay close to the API response shape
|
|
35
36
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
36
37
|
|
|
@@ -160,6 +161,7 @@ development pass, covering:
|
|
|
160
161
|
- `PATCH /config` with a no-op `TimeZone` write against the current value
|
|
161
162
|
- `GET /info?module=General&submodule=Board`
|
|
162
163
|
- `GET /info?module=General&submodule=Lan`
|
|
164
|
+
- `GET /info?module=HeatRecovery`
|
|
163
165
|
- `GET /info/nodes`
|
|
164
166
|
- `GET /info?module=General&submodule=PublicApi`
|
|
165
167
|
- `POST /action/nodes/{node}` with a no-op `SetVentilationState`
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/client.py
RENAMED
|
@@ -281,6 +281,31 @@ class DucoClient:
|
|
|
281
281
|
def _read_wrapped_value(payload: dict[str, Any], key: str) -> Any:
|
|
282
282
|
return payload[key]["Val"]
|
|
283
283
|
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _read_optional_wrapped_int(
|
|
286
|
+
payload: dict[str, Any],
|
|
287
|
+
key: str,
|
|
288
|
+
*,
|
|
289
|
+
path: str,
|
|
290
|
+
) -> int | None:
|
|
291
|
+
"""Read an optional wrapped integer from a typed-stable API branch."""
|
|
292
|
+
if key not in payload:
|
|
293
|
+
return None
|
|
294
|
+
raw_entry = payload[key]
|
|
295
|
+
if not isinstance(raw_entry, dict):
|
|
296
|
+
msg = f"Expected wrapped Val object for {path}.{key}, got {type(raw_entry).__name__}"
|
|
297
|
+
raise DucoError(msg)
|
|
298
|
+
if "Val" not in raw_entry:
|
|
299
|
+
msg = f"Expected wrapped Val object for {path}.{key}"
|
|
300
|
+
raise DucoError(msg)
|
|
301
|
+
|
|
302
|
+
raw_value = raw_entry["Val"]
|
|
303
|
+
if type(raw_value) is not int:
|
|
304
|
+
msg = f"Expected integer value for {path}.{key}, got {type(raw_value).__name__}"
|
|
305
|
+
raise DucoError(msg)
|
|
306
|
+
|
|
307
|
+
return raw_value
|
|
308
|
+
|
|
284
309
|
@staticmethod
|
|
285
310
|
def _read_scalar_value(payload: dict[str, Any], key: str) -> Any:
|
|
286
311
|
raw_value = payload[key]
|
|
@@ -2085,6 +2110,55 @@ class DucoClient:
|
|
|
2085
2110
|
)
|
|
2086
2111
|
return int(self._read_wrapped_value(payload["General"]["PublicApi"], "WriteReqCntRemain"))
|
|
2087
2112
|
|
|
2113
|
+
async def async_get_time_filter_remaining(self) -> int | None:
|
|
2114
|
+
"""Return the remaining heat recovery filter time when the box reports it."""
|
|
2115
|
+
try:
|
|
2116
|
+
payload = await self.async_get_info(module=InfoModuleSelector.HEAT_RECOVERY)
|
|
2117
|
+
except DucoResponseError as err:
|
|
2118
|
+
if err.status == 400 and err.path == "/info":
|
|
2119
|
+
try:
|
|
2120
|
+
error_payload = json.loads(err.body)
|
|
2121
|
+
except json.JSONDecodeError:
|
|
2122
|
+
pass
|
|
2123
|
+
else:
|
|
2124
|
+
if (
|
|
2125
|
+
isinstance(error_payload, dict)
|
|
2126
|
+
and error_payload.get("Code") == 3
|
|
2127
|
+
and error_payload.get("Result") == "FAILED"
|
|
2128
|
+
):
|
|
2129
|
+
return None
|
|
2130
|
+
raise
|
|
2131
|
+
|
|
2132
|
+
if not isinstance(payload, dict):
|
|
2133
|
+
msg = (
|
|
2134
|
+
"Expected object payload from /info?module=HeatRecovery, got "
|
|
2135
|
+
f"{type(payload).__name__}"
|
|
2136
|
+
)
|
|
2137
|
+
raise DucoError(msg)
|
|
2138
|
+
|
|
2139
|
+
heat_recovery = payload.get("HeatRecovery")
|
|
2140
|
+
if heat_recovery is None:
|
|
2141
|
+
return None
|
|
2142
|
+
if not isinstance(heat_recovery, dict):
|
|
2143
|
+
msg = "Expected object payload at HeatRecovery in /info?module=HeatRecovery response"
|
|
2144
|
+
raise DucoError(msg)
|
|
2145
|
+
|
|
2146
|
+
general = heat_recovery.get("General")
|
|
2147
|
+
if general is None:
|
|
2148
|
+
return None
|
|
2149
|
+
if not isinstance(general, dict):
|
|
2150
|
+
msg = (
|
|
2151
|
+
"Expected object payload at HeatRecovery.General in "
|
|
2152
|
+
"/info?module=HeatRecovery response"
|
|
2153
|
+
)
|
|
2154
|
+
raise DucoError(msg)
|
|
2155
|
+
|
|
2156
|
+
return self._read_optional_wrapped_int(
|
|
2157
|
+
general,
|
|
2158
|
+
"TimeFilterRemain",
|
|
2159
|
+
path="HeatRecovery.General",
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2088
2162
|
async def async_get_write_req_remaining(self) -> int:
|
|
2089
2163
|
"""Backward-compatible alias for the old write budget method name."""
|
|
2090
2164
|
caller = _compat_caller()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-duco-connectivity
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Async HTTP client for the local Duco Connectivity API
|
|
5
5
|
Author: Ronald van der Meer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,6 +63,7 @@ in the development examples below.
|
|
|
63
63
|
- HTTP only
|
|
64
64
|
- asynchronous communication via `aiohttp`
|
|
65
65
|
- typed stable config families for the documented `/config` branches
|
|
66
|
+
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
66
67
|
- typed models that stay close to the API response shape
|
|
67
68
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
68
69
|
|
|
@@ -192,6 +193,7 @@ development pass, covering:
|
|
|
192
193
|
- `PATCH /config` with a no-op `TimeZone` write against the current value
|
|
193
194
|
- `GET /info?module=General&submodule=Board`
|
|
194
195
|
- `GET /info?module=General&submodule=Lan`
|
|
196
|
+
- `GET /info?module=HeatRecovery`
|
|
195
197
|
- `GET /info/nodes`
|
|
196
198
|
- `GET /info?module=General&submodule=PublicApi`
|
|
197
199
|
- `POST /action/nodes/{node}` with a no-op `SetVentilationState`
|
|
@@ -3666,6 +3666,103 @@ async def test_get_write_requests_remaining_is_parsed() -> None:
|
|
|
3666
3666
|
assert remaining == 197
|
|
3667
3667
|
|
|
3668
3668
|
|
|
3669
|
+
async def test_get_time_filter_remaining_is_parsed() -> None:
|
|
3670
|
+
"""Heat recovery filter time should be parsed when the box reports it."""
|
|
3671
|
+
payload: dict[str, object] = {"HeatRecovery": {"General": {"TimeFilterRemain": {"Val": 180}}}}
|
|
3672
|
+
mock_response = _response(json_payload=payload)
|
|
3673
|
+
|
|
3674
|
+
async with aiohttp.ClientSession() as session:
|
|
3675
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3676
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
3677
|
+
remaining = await client.async_get_time_filter_remaining()
|
|
3678
|
+
|
|
3679
|
+
assert remaining == 180
|
|
3680
|
+
|
|
3681
|
+
|
|
3682
|
+
async def test_get_time_filter_remaining_returns_none_when_not_reported() -> None:
|
|
3683
|
+
"""Heat recovery filter time should stay optional for boxes that omit it."""
|
|
3684
|
+
payload: dict[str, object] = {"HeatRecovery": {"General": {}}}
|
|
3685
|
+
mock_response = _response(json_payload=payload)
|
|
3686
|
+
|
|
3687
|
+
async with aiohttp.ClientSession() as session:
|
|
3688
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3689
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
3690
|
+
remaining = await client.async_get_time_filter_remaining()
|
|
3691
|
+
|
|
3692
|
+
assert remaining is None
|
|
3693
|
+
|
|
3694
|
+
|
|
3695
|
+
async def test_get_time_filter_remaining_returns_none_when_module_unsupported() -> None:
|
|
3696
|
+
"""Heat recovery filter time should stay optional for boxes without that module."""
|
|
3697
|
+
mock_response = _response(
|
|
3698
|
+
status=400,
|
|
3699
|
+
text_payload='{"Code":3,"Result":"FAILED"}',
|
|
3700
|
+
)
|
|
3701
|
+
|
|
3702
|
+
async with aiohttp.ClientSession() as session:
|
|
3703
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3704
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
3705
|
+
remaining = await client.async_get_time_filter_remaining()
|
|
3706
|
+
|
|
3707
|
+
assert remaining is None
|
|
3708
|
+
|
|
3709
|
+
|
|
3710
|
+
@pytest.mark.parametrize(
|
|
3711
|
+
("payload", "message"),
|
|
3712
|
+
[
|
|
3713
|
+
pytest.param(
|
|
3714
|
+
[],
|
|
3715
|
+
"Expected object payload from /info?module=HeatRecovery, got list",
|
|
3716
|
+
id="root_not_object",
|
|
3717
|
+
),
|
|
3718
|
+
pytest.param(
|
|
3719
|
+
{"HeatRecovery": []},
|
|
3720
|
+
"Expected object payload at HeatRecovery in /info?module=HeatRecovery response",
|
|
3721
|
+
id="heat_recovery_not_object",
|
|
3722
|
+
),
|
|
3723
|
+
pytest.param(
|
|
3724
|
+
{"HeatRecovery": {"General": []}},
|
|
3725
|
+
"Expected object payload at HeatRecovery.General in /info?module=HeatRecovery response",
|
|
3726
|
+
id="general_not_object",
|
|
3727
|
+
),
|
|
3728
|
+
pytest.param(
|
|
3729
|
+
{"HeatRecovery": {"General": {"TimeFilterRemain": 180}}},
|
|
3730
|
+
"Expected wrapped Val object for HeatRecovery.General.TimeFilterRemain, got int",
|
|
3731
|
+
id="leaf_not_object",
|
|
3732
|
+
),
|
|
3733
|
+
pytest.param(
|
|
3734
|
+
{"HeatRecovery": {"General": {"TimeFilterRemain": None}}},
|
|
3735
|
+
"Expected wrapped Val object for HeatRecovery.General.TimeFilterRemain, got NoneType",
|
|
3736
|
+
id="leaf_null",
|
|
3737
|
+
),
|
|
3738
|
+
pytest.param(
|
|
3739
|
+
{"HeatRecovery": {"General": {"TimeFilterRemain": {}}}},
|
|
3740
|
+
"Expected wrapped Val object for HeatRecovery.General.TimeFilterRemain",
|
|
3741
|
+
id="leaf_missing_val",
|
|
3742
|
+
),
|
|
3743
|
+
pytest.param(
|
|
3744
|
+
{"HeatRecovery": {"General": {"TimeFilterRemain": {"Val": "180"}}}},
|
|
3745
|
+
"Expected integer value for HeatRecovery.General.TimeFilterRemain, got str",
|
|
3746
|
+
id="leaf_non_int_val",
|
|
3747
|
+
),
|
|
3748
|
+
],
|
|
3749
|
+
)
|
|
3750
|
+
async def test_get_time_filter_remaining_rejects_malformed_payloads(
|
|
3751
|
+
payload: object,
|
|
3752
|
+
message: str,
|
|
3753
|
+
) -> None:
|
|
3754
|
+
"""Malformed heat recovery payloads should raise DucoError."""
|
|
3755
|
+
mock_response = _response(json_payload=payload)
|
|
3756
|
+
|
|
3757
|
+
async with aiohttp.ClientSession() as session:
|
|
3758
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3759
|
+
with (
|
|
3760
|
+
patch.object(session, "request", _request(mock_response)),
|
|
3761
|
+
pytest.raises(DucoError, match=re.escape(message)),
|
|
3762
|
+
):
|
|
3763
|
+
await client.async_get_time_filter_remaining()
|
|
3764
|
+
|
|
3765
|
+
|
|
3669
3766
|
async def test_get_write_req_remaining_alias_is_parsed() -> None:
|
|
3670
3767
|
"""The old write budget method name should remain available."""
|
|
3671
3768
|
payload: dict[str, object] = {"General": {"PublicApi": {"WriteReqCntRemain": {"Val": 197}}}}
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/__init__.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/__main__.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/src/duco_connectivity/models.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.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
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_api_reference.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_pytest_live_support.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.6.0 → python_duco_connectivity-0.7.1}/tests/test_replay_helpers.py
RENAMED
|
File without changes
|