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_MAX_TEMP,
|
||||||
ATTR_MIN_HUMIDITY,
|
ATTR_MIN_HUMIDITY,
|
||||||
ATTR_MIN_TEMP,
|
ATTR_MIN_TEMP,
|
||||||
|
ATTR_MIN_TEMP_RANGE,
|
||||||
ATTR_PRESET_MODE,
|
ATTR_PRESET_MODE,
|
||||||
ATTR_PRESET_MODES,
|
ATTR_PRESET_MODES,
|
||||||
ATTR_SWING_MODE,
|
ATTR_SWING_MODE,
|
||||||
|
@ -248,6 +249,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||||
"target_temperature_step",
|
"target_temperature_step",
|
||||||
"target_temperature_high",
|
"target_temperature_high",
|
||||||
"target_temperature_low",
|
"target_temperature_low",
|
||||||
|
"min_temperature_range",
|
||||||
"preset_mode",
|
"preset_mode",
|
||||||
"preset_modes",
|
"preset_modes",
|
||||||
"is_aux_heat",
|
"is_aux_heat",
|
||||||
|
@ -302,6 +304,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||||
_attr_target_humidity: float | None = None
|
_attr_target_humidity: float | None = None
|
||||||
_attr_target_temperature_high: float | None
|
_attr_target_temperature_high: float | None
|
||||||
_attr_target_temperature_low: float | None
|
_attr_target_temperature_low: float | None
|
||||||
|
_attr_min_temperature_range: float | None
|
||||||
_attr_target_temperature_step: float | None = None
|
_attr_target_temperature_step: float | None = None
|
||||||
_attr_target_temperature: float | None = None
|
_attr_target_temperature: float | None = None
|
||||||
_attr_temperature_unit: str
|
_attr_temperature_unit: str
|
||||||
|
@ -544,6 +547,37 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||||
hass, self.target_temperature_low, temperature_unit, precision
|
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:
|
if (current_humidity := self.current_humidity) is not None:
|
||||||
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
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
|
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
|
@cached_property
|
||||||
def preset_mode(self) -> str | None:
|
def preset_mode(self) -> str | None:
|
||||||
"""Return the current preset mode, e.g., home, away, temp.
|
"""Return the current preset mode, e.g., home, away, temp.
|
||||||
|
@ -948,6 +990,51 @@ async def async_service_temperature_set(
|
||||||
else:
|
else:
|
||||||
kwargs[value] = temp
|
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)
|
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_HIGH = "target_temp_high"
|
||||||
ATTR_TARGET_TEMP_LOW = "target_temp_low"
|
ATTR_TARGET_TEMP_LOW = "target_temp_low"
|
||||||
ATTR_TARGET_TEMP_STEP = "target_temp_step"
|
ATTR_TARGET_TEMP_STEP = "target_temp_step"
|
||||||
|
ATTR_MIN_TEMP_RANGE = "min_temperature_range"
|
||||||
|
|
||||||
DEFAULT_MIN_TEMP = 7
|
DEFAULT_MIN_TEMP = 7
|
||||||
DEFAULT_MAX_TEMP = 35
|
DEFAULT_MAX_TEMP = 35
|
||||||
|
|
|
@ -269,6 +269,9 @@
|
||||||
},
|
},
|
||||||
"temp_out_of_range": {
|
"temp_out_of_range": {
|
||||||
"message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}."
|
"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 |= (
|
self._attr_supported_features |= (
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
)
|
)
|
||||||
|
self._attr_min_temperature_range = 3
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from homeassistant.components.climate.const import (
|
||||||
ATTR_FAN_MODE,
|
ATTR_FAN_MODE,
|
||||||
ATTR_MAX_TEMP,
|
ATTR_MAX_TEMP,
|
||||||
ATTR_MIN_TEMP,
|
ATTR_MIN_TEMP,
|
||||||
|
ATTR_MIN_TEMP_RANGE,
|
||||||
ATTR_PRESET_MODE,
|
ATTR_PRESET_MODE,
|
||||||
ATTR_SWING_MODE,
|
ATTR_SWING_MODE,
|
||||||
ATTR_TARGET_TEMP_HIGH,
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
|
@ -1050,3 +1051,176 @@ async def test_temperature_validation(
|
||||||
state = hass.states.get("climate.test")
|
state = hass.states.get("climate.test")
|
||||||
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 10
|
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 10
|
||||||
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25
|
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