From 4555ef36cd3bb1aac1e3691bc328dcc68d120a51 Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Tue, 1 Sep 2026 07:16:34 +0200 Subject: [PATCH] Bump python-duco-connectivity to 0.14.0 (#180964) --- homeassistant/components/duco/coordinator.py | 55 +++++++------------- homeassistant/components/duco/manifest.json | 2 +- homeassistant/components/duco/number.py | 8 --- requirements_all.txt | 2 +- tests/components/duco/test_init.py | 21 ++++---- tests/components/duco/test_number.py | 26 --------- tests/components/duco/test_sensor.py | 13 ++--- 7 files changed, 39 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/duco/coordinator.py b/homeassistant/components/duco/coordinator.py index a07aa2501a13..94a6bc60025c 100644 --- a/homeassistant/components/duco/coordinator.py +++ b/homeassistant/components/duco/coordinator.py @@ -10,7 +10,6 @@ from duco_connectivity.exceptions import ( DucoConnectionError, DucoError, DucoResponseError, - DucoUnsupportedCapabilityError, ) from duco_connectivity.models import ( BoardInfo, @@ -53,8 +52,6 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]): config_entry: DucoConfigEntry board_info: BoardInfo _supports_time_filter_remain: bool - _supports_ventilation_temperatures: bool - _supports_bypass_supply_temperature_targets: bool _configured_node_names: dict[int, str] def __init__( @@ -74,8 +71,6 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]): self.client = client self._configured_node_names = {} self._supports_time_filter_remain = True - self._supports_ventilation_temperatures = True - self._supports_bypass_supply_temperature_targets = True async def _async_load_node_names(self) -> None: """Load configured Duco node names during setup.""" @@ -193,38 +188,28 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]): ventilation_temperatures = ( self.data.ventilation_temperatures if self.data else None ) - if self._supports_ventilation_temperatures: - try: - ventilation_temperatures = ( - await self.client.async_get_ventilation_temperature_info() - ) - except DucoUnsupportedCapabilityError: - ventilation_temperatures = None - self._supports_ventilation_temperatures = False - except DucoError as err: - _LOGGER.debug( - "Could not fetch Duco ventilation temperatures", exc_info=err - ) + try: + ventilation_temperatures = ( + await self.client.async_get_ventilation_temperature_info() + ) + except DucoError as err: + _LOGGER.debug("Could not fetch Duco ventilation temperatures", exc_info=err) bypass_supply_temperature_targets: dict[int, BypassSupplyTemperatureTarget] = {} - if self._supports_bypass_supply_temperature_targets: - try: - bypass_supply_temperature_targets = ( - await self.client.async_get_bypass_supply_temperature_targets() - ) - except DucoUnsupportedCapabilityError: - bypass_supply_temperature_targets = {} - self._supports_bypass_supply_temperature_targets = False - except DucoConnectionError as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="cannot_connect", - ) from err - except DucoError as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="api_error", - ) from err + try: + bypass_supply_temperature_targets = ( + await self.client.async_get_bypass_supply_temperature_targets() + ) + except DucoConnectionError as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except DucoError as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="api_error", + ) from err return DucoData( nodes={node.node_id: node for node in nodes}, diff --git a/homeassistant/components/duco/manifest.json b/homeassistant/components/duco/manifest.json index 5588f2f06c04..e38fcba8e341 100644 --- a/homeassistant/components/duco/manifest.json +++ b/homeassistant/components/duco/manifest.json @@ -13,7 +13,7 @@ "iot_class": "local_polling", "loggers": ["duco_connectivity"], "quality_scale": "platinum", - "requirements": ["python-duco-connectivity==0.13.1"], + "requirements": ["python-duco-connectivity==0.14.0"], "zeroconf": [ { "name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*", diff --git a/homeassistant/components/duco/number.py b/homeassistant/components/duco/number.py index c513aabd1a6a..60019b92c959 100644 --- a/homeassistant/components/duco/number.py +++ b/homeassistant/components/duco/number.py @@ -56,14 +56,6 @@ async def async_setup_entry( if (description.key, zone_id) in known_entities: continue - # Skip incomplete metadata because guessing valid limits would expose an invalid control. - if ( - target.minimum is None - or target.maximum is None - or target.increment is None - ): - continue - known_entities.add((description.key, zone_id)) new_entities.append( DucoBypassSupplyTemperatureTargetNumber( diff --git a/requirements_all.txt b/requirements_all.txt index ebecf42f146e..e3aad1a3aa7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2702,7 +2702,7 @@ python-digitalocean==1.13.2 python-dropbox-api==0.1.4 # homeassistant.components.duco -python-duco-connectivity==0.13.1 +python-duco-connectivity==0.14.0 # homeassistant.components.ecobee python-ecobee-api==0.4.1 diff --git a/tests/components/duco/test_init.py b/tests/components/duco/test_init.py index 703510838695..aca738a00a83 100644 --- a/tests/components/duco/test_init.py +++ b/tests/components/duco/test_init.py @@ -13,7 +13,6 @@ from duco_connectivity import ( DucoConnectionError, DucoError, DucoResponseError, - DucoUnsupportedCapabilityError, LanInfo, Node, NodeListActionItemList, @@ -268,20 +267,18 @@ async def test_setup_entry_retries_on_bypass_temperature_failure( assert mock_config_entry.error_reason_translation_placeholders is None -async def test_unsupported_bypass_temperature_capability_is_not_repolled( +async def test_empty_bypass_temperature_targets_are_retried( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + mock_bypass_supply_temperature_targets: dict[int, BypassSupplyTemperatureTarget], mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, ) -> None: - """Test an unsupported bulk bypass target endpoint is not polled again.""" - mock_duco_client.async_get_bypass_supply_temperature_targets.side_effect = ( - DucoUnsupportedCapabilityError( - 400, - "/config", - '{"Code":3,"Result":"FAILED"}', - ) - ) + """Test empty bypass targets are retried and can later create entities.""" + mock_duco_client.async_get_bypass_supply_temperature_targets.side_effect = [ + {}, + mock_bypass_supply_temperature_targets.copy(), + ] mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -296,7 +293,9 @@ async def test_unsupported_bypass_temperature_capability_is_not_repolled( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - mock_duco_client.async_get_bypass_supply_temperature_targets.assert_awaited_once_with() + assert mock_duco_client.async_get_bypass_supply_temperature_targets.await_count == 2 + assert hass.states.get("number.living_bypass_target_1") is not None + assert hass.states.get("number.living_bypass_target_2") is not None async def test_missing_bypass_temperature_targets_are_retried( diff --git a/tests/components/duco/test_number.py b/tests/components/duco/test_number.py index 5f910402520d..d731c2a1e8c0 100644 --- a/tests/components/duco/test_number.py +++ b/tests/components/duco/test_number.py @@ -92,32 +92,6 @@ async def test_bypass_supply_temperature_targets_missing_skips_number_creation( assert hass.states.get(_ZONE_2_ENTITY_ID) is None -@pytest.mark.parametrize( - "field", - [ - pytest.param("minimum", id="missing_minimum"), - pytest.param("maximum", id="missing_maximum"), - pytest.param("increment", id="missing_increment"), - ], -) -@pytest.mark.usefixtures("mock_duco_client") -async def test_bypass_supply_temperature_target_incomplete_metadata_skips_number_creation( - hass: HomeAssistant, - mock_bypass_supply_temperature_targets: dict[int, BypassSupplyTemperatureTarget], - mock_config_entry: MockConfigEntry, - field: str, -) -> None: - """Test incomplete target metadata does not expose an invalid control.""" - mock_bypass_supply_temperature_targets[1] = replace( - mock_bypass_supply_temperature_targets[1], **{field: None} - ) - - await setup_platform_integration(hass, mock_config_entry, [Platform.NUMBER]) - - assert hass.states.get(_ZONE_1_ENTITY_ID) is None - assert hass.states.get(_ZONE_2_ENTITY_ID) is not None - - @pytest.mark.usefixtures("init_integration") async def test_set_bypass_supply_temperature_target( hass: HomeAssistant, diff --git a/tests/components/duco/test_sensor.py b/tests/components/duco/test_sensor.py index 633b8e0b3bc0..0ab867c271d8 100644 --- a/tests/components/duco/test_sensor.py +++ b/tests/components/duco/test_sensor.py @@ -7,7 +7,6 @@ from unittest.mock import AsyncMock from duco_connectivity import ( DucoConnectionError, DucoError, - DucoUnsupportedCapabilityError, Node, NodeGeneralInfo, NodeSensorInfo, @@ -277,15 +276,15 @@ async def test_time_filter_remaining_missing_skips_sensor_creation( assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None -async def test_ventilation_temperatures_missing_skip_sensor_creation( +async def test_empty_ventilation_temperatures_are_retried( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: - """Test unsupported ventilation temperatures never expose temperature states.""" + """Test empty ventilation temperatures are retried and can appear later.""" mock_duco_client.async_get_ventilation_temperature_info.side_effect = [ - DucoUnsupportedCapabilityError(400, "/info", '{"Code":3,"Result":"FAILED"}'), + VentilationTemperatureInfo(), VentilationTemperatureInfo(temp_oda=5.5), ] @@ -298,8 +297,10 @@ async def test_ventilation_temperatures_missing_skip_sensor_creation( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - for entity_id in VENTILATION_TEMPERATURE_ENTITY_IDS: - assert hass.states.get(entity_id) is None + assert mock_duco_client.async_get_ventilation_temperature_info.await_count == 2 + state = hass.states.get("sensor.living_outdoor_air_temperature") + assert state is not None + assert state.state == "5.5" async def test_partial_ventilation_temperatures_only_expose_available_sensor_values(