diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 3c344891fba..bfb45f5b5c4 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -16,7 +16,9 @@ from homeassistant.const import ( CONF_COMMAND_ON, CONF_COMMAND_STATE, CONF_FRIENDLY_NAME, + CONF_ICON, CONF_ICON_TEMPLATE, + CONF_NAME, CONF_SWITCHES, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -26,6 +28,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS @@ -65,26 +68,26 @@ async def async_setup_platform( switches = [] for object_id, device_config in devices.items(): + trigger_entity_config = { + CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), + CONF_NAME: Template(device_config.get(CONF_FRIENDLY_NAME, object_id), hass), + CONF_ICON: device_config.get(CONF_ICON_TEMPLATE), + } + value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - icon_template: Template | None = device_config.get(CONF_ICON_TEMPLATE) - if icon_template is not None: - icon_template.hass = hass - switches.append( CommandSwitch( + trigger_entity_config, object_id, - device_config.get(CONF_FRIENDLY_NAME, object_id), device_config[CONF_COMMAND_ON], device_config[CONF_COMMAND_OFF], device_config.get(CONF_COMMAND_STATE), - icon_template, value_template, device_config[CONF_COMMAND_TIMEOUT], - device_config.get(CONF_UNIQUE_ID), ) ) @@ -95,32 +98,28 @@ async def async_setup_platform( async_add_entities(switches) -class CommandSwitch(SwitchEntity): +class CommandSwitch(ManualTriggerEntity, SwitchEntity): """Representation a switch that can be toggled using shell commands.""" def __init__( self, + config: ConfigType, object_id: str, - friendly_name: str, command_on: str, command_off: str, command_state: str | None, - icon_template: Template | None, value_template: Template | None, timeout: int, - unique_id: str | None, ) -> None: """Initialize the switch.""" + super().__init__(self.hass, config) self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._attr_name = friendly_name self._attr_is_on = False self._command_on = command_on self._command_off = command_off self._command_state = command_state - self._icon_template = icon_template self._value_template = value_template self._timeout = timeout - self._attr_unique_id = unique_id self._attr_should_poll = bool(command_state) async def _switch(self, command: str) -> bool: @@ -169,17 +168,15 @@ class CommandSwitch(SwitchEntity): """Update device state.""" if self._command_state: payload = str(await self.hass.async_add_executor_job(self._query_state)) - if self._icon_template: - self._attr_icon = ( - self._icon_template.async_render_with_possible_json_value(payload) - ) + value = None if self._value_template: - payload = self._value_template.async_render_with_possible_json_value( + value = self._value_template.async_render_with_possible_json_value( payload, None ) self._attr_is_on = None - if payload: - self._attr_is_on = payload.lower() == "true" + if payload or value: + self._attr_is_on = (value or payload).lower() == "true" + self._process_manual_data(payload) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index b6b39e9c32f..c4eb8a1343d 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -29,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback from homeassistant.exceptions import TemplateError +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import config_validation as cv from .entity import Entity @@ -487,9 +488,8 @@ class TriggerBaseEntity(Entity): CONF_NAME, CONF_PICTURE, ): - if itm not in config: + if itm not in config or config[itm] is None: continue - if config[itm].is_static: self._static_rendered[itm] = config[itm].template else: @@ -597,3 +597,36 @@ class TriggerBaseEntity(Entity): "Error rendering %s template for %s: %s", key, self.entity_id, err ) self._rendered = self._static_rendered + + +class ManualTriggerEntity(TriggerBaseEntity): + """Template entity based on manual trigger data.""" + + def __init__( + self, + hass: HomeAssistant, + config: dict, + ) -> None: + """Initialize the entity.""" + TriggerBaseEntity.__init__(self, hass, config) + + @callback + def _process_manual_data(self, value: str | None = None) -> None: + """Process new data manually. + + Implementing class should call this last in update method to render templates. + Ex: self._process_manual_data(payload) + """ + + self.async_write_ha_state() + this = None + if state := self.hass.states.get(self.entity_id): + this = state.as_dict() + + run_variables: dict[str, Any] = {"value": value} + # Silently try if variable is a json and store result in `value_json` if it is. + with contextlib.suppress(*JSON_DECODE_EXCEPTIONS): + run_variables["value_json"] = json_loads(run_variables["value"]) + variables = {"this": this, **(run_variables or {})} + + self._render_templates(variables) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index bc8eadcb22f..69e78379afe 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -440,3 +440,57 @@ async def test_command_failure( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True ) assert "return code 33" in caplog.text + + +async def test_templating(hass: HomeAssistant) -> None: + """Test with templating.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, "switch_status") + await setup_test_entity( + hass, + { + "test": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "value_template": '{{ value=="1" }}', + "icon_template": ( + '{% if this.state=="on" %} mdi:on {% else %} mdi:off {% endif %}' + ), + }, + "test2": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "value_template": '{{ value=="1" }}', + "icon_template": ( + '{% if states("switch.test2")=="on" %} mdi:on {% else %} mdi:off {% endif %}' + ), + }, + }, + ) + + entity_state = hass.states.get("switch.test") + entity_state2 = hass.states.get("switch.test2") + assert entity_state.state == STATE_OFF + assert entity_state2.state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test"}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test2"}, + blocking=True, + ) + + entity_state = hass.states.get("switch.test") + entity_state2 = hass.states.get("switch.test2") + assert entity_state.state == STATE_ON + assert entity_state.attributes.get("icon") == "mdi:on" + assert entity_state2.state == STATE_ON + assert entity_state2.attributes.get("icon") == "mdi:on" diff --git a/tests/components/template/test_manual_trigger_entity.py b/tests/components/template/test_manual_trigger_entity.py new file mode 100644 index 00000000000..19210645a0f --- /dev/null +++ b/tests/components/template/test_manual_trigger_entity.py @@ -0,0 +1,40 @@ +"""Test template trigger entity.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import template +from homeassistant.helpers.template_entity import ManualTriggerEntity + + +async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: + """Test manual trigger template entity.""" + config = { + "name": template.Template("test_entity", hass), + "icon": template.Template( + '{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass + ), + "picture": template.Template( + '{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}', + hass, + ), + } + + entity = ManualTriggerEntity(hass, config) + entity.entity_id = "test.entity" + hass.states.async_set("test.entity", "on") + await entity.async_added_to_hass() + + entity._process_manual_data("on") + await hass.async_block_till_done() + + assert entity.name == "test_entity" + assert entity.icon == "mdi:on" + assert entity.entity_picture == "/local/picture_on" + + hass.states.async_set("test.entity", "off") + await entity.async_added_to_hass() + entity._process_manual_data("off") + await hass.async_block_till_done() + + assert entity.name == "test_entity" + assert entity.icon == "mdi:off" + assert entity.entity_picture == "/local/picture_off"