python-duco-connectivity 0.10.0__tar.gz → 0.12.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.
Files changed (26) hide show
  1. {python_duco_connectivity-0.10.0/src/python_duco_connectivity.egg-info → python_duco_connectivity-0.12.0}/PKG-INFO +5 -3
  2. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/README.md +4 -2
  3. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/pyproject.toml +1 -1
  4. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/client.py +38 -2
  5. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0/src/python_duco_connectivity.egg-info}/PKG-INFO +5 -3
  6. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_api_reference.py +1 -1
  7. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_client.py +83 -4
  8. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/LICENSE +0 -0
  9. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/setup.cfg +0 -0
  10. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/__init__.py +0 -0
  11. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/__main__.py +0 -0
  12. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/cli.py +0 -0
  13. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/exceptions.py +0 -0
  14. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/models.py +0 -0
  15. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/duco_connectivity/py.typed +0 -0
  16. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/python_duco_connectivity.egg-info/SOURCES.txt +0 -0
  17. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/python_duco_connectivity.egg-info/dependency_links.txt +0 -0
  18. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/python_duco_connectivity.egg-info/entry_points.txt +0 -0
  19. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/python_duco_connectivity.egg-info/requires.txt +0 -0
  20. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/src/python_duco_connectivity.egg-info/top_level.txt +0 -0
  21. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_cli.py +0 -0
  22. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_exceptions.py +0 -0
  23. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_local_sample_validation.py +0 -0
  24. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_models.py +0 -0
  25. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.0}/tests/test_pytest_live_support.py +0 -0
  26. {python_duco_connectivity-0.10.0 → python_duco_connectivity-0.12.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.10.0
3
+ Version: 0.12.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
@@ -76,8 +76,9 @@ bypass-target endpoint is unsupported, the relevant helper raises
76
76
  subclass and preserves the HTTP status, path, and response body. A valid
77
77
  ventilation-temperature response always returns a `VentilationTemperatureInfo`
78
78
  model, whose individual temperature fields can be `None` when omitted. The
79
- bypass-target helper returns `None` when its target is omitted from a valid
80
- response.
79
+ bypass-target helper returns a typed target model for successful parameter-
80
+ specific reads and raises `DucoError` if the requested target field is missing
81
+ from an otherwise valid `/config` response.
81
82
 
82
83
  Diagnostic subsystem reads now keep raw component and status strings from
83
84
  `Diag.SubSystems`, so future subsystem names or status values remain available
@@ -183,6 +184,7 @@ For local function probes without activating the environment first:
183
184
  .venv/bin/python -m duco_connectivity --host 192.168.1.10 call async_get_board_info
184
185
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_board_info
185
186
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_ventilation_temperature_info
187
+ .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_targets
186
188
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_target --kwargs '{"zone_id": 1}'
187
189
  ```
188
190
 
@@ -44,8 +44,9 @@ bypass-target endpoint is unsupported, the relevant helper raises
44
44
  subclass and preserves the HTTP status, path, and response body. A valid
45
45
  ventilation-temperature response always returns a `VentilationTemperatureInfo`
46
46
  model, whose individual temperature fields can be `None` when omitted. The
47
- bypass-target helper returns `None` when its target is omitted from a valid
48
- response.
47
+ bypass-target helper returns a typed target model for successful parameter-
48
+ specific reads and raises `DucoError` if the requested target field is missing
49
+ from an otherwise valid `/config` response.
49
50
 
50
51
  Diagnostic subsystem reads now keep raw component and status strings from
51
52
  `Diag.SubSystems`, so future subsystem names or status values remain available
@@ -151,6 +152,7 @@ For local function probes without activating the environment first:
151
152
  .venv/bin/python -m duco_connectivity --host 192.168.1.10 call async_get_board_info
152
153
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_board_info
153
154
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_ventilation_temperature_info
155
+ .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_targets
154
156
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_target --kwargs '{"zone_id": 1}'
155
157
  ```
156
158
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-duco-connectivity"
7
- version = "0.10.0"
7
+ version = "0.12.0"
8
8
  description = "Async HTTP client for the local Duco Connectivity API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -400,6 +400,19 @@ class DucoClient:
400
400
 
401
401
  return cls._build_bypass_supply_temperature_target(zone_id, raw_value)
402
402
 
403
+ @classmethod
404
+ def _extract_bypass_supply_temperature_targets(
405
+ cls,
406
+ config: Config,
407
+ ) -> dict[int, BypassSupplyTemperatureTarget]:
408
+ """Read all bypass supply targets from a typed config response."""
409
+ return {
410
+ zone_id: target
411
+ for zone_id in range(1, 9)
412
+ if (target := cls._extract_bypass_supply_temperature_target(config, zone_id))
413
+ is not None
414
+ }
415
+
403
416
  @staticmethod
404
417
  def _read_scalar_value(payload: dict[str, Any], key: str) -> Any:
405
418
  raw_value = payload[key]
@@ -2295,7 +2308,7 @@ class DucoClient:
2295
2308
  async def async_get_bypass_supply_temperature_target(
2296
2309
  self,
2297
2310
  zone_id: int,
2298
- ) -> BypassSupplyTemperatureTarget | None:
2311
+ ) -> BypassSupplyTemperatureTarget:
2299
2312
  """Return a bypass supply target from `/config` in Celsius."""
