diff --git a/CODEOWNERS b/CODEOWNERS index 3366bfb0885..4e8f78ca873 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1529,8 +1529,8 @@ build.json @home-assistant/supervisor /tests/components/tedee/ @patrickhilker @zweckj /homeassistant/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike -/homeassistant/components/template/ @PhracturedBlue @home-assistant/core -/tests/components/template/ @PhracturedBlue @home-assistant/core +/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core +/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core /homeassistant/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_wall_connector/ @einarhauks diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 0a468994295..40206a5ccbb 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -36,7 +36,6 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify @@ -199,70 +198,31 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore name = self._attr_name assert name is not None self._template = config.get(CONF_VALUE_TEMPLATE) - self._disarm_script = None self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED] self._attr_code_format = config[CONF_CODE_FORMAT].value - if (disarm_action := config.get(CONF_DISARM_ACTION)) is not None: - self._disarm_script = Script(hass, disarm_action, name, DOMAIN) - self._arm_away_script = None - if (arm_away_action := config.get(CONF_ARM_AWAY_ACTION)) is not None: - self._arm_away_script = Script(hass, arm_away_action, name, DOMAIN) - self._arm_home_script = None - if (arm_home_action := config.get(CONF_ARM_HOME_ACTION)) is not None: - self._arm_home_script = Script(hass, arm_home_action, name, DOMAIN) - self._arm_night_script = None - if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: - self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) - self._arm_vacation_script = None - if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: - self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) - self._arm_custom_bypass_script = None - if ( - arm_custom_bypass_action := config.get(CONF_ARM_CUSTOM_BYPASS_ACTION) - ) is not None: - self._arm_custom_bypass_script = Script( - hass, arm_custom_bypass_action, name, DOMAIN - ) - self._trigger_script = None - if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None: - self._trigger_script = Script(hass, trigger_action, name, DOMAIN) + + self._attr_supported_features = AlarmControlPanelEntityFeature(0) + for action_id, supported_feature in ( + (CONF_DISARM_ACTION, 0), + (CONF_ARM_AWAY_ACTION, AlarmControlPanelEntityFeature.ARM_AWAY), + (CONF_ARM_HOME_ACTION, AlarmControlPanelEntityFeature.ARM_HOME), + (CONF_ARM_NIGHT_ACTION, AlarmControlPanelEntityFeature.ARM_NIGHT), + (CONF_ARM_VACATION_ACTION, AlarmControlPanelEntityFeature.ARM_VACATION), + ( + CONF_ARM_CUSTOM_BYPASS_ACTION, + AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + ), + (CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER), + ): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature self._state: AlarmControlPanelState | None = None self._attr_device_info = async_device_info_to_link_from_device_id( hass, config.get(CONF_DEVICE_ID), ) - supported_features = AlarmControlPanelEntityFeature(0) - if self._arm_night_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.ARM_NIGHT - ) - - if self._arm_home_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.ARM_HOME - ) - - if self._arm_away_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.ARM_AWAY - ) - - if self._arm_vacation_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.ARM_VACATION - ) - - if self._arm_custom_bypass_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - ) - - if self._trigger_script is not None: - supported_features = ( - supported_features | AlarmControlPanelEntityFeature.TRIGGER - ) - self._attr_supported_features = supported_features async def async_added_to_hass(self) -> None: """Restore last state.""" @@ -330,7 +290,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore """Arm the panel to Away.""" await self._async_alarm_arm( AlarmControlPanelState.ARMED_AWAY, - script=self._arm_away_script, + script=self._action_scripts.get(CONF_ARM_AWAY_ACTION), code=code, ) @@ -338,7 +298,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore """Arm the panel to Home.""" await self._async_alarm_arm( AlarmControlPanelState.ARMED_HOME, - script=self._arm_home_script, + script=self._action_scripts.get(CONF_ARM_HOME_ACTION), code=code, ) @@ -346,7 +306,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore """Arm the panel to Night.""" await self._async_alarm_arm( AlarmControlPanelState.ARMED_NIGHT, - script=self._arm_night_script, + script=self._action_scripts.get(CONF_ARM_NIGHT_ACTION), code=code, ) @@ -354,7 +314,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore """Arm the panel to Vacation.""" await self._async_alarm_arm( AlarmControlPanelState.ARMED_VACATION, - script=self._arm_vacation_script, + script=self._action_scripts.get(CONF_ARM_VACATION_ACTION), code=code, ) @@ -362,20 +322,22 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore """Arm the panel to Custom Bypass.""" await self._async_alarm_arm( AlarmControlPanelState.ARMED_CUSTOM_BYPASS, - script=self._arm_custom_bypass_script, + script=self._action_scripts.get(CONF_ARM_CUSTOM_BYPASS_ACTION), code=code, ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( - AlarmControlPanelState.DISARMED, script=self._disarm_script, code=code + AlarmControlPanelState.DISARMED, + script=self._action_scripts.get(CONF_DISARM_ACTION), + code=code, ) async def async_alarm_trigger(self, code: str | None = None) -> None: """Trigger the panel.""" await self._async_alarm_arm( AlarmControlPanelState.TRIGGERED, - script=self._trigger_script, + script=self._action_scripts.get(CONF_TRIGGER_ACTION), code=code, ) diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index f43fc242bba..7a205446585 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -23,7 +23,6 @@ from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, ) -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_PRESS, DOMAIN @@ -121,11 +120,8 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity): """Initialize the button.""" super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None - self._command_press = ( - Script(hass, config.get(CONF_PRESS), self._attr_name, DOMAIN) - if config.get(CONF_PRESS, None) is not None - else None - ) + if action := config.get(CONF_PRESS): + self.add_script(CONF_PRESS, action, self._attr_name, DOMAIN) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_state = None self._attr_device_info = async_device_info_to_link_from_device_id( @@ -135,5 +131,5 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" - if self._command_press: - await self.async_run_script(self._command_press, context=self._context) + if script := self._action_scripts.get(CONF_PRESS): + await self.async_run_script(script, context=self._context) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 306b4405c6a..ef5e6bc5758 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -30,7 +30,6 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -103,7 +102,7 @@ PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( ) -async def _async_create_entities(hass, config): +async def _async_create_entities(hass: HomeAssistant, config): """Create the Template cover.""" covers = [] @@ -141,11 +140,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, - config, + config: dict[str, Any], unique_id, - ): + ) -> None: """Initialize the Template cover.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -153,45 +152,40 @@ class CoverTemplate(TemplateEntity, CoverEntity): self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + name = self._attr_name + if TYPE_CHECKING: + assert name is not None self._template = config.get(CONF_VALUE_TEMPLATE) self._position_template = config.get(CONF_POSITION_TEMPLATE) self._tilt_template = config.get(CONF_TILT_TEMPLATE) self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._open_script = None - if (open_action := config.get(OPEN_ACTION)) is not None: - self._open_script = Script(hass, open_action, friendly_name, DOMAIN) - self._close_script = None - if (close_action := config.get(CLOSE_ACTION)) is not None: - self._close_script = Script(hass, close_action, friendly_name, DOMAIN) - self._stop_script = None - if (stop_action := config.get(STOP_ACTION)) is not None: - self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) - self._position_script = None - if (position_action := config.get(POSITION_ACTION)) is not None: - self._position_script = Script(hass, position_action, friendly_name, DOMAIN) - self._tilt_script = None - if (tilt_action := config.get(TILT_ACTION)) is not None: - self._tilt_script = Script(hass, tilt_action, friendly_name, DOMAIN) + + # The config requires (open and close scripts) or a set position script, + # therefore the base supported features will always include them. + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + for action_id, supported_feature in ( + (OPEN_ACTION, 0), + (CLOSE_ACTION, 0), + (STOP_ACTION, CoverEntityFeature.STOP), + (POSITION_ACTION, CoverEntityFeature.SET_POSITION), + (TILT_ACTION, TILT_FEATURES), + ): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + optimistic = config.get(CONF_OPTIMISTIC) self._optimistic = optimistic or ( optimistic is None and not self._template and not self._position_template ) tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC) self._tilt_optimistic = tilt_optimistic or not self._tilt_template - self._position = None + self._position: int | None = None self._is_opening = False self._is_closing = False - self._tilt_value = None - - supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - if self._stop_script is not None: - supported_features |= CoverEntityFeature.STOP - if self._position_script is not None: - supported_features |= CoverEntityFeature.SET_POSITION - if self._tilt_script is not None: - supported_features |= TILT_FEATURES - self._attr_supported_features = supported_features + self._tilt_value: int | None = None @callback def _async_setup_templates(self) -> None: @@ -317,7 +311,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ - if self._position_template or self._position_script: + if self._position_template or self._action_scripts.get(POSITION_ACTION): return self._position return None @@ -331,11 +325,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" - if self._open_script: - await self.async_run_script(self._open_script, context=self._context) - elif self._position_script: + if (open_script := self._action_scripts.get(OPEN_ACTION)) is not None: + await self.async_run_script(open_script, context=self._context) + elif (position_script := self._action_scripts.get(POSITION_ACTION)) is not None: await self.async_run_script( - self._position_script, + position_script, run_variables={"position": 100}, context=self._context, ) @@ -345,11 +339,11 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" - if self._close_script: - await self.async_run_script(self._close_script, context=self._context) - elif self._position_script: + if (close_script := self._action_scripts.get(CLOSE_ACTION)) is not None: + await self.async_run_script(close_script, context=self._context) + elif (position_script := self._action_scripts.get(POSITION_ACTION)) is not None: await self.async_run_script( - self._position_script, + position_script, run_variables={"position": 0}, context=self._context, ) @@ -359,14 +353,14 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Fire the stop action.""" - if self._stop_script: - await self.async_run_script(self._stop_script, context=self._context) + if (stop_script := self._action_scripts.get(STOP_ACTION)) is not None: + await self.async_run_script(stop_script, context=self._context) async def async_set_cover_position(self, **kwargs: Any) -> None: """Set cover position.""" self._position = kwargs[ATTR_POSITION] await self.async_run_script( - self._position_script, + self._action_scripts[POSITION_ACTION], run_variables={"position": self._position}, context=self._context, ) @@ -377,7 +371,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): """Tilt the cover open.""" self._tilt_value = 100 await self.async_run_script( - self._tilt_script, + self._action_scripts[TILT_ACTION], run_variables={"tilt": self._tilt_value}, context=self._context, ) @@ -388,7 +382,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): """Tilt the cover closed.""" self._tilt_value = 0 await self.async_run_script( - self._tilt_script, + self._action_scripts[TILT_ACTION], run_variables={"tilt": self._tilt_value}, context=self._context, ) @@ -399,7 +393,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] await self.async_run_script( - self._tilt_script, + self._action_scripts[TILT_ACTION], run_variables={"tilt": self._tilt_value}, context=self._context, ) diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py new file mode 100644 index 00000000000..dd8623060be --- /dev/null +++ b/homeassistant/components/template/entity.py @@ -0,0 +1,66 @@ +"""Template entity base class.""" + +from collections.abc import Sequence +from typing import Any + +from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.script import Script, _VarsType +from homeassistant.helpers.template import TemplateStateFromEntityId + + +class AbstractTemplateEntity(Entity): + """Actions linked to a template entity.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the entity.""" + + self.hass = hass + self._action_scripts: dict[str, Script] = {} + + @property + def referenced_blueprint(self) -> str | None: + """Return referenced blueprint or None.""" + raise NotImplementedError + + @callback + def _render_script_variables(self) -> dict: + """Render configured variables.""" + raise NotImplementedError + + def add_script( + self, + script_id: str, + config: Sequence[dict[str, Any]], + name: str, + domain: str, + ): + """Add an action script.""" + + # Cannot use self.hass because it may be None in child class + # at instantiation. + self._action_scripts[script_id] = Script( + self.hass, + config, + f"{name} {script_id}", + domain, + ) + + async def async_run_script( + self, + script: Script, + *, + run_variables: _VarsType | None = None, + context: Context | None = None, + ) -> None: + """Run an action script.""" + if run_variables is None: + run_variables = {} + await script.async_run( + run_variables={ + "this": TemplateStateFromEntityId(self.hass, self.entity_id), + **self._render_script_variables(), + **run_variables, + }, + context=context, + ) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 6ed525fd45f..2ca05681f7f 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -32,7 +32,6 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -89,7 +88,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def _async_create_entities(hass, config): +async def _async_create_entities(hass: HomeAssistant, config): """Create the Template Fans.""" fans = [] @@ -127,11 +126,11 @@ class TemplateFan(TemplateEntity, FanEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, - config, + config: dict[str, Any], unique_id, - ): + ) -> None: """Initialize the fan.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -140,7 +139,9 @@ class TemplateFan(TemplateEntity, FanEntity): self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + name = self._attr_name + if TYPE_CHECKING: + assert name is not None self._template = config.get(CONF_VALUE_TEMPLATE) self._percentage_template = config.get(CONF_PERCENTAGE_TEMPLATE) @@ -148,44 +149,28 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) self._direction_template = config.get(CONF_DIRECTION_TEMPLATE) - self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) - self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) - - self._set_percentage_script = None - if set_percentage_action := config.get(CONF_SET_PERCENTAGE_ACTION): - self._set_percentage_script = Script( - hass, set_percentage_action, friendly_name, DOMAIN - ) - - self._set_preset_mode_script = None - if set_preset_mode_action := config.get(CONF_SET_PRESET_MODE_ACTION): - self._set_preset_mode_script = Script( - hass, set_preset_mode_action, friendly_name, DOMAIN - ) - - self._set_oscillating_script = None - if set_oscillating_action := config.get(CONF_SET_OSCILLATING_ACTION): - self._set_oscillating_script = Script( - hass, set_oscillating_action, friendly_name, DOMAIN - ) - - self._set_direction_script = None - if set_direction_action := config.get(CONF_SET_DIRECTION_ACTION): - self._set_direction_script = Script( - hass, set_direction_action, friendly_name, DOMAIN - ) + for action_id in ( + CONF_ON_ACTION, + CONF_OFF_ACTION, + CONF_SET_PERCENTAGE_ACTION, + CONF_SET_PRESET_MODE_ACTION, + CONF_SET_OSCILLATING_ACTION, + CONF_SET_DIRECTION_ACTION, + ): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) self._state: bool | None = False - self._percentage = None - self._preset_mode = None - self._oscillating = None - self._direction = None + self._percentage: int | None = None + self._preset_mode: str | None = None + self._oscillating: bool | None = None + self._direction: str | None = None # Number of valid speeds self._speed_count = config.get(CONF_SPEED_COUNT) # List of valid preset modes - self._preset_modes = config.get(CONF_PRESET_MODES) + self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES) if self._percentage_template: self._attr_supported_features |= FanEntityFeature.SET_SPEED @@ -207,7 +192,7 @@ class TemplateFan(TemplateEntity, FanEntity): return self._speed_count or 100 @property - def preset_modes(self) -> list[str]: + def preset_modes(self) -> list[str] | None: """Get the list of available preset modes.""" return self._preset_modes @@ -244,7 +229,7 @@ class TemplateFan(TemplateEntity, FanEntity): ) -> None: """Turn on the fan.""" await self.async_run_script( - self._on_script, + self._action_scripts[CONF_ON_ACTION], run_variables={ ATTR_PERCENTAGE: percentage, ATTR_PRESET_MODE: preset_mode, @@ -263,7 +248,9 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" - await self.async_run_script(self._off_script, context=self._context) + await self.async_run_script( + self._action_scripts[CONF_OFF_ACTION], context=self._context + ) if self._template is None: self._state = False @@ -273,9 +260,9 @@ class TemplateFan(TemplateEntity, FanEntity): """Set the percentage speed of the fan.""" self._percentage = percentage - if self._set_percentage_script: + if (script := self._action_scripts.get(CONF_SET_PERCENTAGE_ACTION)) is not None: await self.async_run_script( - self._set_percentage_script, + script, run_variables={ATTR_PERCENTAGE: self._percentage}, context=self._context, ) @@ -288,9 +275,11 @@ class TemplateFan(TemplateEntity, FanEntity): """Set the preset_mode of the fan.""" self._preset_mode = preset_mode - if self._set_preset_mode_script: + if ( + script := self._action_scripts.get(CONF_SET_PRESET_MODE_ACTION) + ) is not None: await self.async_run_script( - self._set_preset_mode_script, + script, run_variables={ATTR_PRESET_MODE: self._preset_mode}, context=self._context, ) @@ -301,25 +290,25 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation of the fan.""" - if self._set_oscillating_script is None: + if (script := self._action_scripts.get(CONF_SET_OSCILLATING_ACTION)) is None: return self._oscillating = oscillating await self.async_run_script( - self._set_oscillating_script, + script, run_variables={ATTR_OSCILLATING: self.oscillating}, context=self._context, ) async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - if self._set_direction_script is None: + if (script := self._action_scripts.get(CONF_SET_DIRECTION_ACTION)) is None: return if direction in _VALID_DIRECTIONS: self._direction = direction await self.async_run_script( - self._set_direction_script, + script, run_variables={ATTR_DIRECTION: direction}, context=self._context, ) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 206703ddcce..3369bf3ce0f 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -39,7 +39,6 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import color as color_util @@ -127,7 +126,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def _async_create_entities(hass, config): +async def _async_create_entities(hass: HomeAssistant, config): """Create the Template Lights.""" lights = [] @@ -164,11 +163,11 @@ class LightTemplate(TemplateEntity, LightEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, - config, + config: dict[str, Any], unique_id, - ): + ) -> None: """Initialize the light.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -176,52 +175,31 @@ class LightTemplate(TemplateEntity, LightEntity): self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + self._template = config.get(CONF_VALUE_TEMPLATE) - self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) - self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) - self._level_script = None - if (level_action := config.get(CONF_LEVEL_ACTION)) is not None: - self._level_script = Script(hass, level_action, friendly_name, DOMAIN) self._level_template = config.get(CONF_LEVEL_TEMPLATE) - self._temperature_script = None - if (temperature_action := config.get(CONF_TEMPERATURE_ACTION)) is not None: - self._temperature_script = Script( - hass, temperature_action, friendly_name, DOMAIN - ) self._temperature_template = config.get(CONF_TEMPERATURE_TEMPLATE) - self._color_script = None - if (color_action := config.get(CONF_COLOR_ACTION)) is not None: - self._color_script = Script(hass, color_action, friendly_name, DOMAIN) self._color_template = config.get(CONF_COLOR_TEMPLATE) - self._hs_script = None - if (hs_action := config.get(CONF_HS_ACTION)) is not None: - self._hs_script = Script(hass, hs_action, friendly_name, DOMAIN) self._hs_template = config.get(CONF_HS_TEMPLATE) - self._rgb_script = None - if (rgb_action := config.get(CONF_RGB_ACTION)) is not None: - self._rgb_script = Script(hass, rgb_action, friendly_name, DOMAIN) self._rgb_template = config.get(CONF_RGB_TEMPLATE) - self._rgbw_script = None - if (rgbw_action := config.get(CONF_RGBW_ACTION)) is not None: - self._rgbw_script = Script(hass, rgbw_action, friendly_name, DOMAIN) self._rgbw_template = config.get(CONF_RGBW_TEMPLATE) - self._rgbww_script = None - if (rgbww_action := config.get(CONF_RGBWW_ACTION)) is not None: - self._rgbww_script = Script(hass, rgbww_action, friendly_name, DOMAIN) self._rgbww_template = config.get(CONF_RGBWW_TEMPLATE) - self._effect_script = None - if (effect_action := config.get(CONF_EFFECT_ACTION)) is not None: - self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) self._effect_list_template = config.get(CONF_EFFECT_LIST_TEMPLATE) self._effect_template = config.get(CONF_EFFECT_TEMPLATE) self._max_mireds_template = config.get(CONF_MAX_MIREDS_TEMPLATE) self._min_mireds_template = config.get(CONF_MIN_MIREDS_TEMPLATE) self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION) + for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) + self._state = False self._brightness = None - self._temperature = None + self._temperature: int | None = None self._hs_color = None self._rgb_color = None self._rgbw_color = None @@ -235,21 +213,18 @@ class LightTemplate(TemplateEntity, LightEntity): self._supported_color_modes = None color_modes = {ColorMode.ONOFF} - if self._level_script is not None: - color_modes.add(ColorMode.BRIGHTNESS) - if self._temperature_script is not None: - color_modes.add(ColorMode.COLOR_TEMP) - if self._hs_script is not None: - color_modes.add(ColorMode.HS) - if self._color_script is not None: - color_modes.add(ColorMode.HS) - if self._rgb_script is not None: - color_modes.add(ColorMode.RGB) - if self._rgbw_script is not None: - color_modes.add(ColorMode.RGBW) - if self._rgbww_script is not None: - color_modes.add(ColorMode.RGBWW) - + for action_id, color_mode in ( + (CONF_TEMPERATURE_ACTION, ColorMode.COLOR_TEMP), + (CONF_LEVEL_ACTION, ColorMode.BRIGHTNESS), + (CONF_COLOR_ACTION, ColorMode.HS), + (CONF_HS_ACTION, ColorMode.HS), + (CONF_RGB_ACTION, ColorMode.RGB), + (CONF_RGBW_ACTION, ColorMode.RGBW), + (CONF_RGBWW_ACTION, ColorMode.RGBWW), + ): + if (action_config := config.get(action_id)) is not None: + self.add_script(action_id, action_config, name, DOMAIN) + color_modes.add(color_mode) self._supported_color_modes = filter_supported_color_modes(color_modes) if len(self._supported_color_modes) > 1: self._color_mode = ColorMode.UNKNOWN @@ -257,7 +232,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._color_mode = next(iter(self._supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) - if self._effect_script is not None: + if self._action_scripts.get(CONF_EFFECT_ACTION) is not None: self._attr_supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: self._attr_supported_features |= LightEntityFeature.TRANSITION @@ -321,12 +296,12 @@ class LightTemplate(TemplateEntity, LightEntity): return self._effect_list @property - def color_mode(self): + def color_mode(self) -> ColorMode | None: """Return current color mode.""" return self._color_mode @property - def supported_color_modes(self): + def supported_color_modes(self) -> set[ColorMode] | None: """Flag supported color modes.""" return self._supported_color_modes @@ -555,17 +530,28 @@ class LightTemplate(TemplateEntity, LightEntity): if ATTR_TRANSITION in kwargs and self._supports_transition is True: common_params["transition"] = kwargs[ATTR_TRANSITION] - if ATTR_COLOR_TEMP_KELVIN in kwargs and self._temperature_script: + if ( + ATTR_COLOR_TEMP_KELVIN in kwargs + and ( + temperature_script := self._action_scripts.get(CONF_TEMPERATURE_ACTION) + ) + is not None + ): common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired( kwargs[ATTR_COLOR_TEMP_KELVIN] ) await self.async_run_script( - self._temperature_script, + temperature_script, run_variables=common_params, context=self._context, ) - elif ATTR_EFFECT in kwargs and self._effect_script: + elif ( + ATTR_EFFECT in kwargs + and (effect_script := self._action_scripts.get(CONF_EFFECT_ACTION)) + is not None + ): + assert self._effect_list is not None effect = kwargs[ATTR_EFFECT] if effect not in self._effect_list: _LOGGER.error( @@ -579,27 +565,38 @@ class LightTemplate(TemplateEntity, LightEntity): common_params["effect"] = effect await self.async_run_script( - self._effect_script, run_variables=common_params, context=self._context + effect_script, run_variables=common_params, context=self._context ) - elif ATTR_HS_COLOR in kwargs and self._color_script: + elif ( + ATTR_HS_COLOR in kwargs + and (color_script := self._action_scripts.get(CONF_COLOR_ACTION)) + is not None + ): hs_value = kwargs[ATTR_HS_COLOR] common_params["hs"] = hs_value common_params["h"] = int(hs_value[0]) common_params["s"] = int(hs_value[1]) await self.async_run_script( - self._color_script, run_variables=common_params, context=self._context + color_script, run_variables=common_params, context=self._context ) - elif ATTR_HS_COLOR in kwargs and self._hs_script: + elif ( + ATTR_HS_COLOR in kwargs + and (hs_script := self._action_scripts.get(CONF_HS_ACTION)) is not None + ): hs_value = kwargs[ATTR_HS_COLOR] common_params["hs"] = hs_value common_params["h"] = int(hs_value[0]) common_params["s"] = int(hs_value[1]) await self.async_run_script( - self._hs_script, run_variables=common_params, context=self._context + hs_script, run_variables=common_params, context=self._context ) - elif ATTR_RGBWW_COLOR in kwargs and self._rgbww_script: + elif ( + ATTR_RGBWW_COLOR in kwargs + and (rgbww_script := self._action_scripts.get(CONF_RGBWW_ACTION)) + is not None + ): rgbww_value = kwargs[ATTR_RGBWW_COLOR] common_params["rgbww"] = rgbww_value common_params["rgb"] = ( @@ -614,9 +611,12 @@ class LightTemplate(TemplateEntity, LightEntity): common_params["ww"] = int(rgbww_value[4]) await self.async_run_script( - self._rgbww_script, run_variables=common_params, context=self._context + rgbww_script, run_variables=common_params, context=self._context ) - elif ATTR_RGBW_COLOR in kwargs and self._rgbw_script: + elif ( + ATTR_RGBW_COLOR in kwargs + and (rgbw_script := self._action_scripts.get(CONF_RGBW_ACTION)) is not None + ): rgbw_value = kwargs[ATTR_RGBW_COLOR] common_params["rgbw"] = rgbw_value common_params["rgb"] = ( @@ -630,9 +630,12 @@ class LightTemplate(TemplateEntity, LightEntity): common_params["w"] = int(rgbw_value[3]) await self.async_run_script( - self._rgbw_script, run_variables=common_params, context=self._context + rgbw_script, run_variables=common_params, context=self._context ) - elif ATTR_RGB_COLOR in kwargs and self._rgb_script: + elif ( + ATTR_RGB_COLOR in kwargs + and (rgb_script := self._action_scripts.get(CONF_RGB_ACTION)) is not None + ): rgb_value = kwargs[ATTR_RGB_COLOR] common_params["rgb"] = rgb_value common_params["r"] = int(rgb_value[0]) @@ -640,15 +643,21 @@ class LightTemplate(TemplateEntity, LightEntity): common_params["b"] = int(rgb_value[2]) await self.async_run_script( - self._rgb_script, run_variables=common_params, context=self._context + rgb_script, run_variables=common_params, context=self._context ) - elif ATTR_BRIGHTNESS in kwargs and self._level_script: + elif ( + ATTR_BRIGHTNESS in kwargs + and (level_script := self._action_scripts.get(CONF_LEVEL_ACTION)) + is not None + ): await self.async_run_script( - self._level_script, run_variables=common_params, context=self._context + level_script, run_variables=common_params, context=self._context ) else: await self.async_run_script( - self._on_script, run_variables=common_params, context=self._context + self._action_scripts[CONF_ON_ACTION], + run_variables=common_params, + context=self._context, ) if optimistic_set: @@ -656,14 +665,15 @@ class LightTemplate(TemplateEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" + off_script = self._action_scripts[CONF_OFF_ACTION] if ATTR_TRANSITION in kwargs and self._supports_transition is True: await self.async_run_script( - self._off_script, + off_script, run_variables={"transition": kwargs[ATTR_TRANSITION]}, context=self._context, ) else: - await self.async_run_script(self._off_script, context=self._context) + await self.async_run_script(off_script, context=self._context) if self._template is None: self._state = False self.async_write_ha_state() @@ -1013,7 +1023,7 @@ class LightTemplate(TemplateEntity, LightEntity): if render in (None, "None", ""): self._supports_transition = False return - self._attr_supported_features &= ~LightEntityFeature.TRANSITION + self._attr_supported_features &= LightEntityFeature.EFFECT self._supports_transition = bool(render) if self._supports_transition: self._attr_supported_features |= LightEntityFeature.TRANSITION diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 0804f92e46d..b19cadff26c 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -23,7 +23,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -90,13 +89,18 @@ class TemplateLock(TemplateEntity, LockEntity): ) self._state: LockState | None = None name = self._attr_name - assert name + if TYPE_CHECKING: + assert name is not None + self._state_template = config.get(CONF_VALUE_TEMPLATE) - self._command_lock = Script(hass, config[CONF_LOCK], name, DOMAIN) - self._command_unlock = Script(hass, config[CONF_UNLOCK], name, DOMAIN) - if CONF_OPEN in config: - self._command_open = Script(hass, config[CONF_OPEN], name, DOMAIN) - self._attr_supported_features |= LockEntityFeature.OPEN + for action_id, supported_feature in ( + (CONF_LOCK, 0), + (CONF_UNLOCK, 0), + (CONF_OPEN, LockEntityFeature.OPEN), + ): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE) self._code_format: str | None = None self._code_format_template_error: TemplateError | None = None @@ -210,7 +214,9 @@ class TemplateLock(TemplateEntity, LockEntity): tpl_vars = {ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None} await self.async_run_script( - self._command_lock, run_variables=tpl_vars, context=self._context + self._action_scripts[CONF_LOCK], + run_variables=tpl_vars, + context=self._context, ) async def async_unlock(self, **kwargs: Any) -> None: @@ -226,7 +232,9 @@ class TemplateLock(TemplateEntity, LockEntity): tpl_vars = {ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None} await self.async_run_script( - self._command_unlock, run_variables=tpl_vars, context=self._context + self._action_scripts[CONF_UNLOCK], + run_variables=tpl_vars, + context=self._context, ) async def async_open(self, **kwargs: Any) -> None: @@ -242,7 +250,9 @@ class TemplateLock(TemplateEntity, LockEntity): tpl_vars = {ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None} await self.async_run_script( - self._command_open, run_variables=tpl_vars, context=self._context + self._action_scripts[CONF_OPEN], + run_variables=tpl_vars, + context=self._context, ) def _raise_template_error_if_available(self): diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index f1225f74f06..32bfd8ce02e 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -2,7 +2,7 @@ "domain": "template", "name": "Template", "after_dependencies": ["group"], - "codeowners": ["@PhracturedBlue", "@home-assistant/core"], + "codeowners": ["@Petro31", "@PhracturedBlue", "@home-assistant/core"], "config_flow": true, "dependencies": ["blueprint"], "documentation": "https://www.home-assistant.io/integrations/template", diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 661dbb45dc1..6661afc619c 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -157,9 +157,7 @@ class TemplateNumber(TemplateEntity, NumberEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - self._command_set_value = Script( - hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN - ) + self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], self._attr_name, DOMAIN) self._step_template = config[CONF_STEP] self._min_value_template = config[CONF_MIN] @@ -210,9 +208,9 @@ class TemplateNumber(TemplateEntity, NumberEntity): if self._optimistic: self._attr_native_value = value self.async_write_ha_state() - if self._command_set_value: + if (set_value := self._action_scripts.get(CONF_SET_VALUE)) is not None: await self.async_run_script( - self._command_set_value, + set_value, run_variables={ATTR_VALUE: value}, context=self._context, ) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index a42ee3d0612..d3b879a695d 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -143,8 +143,8 @@ class TemplateSelect(TemplateEntity, SelectEntity): assert self._attr_name is not None self._value_template = config[CONF_STATE] if (selection_option := config.get(CONF_SELECT_OPTION)) is not None: - self._command_select_option = Script( - hass, selection_option, self._attr_name, DOMAIN + self.add_script( + CONF_SELECT_OPTION, selection_option, self._attr_name, DOMAIN ) self._options_template = config[ATTR_OPTIONS] self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False) @@ -177,9 +177,9 @@ class TemplateSelect(TemplateEntity, SelectEntity): if self._optimistic: self._attr_current_option = option self.async_write_ha_state() - if self._command_select_option: + if (select_option := self._action_scripts.get(CONF_SELECT_OPTION)) is not None: await self.async_run_script( - self._command_select_option, + select_option, run_variables={ATTR_OPTION: option}, context=self._context, ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 756866cfd44..148648a7a3c 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -33,7 +33,6 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN @@ -74,7 +73,7 @@ SWITCH_CONFIG_SCHEMA = vol.Schema( ) -async def _async_create_entities(hass, config): +async def _async_create_entities(hass: HomeAssistant, config: ConfigType): """Create the Template switches.""" switches = [] @@ -134,11 +133,11 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, - config, + config: ConfigType, unique_id, - ): + ) -> None: """Initialize the Template switch.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -147,18 +146,16 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + name = self._attr_name + if TYPE_CHECKING: + assert name is not None self._template = config.get(CONF_VALUE_TEMPLATE) - self._on_script = ( - Script(hass, config.get(CONF_TURN_ON), friendly_name, DOMAIN) - if config.get(CONF_TURN_ON) is not None - else None - ) - self._off_script = ( - Script(hass, config.get(CONF_TURN_OFF), friendly_name, DOMAIN) - if config.get(CONF_TURN_OFF) is not None - else None - ) + + if on_action := config.get(CONF_TURN_ON): + self.add_script(CONF_TURN_ON, on_action, name, DOMAIN) + if off_action := config.get(CONF_TURN_OFF): + self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN) + self._state: bool | None = False self._attr_assumed_state = self._template is None self._attr_device_info = async_device_info_to_link_from_device_id( @@ -209,16 +206,16 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Fire the on action.""" - if self._on_script: - await self.async_run_script(self._on_script, context=self._context) + if (on_script := self._action_scripts.get(CONF_TURN_ON)) is not None: + await self.async_run_script(on_script, context=self._context) if self._template is None: self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Fire the off action.""" - if self._off_script: - await self.async_run_script(self._off_script, context=self._context) + if (off_script := self._action_scripts.get(CONF_TURN_OFF)) is not None: + await self.async_run_script(off_script, context=self._context) if self._template is None: self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 8f9edca5976..93ba1fa7471 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -24,7 +24,6 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, - Context, Event, EventStateChangedData, HomeAssistant, @@ -41,7 +40,7 @@ from homeassistant.helpers.event import ( TrackTemplateResultInfo, async_track_template_result, ) -from homeassistant.helpers.script import Script, _VarsType +from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.start import async_at_start from homeassistant.helpers.template import ( Template, @@ -61,6 +60,7 @@ from .const import ( CONF_AVAILABILITY_TEMPLATE, CONF_PICTURE, ) +from .entity import AbstractTemplateEntity _LOGGER = logging.getLogger(__name__) @@ -248,7 +248,7 @@ class _TemplateAttribute: return -class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module +class TemplateEntity(AbstractTemplateEntity): # pylint: disable=hass-enforce-class-module """Entity that uses templates to calculate attributes.""" _attr_available = True @@ -268,6 +268,7 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module unique_id: str | None = None, ) -> None: """Template Entity.""" + super().__init__(hass) self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} self._template_result_info: TrackTemplateResultInfo | None = None self._attr_extra_state_attributes = {} @@ -285,6 +286,7 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module ] | None ) = None + self._run_variables: ScriptVariables | dict if config is None: self._attribute_templates = attribute_templates self._availability_template = availability_template @@ -339,18 +341,6 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module variables=variables, parse_result=False ) - @callback - def _render_variables(self) -> dict: - if isinstance(self._run_variables, dict): - return self._run_variables - - return self._run_variables.async_render( - self.hass, - { - "this": TemplateStateFromEntityId(self.hass, self.entity_id), - }, - ) - @callback def _update_available(self, result: str | TemplateError) -> None: if isinstance(result, TemplateError): @@ -387,6 +377,18 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module return None return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]) + def _render_script_variables(self) -> dict[str, Any]: + """Render configured variables.""" + if isinstance(self._run_variables, dict): + return self._run_variables + + return self._run_variables.async_render( + self.hass, + { + "this": TemplateStateFromEntityId(self.hass, self.entity_id), + }, + ) + def add_template_attribute( self, attribute: str, @@ -488,7 +490,7 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module variables = { "this": TemplateStateFromEntityId(self.hass, self.entity_id), - **self._render_variables(), + **self._render_script_variables(), } for template, attributes in self._template_attrs.items(): @@ -581,22 +583,3 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module """Call for forced update.""" assert self._template_result_info self._template_result_info.async_refresh() - - async def async_run_script( - self, - script: Script, - *, - run_variables: _VarsType | None = None, - context: Context | None = None, - ) -> None: - """Run an action script.""" - if run_variables is None: - run_variables = {} - await script.async_run( - run_variables={ - "this": TemplateStateFromEntityId(self.hass, self.entity_id), - **self._render_variables(), - **run_variables, - }, - context=context, - ) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index b977f4e659a..ba7c330dad2 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -33,7 +33,6 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -90,7 +89,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def _async_create_entities(hass, config): +async def _async_create_entities(hass: HomeAssistant, config: ConfigType): """Create the Template Vacuums.""" vacuums = [] @@ -127,11 +126,11 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): def __init__( self, - hass, + hass: HomeAssistant, object_id, - config, + config: ConfigType, unique_id, - ): + ) -> None: """Initialize the vacuum.""" super().__init__( hass, config=config, fallback_name=object_id, unique_id=unique_id @@ -139,7 +138,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - friendly_name = self._attr_name + name = self._attr_name + if TYPE_CHECKING: + assert name is not None self._template = config.get(CONF_VALUE_TEMPLATE) self._battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) @@ -148,43 +149,18 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): VacuumEntityFeature.START | VacuumEntityFeature.STATE ) - self._start_script = Script(hass, config[SERVICE_START], friendly_name, DOMAIN) - - self._pause_script = None - if pause_action := config.get(SERVICE_PAUSE): - self._pause_script = Script(hass, pause_action, friendly_name, DOMAIN) - self._attr_supported_features |= VacuumEntityFeature.PAUSE - - self._stop_script = None - if stop_action := config.get(SERVICE_STOP): - self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) - self._attr_supported_features |= VacuumEntityFeature.STOP - - self._return_to_base_script = None - if return_to_base_action := config.get(SERVICE_RETURN_TO_BASE): - self._return_to_base_script = Script( - hass, return_to_base_action, friendly_name, DOMAIN - ) - self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME - - self._clean_spot_script = None - if clean_spot_action := config.get(SERVICE_CLEAN_SPOT): - self._clean_spot_script = Script( - hass, clean_spot_action, friendly_name, DOMAIN - ) - self._attr_supported_features |= VacuumEntityFeature.CLEAN_SPOT - - self._locate_script = None - if locate_action := config.get(SERVICE_LOCATE): - self._locate_script = Script(hass, locate_action, friendly_name, DOMAIN) - self._attr_supported_features |= VacuumEntityFeature.LOCATE - - self._set_fan_speed_script = None - if set_fan_speed_action := config.get(SERVICE_SET_FAN_SPEED): - self._set_fan_speed_script = Script( - hass, set_fan_speed_action, friendly_name, DOMAIN - ) - self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED + for action_id, supported_feature in ( + (SERVICE_START, 0), + (SERVICE_PAUSE, VacuumEntityFeature.PAUSE), + (SERVICE_STOP, VacuumEntityFeature.STOP), + (SERVICE_RETURN_TO_BASE, VacuumEntityFeature.RETURN_HOME), + (SERVICE_CLEAN_SPOT, VacuumEntityFeature.CLEAN_SPOT), + (SERVICE_LOCATE, VacuumEntityFeature.LOCATE), + (SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED), + ): + if action_config := config.get(action_id): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature self._state = None self._battery_level = None @@ -203,62 +179,50 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): async def async_start(self) -> None: """Start or resume the cleaning task.""" - await self.async_run_script(self._start_script, context=self._context) + await self.async_run_script( + self._action_scripts[SERVICE_START], context=self._context + ) async def async_pause(self) -> None: """Pause the cleaning task.""" - if self._pause_script is None: - return - - await self.async_run_script(self._pause_script, context=self._context) + if (script := self._action_scripts.get(SERVICE_PAUSE)) is not None: + await self.async_run_script(script, context=self._context) async def async_stop(self, **kwargs: Any) -> None: """Stop the cleaning task.""" - if self._stop_script is None: - return - - await self.async_run_script(self._stop_script, context=self._context) + if (script := self._action_scripts.get(SERVICE_STOP)) is not None: + await self.async_run_script(script, context=self._context) async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" - if self._return_to_base_script is None: - return - - await self.async_run_script(self._return_to_base_script, context=self._context) + if (script := self._action_scripts.get(SERVICE_RETURN_TO_BASE)) is not None: + await self.async_run_script(script, context=self._context) async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" - if self._clean_spot_script is None: - return - - await self.async_run_script(self._clean_spot_script, context=self._context) + if (script := self._action_scripts.get(SERVICE_CLEAN_SPOT)) is not None: + await self.async_run_script(script, context=self._context) async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" - if self._locate_script is None: - return - - await self.async_run_script(self._locate_script, context=self._context) + if (script := self._action_scripts.get(SERVICE_LOCATE)) is not None: + await self.async_run_script(script, context=self._context) async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - if self._set_fan_speed_script is None: - return - - if fan_speed in self._attr_fan_speed_list: - self._attr_fan_speed = fan_speed - await self.async_run_script( - self._set_fan_speed_script, - run_variables={ATTR_FAN_SPEED: fan_speed}, - context=self._context, - ) - else: + if fan_speed not in self._attr_fan_speed_list: _LOGGER.error( "Received invalid fan speed: %s for entity %s. Expected: %s", fan_speed, self.entity_id, self._attr_fan_speed_list, ) + return + + if (script := self._action_scripts.get(SERVICE_SET_FAN_SPEED)) is not None: + await self.async_run_script( + script, run_variables={ATTR_FAN_SPEED: fan_speed}, context=self._context + ) @callback def _async_setup_templates(self) -> None: diff --git a/tests/components/template/test_entity.py b/tests/components/template/test_entity.py new file mode 100644 index 00000000000..67a85839982 --- /dev/null +++ b/tests/components/template/test_entity.py @@ -0,0 +1,17 @@ +"""Test abstract template entity.""" + +import pytest + +from homeassistant.components.template import entity as abstract_entity +from homeassistant.core import HomeAssistant + + +async def test_template_entity_not_implemented(hass: HomeAssistant) -> None: + """Test abstract template entity raises not implemented error.""" + + entity = abstract_entity.AbstractTemplateEntity(None) + with pytest.raises(NotImplementedError): + _ = entity.referenced_blueprint + + with pytest.raises(NotImplementedError): + entity._render_script_variables() diff --git a/tests/components/template/test_template_entity.py b/tests/components/template/test_template_entity.py index c09a09750fe..d66fc2710c9 100644 --- a/tests/components/template/test_template_entity.py +++ b/tests/components/template/test_template_entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import template async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: """Test template entity requires hass to be set before accepting templates.""" - entity = template_entity.TemplateEntity(hass) + entity = template_entity.TemplateEntity(None) with pytest.raises(ValueError, match="^hass cannot be None"): entity.add_template_attribute("_hello", template.Template("Hello"))