mirror of
https://github.com/home-assistant/core.git
synced 2025-10-04 09:19:28 +00:00
Compare commits
18 Commits
2025.10.0
...
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