mirror of
				https://github.com/home-assistant/core.git
				synced 2025-11-01 06:59:31 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			2025.11.0b
			...
			manual_tri
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 635669278c | ||
|   | 1c5eb92c9c | ||
|   | 3337dd4ed7 | ||
|   | f1d21685e6 | ||
|   | 73f27549e4 | ||
|   | 1882b914dc | ||
|   | 06f99dc9ba | ||
|   | 2e2c718d94 | ||
|   | b8f56a6ed6 | ||
|   | db37dbec03 | ||
|   | 579f44468e | ||
|   | d452e957c9 | ||
|   | 5f9bcd583b | ||
|   | c0c508c7a2 | ||
|   | 13f5adfa84 | ||
|   | a07a3a61bf | ||
|   | 848162debd | ||
|   | 07cd669bc1 | 
| @@ -121,6 +121,7 @@ class TriggerBaseEntity(Entity): | ||||
|         self._rendered = dict(self._static_rendered) | ||||
|         self._parse_result = {CONF_AVAILABILITY} | ||||
|         self._attr_device_class = config.get(CONF_DEVICE_CLASS) | ||||
|         self._render_error = False | ||||
|  | ||||
|     @property | ||||
|     def name(self) -> str | None: | ||||
| @@ -146,7 +147,7 @@ class TriggerBaseEntity(Entity): | ||||
|     def available(self) -> bool: | ||||
|         """Return availability of the entity.""" | ||||
|         return ( | ||||
|             self._rendered is not self._static_rendered | ||||
|             self._render_error is False | ||||
|             and | ||||
|             # Check against False so `None` is ok | ||||
|             self._rendered.get(CONF_AVAILABILITY) is not False | ||||
| @@ -176,12 +177,34 @@ class TriggerBaseEntity(Entity): | ||||
|                 extra_state_attributes[attr] = last_state.attributes[attr] | ||||
|             self._rendered[CONF_ATTRIBUTES] = extra_state_attributes | ||||
|  | ||||
|     def _render_availability_template(self, variables: dict[str, Any]) -> None: | ||||
|         """Render availability template.""" | ||||
|         self._render_error = False | ||||
|         rendered = {**self._static_rendered, **self._rendered} | ||||
|         key = CONF_AVAILABILITY | ||||
|         try: | ||||
|             if key in self._to_render_simple: | ||||
|                 rendered[key] = self._config[key].async_render( | ||||
|                     variables, | ||||
|                     parse_result=key in self._parse_result, | ||||
|                 ) | ||||
|         except TemplateError as err: | ||||
|             logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( | ||||
|                 "Error rendering %s template for %s: %s", key, self.entity_id, err | ||||
|             ) | ||||
|             self._render_error = True | ||||
|         self._rendered = rendered | ||||
|  | ||||
|     def _render_templates(self, variables: dict[str, Any]) -> None: | ||||
|         """Render templates.""" | ||||
|         self._render_availability_template(variables) | ||||
|         rendered = dict(self._rendered) | ||||
|         if CONF_AVAILABILITY in rendered and rendered[CONF_AVAILABILITY] is False: | ||||
|             return | ||||
|         try: | ||||
|             rendered = dict(self._static_rendered) | ||||
|  | ||||
|             for key in self._to_render_simple: | ||||
|                 if key == CONF_AVAILABILITY: | ||||
|                     continue | ||||
|                 rendered[key] = self._config[key].async_render( | ||||
|                     variables, | ||||
|                     parse_result=key in self._parse_result, | ||||
| @@ -198,13 +221,13 @@ class TriggerBaseEntity(Entity): | ||||
|                     self._config[CONF_ATTRIBUTES], | ||||
|                     variables, | ||||
|                 ) | ||||
|  | ||||
|             self._rendered = rendered | ||||
|         except TemplateError as err: | ||||
|             logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( | ||||
|                 "Error rendering %s template for %s: %s", key, self.entity_id, err | ||||
|             ) | ||||
|             self._rendered = self._static_rendered | ||||
|             self._render_error = True | ||||
|             return | ||||
|         self._rendered = rendered | ||||
|  | ||||
|  | ||||
| class ManualTriggerEntity(TriggerBaseEntity): | ||||
| @@ -230,16 +253,15 @@ class ManualTriggerEntity(TriggerBaseEntity): | ||||
|         Implementing class should call this last in update method to render templates. | ||||
|         Ex: self._process_manual_data(payload) | ||||
|         """ | ||||
|  | ||||
|         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": TemplateStateFromEntityId(self.hass, self.entity_id), | ||||
|             **(run_variables or {}), | ||||
|         } | ||||
|  | ||||
|         self._render_templates(variables) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -808,3 +808,52 @@ async def test_availability( | ||||
|     entity_state = hass.states.get("sensor.test") | ||||
|     assert entity_state | ||||
|     assert entity_state.state == STATE_UNAVAILABLE | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "get_config", | ||||
|     [ | ||||
|         { | ||||
|             "command_line": [ | ||||
|                 { | ||||
|                     "sensor": { | ||||
|                         "name": "Test", | ||||
|                         "command": "echo {{ states.sensor.input_sensor.state }}", | ||||
|                         "availability": "{{ value|is_number}}", | ||||
|                         "unit_of_measurement": " ", | ||||
|                         "state_class": "measurement", | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
| ) | ||||
| async def test_template_render_not_break_for_availability( | ||||
|     hass: HomeAssistant, load_yaml_integration: None | ||||
| ) -> None: | ||||
|     """Ensure command with templates get rendered properly.""" | ||||
|     hass.states.async_set("sensor.input_sensor", "sensor_value") | ||||
|  | ||||
|     # Give time for template to load | ||||
|     async_fire_time_changed( | ||||
|         hass, | ||||
|         dt_util.utcnow() + timedelta(minutes=1), | ||||
|     ) | ||||
|     await hass.async_block_till_done(wait_background_tasks=True) | ||||
|  | ||||
|     entity_state = hass.states.get("sensor.test") | ||||
|     assert entity_state | ||||
|     assert entity_state.state == STATE_UNAVAILABLE | ||||
|  | ||||
|     hass.states.async_set("sensor.input_sensor", "1") | ||||
|  | ||||
|     # Give time for template to load | ||||
|     async_fire_time_changed( | ||||
|         hass, | ||||
|         dt_util.utcnow() + timedelta(minutes=1), | ||||
|     ) | ||||
|     await hass.async_block_till_done(wait_background_tasks=True) | ||||
|  | ||||
|     entity_state = hass.states.get("sensor.test") | ||||
|     assert entity_state | ||||
|     assert entity_state.state == "1" | ||||
|   | ||||
| @@ -1054,3 +1054,54 @@ async def test_availability_in_config(hass: HomeAssistant) -> None: | ||||
|  | ||||
|     state = hass.states.get("sensor.rest_sensor") | ||||
|     assert state.state == STATE_UNAVAILABLE | ||||
|  | ||||
|  | ||||
| @respx.mock | ||||
| async def test_json_response_with_availability(hass: HomeAssistant) -> None: | ||||
|     """Test availability with complex json.""" | ||||
|  | ||||
|     respx.get("http://localhost").respond( | ||||
|         status_code=HTTPStatus.OK, | ||||
|         json={"heartbeatList": {"1": [{"status": 1, "ping": 21.4}]}}, | ||||
|     ) | ||||
|     assert await async_setup_component( | ||||
|         hass, | ||||
|         DOMAIN, | ||||
|         { | ||||
|             DOMAIN: [ | ||||
|                 { | ||||
|                     "resource": "http://localhost", | ||||
|                     "sensor": [ | ||||
|                         { | ||||
|                             "unique_id": "complex_json", | ||||
|                             "name": "complex_json", | ||||
|                             "value_template": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.ping }}', | ||||
|                             "availability": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.status == 1 and is_number(v.ping) }}', | ||||
|                             "unit_of_measurement": "ms", | ||||
|                             "state_class": "measurement", | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|     ) | ||||
|     await async_setup_component(hass, "homeassistant", {}) | ||||
|     await hass.async_block_till_done() | ||||
|     assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 | ||||
|  | ||||
|     state = hass.states.get("sensor.complex_json") | ||||
|     assert state.state == "21.4" | ||||
|  | ||||
|     respx.get("http://localhost").respond( | ||||
|         status_code=HTTPStatus.OK, | ||||
|         json={"heartbeatList": {"1": [{"status": 0, "ping": None}]}}, | ||||
|     ) | ||||
|     await hass.services.async_call( | ||||
|         "homeassistant", | ||||
|         "update_entity", | ||||
|         {ATTR_ENTITY_ID: ["sensor.complex_json"]}, | ||||
|         blocking=True, | ||||
|     ) | ||||
|     await hass.async_block_till_done() | ||||
|     state = hass.states.get("sensor.complex_json") | ||||
|     assert state.state == STATE_UNAVAILABLE | ||||
|   | ||||
| @@ -2,7 +2,9 @@ | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from homeassistant.components import sensor | ||||
| from homeassistant.components.template import template_entity | ||||
| from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers import template | ||||
|  | ||||
| @@ -22,3 +24,46 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: | ||||
|     entity.add_template_attribute("_hello", tpl_with_hass) | ||||
|  | ||||
|     assert len(entity._template_attrs.get(tpl_with_hass, [])) == 1 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)]) | ||||
| @pytest.mark.parametrize( | ||||
|     "config", | ||||
|     [ | ||||
|         { | ||||
|             "sensor": { | ||||
|                 "platform": "template", | ||||
|                 "sensors": { | ||||
|                     "test_template_sensor": { | ||||
|                         "value_template": "{{ states.sensor.test_sensor.state }}", | ||||
|                         "availability_template": "{{ is_state('sensor.test_sensor', 'on') }}", | ||||
|                         "icon_template": "{% if states.sensor.test_sensor.state == 'on' %}mdi:on{% else %}mdi:off{% endif %}", | ||||
|                     } | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| ) | ||||
| @pytest.mark.usefixtures("start_ha") | ||||
| async def test_unavailable_does_not_render_other_state_attributes( | ||||
|     hass: HomeAssistant, | ||||
| ) -> None: | ||||
|     """Test when entity goes unavailable, other state attributes are not rendered.""" | ||||
|     hass.states.async_set("sensor.test_sensor", STATE_OFF) | ||||
|  | ||||
|     # When template returns true.. | ||||
|     hass.states.async_set("sensor.test_sensor", STATE_ON) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     # Device State should not be unavailable | ||||
|     assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE | ||||
|     assert hass.states.get("sensor.test_template_sensor").attributes["icon"] == "mdi:on" | ||||
|  | ||||
|     # When Availability template returns false | ||||
|     hass.states.async_set("sensor.test_sensor", STATE_OFF) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     # device state should be unavailable | ||||
|     assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE | ||||
|     # Icon should be mdi:on as going unavailable does not render state attributes | ||||
|     assert hass.states.get("sensor.test_template_sensor").attributes["icon"] == "mdi:on" | ||||
|   | ||||
| @@ -1,18 +1,27 @@ | ||||
| """Test template trigger entity.""" | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from homeassistant.const import CONF_ICON, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers import template | ||||
| from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity | ||||
| from homeassistant.helpers.trigger_template_entity import ( | ||||
|     CONF_AVAILABILITY, | ||||
|     CONF_PICTURE, | ||||
|     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( | ||||
|         CONF_NAME: template.Template("test_entity", hass), | ||||
|         CONF_ICON: template.Template( | ||||
|             '{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass | ||||
|         ), | ||||
|         "picture": template.Template( | ||||
|         CONF_PICTURE: template.Template( | ||||
|             '{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}', | ||||
|             hass, | ||||
|         ), | ||||
| @@ -20,21 +29,137 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: | ||||
|  | ||||
|     entity = ManualTriggerEntity(hass, config) | ||||
|     entity.entity_id = "test.entity" | ||||
|     hass.states.async_set("test.entity", "on") | ||||
|     hass.states.async_set("test.entity", STATE_ON) | ||||
|     await entity.async_added_to_hass() | ||||
|  | ||||
|     entity._process_manual_data("on") | ||||
|     entity._process_manual_data(STATE_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") | ||||
|     hass.states.async_set("test.entity", STATE_OFF) | ||||
|     await entity.async_added_to_hass() | ||||
|     entity._process_manual_data("off") | ||||
|     entity._process_manual_data(STATE_OFF) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert entity.name == "test_entity" | ||||
|     assert entity.icon == "mdi:off" | ||||
|     assert entity.entity_picture == "/local/picture_off" | ||||
|  | ||||
|  | ||||
| async def test_trigger_template_availability(hass: HomeAssistant) -> None: | ||||
|     """Test manual trigger template entity availability template.""" | ||||
|     config = { | ||||
|         CONF_NAME: template.Template("test_entity", hass), | ||||
|         CONF_ICON: template.Template( | ||||
|             '{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass | ||||
|         ), | ||||
|         CONF_PICTURE: template.Template( | ||||
|             '{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}', | ||||
|             hass, | ||||
|         ), | ||||
|         CONF_AVAILABILITY: template.Template('{{ has_value("test.entity") }}', hass), | ||||
|     } | ||||
|  | ||||
|     entity = ManualTriggerEntity(hass, config) | ||||
|     entity.entity_id = "test.entity" | ||||
|     hass.states.async_set("test.entity", STATE_ON) | ||||
|     await entity.async_added_to_hass() | ||||
|  | ||||
|     entity._process_manual_data(STATE_ON) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert entity.name == "test_entity" | ||||
|     assert entity.icon == "mdi:on" | ||||
|     assert entity.entity_picture == "/local/picture_on" | ||||
|     assert entity.available is True | ||||
|  | ||||
|     hass.states.async_set("test.entity", STATE_OFF) | ||||
|     await entity.async_added_to_hass() | ||||
|     entity._process_manual_data(STATE_OFF) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert entity.name == "test_entity" | ||||
|     assert entity.icon == "mdi:off" | ||||
|     assert entity.entity_picture == "/local/picture_off" | ||||
|     assert entity.available is True | ||||
|  | ||||
|     hass.states.async_set("test.entity", STATE_UNKNOWN) | ||||
|     await entity.async_added_to_hass() | ||||
|     entity._process_manual_data(STATE_UNKNOWN) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert entity.name == "test_entity" | ||||
|     assert entity.icon == "mdi:off" | ||||
|     assert entity.entity_picture == "/local/picture_off" | ||||
|     assert entity.available is False | ||||
|  | ||||
|  | ||||
| async def test_trigger_template_availability_fails( | ||||
|     hass: HomeAssistant, caplog: pytest.LogCaptureFixture | ||||
| ) -> None: | ||||
|     """Test manual trigger template entity when availability render fails.""" | ||||
|     config = { | ||||
|         CONF_NAME: template.Template("test_entity", hass), | ||||
|         CONF_ICON: template.Template( | ||||
|             '{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass | ||||
|         ), | ||||
|         CONF_PICTURE: template.Template( | ||||
|             '{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}', | ||||
|             hass, | ||||
|         ), | ||||
|         CONF_AVAILABILITY: template.Template("{{ incorrect ", hass), | ||||
|     } | ||||
|  | ||||
|     entity = ManualTriggerEntity(hass, config) | ||||
|     entity.entity_id = "test.entity" | ||||
|     hass.states.async_set("test.entity", STATE_ON) | ||||
|     await entity.async_added_to_hass() | ||||
|  | ||||
|     entity._process_manual_data(STATE_ON) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert "Error rendering availability template for test.entity" in caplog.text | ||||
|  | ||||
|  | ||||
| async def test_trigger_template_complex(hass: HomeAssistant) -> None: | ||||
|     """Test manual trigger template entity complex template.""" | ||||
|     complex_template = """ | ||||
|     {% set d = {'test_key':'test_data'} %} | ||||
|     {{ dict(d) }} | ||||
|  | ||||
| """ | ||||
|     config = { | ||||
|         CONF_NAME: template.Template("test_entity", hass), | ||||
|         CONF_ICON: template.Template( | ||||
|             '{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass | ||||
|         ), | ||||
|         CONF_PICTURE: template.Template( | ||||
|             '{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}', | ||||
|             hass, | ||||
|         ), | ||||
|         CONF_AVAILABILITY: template.Template('{{ has_value("test.entity") }}', hass), | ||||
|         "other_key": template.Template(complex_template, hass), | ||||
|     } | ||||
|  | ||||
|     class TestEntity(ManualTriggerEntity): | ||||
|         """Test entity class.""" | ||||
|  | ||||
|         extra_template_keys_complex = ("other_key",) | ||||
|  | ||||
|         @property | ||||
|         def some_other_key(self) -> dict[str, Any] | None: | ||||
|             """Return extra attributes.""" | ||||
|             return self._rendered.get("other_key") | ||||
|  | ||||
|     entity = TestEntity(hass, config) | ||||
|     entity.entity_id = "test.entity" | ||||
|     hass.states.async_set("test.entity", STATE_ON) | ||||
|     await entity.async_added_to_hass() | ||||
|  | ||||
|     entity._process_manual_data(STATE_ON) | ||||
|     await hass.async_block_till_done() | ||||
|  | ||||
|     assert entity.some_other_key == {"test_key": "test_data"} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user