mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Delegate Duco bypass target policy to the library (#180980)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user