2300
2313
  parameter = self._validate_bypass_zone_id(zone_id)
2301
2314
  try:
@@ -2312,7 +2325,30 @@ class DucoClient:
2312
2325
  err.body,
2313
2326
  ) from err
2314
2327
  raise
2315
- return self._extract_bypass_supply_temperature_target(config, zone_id)
2328
+ target = self._extract_bypass_supply_temperature_target(config, zone_id)
2329
+ if target is None:
2330
+ msg = f"Expected {parameter} in /config response"
2331
+ raise DucoError(msg)
2332
+ return target
2333
+
2334
+ async def async_get_bypass_supply_temperature_targets(
2335
+ self,
2336
+ ) -> dict[int, BypassSupplyTemperatureTarget]:
2337
+ """Return all bypass supply targets exposed through `/config` in Celsius."""
2338
+ try:
2339
+ config = await self.async_get_config(
2340
+ module=ConfigModuleSelector.HEAT_RECOVERY,
2341
+ submodule=ConfigHeatRecoverySubmoduleSelector.BYPASS,
2342
+ )
2343
+ except DucoResponseError as err:
2344
+ if _is_unsupported_optional_endpoint_error(err, "/config"):
2345
+ raise DucoUnsupportedCapabilityError(
2346
+ err.status,
2347
+ err.path,
2348
+ err.body,
2349
+ ) from err
2350
+ raise
2351
+ return self._extract_bypass_supply_temperature_targets(config)
2316
2352
 
