Support variables, icon, and picture for all compatible template platforms (#145893)

* Fix template entity variables in blueprints

* add picture and icon tests

* add variable test for all platforms

* apply comments

* Update all test names
This commit is contained in:
Petro31
2025-06-18 10:49:46 -04:00
committed by GitHub
parent d01758cea8
commit bcb87cf812
31 changed files with 754 additions and 435 deletions

View File

@ -41,13 +41,12 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .const import CONF_OBJECT_ID, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
@ -105,15 +104,10 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.All(
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
): cv.enum(TemplateCodeFormat),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
@ -419,9 +413,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AbstractTemplateAlarmControlPane
unique_id: str | None,
) -> None:
"""Initialize the panel."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateAlarmControlPanel.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@ -26,29 +26,19 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_PRESS, DOMAIN
from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
)
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Template Button"
DEFAULT_OPTIMISTIC = False
BUTTON_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
)
BUTTON_SCHEMA = vol.Schema(
{
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
CONFIG_BUTTON_SCHEMA = vol.Schema(
{

View File

@ -37,14 +37,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .const import CONF_OBJECT_ID, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
@ -100,21 +99,16 @@ COVER_SCHEMA = vol.All(
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_POSITION): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_TILT): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema),
cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION),
)
@ -463,9 +457,7 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover):
unique_id,
) -> None:
"""Initialize the Template cover."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateCover.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@ -37,14 +37,13 @@ from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .const import CONF_OBJECT_ID, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
@ -85,12 +84,10 @@ FAN_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_DIRECTION): cv.template,
vol.Optional(CONF_NAME): cv.template,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_OSCILLATING): cv.template,
vol.Optional(CONF_PERCENTAGE): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_PRESET_MODE): cv.template,
vol.Optional(CONF_PRESET_MODES): cv.ensure_list,
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
@ -99,11 +96,8 @@ FAN_SCHEMA = vol.All(
vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
LEGACY_FAN_SCHEMA = vol.All(
@ -488,9 +482,7 @@ class TemplateFan(TemplateEntity, AbstractTemplateFan):
unique_id,
) -> None:
"""Initialize the fan."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateFan.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@ -29,7 +29,10 @@ from homeassistant.util import dt as dt_util
from . import TriggerUpdateCoordinator
from .const import CONF_PICTURE
from .template_entity import TemplateEntity, make_template_entity_common_schema
from .template_entity import (
TemplateEntity,
make_template_entity_common_modern_attributes_schema,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -43,7 +46,7 @@ IMAGE_SCHEMA = vol.Schema(
vol.Required(CONF_URL): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
}
).extend(make_template_entity_common_schema(DEFAULT_NAME).schema)
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
IMAGE_CONFIG_SCHEMA = vol.Schema(

View File

@ -49,14 +49,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import color as color_util
from . import TriggerUpdateCoordinator
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .const import CONF_OBJECT_ID, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
@ -124,38 +123,31 @@ LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | {
DEFAULT_NAME = "Template Light"
LIGHT_SCHEMA = (
vol.Schema(
{
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
vol.Inclusive(CONF_EFFECT, "effect"): cv.template,
vol.Optional(CONF_HS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_HS): cv.template,
vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_LEVEL): cv.template,
vol.Optional(CONF_MAX_MIREDS): cv.template,
vol.Optional(CONF_MIN_MIREDS): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB): cv.template,
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGBW): cv.template,
vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGBWW): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TEMPERATURE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
)
LIGHT_SCHEMA = vol.Schema(
{
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
vol.Inclusive(CONF_EFFECT, "effect"): cv.template,
vol.Optional(CONF_HS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_HS): cv.template,
vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_LEVEL): cv.template,
vol.Optional(CONF_MAX_MIREDS): cv.template,
vol.Optional(CONF_MIN_MIREDS): cv.template,
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB): cv.template,
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGBW): cv.template,
vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGBWW): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TEMPERATURE): cv.template,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
LEGACY_LIGHT_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
@ -955,9 +947,7 @@ class LightTemplate(TemplateEntity, AbstractTemplateLight):
unique_id: str | None,
) -> None:
"""Initialize the light."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateLight.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@ -31,10 +31,9 @@ from .const import CONF_PICTURE, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
@ -57,17 +56,13 @@ LOCK_SCHEMA = vol.All(
{
vol.Optional(CONF_CODE_FORMAT): cv.template,
vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_OPEN): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PICTURE): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
@ -313,9 +308,7 @@ class TemplateLock(TemplateEntity, AbstractTemplateLock):
unique_id: str | None,
) -> None:
"""Initialize the lock."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=DEFAULT_NAME, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateLock.__init__(self, config)
name = self._attr_name
if TYPE_CHECKING:

View File

@ -35,11 +35,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
)
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -49,23 +45,17 @@ CONF_SET_VALUE = "set_value"
DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False
NUMBER_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Required(CONF_STEP): cv.template,
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
)
NUMBER_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Required(CONF_STEP): cv.template,
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
NUMBER_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,

View File

@ -32,11 +32,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import DOMAIN
from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
)
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -47,20 +43,14 @@ CONF_SELECT_OPTION = "select_option"
DEFAULT_NAME = "Template Select"
DEFAULT_OPTIMISTIC = False
SELECT_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Required(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
)
SELECT_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Required(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
SELECT_CONFIG_SCHEMA = vol.Schema(

View File

@ -40,13 +40,12 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import CONF_OBJECT_ID, CONF_PICTURE, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .const import CONF_OBJECT_ID, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
@ -60,20 +59,13 @@ LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | {
DEFAULT_NAME = "Template Switch"
SWITCH_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_PICTURE): cv.template,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
)
SWITCH_SCHEMA = vol.Schema(
{
vol.Optional(CONF_STATE): cv.template,
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
LEGACY_SWITCH_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
@ -228,7 +220,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
unique_id: str | None,
) -> None:
"""Initialize the Template switch."""
super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
super().__init__(hass, config=config, unique_id=unique_id)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass

View File

@ -94,16 +94,24 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = (
)
def make_template_entity_common_schema(default_name: str) -> vol.Schema:
def make_template_entity_common_modern_schema(
default_name: str,
) -> vol.Schema:
"""Return a schema with default name."""
return (
vol.Schema(
{
vol.Optional(CONF_AVAILABILITY): cv.template,
}
)
.extend(make_template_entity_base_schema(default_name).schema)
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
return vol.Schema(
{
vol.Optional(CONF_AVAILABILITY): cv.template,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
}
).extend(make_template_entity_base_schema(default_name).schema)
def make_template_entity_common_modern_attributes_schema(
default_name: str,
) -> vol.Schema:
"""Return a schema with default name."""
return make_template_entity_common_modern_schema(default_name).extend(
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema
)

View File

@ -38,16 +38,14 @@ from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .const import CONF_OBJECT_ID, DOMAIN
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA,
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_attributes_schema,
rewrite_common_legacy_to_modern_conf,
)
@ -60,6 +58,8 @@ CONF_FAN_SPEED_LIST = "fan_speeds"
CONF_FAN_SPEED = "fan_speed"
CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
DEFAULT_NAME = "Template Vacuum"
ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}"
_VALID_STATES = [
VacuumActivity.CLEANING,
@ -80,13 +80,9 @@ VACUUM_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list,
vol.Optional(CONF_FAN_SPEED): cv.template,
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA,
vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA,
vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA,
@ -95,10 +91,7 @@ VACUUM_SCHEMA = vol.All(
vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA,
vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA,
}
)
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
)
LEGACY_VACUUM_SCHEMA = vol.All(
@ -353,9 +346,7 @@ class TemplateVacuum(TemplateEntity, AbstractTemplateVacuum):
unique_id,
) -> None:
"""Initialize the vacuum."""
TemplateEntity.__init__(
self, hass, config=config, fallback_name=None, unique_id=unique_id
)
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
AbstractTemplateVacuum.__init__(self, config)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@ -32,7 +32,6 @@ from homeassistant.components.weather import (
WeatherEntityFeature,
)
from homeassistant.const import (
CONF_NAME,
CONF_TEMPERATURE_UNIT,
CONF_UNIQUE_ID,
STATE_UNAVAILABLE,
@ -53,7 +52,11 @@ from homeassistant.util.unit_conversion import (
)
from .coordinator import TriggerUpdateCoordinator
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
from .template_entity import (
TemplateEntity,
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
CHECK_FORECAST_KEYS = (
@ -104,33 +107,33 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
CONF_DEW_POINT_TEMPLATE = "dew_point_template"
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
DEFAULT_NAME = "Template Weather"
WEATHER_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
}
)
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
PLATFORM_SCHEMA = WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema)

View File

@ -16,6 +16,22 @@ from homeassistant.components.blueprint import (
DomainBlueprints,
)
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
from homeassistant.components.template.config import (
DOMAIN_ALARM_CONTROL_PANEL,
DOMAIN_BINARY_SENSOR,
DOMAIN_COVER,
DOMAIN_FAN,
DOMAIN_IMAGE,
DOMAIN_LIGHT,
DOMAIN_LOCK,
DOMAIN_NUMBER,
DOMAIN_SELECT,
DOMAIN_SENSOR,
DOMAIN_SWITCH,
DOMAIN_VACUUM,
DOMAIN_WEATHER,
)
from homeassistant.const import STATE_ON
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
@ -459,3 +475,51 @@ async def test_no_blueprint(hass: HomeAssistant) -> None:
template.helpers.blueprint_in_template(hass, "binary_sensor.test_entity")
is None
)
@pytest.mark.parametrize(
("domain", "set_state", "expected"),
[
(DOMAIN_ALARM_CONTROL_PANEL, STATE_ON, "armed_home"),
(DOMAIN_BINARY_SENSOR, STATE_ON, STATE_ON),
(DOMAIN_COVER, STATE_ON, "open"),
(DOMAIN_FAN, STATE_ON, STATE_ON),
(DOMAIN_IMAGE, "test.jpg", "2025-06-13T00:00:00+00:00"),
(DOMAIN_LIGHT, STATE_ON, STATE_ON),
(DOMAIN_LOCK, STATE_ON, "locked"),
(DOMAIN_NUMBER, "1", "1.0"),
(DOMAIN_SELECT, "option1", "option1"),
(DOMAIN_SENSOR, "foo", "foo"),
(DOMAIN_SWITCH, STATE_ON, STATE_ON),
(DOMAIN_VACUUM, "cleaning", "cleaning"),
(DOMAIN_WEATHER, "sunny", "sunny"),
],
)
@pytest.mark.freeze_time("2025-06-13 00:00:00+00:00")
async def test_variables_for_entity(
hass: HomeAssistant, domain: str, set_state: str, expected: str
) -> None:
"""Test regular template entities via blueprint with variables defined."""
hass.states.async_set("sensor.test_state", set_state)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"use_blueprint": {
"path": f"test_{domain}_with_variables.yaml",
"input": {"sensor": "sensor.test_state"},
},
"name": "Test",
},
]
},
)
await hass.async_block_till_done()
state = hass.states.get(f"{domain}.test")
assert state is not None
assert state.state == expected

View File

@ -11,7 +11,10 @@ from homeassistant import setup
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.template import DOMAIN
from homeassistant.components.template.button import DEFAULT_NAME
from homeassistant.components.template.const import CONF_PICTURE
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_ICON,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_FRIENDLY_NAME,
@ -247,6 +250,49 @@ async def test_name_template(hass: HomeAssistant) -> None:
)
@pytest.mark.parametrize(
("field", "attribute", "test_template", "expected"),
[
(CONF_ICON, ATTR_ICON, "mdi:test{{ 1 + 1 }}", "mdi:test2"),
(CONF_PICTURE, ATTR_ENTITY_PICTURE, "test{{ 1 + 1 }}.jpg", "test2.jpg"),
],
)
async def test_templated_optional_config(
hass: HomeAssistant,
field: str,
attribute: str,
test_template: str,
expected: str,
) -> None:
"""Test optional config templates."""
with assert_setup_component(1, "template"):
assert await setup.async_setup_component(
hass,
"template",
{
"template": {
"button": {
"press": {"service": "script.press"},
field: test_template,
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
_verify(
hass,
STATE_UNKNOWN,
{
attribute: expected,
},
"button.template_button",
)
async def test_unique_id(hass: HomeAssistant) -> None:
"""Test: unique id is ok."""
with assert_setup_component(1, "template"):

View File

@ -21,10 +21,13 @@ from homeassistant.components.number import (
SERVICE_SET_VALUE as NUMBER_SERVICE_SET_VALUE,
)
from homeassistant.components.template import DOMAIN
from homeassistant.components.template.const import CONF_PICTURE
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
ATTR_ICON,
CONF_ENTITY_ID,
CONF_ICON,
CONF_UNIT_OF_MEASUREMENT,
STATE_UNKNOWN,
)
@ -58,6 +61,20 @@ _VALUE_INPUT_NUMBER_CONFIG = {
}
}
TEST_STATE_ENTITY_ID = "number.test_state"
TEST_STATE_TRIGGER = {
"trigger": {
"trigger": "state",
"entity_id": [TEST_STATE_ENTITY_ID],
},
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
],
}
TEST_REQUIRED = {"state": "0", "step": "1", "set_value": []}
async def async_setup_modern_format(
hass: HomeAssistant, count: int, number_config: dict[str, Any]
@ -77,6 +94,24 @@ async def async_setup_modern_format(
await hass.async_block_till_done()
async def async_setup_trigger_format(
hass: HomeAssistant, count: int, number_config: dict[str, Any]
) -> None:
"""Do setup of number integration via trigger format."""
config = {"template": {**TEST_STATE_TRIGGER, "number": number_config}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
async def setup_number(
hass: HomeAssistant,
@ -89,6 +124,10 @@ async def setup_number(
await async_setup_modern_format(
hass, count, {"name": _TEST_OBJECT_ID, **number_config}
)
if style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass, count, {"name": _TEST_OBJECT_ID, **number_config}
)
async def test_setup_config_entry(
@ -446,119 +485,49 @@ def _verify(
assert attributes.get(CONF_UNIT_OF_MEASUREMENT) == expected_unit_of_measurement
async def test_icon_template(hass: HomeAssistant) -> None:
"""Test template numbers with icon templates."""
with assert_setup_component(1, "input_number"):
assert await setup.async_setup_component(
hass,
"input_number",
{"input_number": _VALUE_INPUT_NUMBER_CONFIG},
)
with assert_setup_component(1, "template"):
assert await setup.async_setup_component(
hass,
"template",
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("style", "initial_expected_state"),
[(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
)
@pytest.mark.parametrize(
("number_config", "attribute", "expected"),
[
(
{
"template": {
"unique_id": "b",
"number": {
"state": f"{{{{ states('{_VALUE_INPUT_NUMBER}') }}}}",
"step": 1,
"min": 0,
"max": 100,
"set_value": {
"service": "input_number.set_value",
"data_template": {
"entity_id": _VALUE_INPUT_NUMBER,
"value": "{{ value }}",
},
},
"icon": "{% if ((states.input_number.value.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}",
},
}
CONF_ICON: "{% if states.number.test_state.state == '1' %}mdi:check{% endif %}",
**TEST_REQUIRED,
},
)
hass.states.async_set(_VALUE_INPUT_NUMBER, 49)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get(_TEST_NUMBER)
assert float(state.state) == 49
assert state.attributes[ATTR_ICON] == "mdi:less"
await hass.services.async_call(
INPUT_NUMBER_DOMAIN,
INPUT_NUMBER_SERVICE_SET_VALUE,
{CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(_TEST_NUMBER)
assert float(state.state) == 51
assert state.attributes[ATTR_ICON] == "mdi:greater"
async def test_icon_template_with_trigger(hass: HomeAssistant) -> None:
"""Test template numbers with icon templates."""
with assert_setup_component(1, "input_number"):
assert await setup.async_setup_component(
hass,
"input_number",
{"input_number": _VALUE_INPUT_NUMBER_CONFIG},
)
with assert_setup_component(1, "template"):
assert await setup.async_setup_component(
hass,
"template",
ATTR_ICON,
"mdi:check",
),
(
{
"template": {
"trigger": {"platform": "state", "entity_id": _VALUE_INPUT_NUMBER},
"unique_id": "b",
"number": {
"state": "{{ trigger.to_state.state }}",
"step": 1,
"min": 0,
"max": 100,
"set_value": {
"service": "input_number.set_value",
"data_template": {
"entity_id": _VALUE_INPUT_NUMBER,
"value": "{{ value }}",
},
},
"icon": "{% if ((trigger.to_state.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}",
},
}
CONF_PICTURE: "{% if states.number.test_state.state == '1' %}check.jpg{% endif %}",
**TEST_REQUIRED,
},
)
ATTR_ENTITY_PICTURE,
"check.jpg",
),
],
)
@pytest.mark.usefixtures("setup_number")
async def test_templated_optional_config(
hass: HomeAssistant,
attribute: str,
expected: str,
initial_expected_state: str | None,
) -> None:
"""Test optional config templates."""
state = hass.states.get(_TEST_NUMBER)
assert state.attributes.get(attribute) == initial_expected_state
hass.states.async_set(_VALUE_INPUT_NUMBER, 49)
await hass.async_block_till_done()
await hass.async_start()
state = hass.states.async_set(TEST_STATE_ENTITY_ID, "1")
await hass.async_block_till_done()
state = hass.states.get(_TEST_NUMBER)
assert float(state.state) == 49
assert state.attributes[ATTR_ICON] == "mdi:less"
await hass.services.async_call(
INPUT_NUMBER_DOMAIN,
INPUT_NUMBER_SERVICE_SET_VALUE,
{CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(_TEST_NUMBER)
assert float(state.state) == 51
assert state.attributes[ATTR_ICON] == "mdi:greater"
assert state.attributes[attribute] == expected
async def test_device_id(

View File

@ -21,7 +21,15 @@ from homeassistant.components.select import (
SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION,
)
from homeassistant.components.template import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN
from homeassistant.components.template.const import CONF_PICTURE
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
ATTR_ICON,
CONF_ENTITY_ID,
CONF_ICON,
STATE_UNKNOWN,
)
from homeassistant.core import Context, HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
@ -34,6 +42,24 @@ _TEST_OBJECT_ID = "template_select"
_TEST_SELECT = f"select.{_TEST_OBJECT_ID}"
# Represent for select's current_option
_OPTION_INPUT_SELECT = "input_select.option"
TEST_STATE_ENTITY_ID = "select.test_state"
TEST_STATE_TRIGGER = {
"trigger": {
"trigger": "state",
"entity_id": [_OPTION_INPUT_SELECT, TEST_STATE_ENTITY_ID],
},
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
],
}
TEST_OPTIONS = {
"state": "test",
"options": "{{ ['test', 'yes', 'no'] }}",
"select_option": [],
}
async def async_setup_modern_format(
@ -54,6 +80,24 @@ async def async_setup_modern_format(
await hass.async_block_till_done()
async def async_setup_trigger_format(
hass: HomeAssistant, count: int, select_config: dict[str, Any]
) -> None:
"""Do setup of select integration via trigger format."""
config = {"template": {**TEST_STATE_TRIGGER, "select": select_config}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
async def setup_select(
hass: HomeAssistant,
@ -66,6 +110,10 @@ async def setup_select(
await async_setup_modern_format(
hass, count, {"name": _TEST_OBJECT_ID, **select_config}
)
if style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass, count, {"name": _TEST_OBJECT_ID, **select_config}
)
async def test_setup_config_entry(
@ -395,138 +443,49 @@ def _verify(
assert attributes.get(SELECT_ATTR_OPTIONS) == expected_options
async def test_template_icon_with_entities(hass: HomeAssistant) -> None:
"""Test templates with values from other entities."""
with assert_setup_component(1, "input_select"):
assert await setup.async_setup_component(
hass,
"input_select",
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("style", "initial_expected_state"),
[(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
)
@pytest.mark.parametrize(
("select_config", "attribute", "expected"),
[
(
{
"input_select": {
"option": {
"options": ["a", "b"],
"initial": "a",
"name": "Option",
},
}
**TEST_OPTIONS,
CONF_ICON: "{% if states.select.test_state.state == 'yes' %}mdi:check{% endif %}",
},
)
with assert_setup_component(1, "template"):
assert await setup.async_setup_component(
hass,
"template",
ATTR_ICON,
"mdi:check",
),
(
{
"template": {
"unique_id": "b",
"select": {
"state": f"{{{{ states('{_OPTION_INPUT_SELECT}') }}}}",
"options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}",
"select_option": {
"service": "input_select.select_option",
"data": {
"entity_id": _OPTION_INPUT_SELECT,
"option": "{{ option }}",
},
},
"optimistic": True,
"unique_id": "a",
"icon": f"{{% if (states('{_OPTION_INPUT_SELECT}') == 'a') %}}mdi:greater{{% else %}}mdi:less{{% endif %}}",
},
}
**TEST_OPTIONS,
CONF_PICTURE: "{% if states.select.test_state.state == 'yes' %}check.jpg{% endif %}",
},
)
ATTR_ENTITY_PICTURE,
"check.jpg",
),
],
)
@pytest.mark.usefixtures("setup_select")
async def test_templated_optional_config(
hass: HomeAssistant,
attribute: str,
expected: str,
initial_expected_state: str | None,
) -> None:
"""Test optional config templates."""
state = hass.states.get(_TEST_SELECT)
assert state.attributes.get(attribute) == initial_expected_state
await hass.async_block_till_done()
await hass.async_start()
state = hass.states.async_set(TEST_STATE_ENTITY_ID, "yes")
await hass.async_block_till_done()
state = hass.states.get(_TEST_SELECT)
assert state.state == "a"
assert state.attributes[ATTR_ICON] == "mdi:greater"
await hass.services.async_call(
INPUT_SELECT_DOMAIN,
INPUT_SELECT_SERVICE_SELECT_OPTION,
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(_TEST_SELECT)
assert state.state == "b"
assert state.attributes[ATTR_ICON] == "mdi:less"
async def test_template_icon_with_trigger(hass: HomeAssistant) -> None:
"""Test trigger based template select."""
with assert_setup_component(1, "input_select"):
assert await setup.async_setup_component(
hass,
"input_select",
{
"input_select": {
"option": {
"options": ["a", "b"],
"initial": "a",
"name": "Option",
},
}
},
)
assert await setup.async_setup_component(
hass,
"template",
{
"template": {
"trigger": {"platform": "state", "entity_id": _OPTION_INPUT_SELECT},
"select": {
"unique_id": "b",
"state": "{{ trigger.to_state.state }}",
"options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}",
"select_option": {
"service": "input_select.select_option",
"data": {
"entity_id": _OPTION_INPUT_SELECT,
"option": "{{ option }}",
},
},
"optimistic": True,
"icon": "{% if (trigger.to_state.state or '') == 'a' %}mdi:greater{% else %}mdi:less{% endif %}",
},
},
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
await hass.services.async_call(
INPUT_SELECT_DOMAIN,
INPUT_SELECT_SERVICE_SELECT_OPTION,
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(_TEST_SELECT)
assert state is not None
assert state.state == "b"
assert state.attributes[ATTR_ICON] == "mdi:less"
await hass.services.async_call(
INPUT_SELECT_DOMAIN,
INPUT_SELECT_SERVICE_SELECT_OPTION,
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "a"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(_TEST_SELECT)
assert state.state == "a"
assert state.attributes[ATTR_ICON] == "mdi:greater"
assert state.attributes[attribute] == expected
async def test_device_id(

View File

@ -5,6 +5,8 @@ from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import template
from homeassistant.components.template.const import CONF_PICTURE
from homeassistant.components.weather import (
ATTR_WEATHER_APPARENT_TEMPERATURE,
ATTR_WEATHER_CLOUD_COVERAGE,
@ -21,12 +23,21 @@ from homeassistant.components.weather import (
SERVICE_GET_FORECASTS,
Forecast,
)
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_ENTITY_PICTURE,
ATTR_ICON,
CONF_ICON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Context, HomeAssistant, State
from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .conftest import ConfigurationStyle
from tests.common import (
assert_setup_component,
async_mock_restore_state_shutdown_restart,
@ -35,6 +46,80 @@ from tests.common import (
ATTR_FORECAST = "forecast"
TEST_OBJECT_ID = "template_weather"
TEST_WEATHER = f"weather.{TEST_OBJECT_ID}"
TEST_STATE_ENTITY_ID = "weather.test_state"
TEST_STATE_TRIGGER = {
"trigger": {
"trigger": "state",
"entity_id": [TEST_STATE_ENTITY_ID],
},
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
],
}
TEST_REQUIRED = {
"condition_template": "cloudy",
"temperature_template": "{{ 20 }}",
"humidity_template": "{{ 25 }}",
}
async def async_setup_modern_format(
hass: HomeAssistant, count: int, weather_config: dict[str, Any]
) -> None:
"""Do setup of weather integration via new format."""
config = {"template": {"weather": weather_config}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
async def async_setup_trigger_format(
hass: HomeAssistant, count: int, weather_config: dict[str, Any]
) -> None:
"""Do setup of weather integration via trigger format."""
config = {"template": {**TEST_STATE_TRIGGER, "weather": weather_config}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
async def setup_weather(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
weather_config: dict[str, Any],
) -> None:
"""Do setup of weather integration."""
if style == ConfigurationStyle.MODERN:
await async_setup_modern_format(
hass, count, {"name": TEST_OBJECT_ID, **weather_config}
)
if style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass, count, {"name": TEST_OBJECT_ID, **weather_config}
)
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
@pytest.mark.parametrize(
@ -990,3 +1075,48 @@ async def test_new_style_template_state_text(hass: HomeAssistant) -> None:
assert state is not None
assert state.state == "sunny"
assert state.attributes.get(v_attr) == value
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("style", "initial_expected_state"),
[(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
)
@pytest.mark.parametrize(
("weather_config", "attribute", "expected"),
[
(
{
CONF_ICON: "{% if states.weather.test_state.state == 'sunny' %}mdi:check{% endif %}",
**TEST_REQUIRED,
},
ATTR_ICON,
"mdi:check",
),
(
{
CONF_PICTURE: "{% if states.weather.test_state.state == 'sunny' %}check.jpg{% endif %}",
**TEST_REQUIRED,
},
ATTR_ENTITY_PICTURE,
"check.jpg",
),
],
)
@pytest.mark.usefixtures("setup_weather")
async def test_templated_optional_config(
hass: HomeAssistant,
attribute: str,
expected: str,
initial_expected_state: str | None,
) -> None:
"""Test optional config templates."""
state = hass.states.get(TEST_WEATHER)
assert state.attributes.get(attribute) == initial_expected_state
state = hass.states.async_set(TEST_STATE_ENTITY_ID, "sunny")
await hass.async_block_till_done()
state = hass.states.get(TEST_WEATHER)
assert state.attributes[attribute] == expected

View File

@ -0,0 +1,16 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
alarm_control_panel:
availability: "{{ sensor | has_value }}"
state: "{{ 'armed_home' if is_state(sensor,'on') else 'disarmed' }}"

View File

@ -0,0 +1,16 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
binary_sensor:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor, 'on') }}"

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
cover:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor,'on') }}"
open_cover: []
close_cover: []

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
fan:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor,'on') }}"
turn_on: []
turn_off: []

View File

@ -0,0 +1,16 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
image:
availability: "{{ sensor | has_value }}"
url: "{{ states(sensor) }}"

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
light:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor,'on') }}"
turn_on: []
turn_off: []

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
lock:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor,'on') }}"
lock: []
unlock: []

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
number:
availability: "{{ sensor | has_value }}"
state: "{{ states(sensor) }}"
set_value: []
step: 1

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
select:
availability: "{{ sensor | has_value }}"
state: "{{ states(sensor) }}"
options: "{{ ['option1', 'option2'] }}"
select_option: []

View File

@ -0,0 +1,16 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
sensor:
availability: "{{ sensor | has_value }}"
state: "{{ states(sensor) }}"

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
switch:
availability: "{{ sensor | has_value }}"
state: "{{ is_state(sensor,'on') }}"
turn_on: []
turn_off: []

View File

@ -0,0 +1,17 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
vacuum:
availability: "{{ sensor | has_value }}"
state: "{{ states(sensor) }}"
start: []

View File

@ -0,0 +1,18 @@
blueprint:
name: Test With Variables
description: Creates a test with variables
domain: template
input:
sensor:
name: Sensor Entity
description: The sensor entity
selector:
entity:
domain: sensor
variables:
sensor: !input sensor
weather:
availability: "{{ sensor | has_value }}"
condition_template: "{{ states(sensor) }}"
temperature_template: "{{ 20 }}"
humidity_template: "{{ 25 }}"