python-duco-connectivity 0.8.0__tar.gz → 0.9.0__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.8.0/src/python_duco_connectivity.egg-info → python_duco_connectivity-0.9.0}/PKG-INFO +3 -1
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/README.md +2 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/pyproject.toml +1 -1
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/client.py +37 -20
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0/src/python_duco_connectivity.egg-info}/PKG-INFO +3 -1
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_client.py +56 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/LICENSE +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/setup.cfg +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/__init__.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/__main__.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/cli.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/exceptions.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/models.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/py.typed +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/python_duco_connectivity.egg-info/SOURCES.txt +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/python_duco_connectivity.egg-info/dependency_links.txt +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/python_duco_connectivity.egg-info/entry_points.txt +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/python_duco_connectivity.egg-info/requires.txt +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/python_duco_connectivity.egg-info/top_level.txt +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_api_reference.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_cli.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_exceptions.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_local_sample_validation.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_models.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_pytest_live_support.py +0 -0
- {python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/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.9.0
|
|
4
4
|
Summary: Async HTTP client for the local Duco Connectivity API
|
|
5
5
|
Author: Ronald van der Meer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -64,6 +64,8 @@ in the development examples below.
|
|
|
64
64
|
- asynchronous communication via `aiohttp`
|
|
65
65
|
- typed stable config families for the documented `/config` branches
|
|
66
66
|
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
67
|
+
- optional temperature helpers that return `None` when the box reports their
|
|
68
|
+
endpoint as unavailable
|
|
67
69
|
- typed models that stay close to the API response shape
|
|
68
70
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
69
71
|
|
|
@@ -32,6 +32,8 @@ in the development examples below.
|
|
|
32
32
|
- asynchronous communication via `aiohttp`
|
|
33
33
|
- typed stable config families for the documented `/config` branches
|
|
34
34
|
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
35
|
+
- optional temperature helpers that return `None` when the box reports their
|
|
36
|
+
endpoint as unavailable
|
|
35
37
|
- typed models that stay close to the API response shape
|
|
36
38
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
37
39
|
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/client.py
RENAMED
|
@@ -105,6 +105,23 @@ def _compat_caller() -> str | None:
|
|
|
105
105
|
return None
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _is_unsupported_optional_endpoint_error(error: DucoResponseError, path: str) -> bool:
|
|
109
|
+
"""Return whether a Duco response reports an unavailable optional endpoint."""
|
|
110
|
+
if error.status != 400 or error.path != path:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
error_payload = cast(object, json.loads(error.body))
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
if not isinstance(error_payload, dict):
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
response = cast(dict[str, object], error_payload)
|
|
122
|
+
return response.get("Code") == 3 and response.get("Result") == "FAILED"
|
|
123
|
+
|
|
124
|
+
|
|
108
125
|
class DucoClient:
|
|
109
126
|
"""Client for a Duco box that exposes the local HTTP API."""
|
|
110
127
|
|
|
@@ -2191,18 +2208,8 @@ class DucoClient:
|
|
|
2191
2208
|
try:
|
|
2192
2209
|
payload = await self.async_get_info(module=InfoModuleSelector.HEAT_RECOVERY)
|
|
2193
2210
|
except DucoResponseError as err:
|
|
2194
|
-
if err
|
|
2195
|
-
|
|
2196
|
-
error_payload = json.loads(err.body)
|
|
2197
|
-
except json.JSONDecodeError:
|
|
2198
|
-
pass
|
|
2199
|
-
else:
|
|
2200
|
-
if (
|
|
2201
|
-
isinstance(error_payload, dict)
|
|
2202
|
-
and error_payload.get("Code") == 3
|
|
2203
|
-
and error_payload.get("Result") == "FAILED"
|
|
2204
|
-
):
|
|
2205
|
-
return None
|
|
2211
|
+
if _is_unsupported_optional_endpoint_error(err, "/info"):
|
|
2212
|
+
return None
|
|
2206
2213
|
raise
|
|
2207
2214
|
|
|
2208
2215
|
if not isinstance(payload, dict):
|
|
@@ -2235,9 +2242,14 @@ class DucoClient:
|
|
|
2235
2242
|
path="HeatRecovery.General",
|
|
2236
2243
|
)
|
|
2237
2244
|
|
|
2238
|
-
async def async_get_ventilation_temperature_info(self) -> VentilationTemperatureInfo:
|
|
2239
|
-
"""Return ventilation temperatures
|
|
2240
|
-
|
|
2245
|
+
async def async_get_ventilation_temperature_info(self) -> VentilationTemperatureInfo | None:
|
|
2246
|
+
"""Return ventilation temperatures when the box exposes them, in Celsius."""
|
|
2247
|
+
try:
|
|
2248
|
+
payload = await self.async_get_info(module=InfoModuleSelector.VENTILATION)
|
|
2249
|
+
except DucoResponseError as err:
|
|
2250
|
+
if _is_unsupported_optional_endpoint_error(err, "/info"):
|
|
2251
|
+
return None
|
|
2252
|
+
raise
|
|
2241
2253
|
|
|
2242
2254
|
if not isinstance(payload, dict):
|
|
2243
2255
|
msg = (
|
|
@@ -2281,11 +2293,16 @@ class DucoClient:
|
|
|
2281
2293
|
) -> BypassSupplyTemperatureTarget | None:
|
|
2282
2294
|
"""Return a bypass supply target from `/config` in Celsius."""
|
|
2283
2295
|
parameter = self._validate_bypass_zone_id(zone_id)
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2296
|
+
try:
|
|
2297
|
+
config = await self.async_get_config(
|
|
2298
|
+
module=ConfigModuleSelector.HEAT_RECOVERY,
|
|
2299
|
+
submodule=ConfigHeatRecoverySubmoduleSelector.BYPASS,
|
|
2300
|
+
parameter=parameter,
|
|
2301
|
+
)
|
|
2302
|
+
except DucoResponseError as err:
|
|
2303
|
+
if _is_unsupported_optional_endpoint_error(err, "/config"):
|
|
2304
|
+
return None
|
|
2305
|
+
raise
|
|
2289
2306
|
return self._extract_bypass_supply_temperature_target(config, zone_id)
|
|
2290
2307
|
|
|
2291
2308
|
async def async_set_bypass_supply_temperature_target(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-duco-connectivity
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Async HTTP client for the local Duco Connectivity API
|
|
5
5
|
Author: Ronald van der Meer
|
|
6
6
|
License-Expression: MIT
|
|
@@ -64,6 +64,8 @@ in the development examples below.
|
|
|
64
64
|
- asynchronous communication via `aiohttp`
|
|
65
65
|
- typed stable config families for the documented `/config` branches
|
|
66
66
|
- typed helpers for stable `/info` fields such as heat recovery filter time
|
|
67
|
+
- optional temperature helpers that return `None` when the box reports their
|
|
68
|
+
endpoint as unavailable
|
|
67
69
|
- typed models that stay close to the API response shape
|
|
68
70
|
- preserved `raw_payload` data on typed response models for forward compatibility
|
|
69
71
|
|
|
@@ -3688,6 +3688,34 @@ async def test_get_ventilation_temperature_info_returns_empty_model_when_sensor_
|
|
|
3688
3688
|
assert payload.raw_payload == {}
|
|
3689
3689
|
|
|
3690
3690
|
|
|
3691
|
+
async def test_get_ventilation_temperature_info_returns_none_when_module_unsupported() -> None:
|
|
3692
|
+
"""Unsupported ventilation modules should be treated as an absent optional capability."""
|
|
3693
|
+
mock_response = _response(
|
|
3694
|
+
status=400,
|
|
3695
|
+
text_payload='{"Code":3,"Result":"FAILED"}',
|
|
3696
|
+
)
|
|
3697
|
+
|
|
3698
|
+
async with aiohttp.ClientSession() as session:
|
|
3699
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3700
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
3701
|
+
payload = await client.async_get_ventilation_temperature_info()
|
|
3702
|
+
|
|
3703
|
+
assert payload is None
|
|
3704
|
+
|
|
3705
|
+
|
|
3706
|
+
async def test_get_ventilation_temperature_info_reraises_other_client_errors() -> None:
|
|
3707
|
+
"""Unexpected ventilation endpoint failures should remain visible to callers."""
|
|
3708
|
+
mock_response = _response(status=400, text_payload='{"Code":4,"Result":"FAILED"}')
|
|
3709
|
+
|
|
3710
|
+
async with aiohttp.ClientSession() as session:
|
|
3711
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3712
|
+
with (
|
|
3713
|
+
patch.object(session, "request", _request(mock_response)),
|
|
3714
|
+
pytest.raises(DucoResponseError),
|
|
3715
|
+
):
|
|
3716
|
+
await client.async_get_ventilation_temperature_info()
|
|
3717
|
+
|
|
3718
|
+
|
|
3691
3719
|
@pytest.mark.parametrize(
|
|
3692
3720
|
("payload", "message"),
|
|
3693
3721
|
[
|
|
@@ -3772,6 +3800,34 @@ async def test_get_bypass_supply_temperature_target_returns_none_when_not_report
|
|
|
3772
3800
|
assert payload is None
|
|
3773
3801
|
|
|
3774
3802
|
|
|
3803
|
+
async def test_get_bypass_supply_temperature_target_returns_none_when_unsupported() -> None:
|
|
3804
|
+
"""Unsupported bypass targets should be treated as an absent optional capability."""
|
|
3805
|
+
mock_response = _response(
|
|
3806
|
+
status=400,
|
|
3807
|
+
text_payload='{"Code":3,"Result":"FAILED"}',
|
|
3808
|
+
)
|
|
3809
|
+
|
|
3810
|
+
async with aiohttp.ClientSession() as session:
|
|
3811
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3812
|
+
with patch.object(session, "request", _request(mock_response)):
|
|
3813
|
+
payload = await client.async_get_bypass_supply_temperature_target(1)
|
|
3814
|
+
|
|
3815
|
+
assert payload is None
|
|
3816
|
+
|
|
3817
|
+
|
|
3818
|
+
async def test_get_bypass_supply_temperature_target_reraises_other_client_errors() -> None:
|
|
3819
|
+
"""Unexpected bypass target failures should remain visible to callers."""
|
|
3820
|
+
mock_response = _response(status=400, text_payload='{"Code":4,"Result":"FAILED"}')
|
|
3821
|
+
|
|
3822
|
+
async with aiohttp.ClientSession() as session:
|
|
3823
|
+
client = DucoClient(session=session, host="192.0.2.94")
|
|
3824
|
+
with (
|
|
3825
|
+
patch.object(session, "request", _request(mock_response)),
|
|
3826
|
+
pytest.raises(DucoResponseError),
|
|
3827
|
+
):
|
|
3828
|
+
await client.async_get_bypass_supply_temperature_target(1)
|
|
3829
|
+
|
|
3830
|
+
|
|
3775
3831
|
@pytest.mark.parametrize("zone_id", [0, 9, "1"])
|
|
3776
3832
|
async def test_bypass_supply_temperature_helpers_reject_invalid_zone_ids(zone_id: object) -> None:
|
|
3777
3833
|
"""Bypass target helpers should validate the supported zone range."""
|
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/__init__.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/__main__.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/src/duco_connectivity/models.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/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.8.0 → python_duco_connectivity-0.9.0}/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.8.0 → python_duco_connectivity-0.9.0}/tests/test_pytest_live_support.py
RENAMED
|
File without changes
|
{python_duco_connectivity-0.8.0 → python_duco_connectivity-0.9.0}/tests/test_replay_helpers.py
RENAMED
|
File without changes
|