Add climate platform to template integration (#173033)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Petro31
2026-09-16 20:31:59 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent de97442384
commit e48779cbf9
10 changed files with 3645 additions and 4 deletions
@@ -0,0 +1,897 @@
"""Support for Template climates."""
from collections.abc import Callable
import contextlib
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal
from typing import TYPE_CHECKING, Any, Self, override
import probatio
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN,
ENTITY_ID_FORMAT,
ClimateEntity,
ClimateEntityCapabilityAttribute,
ClimateEntityFeature,
ClimateEntityStateAttribute,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_NAME,
CONF_TEMPERATURE_UNIT,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.unit_conversion import TemperatureConverter
from . import TriggerUpdateCoordinator, validators as tcv
from .const import DOMAIN
from .entity import AbstractTemplateEntity
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .schemas import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
make_template_entity_common_schema,
)
from .template_entity import TemplateEntity
from .trigger_entity import TriggerEntity
DEFAULT_NAME = "Template Climate"
CONF_CURRENT_HUMIDITY = "current_humidity"
CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_FAN_MODE = "fan_mode"
CONF_FAN_MODES = "fan_modes"
CONF_HVAC_ACTION = "hvac_action"
CONF_HVAC_MODE = "hvac_mode"
CONF_HVAC_MODES = "hvac_modes"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_MAX_TEMPERATURE = "max_temperature"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MIN_TEMPERATURE = "min_temperature"
CONF_PRECISION = "precision"
CONF_PRESET_MODE = "preset_mode"
CONF_PRESET_MODES = "preset_modes"
CONF_SWING_HORIZONTAL_MODE = "swing_horizontal_mode"
CONF_SWING_HORIZONTAL_MODES = "swing_horizontal_modes"
CONF_SWING_MODE = "swing_mode"
CONF_SWING_MODES = "swing_modes"
CONF_TARGET_HUMIDITY = "target_humidity"
CONF_TARGET_HUMIDITY_STEP = "target_humidity_step"
CONF_TARGET_TEMPERATURE = "target_temperature"
CONF_TARGET_TEMPERATURE_HIGH = "target_temperature_high"
CONF_TARGET_TEMPERATURE_LOW = "target_temperature_low"
CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step"
SET_FAN_MODE_ACTION = "set_fan_mode"
SET_HUMIDITY_ACTION = "set_humidity"
SET_HVAC_MODE_ACTION = "set_hvac_mode"
SET_PRESET_MODE_ACTION = "set_preset_mode"
SET_SWING_HORIZONTAL_MODE_ACTION = "set_swing_horizontal_mode"
SET_SWING_MODE_ACTION = "set_swing_mode"
SET_TEMPERATURE_ACTION = "set_temperature"
SCRIPT_FIELDS = (
SET_FAN_MODE_ACTION,
SET_HUMIDITY_ACTION,
SET_HVAC_MODE_ACTION,
SET_PRESET_MODE_ACTION,
SET_SWING_HORIZONTAL_MODE_ACTION,
SET_SWING_MODE_ACTION,
SET_TEMPERATURE_ACTION,
)
_BLOCKED_ATTRIBUTES = tcv.BlockedTemplateAttributes(
attributes=(ClimateEntityCapabilityAttribute, ClimateEntityStateAttribute)
)
def _round_to_step(value: float, step: float) -> float:
"""Round a temperature to the nearest step using half-up midpoint handling."""
decimal_value = Decimal(str(value))
decimal_step = Decimal(str(step))
return float(
(decimal_value / decimal_step).quantize(0, ROUND_HALF_UP) * decimal_step
)
CLIMATE_COMMON_SCHEMA = probatio.Schema(
{
probatio.Optional(CONF_CURRENT_HUMIDITY): cv.template,
probatio.Optional(CONF_CURRENT_TEMPERATURE): cv.template,
probatio.Optional(CONF_FAN_MODE): cv.template,
probatio.Optional(CONF_FAN_MODES): cv.template,
probatio.Optional(CONF_HVAC_ACTION): cv.template,
probatio.Optional(CONF_HVAC_MODE): cv.template,
probatio.Required(CONF_HVAC_MODES): cv.template,
probatio.Optional(CONF_MAX_HUMIDITY): probatio.Coerce(int),
probatio.Optional(CONF_MAX_TEMPERATURE): probatio.Coerce(float),
probatio.Optional(CONF_MIN_HUMIDITY): probatio.Coerce(int),
probatio.Optional(CONF_MIN_TEMPERATURE): probatio.Coerce(float),
probatio.Optional(CONF_PRECISION): probatio.Any(
PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE
),
probatio.Optional(CONF_PRESET_MODE): cv.template,
probatio.Optional(CONF_PRESET_MODES): cv.template,
probatio.Optional(CONF_SWING_MODE): cv.template,
probatio.Optional(CONF_SWING_MODES): cv.template,
probatio.Optional(CONF_SWING_HORIZONTAL_MODE): cv.template,
probatio.Optional(CONF_SWING_HORIZONTAL_MODES): cv.template,
probatio.Optional(CONF_TARGET_HUMIDITY): cv.template,
probatio.Optional(CONF_TARGET_HUMIDITY_STEP): probatio.All(
probatio.Coerce(int), probatio.Range(min=1)
),
probatio.Inclusive(
CONF_TARGET_TEMPERATURE_HIGH, "temperature_limits"
): cv.template,
probatio.Inclusive(
CONF_TARGET_TEMPERATURE_LOW, "temperature_limits"
): cv.template,
probatio.Optional(CONF_TARGET_TEMPERATURE_STEP): probatio.All(
probatio.Coerce(float), probatio.Range(min=PRECISION_TENTHS)
),
probatio.Optional(CONF_TARGET_TEMPERATURE): cv.template,
probatio.Optional(CONF_TEMPERATURE_UNIT): probatio.In(
TemperatureConverter.VALID_UNITS
),
probatio.Optional(SET_FAN_MODE_ACTION): cv.SCRIPT_SCHEMA,
probatio.Optional(SET_HUMIDITY_ACTION): cv.SCRIPT_SCHEMA,
probatio.Required(SET_HVAC_MODE_ACTION): cv.SCRIPT_SCHEMA,
probatio.Optional(SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
probatio.Optional(SET_SWING_HORIZONTAL_MODE_ACTION): cv.SCRIPT_SCHEMA,
probatio.Optional(SET_SWING_MODE_ACTION): cv.SCRIPT_SCHEMA,
probatio.Optional(SET_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
},
)
_CLIMATE_VALIDATION = (
tcv.inclusive_group("fan_mode", CONF_FAN_MODE, CONF_FAN_MODES, SET_FAN_MODE_ACTION),
tcv.inclusive_group(
"preset_mode", CONF_PRESET_MODE, CONF_PRESET_MODES, SET_PRESET_MODE_ACTION
),
tcv.inclusive_group(
"swing_mode", CONF_SWING_MODE, CONF_SWING_MODES, SET_SWING_MODE_ACTION
),
tcv.inclusive_group(
"swing_horizontal_mode",
CONF_SWING_HORIZONTAL_MODE,
CONF_SWING_HORIZONTAL_MODES,
SET_SWING_HORIZONTAL_MODE_ACTION,
),
tcv.requires_option(CONF_TARGET_HUMIDITY, SET_HUMIDITY_ACTION),
tcv.requires_option(CONF_TARGET_TEMPERATURE, SET_TEMPERATURE_ACTION),
tcv.requires_option(CONF_TARGET_TEMPERATURE_HIGH, SET_TEMPERATURE_ACTION),
tcv.requires_option(CONF_TARGET_TEMPERATURE_LOW, SET_TEMPERATURE_ACTION),
)
CLIMATE_YAML_SCHEMA = probatio.All(
CLIMATE_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA).extend(
make_template_entity_common_schema(
CLIMATE_DOMAIN,
DEFAULT_NAME,
_BLOCKED_ATTRIBUTES,
).schema
),
*_CLIMATE_VALIDATION,
)
CLIMATE_CONFIG_ENTRY_SCHEMA = probatio.All(
CLIMATE_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema),
*_CLIMATE_VALIDATION,
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the template climates."""
await async_setup_template_platform(
hass,
CLIMATE_DOMAIN,
config,
StateClimateEntity,
TriggerClimateEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateClimateEntity,
CLIMATE_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)
@callback
def async_create_preview_climate(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateClimateEntity:
"""Create a preview."""
return async_setup_template_preview(
hass,
name,
config,
StateClimateEntity,
CLIMATE_CONFIG_ENTRY_SCHEMA,
)
def _string_to_list(result: str) -> list[str]:
for char in "()[] ":
result = result.replace(char, "")
return list(result.split(","))
def hvac_modes_list(
entity: AbstractTemplateClimate,
) -> Callable[[Any], list[HVACMode] | None]:
"""Convert the result to a list of numbers that represent hvac modes."""
expected = f"expected a list of hvac modes: [{', '.join([str(item) for item in HVACMode])}]"
def convert(result: Any) -> list[HVACMode] | None:
if tcv.check_result_for_none(result):
return []
if isinstance(result, str):
with contextlib.suppress(ValueError):
result = _string_to_list(result)
if isinstance(result, (list, tuple)) and all(
isinstance(value, str) for value in result
):
validated = []
invalid = []
for item in result:
if item in HVACMode:
validated.append(HVACMode(item))
else:
invalid.append(item)
if invalid:
tcv.log_validation_result_error(
entity, CONF_HVAC_MODES, result, expected
)
return validated
tcv.log_validation_result_error(entity, CONF_HVAC_MODES, result, expected)
return []
return convert
@dataclass(kw_only=True)
class ClimateExtraStoredData(ExtraStoredData):
"""Object to hold extra stored data."""
current_humidity: float | None
current_temperature: float | None
fan_mode: str | None
fan_modes: list[str] | None
hvac_action: HVACAction | None
hvac_mode: HVACMode | None
hvac_modes: list[HVACMode] | None
preset_mode: str | None
preset_modes: list[str] | None
swing_mode: str | None
swing_modes: list[str] | None
swing_horizontal_mode: str | None
swing_horizontal_modes: list[str] | None
target_humidity: float | None = None
target_temperature_high: float | None
target_temperature_low: float | None
target_temperature: float | None
@override
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the extra data."""
return {
"current_humidity": self.current_humidity,
"current_temperature": self.current_temperature,
"fan_mode": self.fan_mode,
"fan_modes": self.fan_modes,
"hvac_action": self.hvac_action.value if self.hvac_action else None,
"hvac_mode": self.hvac_mode.value if self.hvac_mode else None,
"hvac_modes": (
[mode.value for mode in self.hvac_modes] if self.hvac_modes else None
),
"preset_mode": self.preset_mode,
"preset_modes": self.preset_modes,
"swing_mode": self.swing_mode,
"swing_modes": self.swing_modes,
"swing_horizontal_mode": self.swing_horizontal_mode,
"swing_horizontal_modes": self.swing_horizontal_modes,
"target_humidity": self.target_humidity,
"target_temperature_high": self.target_temperature_high,
"target_temperature_low": self.target_temperature_low,
"target_temperature": self.target_temperature,
}
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
"""Initialize a stored state from a dict."""
try:
hvac_action: HVACAction | None = None
if _hvac_action := restored["hvac_action"]:
hvac_action = HVACAction(_hvac_action)
hvac_mode: HVACMode | None = None
if _hvac_mode := restored["hvac_mode"]:
hvac_mode = HVACMode(_hvac_mode)
hvac_modes: list[HVACMode] | None = None
if _hvac_modes := restored["hvac_modes"]:
hvac_modes = [HVACMode(item) for item in _hvac_modes]
return cls(
current_humidity=restored["current_humidity"],
current_temperature=restored["current_temperature"],
fan_mode=restored["fan_mode"],
fan_modes=restored["fan_modes"],
hvac_action=hvac_action,
hvac_mode=hvac_mode,
hvac_modes=hvac_modes,
preset_mode=restored["preset_mode"],
preset_modes=restored["preset_modes"],
swing_mode=restored["swing_mode"],
swing_modes=restored["swing_modes"],
swing_horizontal_mode=restored["swing_horizontal_mode"],
swing_horizontal_modes=restored["swing_horizontal_modes"],
target_humidity=restored["target_humidity"],
target_temperature_high=restored["target_temperature_high"],
target_temperature_low=restored["target_temperature_low"],
target_temperature=restored["target_temperature"],
)
except KeyError, ValueError:
return None
class AbstractTemplateClimate(AbstractTemplateEntity, ClimateEntity, RestoreEntity):
"""Representation of template climate features."""
_entity_id_format = ENTITY_ID_FORMAT
_optimistic_entity = True
_state_option = CONF_HVAC_MODE
_restore_state_extra_data = ClimateExtraStoredData
_restore_state_properties = ("_attr_hvac_mode",)
_blocked_attributes = _BLOCKED_ATTRIBUTES
# The super init is not called because TemplateEntity
# and TriggerEntity will call
# AbstractTemplateEntity.__init__. This ensures that
# the __init__ on AbstractTemplateEntity is not
# called twice.
def __init__( # pylint: disable=super-init-not-called
self, hass: HomeAssistant, name: str, config: dict[str, Any]
) -> None:
"""Initialize the features."""
self._attr_temperature_unit = (
config.get(CONF_TEMPERATURE_UNIT) or hass.config.units.temperature_unit
)
self._attr_target_humidity_step = config.get(CONF_TARGET_HUMIDITY_STEP)
self._attr_target_temperature_step = config.get(CONF_TARGET_TEMPERATURE_STEP)
# Only set these options when it exists in the configuration in order
# to properly use default values set by the upstream class.
for attr, option in (
("_attr_max_temp", CONF_MAX_TEMPERATURE),
("_attr_min_temp", CONF_MIN_TEMPERATURE),
("_attr_max_humidity", CONF_MAX_HUMIDITY),
("_attr_min_humidity", CONF_MIN_HUMIDITY),
("_attr_precision", CONF_PRECISION),
):
if (option_value := config.get(option)) is not None:
setattr(self, attr, option_value)
self._attr_hvac_mode = None
self._attr_hvac_modes = []
self._attr_fan_mode = None
self._attr_fan_modes = None
self._attr_preset_mode = None
self._attr_preset_modes = None
self._attr_swing_mode = None
self._attr_swing_modes = None
self._attr_swing_horizontal_mode = None
self._attr_swing_horizontal_modes = None
self._attr_target_temperature_low = None
self._attr_target_temperature_high = None
self.setup_template(
CONF_HVAC_MODES,
"_attr_hvac_modes",
hvac_modes_list(self),
self._update_hvac_modes,
none_on_template_error=False,
)
self.setup_state_template(
"_attr_hvac_mode",
tcv.item_in_list(self, CONF_HVAC_MODE, "_attr_hvac_modes", CONF_HVAC_MODES),
self._update_hvac_mode,
)
self.setup_template(
CONF_HVAC_ACTION,
"_attr_hvac_action",
tcv.strenum(self, CONF_HVAC_ACTION, HVACAction),
)
self.add_assumed_attribute(
"_attr_hvac_action", CONF_HVAC_ACTION, SET_HVAC_MODE_ACTION
)
self.setup_template(
CONF_CURRENT_TEMPERATURE,
"_attr_current_temperature",
tcv.number(self, CONF_CURRENT_TEMPERATURE),
)
for option, attr in (
(
CONF_TARGET_TEMPERATURE,
"_attr_target_temperature",
),
(CONF_TARGET_TEMPERATURE_LOW, "_attr_target_temperature_low"),
(CONF_TARGET_TEMPERATURE_HIGH, "_attr_target_temperature_high"),
):
self.setup_template(
option,
attr,
tcv.number(self, option, self.min_temp, self.max_temp),
on_update=self._update_target_temperature(attr),
)
self.add_assumed_attribute(attr, option, SET_TEMPERATURE_ACTION)
self.setup_template(
CONF_TARGET_HUMIDITY,
"_attr_target_humidity",
tcv.number(
self,
CONF_TARGET_HUMIDITY,
self._attr_min_humidity,
self._attr_max_humidity,
int,
),
self._update_target_humidity,
)
self.add_assumed_attribute(
"_attr_target_humidity", CONF_TARGET_HUMIDITY, SET_HUMIDITY_ACTION
)
self.setup_template(
CONF_CURRENT_HUMIDITY,
"_attr_current_humidity",
tcv.number(self, CONF_CURRENT_HUMIDITY, 0, 100, int),
)
self.setup_template(
CONF_FAN_MODES,
"_attr_fan_modes",
tcv.list_of_strings(self, CONF_FAN_MODES),
)
self.setup_template(
CONF_FAN_MODE,
"_attr_fan_mode",
tcv.item_in_list(self, CONF_FAN_MODE, "_attr_fan_modes", CONF_FAN_MODES),
)
self.add_assumed_attribute("_attr_fan_mode", CONF_FAN_MODE, SET_FAN_MODE_ACTION)
self.setup_template(
CONF_SWING_MODES,
"_attr_swing_modes",
tcv.list_of_strings(self, CONF_SWING_MODES),
)
self.setup_template(
CONF_SWING_MODE,
"_attr_swing_mode",
tcv.item_in_list(
self, CONF_SWING_MODE, "_attr_swing_modes", CONF_SWING_MODES
),
)
self.add_assumed_attribute(
"_attr_swing_mode", CONF_SWING_MODE, SET_SWING_MODE_ACTION
)
self.setup_template(
CONF_SWING_HORIZONTAL_MODES,
"_attr_swing_horizontal_modes",
tcv.list_of_strings(self, CONF_SWING_HORIZONTAL_MODES),
)
self.setup_template(
CONF_SWING_HORIZONTAL_MODE,
"_attr_swing_horizontal_mode",
tcv.item_in_list(
self,
CONF_SWING_HORIZONTAL_MODE,
"_attr_swing_horizontal_modes",
CONF_SWING_HORIZONTAL_MODES,
),
)
self.add_assumed_attribute(
"_attr_swing_horizontal_mode",
CONF_SWING_HORIZONTAL_MODE,
SET_SWING_HORIZONTAL_MODE_ACTION,
)
self.setup_template(
CONF_PRESET_MODES,
"_attr_preset_modes",
tcv.list_of_strings(self, CONF_PRESET_MODES),
)
self.setup_template(
CONF_PRESET_MODE,
"_attr_preset_mode",
tcv.item_in_list(
self,
CONF_PRESET_MODE,
"_attr_preset_modes",
CONF_PRESET_MODES,
),
)
self.add_assumed_attribute(
"_attr_preset_mode", CONF_PRESET_MODE, SET_PRESET_MODE_ACTION
)
self._attr_supported_features = ClimateEntityFeature(0)
for action_id, supported_feature in (
(SET_FAN_MODE_ACTION, ClimateEntityFeature.FAN_MODE),
(SET_HUMIDITY_ACTION, ClimateEntityFeature.TARGET_HUMIDITY),
(SET_HVAC_MODE_ACTION, 0),
(SET_PRESET_MODE_ACTION, ClimateEntityFeature.PRESET_MODE),
(
SET_SWING_HORIZONTAL_MODE_ACTION,
ClimateEntityFeature.SWING_HORIZONTAL_MODE,
),
(SET_SWING_MODE_ACTION, ClimateEntityFeature.SWING_MODE),
(SET_TEMPERATURE_ACTION, ClimateEntityFeature.TARGET_TEMPERATURE),
):
if (action_config := self._config.get(action_id)) is not None:
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
if (
(
CONF_TARGET_TEMPERATURE_HIGH in self._templates
and CONF_TARGET_TEMPERATURE_LOW in self._templates
)
or (
CONF_TARGET_TEMPERATURE_HIGH in self._assumed_attributes
and CONF_TARGET_TEMPERATURE_LOW in self._assumed_attributes
)
) and SET_TEMPERATURE_ACTION in self._action_scripts:
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
def _update_hvac_mode(self, render) -> None:
if render is None:
self._attr_hvac_mode = None
return
self._attr_hvac_mode = HVACMode(render)
def _update_hvac_modes(self, render) -> None:
if isinstance(render, TemplateError):
self._attr_hvac_modes = []
return
if HVACMode.OFF in render:
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
else:
self._attr_supported_features &= ~ClimateEntityFeature.TURN_OFF
if any(
mode in render
for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL)
) or (
len(render) == 2
and HVACMode.OFF in render
and any(mode != HVACMode.OFF for mode in render)
):
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
else:
self._attr_supported_features &= ~ClimateEntityFeature.TURN_ON
self._attr_hvac_modes = render
def _round_temperature_value(self, value: Any) -> float:
if self._attr_target_temperature_step is None:
return value
rounded = _round_to_step(float(value), self._attr_target_temperature_step)
return min(self.max_temp, max(self.min_temp, rounded))
def _update_target_temperature(self, attr: str) -> Callable[[Any], None]:
def update(result: Any) -> None:
if result is None:
setattr(self, attr, None)
return
value = self._round_temperature_value(result)
setattr(self, attr, value)
return update
def _round_humidity_value(self, value: Any) -> int:
result = (
value
if self._attr_target_humidity_step is None
else int(
_round_to_step(
float(value) / 10.0, self._attr_target_humidity_step / 10.0
)
* 10.0
)
)
return int(min(self.max_humidity, max(self.min_humidity, result)))
def _update_target_humidity(
self,
result,
) -> None:
if result is None:
self._attr_target_humidity = None
return
self._attr_target_humidity = self._round_humidity_value(result)
async def _async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if script := self._action_scripts.get(SET_HVAC_MODE_ACTION):
await self.async_run_script(
script,
run_variables={"hvac_mode": hvac_mode},
context=self._context,
)
@override
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
await self._async_set_hvac_mode(hvac_mode)
if self._attr_assumed_state:
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
@override
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
if script := self._action_scripts.get(SET_PRESET_MODE_ACTION):
await self.async_run_script(
script,
run_variables={"preset_mode": preset_mode},
context=self._context,
)
self.write_assumed_attribute(CONF_PRESET_MODE, preset_mode)
@override
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
if script := self._action_scripts.get(SET_FAN_MODE_ACTION):
await self.async_run_script(
script,
run_variables={"fan_mode": fan_mode},
context=self._context,
)
self.write_assumed_attribute(CONF_FAN_MODE, fan_mode)
@override
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode."""
if script := self._action_scripts.get(SET_SWING_MODE_ACTION):
await self.async_run_script(
script,
run_variables={"swing_mode": swing_mode},
context=self._context,
)
self.write_assumed_attribute(CONF_SWING_MODE, swing_mode)
@override
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set the swing horizontal mode."""
if script := self._action_scripts.get(SET_SWING_HORIZONTAL_MODE_ACTION):
await self.async_run_script(
script,
run_variables={"swing_horizontal_mode": swing_horizontal_mode},
context=self._context,
)
self.write_assumed_attribute(CONF_SWING_HORIZONTAL_MODE, swing_horizontal_mode)
@override
async def async_set_humidity(self, humidity: int) -> None:
"""Set the target humidity."""
rounded = self._round_humidity_value(humidity)
if script := self._action_scripts.get(SET_HUMIDITY_ACTION):
await self.async_run_script(
script,
run_variables={"humidity": rounded},
context=self._context,
)
self.write_assumed_attribute(CONF_TARGET_HUMIDITY, rounded)
@override
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set one or more target temperatures."""
common_params: dict[str, Any] = {
"temperature": None,
"target_temp_high": None,
"target_temp_low": None,
"hvac_mode": None,
}
write_state = False
breadcrumb = f"{SET_TEMPERATURE_ACTION} {ATTR_HVAC_MODE}"
if (hvac_value := kwargs.get(ATTR_HVAC_MODE)) and (
hvac_mode := tcv.strenum(self, breadcrumb, HVACMode)(hvac_value)
) is not None:
self._valid_mode_or_raise("hvac", hvac_mode, self.hvac_modes)
common_params["hvac_mode"] = hvac_mode
await self._async_set_hvac_mode(HVACMode(hvac_mode))
if self._attr_assumed_state:
self._attr_hvac_mode = hvac_mode
write_state = True
updates = []
for option, attr, param in (
(
CONF_TARGET_TEMPERATURE,
ATTR_TEMPERATURE,
"temperature",
),
(
CONF_TARGET_TEMPERATURE_HIGH,
ATTR_TARGET_TEMP_HIGH,
"target_temp_high",
),
(
CONF_TARGET_TEMPERATURE_LOW,
ATTR_TARGET_TEMP_LOW,
"target_temp_low",
),
):
if (value := kwargs.get(attr)) is not None and (
validated := tcv.number(
self,
f"{SET_TEMPERATURE_ACTION} {attr}",
self.min_temp,
self.max_temp,
)(value)
) is not None:
rounded = self._round_temperature_value(validated)
common_params[param] = rounded
updates.append((option, rounded))
if script := self._action_scripts.get(SET_TEMPERATURE_ACTION):
await self.async_run_script(
script,
run_variables=common_params,
context=self._context,
)
for option, value in updates:
if self.update_assumed_attribute(option, value):
write_state = True
if write_state:
self.async_write_ha_state()
@property
@override
def extra_restore_state_data(self) -> ClimateExtraStoredData:
"""Return climate specific state data to be restored."""
return ClimateExtraStoredData(
current_humidity=self._attr_current_humidity,
current_temperature=self._attr_current_temperature,
fan_mode=self._attr_fan_mode,
fan_modes=self._attr_fan_modes,
hvac_action=self._attr_hvac_action,
hvac_mode=self._attr_hvac_mode,
hvac_modes=self._attr_hvac_modes,
preset_mode=self._attr_preset_mode,
preset_modes=self._attr_preset_modes,
swing_mode=self._attr_swing_mode,
swing_modes=self._attr_swing_modes,
swing_horizontal_mode=self._attr_swing_horizontal_mode,
swing_horizontal_modes=self._attr_swing_horizontal_modes,
target_humidity=self._attr_target_humidity,
target_temperature_high=self._attr_target_temperature_high,
target_temperature_low=self._attr_target_temperature_low,
target_temperature=self._attr_target_temperature,
)
@override
def restore_extra_data(self, extra_data: ClimateExtraStoredData) -> None:
"""Restore the extra data."""
self._attr_current_humidity = extra_data.current_humidity
self._attr_current_temperature = extra_data.current_temperature
self._attr_fan_mode = extra_data.fan_mode
self._attr_fan_modes = extra_data.fan_modes
self._attr_hvac_action = extra_data.hvac_action
self._attr_hvac_mode = extra_data.hvac_mode
self._update_hvac_modes(extra_data.hvac_modes or [])
self._attr_preset_mode = extra_data.preset_mode
self._attr_preset_modes = extra_data.preset_modes
self._attr_swing_mode = extra_data.swing_mode
self._attr_swing_modes = extra_data.swing_modes
self._attr_swing_horizontal_mode = extra_data.swing_horizontal_mode
self._attr_swing_horizontal_modes = extra_data.swing_horizontal_modes
self._attr_target_humidity = extra_data.target_humidity
self._attr_target_temperature_high = extra_data.target_temperature_high
self._attr_target_temperature_low = extra_data.target_temperature_low
self._attr_target_temperature = extra_data.target_temperature
class StateClimateEntity(TemplateEntity, AbstractTemplateClimate):
"""Representation of a state-based template climate."""
_attr_should_poll = False
def __init__(
self,
hass: HomeAssistant,
config: ConfigType,
unique_id: str | None,
) -> None:
"""Initialize the state-based template climate."""
TemplateEntity.__init__(self, hass, config, unique_id)
name = self._attr_name
if TYPE_CHECKING:
assert name is not None
AbstractTemplateClimate.__init__(self, hass, name, config)
class TriggerClimateEntity(TriggerEntity, AbstractTemplateClimate):
"""Representation of a trigger-based template climate."""
domain = CLIMATE_DOMAIN
def __init__(
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
config: ConfigType,
) -> None:
"""Initialize the trigger-based template climate."""
TriggerEntity.__init__(self, hass, coordinator, config)
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
AbstractTemplateClimate.__init__(self, hass, name, config)
@@ -17,6 +17,7 @@ from homeassistant.components.blueprint import (
schemas as blueprint_schemas,
)
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
@@ -60,6 +61,7 @@ from . import (
alarm_control_panel as alarm_control_panel_platform,
binary_sensor as binary_sensor_platform,
button as button_platform,
climate as climate_platform,
cover as cover_platform,
device_tracker as device_tracker_platform,
event as event_platform,
@@ -89,6 +91,7 @@ _DEFAULT_NAMES = {
Platform.BINARY_SENSOR: binary_sensor_platform.DEFAULT_NAME,
Platform.BUTTON: button_platform.DEFAULT_NAME,
Platform.COVER: cover_platform.DEFAULT_NAME,
Platform.CLIMATE: climate_platform.DEFAULT_NAME,
Platform.DEVICE_TRACKER: device_tracker_platform.DEFAULT_NAME,
Platform.EVENT: event_platform.DEFAULT_NAME,
Platform.FAN: fan_platform.DEFAULT_NAME,
@@ -250,6 +253,9 @@ CONFIG_SECTION_SCHEMA = probatio.All(
probatio.Optional(BUTTON_DOMAIN): probatio.All(
cv.ensure_list, [button_platform.BUTTON_YAML_SCHEMA]
),
probatio.Optional(CLIMATE_DOMAIN): probatio.All(
cv.ensure_list, [climate_platform.CLIMATE_YAML_SCHEMA]
),
probatio.Optional(COVER_DOMAIN): probatio.All(
cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA]
),
@@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_URL,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
DEGREE,
Platform,
UnitOfTemperature,
)
@@ -37,6 +38,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowMenuStep,
)
from . import validators as tcv
from .alarm_control_panel import (
CONF_ARM_AWAY_ACTION,
CONF_ARM_CUSTOM_BYPASS_ACTION,
@@ -51,6 +53,18 @@ from .alarm_control_panel import (
async_create_preview_alarm_control_panel,
)
from .binary_sensor import async_create_preview_binary_sensor
from .climate import (
CONF_CURRENT_TEMPERATURE,
CONF_HVAC_ACTION,
CONF_HVAC_MODE,
CONF_HVAC_MODES,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_TARGET_TEMPERATURE,
SET_HVAC_MODE_ACTION,
SET_TEMPERATURE_ACTION,
async_create_preview_climate,
)
from .const import (
CONF_ADDITIONAL_OPTIONS,
CONF_AVAILABILITY,
@@ -196,6 +210,40 @@ def generate_schema(domain: str, flow_type: str) -> probatio.Schema:
),
}
if domain == Platform.CLIMATE:
schema |= {
probatio.Required(CONF_HVAC_MODES): selector.TemplateSelector(),
probatio.Optional(CONF_HVAC_MODE): selector.TemplateSelector(),
probatio.Required(SET_HVAC_MODE_ACTION): selector.ActionSelector(),
probatio.Optional(CONF_HVAC_ACTION): selector.TemplateSelector(),
probatio.Optional(CONF_CURRENT_TEMPERATURE): selector.TemplateSelector(),
probatio.Optional(CONF_TARGET_TEMPERATURE): selector.TemplateSelector(),
probatio.Optional(SET_TEMPERATURE_ACTION): selector.ActionSelector(),
probatio.Optional(CONF_TEMPERATURE_UNIT): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[cls.value for cls in UnitOfTemperature],
mode=selector.SelectSelectorMode.DROPDOWN,
sort=True,
),
),
}
additional_options |= {
probatio.Optional(CONF_MIN_TEMPERATURE): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement=DEGREE,
step=0.1,
)
),
probatio.Optional(CONF_MAX_TEMPERATURE): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement=DEGREE,
step=0.1,
)
),
}
if domain == Platform.COVER:
schema |= _SCHEMA_STATE | {
probatio.Inclusive(
@@ -505,6 +553,10 @@ def validate_user_input(
if template_type == Platform.SENSOR:
_validate_unit(user_input)
_validate_state_class(user_input)
if template_type == Platform.CLIMATE:
tcv.requires_option(CONF_TARGET_TEMPERATURE, SET_TEMPERATURE_ACTION)(
user_input
)
return {"template_type": template_type} | user_input
return _validate_user_input
@@ -514,6 +566,7 @@ TEMPLATE_TYPES = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.DEVICE_TRACKER,
Platform.EVENT,
@@ -546,6 +599,11 @@ CONFIG_FLOW = {
config_schema(Platform.BUTTON),
validate_user_input=validate_user_input(Platform.BUTTON),
),
Platform.CLIMATE: SchemaFlowFormStep(
config_schema(Platform.CLIMATE),
preview="template",
validate_user_input=validate_user_input(Platform.CLIMATE),
),
Platform.COVER: SchemaFlowFormStep(
config_schema(Platform.COVER),
preview="template",
@@ -635,6 +693,11 @@ OPTIONS_FLOW = {
options_schema(Platform.BUTTON),
validate_user_input=validate_user_input(Platform.BUTTON),
),
Platform.CLIMATE: SchemaFlowFormStep(
options_schema(Platform.CLIMATE),
preview="template",
validate_user_input=validate_user_input(Platform.CLIMATE),
),
Platform.COVER: SchemaFlowFormStep(
options_schema(Platform.COVER),
preview="template",
@@ -713,6 +776,7 @@ CREATE_PREVIEW_ENTITY: dict[
] = {
Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel,
Platform.BINARY_SENSOR: async_create_preview_binary_sensor,
Platform.CLIMATE: async_create_preview_climate,
Platform.COVER: async_create_preview_cover,
Platform.DEVICE_TRACKER: async_create_preview_tracker,
Platform.EVENT: async_create_preview_event,
@@ -25,6 +25,7 @@ PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.DEVICE_TRACKER,
Platform.EVENT,
@@ -64,6 +64,7 @@ class AbstractTemplateEntity(Entity):
self._templates: dict[str, EntityTemplate] = {}
self._action_scripts: dict[str, Script] = {}
self._attr_extra_state_attributes = {}
self._assumed_attributes: dict[str, str] = {}
self._attribute_templates: dict[str, Template] | None = None
self._attributes_template: Template | None = None
@@ -201,6 +202,24 @@ class AbstractTemplateEntity(Entity):
domain,
)
def add_assumed_attribute(self, attr: str, option: str, action_option: str):
"""Add an optimistic option."""
if option not in self._config and action_option in self._config:
self._assumed_attributes[option] = attr
def update_assumed_attribute(self, option: str, value: Any) -> bool:
"""If the attribute is assumed, update attribute with the new value."""
attr = self._assumed_attributes.get(option)
if assumed_attribute := attr is not None:
setattr(self, attr, value)
return assumed_attribute
def write_assumed_attribute(self, option: str, value: Any) -> None:
"""If the attribute is assumed, write the value to the attribute and update the ha state."""
if self.update_assumed_attribute(option, value):
self.async_write_ha_state()
@override
async def async_will_remove_from_hass(self) -> None:
"""Clean up scripts when removing from Home Assistant."""
+87 -4
View File
@@ -7,6 +7,8 @@
"device_class": "Device class",
"device_id_description": "Select a device to link to this entity.",
"state": "State",
"temperature_unit": "Temperature unit",
"temperature_unit_description": "The unit for any temperature template output. One of `°C`, `°F`, or `K`.",
"turn_off": "Actions on turn off",
"turn_on": "Actions on turn on",
"unit_of_measurement": "Unit of measurement"
@@ -102,6 +104,46 @@
},
"title": "Template button"
},
"climate": {
"data": {
"current_temperature": "[%key:component::climate::entity_component::_::state_attributes::current_temperature::name%]",
"device_id": "[%key:common::config_flow::data::device%]",
"hvac_action": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::name%]",
"hvac_mode": "HVAC mode",
"hvac_modes": "[%key:component::climate::entity_component::_::state_attributes::hvac_modes::name%]",
"name": "[%key:common::config_flow::data::name%]",
"set_hvac_mode": "Actions on set HVAC mode",
"set_temperature": "Actions on set temperature",
"target_temperature": "[%key:component::climate::entity_component::_::state_attributes::temperature::name%]",
"temperature_unit": "[%key:component::template::common::temperature_unit%]"
},
"data_description": {
"current_temperature": "Defines a template for the current temperature.",
"device_id": "[%key:component::template::common::device_id_description%]",
"hvac_action": "Defines a template for the current HVAC action. Valid HVAC actions are `cooling`, `defrosting`, `drying`, `fan`, `heating`, `idle`, `off`, and `preheating`.",
"hvac_mode": "Defines a template for the current HVAC mode. Valid HVAC modes are `auto`, `cool`, `dry`, `fan_only`, `heat`, `heat_cool` and `off`.",
"hvac_modes": "Defines a template for a list of available HVAC modes. Valid HVAC modes for the list are `auto`, `cool`, `dry`, `fan_only`, `heat`, `heat_cool` and `off`.",
"set_hvac_mode": "Defines actions to run when the climate is given a `set_hvac_mode` command. Receives variable `hvac_mode`.",
"set_temperature": "Defines actions to run when the climate is given a `set_temperature` command. Receives variable `hvac_mode`, `temperature`, `target_temp_high`, and `target_temp_low`. If `hvac_mode` is received, the `set_hvac_mode` actions are executed before the actions in `set_temperature`.",
"target_temperature": "Defines a template for the target temperature.",
"temperature_unit": "[%key:component::template::common::temperature_unit_description%]"
},
"sections": {
"additional_options": {
"data": {
"availability": "[%key:component::template::common::availability%]",
"max_temperature": "[%key:component::climate::entity_component::_::state_attributes::max_temp::name%]",
"min_temperature": "[%key:component::climate::entity_component::_::state_attributes::min_temp::name%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
},
"name": "[%key:component::template::common::additional_options%]"
}
},
"title": "Template Thermostat"
},
"cover": {
"data": {
"close_cover": "Actions on close",
@@ -483,6 +525,7 @@
"alarm_control_panel": "[%key:component::alarm_control_panel::title%]",
"binary_sensor": "[%key:component::binary_sensor::title%]",
"button": "[%key:component::button::title%]",
"climate": "[%key:component::climate::title%]",
"cover": "[%key:component::cover::title%]",
"device_tracker": "[%key:component::device_tracker::title%]",
"event": "[%key:component::event::title%]",
@@ -550,7 +593,7 @@
"humidity": "Humidity",
"name": "[%key:common::config_flow::data::name%]",
"temperature": "Temperature",
"temperature_unit": "Temperature unit"
"temperature_unit": "[%key:component::template::common::temperature_unit%]"
},
"data_description": {
"condition": "Defines a template to get the current weather condition",
@@ -559,7 +602,7 @@
"forecast_hourly": "Defines a template to get the [hourly forecast data]({hourly_link})",
"humidity": "Defines a template to get the current humidity",
"temperature": "Defines a template to get the current temperature",
"temperature_unit": "The temperature unit"
"temperature_unit": "[%key:component::template::common::temperature_unit_description%]"
},
"sections": {
"additional_options": {
@@ -701,6 +744,46 @@
},
"title": "[%key:component::template::config::step::button::title%]"
},
"climate": {
"data": {
"current_temperature": "[%key:component::climate::entity_component::_::state_attributes::current_temperature::name%]",
"device_id": "[%key:common::config_flow::data::device%]",
"hvac_action": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::name%]",
"hvac_mode": "[%key:component::template::config::step::climate::data::hvac_mode%]",
"hvac_modes": "[%key:component::climate::entity_component::_::state_attributes::hvac_modes::name%]",
"name": "[%key:common::config_flow::data::name%]",
"set_hvac_mode": "[%key:component::template::config::step::climate::data::set_hvac_mode%]",
"set_temperature": "[%key:component::template::config::step::climate::data::set_temperature%]",
"target_temperature": "[%key:component::climate::entity_component::_::state_attributes::temperature::name%]",
"temperature_unit": "[%key:component::template::common::temperature_unit%]"
},
"data_description": {
"current_temperature": "[%key:component::template::config::step::climate::data_description::current_temperature%]",
"device_id": "[%key:component::template::common::device_id_description%]",
"hvac_action": "[%key:component::template::config::step::climate::data_description::hvac_action%]",
"hvac_mode": "[%key:component::template::config::step::climate::data_description::hvac_mode%]",
"hvac_modes": "[%key:component::template::config::step::climate::data_description::hvac_modes%]",
"set_hvac_mode": "[%key:component::template::config::step::climate::data_description::set_hvac_mode%]",
"set_temperature": "[%key:component::template::config::step::climate::data_description::set_temperature%]",
"target_temperature": "[%key:component::template::config::step::climate::data_description::target_temperature%]",
"temperature_unit": "[%key:component::template::common::temperature_unit_description%]"
},
"sections": {
"additional_options": {
"data": {
"availability": "[%key:component::template::common::availability%]",
"max_temperature": "[%key:component::climate::entity_component::_::state_attributes::max_temp::name%]",
"min_temperature": "[%key:component::climate::entity_component::_::state_attributes::min_temp::name%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
},
"name": "[%key:component::template::common::additional_options%]"
}
},
"title": "[%key:component::template::config::step::climate::title%]"
},
"cover": {
"data": {
"close_cover": "[%key:component::template::config::step::cover::data::close_cover%]",
@@ -1115,7 +1198,7 @@
"humidity": "[%key:component::template::config::step::weather::data::humidity%]",
"name": "[%key:common::config_flow::data::name%]",
"temperature": "[%key:component::template::config::step::weather::data::temperature%]",
"temperature_unit": "[%key:component::template::config::step::weather::data::temperature_unit%]"
"temperature_unit": "[%key:component::template::common::temperature_unit%]"
},
"data_description": {
"condition": "[%key:component::template::config::step::weather::data_description::condition%]",
@@ -1124,7 +1207,7 @@
"forecast_hourly": "[%key:component::template::config::step::weather::data_description::forecast_hourly%]",
"humidity": "[%key:component::template::config::step::weather::data_description::humidity%]",
"temperature": "[%key:component::template::config::step::weather::data_description::temperature%]",
"temperature_unit": "[%key:component::template::config::step::weather::data_description::temperature_unit%]"
"temperature_unit": "[%key:component::template::common::temperature_unit_description%]"
},
"sections": {
"additional_options": {
@@ -463,3 +463,42 @@ def check_conditions(
)
return condition_result
def inclusive_group(name: str, optional: str, *required: str) -> Callable[[dict], dict]:
"""Validate an inclusive group of configuration options, with 1 optional option.
The optional member requires all required options, however the required options
do not require the optional option.
"""
_all = {optional, *required}
_required = set(required)
def verify(obj: dict) -> dict:
options = set(obj.keys())
if not (common := options.intersection(_all)) or common in (_required, _all):
return obj
missing = _required - common
raise probatio.Invalid(
f"Some required option(s) are missing from inclusive group '{name}', expected missing options: {', '.join(missing)}."
)
return verify
def requires_option(option: str, required_option: str) -> Callable[[dict], dict]:
"""Validate a pair of options.
Raises probatio.Invalid if required_option is missing when option is present.
"""
def verify(obj: dict) -> dict:
if (option in obj and required_option in obj) or option not in obj:
return obj
raise probatio.Invalid(
f"Required option: '{required_option}' is missing for option '{option}'. Remove '{option}' from your config or add '{required_option}'."
)
return verify
@@ -0,0 +1,24 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: None,
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'My template',
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 384>,
}),
'context': <ANY>,
'entity_id': 'climate.my_template',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---
File diff suppressed because it is too large Load Diff
@@ -85,6 +85,22 @@ BINARY_SENSOR_OPTIONS = {
{},
{},
),
(
"climate",
{"hvac_mode": "{{ states('climate.one') }}"},
"heat",
{"one": "heat", "two": "cool"},
{},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
{},
),
(
"sensor",
{
@@ -380,6 +396,18 @@ async def test_config_flow(
{},
{},
),
(
"climate",
{"hvac_mode": "{{ 'heat' }}"},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
),
(
"switch",
{"value_template": "{{ false }}"},
@@ -651,6 +679,23 @@ async def test_config_flow_device(
"state",
None,
),
(
"climate",
{"hvac_mode": "{{ states('climate.one') }}"},
{"hvac_mode": "{{ states('climate.two') }}"},
["heat", "cool"],
{"one": "heat", "two": "cool"},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
"state",
None,
),
(
"event",
{"event_type": "{{ states('event.one') }}"},
@@ -1701,6 +1746,18 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{},
{},
),
(
"climate",
{"hvac_mode": "{{ states('climate.one') }}"},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
{
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
"set_hvac_mode": [],
},
),
(
"cover",
{"state": "{{ states('cover.one') }}"},