diff --git a/homeassistant/components/duco/number.py b/homeassistant/components/duco/number.py index 60019b92c959..dcb87784a45a 100644 --- a/homeassistant/components/duco/number.py +++ b/homeassistant/components/duco/number.py @@ -1,6 +1,5 @@ """Number platform for the Duco integration.""" -from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal import logging from typing import override @@ -122,32 +121,18 @@ class DucoBypassSupplyTemperatureTargetNumber(DucoEntity, NumberEntity): ) return target.value if target else None - def _normalize_step_value(self, value: float) -> float: - """Normalize converted temperature values to the nearest supported native step.""" - if self.unit_of_measurement == self.native_unit_of_measurement: - return value - - # Home Assistant converts service values from the configured temperature - # unit first, which can land between valid Duco Celsius increments. - minimum = Decimal(str(self.native_min_value)) - step = Decimal(str(self.native_step)) - steps = ((Decimal(str(value)) - minimum) / step).to_integral_value( - rounding=ROUND_HALF_UP - ) - # Rounding up may overshoot when the range is not a whole number of steps. - max_steps = ( - (Decimal(str(self.native_max_value)) - minimum) / step - ).to_integral_value(rounding=ROUND_DOWN) - return float(minimum + (min(steps, max_steps) * step)) - @override async def async_set_native_value(self, value: float) -> None: """Set the bypass supply temperature target.""" - value = self._normalize_step_value(value) - if ( - (Decimal(str(value)) - Decimal(str(self.native_min_value))) - / Decimal(str(self.native_step)) - ) % 1 != 0: + target = self.coordinator.data.bypass_supply_temperature_targets[self._zone_id] + + try: + if self.unit_of_measurement != self.native_unit_of_measurement: + value = target.normalize_value(value) + await self.coordinator.client.async_set_bypass_supply_temperature_target( + self._zone_id, value, target=target + ) + except ValueError as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="invalid_bypass_supply_temperature_target_step", @@ -156,12 +141,7 @@ class DucoBypassSupplyTemperatureTargetNumber(DucoEntity, NumberEntity): "minimum": str(self.native_min_value), "increment": str(self.native_step), }, - ) - - try: - await self.coordinator.client.async_set_bypass_supply_temperature_target( - self._zone_id, value - ) + ) from err except DucoRateLimitError as err: _LOGGER.warning( "Duco write rate limit exceeded for bypass target zone %s", diff --git a/tests/components/duco/conftest.py b/tests/components/duco/conftest.py index d92956d82b89..8dbf86d35fdf 100644 --- a/tests/components/duco/conftest.py +++ b/tests/components/duco/conftest.py @@ -276,6 +276,18 @@ def mock_duco_client( mock_ventilation_temperature_info: VentilationTemperatureInfo, ) -> Generator[AsyncMock]: """Return a mocked DucoClient used by both the integration and config flow.""" + + def set_bypass_supply_temperature_target( + zone_id: int, + temperature: float, + *, + target: BypassSupplyTemperatureTarget, + ) -> None: + target.validate_value(temperature) + mock_bypass_supply_temperature_targets[zone_id] = replace( + target, value=temperature + ) + with ( patch( "homeassistant.components.duco.DucoClient", @@ -301,15 +313,7 @@ def mock_duco_client( mock_bypass_supply_temperature_targets.copy ) client.async_set_bypass_supply_temperature_target.side_effect = ( - lambda zone_id, temperature: ( - mock_bypass_supply_temperature_targets.__setitem__( - zone_id, - replace( - mock_bypass_supply_temperature_targets[zone_id], - value=temperature, - ), - ) - ) + set_bypass_supply_temperature_target ) client.async_get_diagnostics.return_value = [ DiagComponent(component="Ventilation", status="Ok") diff --git a/tests/components/duco/test_number.py b/tests/components/duco/test_number.py index d731c2a1e8c0..a5c2f9e273df 100644 --- a/tests/components/duco/test_number.py +++ b/tests/components/duco/test_number.py @@ -95,9 +95,12 @@ async def test_bypass_supply_temperature_targets_missing_skips_number_creation( @pytest.mark.usefixtures("init_integration") async def test_set_bypass_supply_temperature_target( hass: HomeAssistant, + mock_bypass_supply_temperature_targets: dict[int, BypassSupplyTemperatureTarget], mock_duco_client: AsyncMock, ) -> None: """Test setting a bypass target refreshes the number from the box.""" + target = mock_bypass_supply_temperature_targets[1] + await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -106,7 +109,7 @@ async def test_set_bypass_supply_temperature_target( ) mock_duco_client.async_set_bypass_supply_temperature_target.assert_awaited_once_with( - 1, 20.5 + 1, 20.5, target=target ) state = hass.states.get(_ZONE_1_ENTITY_ID) assert state is not None @@ -126,6 +129,7 @@ async def test_set_bypass_supply_temperature_target_honors_increment_metadata( increment=0.5, maximum=25.5, ) + target = mock_bypass_supply_temperature_targets[1] await setup_platform_integration(hass, mock_config_entry, [Platform.NUMBER]) @@ -137,7 +141,7 @@ async def test_set_bypass_supply_temperature_target_honors_increment_metadata( ) mock_duco_client.async_set_bypass_supply_temperature_target.assert_awaited_once_with( - 1, 20.5 + 1, 20.5, target=target ) with pytest.raises( @@ -166,6 +170,7 @@ async def test_set_bypass_supply_temperature_target_in_fahrenheit_units( increment=0.5, maximum=25.5, ) + target = mock_bypass_supply_temperature_targets[1] await setup_platform_integration(hass, mock_config_entry, [Platform.NUMBER]) @@ -177,7 +182,7 @@ async def test_set_bypass_supply_temperature_target_in_fahrenheit_units( ) mock_duco_client.async_set_bypass_supply_temperature_target.assert_awaited_once_with( - 1, 20.5 + 1, 20.5, target=target ) state = hass.states.get(_ZONE_1_ENTITY_ID) assert state is not None @@ -198,6 +203,7 @@ async def test_set_bypass_supply_temperature_target_stays_within_maximum( increment=0.5, maximum=24.8, ) + target = mock_bypass_supply_temperature_targets[1] await setup_platform_integration(hass, mock_config_entry, [Platform.NUMBER]) @@ -209,7 +215,7 @@ async def test_set_bypass_supply_temperature_target_stays_within_maximum( ) mock_duco_client.async_set_bypass_supply_temperature_target.assert_awaited_once_with( - 1, 24.5 + 1, 24.5, target=target )