mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
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:
co-authored by
Copilot Autofix powered by AI
parent
de97442384
commit
e48779cbf9
@@ -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."""
|
||||
|
||||
@@ -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') }}"},
|
||||
|
||||
Reference in New Issue
Block a user