2317
2353
  async def async_set_bypass_supply_temperature_target(
2318
2354
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-duco-connectivity
3
- Version: 0.10.0
3
+ Version: 0.12.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
@@ -76,8 +76,9 @@ bypass-target endpoint is unsupported, the relevant helper raises
76
76
  subclass and preserves the HTTP status, path, and response body. A valid
77
77
  ventilation-temperature response always returns a `VentilationTemperatureInfo`
78
78
  model, whose individual temperature fields can be `None` when omitted. The
79
- bypass-target helper returns `None` when its target is omitted from a valid
80
- response.
79
+ bypass-target helper returns a typed target model for successful parameter-
80
+ specific reads and raises `DucoError` if the requested target field is missing
81
+ from an otherwise valid `/config` response.
81
82
 
82
83
  Diagnostic subsystem reads now keep raw component and status strings from
83
84
  `Diag.SubSystems`, so future subsystem names or status values remain available
@@ -183,6 +184,7 @@ For local function probes without activating the environment first:
183
184
  .venv/bin/python -m duco_connectivity --host 192.168.1.10 call async_get_board_info
184
185
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_board_info
185
186
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_ventilation_temperature_info
187
+ .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_targets
186
188
  .venv/bin/duco-probe --host 192.168.1.10 call async_get_bypass_supply_temperature_target --kwargs '{"zone_id": 1}'
187
189
  ```
188
190
 
@@ -90,7 +90,7 @@ def test_api_reference_documents_optional_capability_contracts() -> None:
90
90
  content = render_api_reference()
91
91
 
92
92
  assert "optional endpoint as unsupported" in content
93
- assert "target is omitted from an otherwise valid response" in content
93
+ assert "without the requested target field in the response" in content
94
94
  assert "optional target endpoint is unsupported" in content
95
95
 
96
96
 
@@ -3794,16 +3794,95 @@ async def test_get_bypass_supply_temperature_target_returns_converted_values(
3794
3794
  }
3795
3795
 
3796
3796
 
3797
- async def test_get_bypass_supply_temperature_target_returns_none_when_not_reported() -> None:
3798
- """Missing bypass targets should return None instead of failing."""
3797
+ async def test_get_bypass_supply_temperature_targets_returns_available_targets(
3798
+ config_data: dict[str, object],
3799
+ ) -> None:
3800
+ """Bulk bypass target reads should return all available targets in Celsius."""
3801
+ mock_response = _response(json_payload=config_data)
3802
+
3803
+ async with aiohttp.ClientSession() as session:
3804
+ client = DucoClient(session=session, host="192.0.2.94")
3805
+ request_mock = _request(mock_response)
3806
+ with patch.object(session, "request", request_mock):
3807
+ targets = await client.async_get_bypass_supply_temperature_targets()
3808
+
3809
+ assert targets == {
3810
+ 1: BypassSupplyTemperatureTarget(
3811
+ zone_id=1,
3812
+ value=18.0,
3813
+ minimum=12.0,
3814
+ increment=0.5,
3815
+ maximum=22.0,
3816
+ ),
3817
+ 2: BypassSupplyTemperatureTarget(
3818
+ zone_id=2,
3819
+ value=18.5,
3820
+ minimum=12.0,
3821
+ increment=0.5,
3822
+ maximum=22.0,
3823
+ ),
3824
+ }
3825
+ assert request_mock.call_args.kwargs["params"] == {
3826
+ "module": "HeatRecovery",
3827
+ "submodule": "Bypass",
3828
+ }
3829
+
3830
+
3831
+ async def test_get_bypass_supply_temperature_targets_returns_empty_when_absent() -> None:
3832
+ """Bulk bypass target reads should omit targets absent from the response."""
3799
3833
  mock_response = _response(json_payload={"HeatRecovery": {"Bypass": {}}})
3800
3834
 
3801
3835
  async with aiohttp.ClientSession() as session:
3802
3836
  client = DucoClient(session=session, host="192.0.2.94")
3803
3837
  with patch.object(session, "request", _request(mock_response)):
3804
- payload = await client.async_get_bypass_supply_temperature_target(1)
3838
+ targets = await client.async_get_bypass_supply_temperature_targets()
3805
3839
 
3806
- assert payload is None
3840
+ assert targets == {}
3841
+
3842
+
3843
+ async def test_get_bypass_supply_temperature_targets_raises_when_unsupported() -> None:
3844
+ """Unsupported bulk bypass target reads should raise a typed capability error."""
3845
+ mock_response = _response(
3846
+ status=400,
3847
+ text_payload='{"Code":3,"Result":"FAILED"}',
3848
+ )
3849
+
3850
+ async with aiohttp.ClientSession() as session:
3851
+ client = DucoClient(session=session, host="192.0.2.94")
3852
+ with (
3853
+ patch.object(session, "request", _request(mock_response)),
3854
+ pytest.raises(DucoUnsupportedCapabilityError),
3855
+ ):
3856
+ await client.async_get_bypass_supply_temperature_targets()
3857
+
3858
+
3859
+ async def test_get_bypass_supply_temperature_targets_reraises_other_client_errors() -> None:
3860
+ """Unexpected bulk bypass target failures should remain visible to callers."""
3861
+ mock_response = _response(status=400, text_payload='{"Code":4,"Result":"FAILED"}')
3862
+
3863
+ async with aiohttp.ClientSession() as session:
3864
+ client = DucoClient(session=session, host="192.0.2.94")
3865
+ with (
3866
+ patch.object(session, "request", _request(mock_response)),
3867
+ pytest.raises(DucoResponseError),
3868
+ ):
3869
+ await client.async_get_bypass_supply_temperature_targets()
3870
+
3871
+
3872
+ async def test_get_bypass_supply_temperature_target_raises_when_not_reported() -> None:
3873
+ """Missing bypass targets should fail for parameter-specific helper reads."""
3874
+ mock_response = _response(json_payload={"HeatRecovery": {"Bypass": {}}})
3875
+
3876
+ async with aiohttp.ClientSession() as session:
3877
+ client = DucoClient(session=session, host="192.0.2.94")
3878
+ with (
3879
+ patch.object(session, "request", _request(mock_response)),
3880
+ pytest.raises(
3881
+ DucoError,
3882
+ match=re.escape("Expected TempSupTgtZone1 in /config response"),
3883
+ ),
3884
+ ):
3885
+ await client.async_get_bypass_supply_temperature_target(1)
3807
3886
 
3808
3887
 
3809
3888
  async def test_get_bypass_supply_temperature_target_raises_when_unsupported() -> None: