Compare commits
10 commits
dev
...
climate-de
Author | SHA1 | Date | |
---|---|---|---|
|
cba8da13ae | ||
|
95d837e7ac | ||
|
5b5f0b5883 | ||
|
df18430e43 | ||
|
719ccc1395 | ||
|
dd177e1a4c | ||
|
b1056b60b5 | ||
|
0c1f23ffc8 | ||
|
4d7282fb46 | ||
|
11885b69a6 |
5 changed files with 266 additions and 0 deletions
|
@ -67,6 +67,7 @@ from .const import ( # noqa: F401
|
|||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_MIN_TEMP_RANGE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_SWING_MODE,
|
||||
|
@ -248,6 +249,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||
"target_temperature_step",
|
||||
"target_temperature_high",
|
||||
"target_temperature_low",
|
||||
"min_temperature_range",
|
||||
"preset_mode",
|
||||
"preset_modes",
|
||||
"is_aux_heat",
|
||||
|
@ -302,6 +304,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||
_attr_target_humidity: float | None = None
|
||||
_attr_target_temperature_high: float | None
|
||||
_attr_target_temperature_low: float | None
|
||||
_attr_min_temperature_range: float | None
|
||||
_attr_target_temperature_step: float | None = None
|
||||
_attr_target_temperature: float | None = None
|
||||
_attr_temperature_unit: str
|
||||
|
@ -544,6 +547,37 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
hass, self.target_temperature_low, temperature_unit, precision
|
||||
)
|
||||
if (
|
||||
hasattr(self, "min_temperature_range")
|
||||
and (min_temp_range := self.min_temperature_range) is not None
|
||||
):
|
||||
data[ATTR_MIN_TEMP_RANGE] = min_temp_range
|
||||
if (
|
||||
self.hass.config.units.temperature_unit != temperature_unit
|
||||
and temperature_unit == UnitOfTemperature.CELSIUS
|
||||
and (
|
||||
show_temp_range := show_temp(
|
||||
hass,
|
||||
min_temp_range,
|
||||
temperature_unit,
|
||||
precision,
|
||||
)
|
||||
)
|
||||
):
|
||||
data[ATTR_MIN_TEMP_RANGE] = show_temp_range - 32
|
||||
elif (
|
||||
self.hass.config.units.temperature_unit != temperature_unit
|
||||
and temperature_unit == UnitOfTemperature.FAHRENHEIT
|
||||
and (
|
||||
show_temp_range := show_temp(
|
||||
hass,
|
||||
min_temp_range,
|
||||
temperature_unit,
|
||||
precision,
|
||||
)
|
||||
)
|
||||
):
|
||||
data[ATTR_MIN_TEMP_RANGE] = show_temp_range + 32
|
||||
|
||||
if (current_humidity := self.current_humidity) is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
||||
|
@ -634,6 +668,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||
"""
|
||||
return self._attr_target_temperature_low
|
||||
|
||||
@cached_property
|
||||
def min_temperature_range(self) -> float | None:
|
||||
"""Return the minimum setpoint deadband when using a temperature range.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
return self._attr_min_temperature_range
|
||||
|
||||
@cached_property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
@ -948,6 +990,51 @@ async def async_service_temperature_set(
|
|||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
if not hasattr(entity, "min_temperature_range"):
|
||||
await entity.async_set_temperature(**kwargs)
|
||||
return
|
||||
|
||||
if (
|
||||
(min_temp_range := entity.min_temperature_range)
|
||||
and (target_low_temp := kwargs.get(ATTR_TARGET_TEMP_LOW))
|
||||
and (target_high_temp := kwargs.get(ATTR_TARGET_TEMP_HIGH))
|
||||
and (target_high_temp - target_low_temp) < min_temp_range
|
||||
):
|
||||
# Ensure target_low_temp is not higher than target_high_temp.
|
||||
if target_low_temp > target_high_temp:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="low_temp_higher_than_high_temp",
|
||||
)
|
||||
|
||||
# Ensure deadband between target temperatures.
|
||||
initial_low_temp = target_low_temp
|
||||
initial_high_temp = target_high_temp
|
||||
if (target_high_temp - target_low_temp) < min_temp_range:
|
||||
target_low_temp = target_high_temp - min_temp_range
|
||||
# Increasing the target temperatures if needed when
|
||||
# target_low_temp is below min_temp
|
||||
if target_low_temp < min_temp:
|
||||
target_low_temp = min_temp
|
||||
target_high_temp = target_low_temp + min_temp_range
|
||||
|
||||
_LOGGER.debug(
|
||||
"Moved low temperature from %d %s to %d %s and high temperature %d %s to %d %s"
|
||||
" due to deadband set to %d",
|
||||
initial_low_temp,
|
||||
temp_unit,
|
||||
target_low_temp,
|
||||
temp_unit,
|
||||
initial_high_temp,
|
||||
temp_unit,
|
||||
target_high_temp,
|
||||
temp_unit,
|
||||
min_temp_range,
|
||||
)
|
||||
|
||||
kwargs[ATTR_TARGET_TEMP_HIGH] = target_high_temp
|
||||
kwargs[ATTR_TARGET_TEMP_LOW] = target_low_temp
|
||||
|
||||
await entity.async_set_temperature(**kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ ATTR_SWING_MODE = "swing_mode"
|
|||
ATTR_TARGET_TEMP_HIGH = "target_temp_high"
|
||||
ATTR_TARGET_TEMP_LOW = "target_temp_low"
|
||||
ATTR_TARGET_TEMP_STEP = "target_temp_step"
|
||||
ATTR_MIN_TEMP_RANGE = "min_temperature_range"
|
||||
|
||||
DEFAULT_MIN_TEMP = 7
|
||||
DEFAULT_MAX_TEMP = 35
|
||||
|
|
|
@ -269,6 +269,9 @@
|
|||
},
|
||||
"temp_out_of_range": {
|
||||
"message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}."
|
||||
},
|
||||
"low_temp_higher_than_high_temp": {
|
||||
"message": "Target low temperature can not be higher than target high temperature."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ class DemoClimate(ClimateEntity):
|
|||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
self._attr_min_temperature_range = 3
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ from homeassistant.components.climate.const import (
|
|||
ATTR_FAN_MODE,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_MIN_TEMP_RANGE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
|
@ -1050,3 +1051,176 @@ async def test_temperature_validation(
|
|||
state = hass.states.get("climate.test")
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 10
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25
|
||||
|
||||
|
||||
async def test_temperature_range_deadband(
|
||||
hass: HomeAssistant,
|
||||
register_test_integration: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test temperature range with deadband."""
|
||||
|
||||
class MockClimateEntityTargetTemp(MockClimateEntity):
|
||||
"""Mock climate class with mocked aux heater."""
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.SWING_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_min_temperature_range = 3
|
||||
_attr_target_temperature_high = 16
|
||||
_attr_target_temperature_low = 11
|
||||
_attr_max_temp = 20
|
||||
_attr_min_temp = 10
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
self._attr_target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
self._attr_target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
|
||||
test_climate = MockClimateEntityTargetTemp(
|
||||
name="Test",
|
||||
unique_id="unique_climate_test",
|
||||
)
|
||||
|
||||
setup_test_component_platform(
|
||||
hass, DOMAIN, entities=[test_climate], from_config_entry=True
|
||||
)
|
||||
await hass.config_entries.async_setup(register_test_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("climate.test")
|
||||
assert state is not None
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 15,
|
||||
ATTR_TARGET_TEMP_LOW: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 10.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 15.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 3.0
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 20,
|
||||
ATTR_TARGET_TEMP_LOW: 11,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 11.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 20.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 3.0
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 20,
|
||||
ATTR_TARGET_TEMP_LOW: 18,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 17.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 20.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 3.0
|
||||
|
||||
assert (
|
||||
"Moved low temperature from 18 °C to 17 °C and high temperature 20 °C to 20 °C due to deadband set to 3"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
# Raises as not allowed for min temp to be higher than high temp
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 18,
|
||||
ATTR_TARGET_TEMP_LOW: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 12,
|
||||
ATTR_TARGET_TEMP_LOW: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 10.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 13.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 3.0
|
||||
|
||||
hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
|
||||
# Does not raise as 5 degrees Fahrenheit is within the 3 degrees Celsius range
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 60, # 15.5 C
|
||||
ATTR_TARGET_TEMP_LOW: 55, # 12.7 C
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 55.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 60.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 5.0
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 60,
|
||||
ATTR_TARGET_TEMP_LOW: 56,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 55.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 60.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 5.0
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
"entity_id": "climate.test",
|
||||
ATTR_TARGET_TEMP_HIGH: 54,
|
||||
ATTR_TARGET_TEMP_LOW: 50,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("climate.test")
|
||||
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 50.0
|
||||
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 55.0
|
||||
assert state.attributes[ATTR_MIN_TEMP_RANGE] == 5.0
|
||||
|
||||
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue