From cfe681f44141cbed58f0d850bbc20a285f21a008 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 9 Oct 2024 18:59:04 +0000 Subject: [PATCH 1/5] Fix availability for manual trigger entities --- .../helpers/trigger_template_entity.py | 26 ++++++++-- tests/components/command_line/test_sensor.py | 49 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index 7f8ad41d7bb..fff25568222 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import TemplateError +from homeassistant.util import dt as dt_util from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import config_validation as cv @@ -231,10 +232,26 @@ class ManualTriggerEntity(TriggerBaseEntity): 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() + now = dt_util.utcnow() + attr = {} + if state_attributes := self.state_attributes: + attr.update(state_attributes) + if extra_state_attributes := self.extra_state_attributes: + attr.update(extra_state_attributes) + + state = State( + self.entity_id, + str(value), + attr, + now, + now, + now, + None, + True, + None, + now.timestamp(), + ) + 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. @@ -243,6 +260,7 @@ class ManualTriggerEntity(TriggerBaseEntity): variables = {"this": this, **(run_variables or {})} self._render_templates(variables) + self.async_write_ha_state() class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity): diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index f7879b334cd..bf1c15a2a49 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -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" From f09cea640e4fb26437f556130d3e5731d1fcde90 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 9 Oct 2024 20:07:33 +0000 Subject: [PATCH 2/5] Fixes --- homeassistant/helpers/trigger_template_entity.py | 7 +++++-- tests/components/command_line/test_switch.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index fff25568222..24e66a30c45 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, + MAX_LENGTH_STATE_STATE, ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import TemplateError @@ -238,10 +239,12 @@ class ManualTriggerEntity(TriggerBaseEntity): attr.update(state_attributes) if extra_state_attributes := self.extra_state_attributes: attr.update(extra_state_attributes) - + entity_state = str(self.state) + if len(entity_state) > MAX_LENGTH_STATE_STATE: + entity_state = entity_state[:MAX_LENGTH_STATE_STATE] state = State( self.entity_id, - str(value), + str(entity_state), attr, now, now, diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 549e729892c..ed8032d7789 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -564,7 +564,7 @@ async def test_templating(hass: HomeAssistant) -> None: "command_off": f"echo 0 > {path}", "value_template": '{{ value=="1" }}', "icon": ( - '{% if states("switch.test2")=="on" %} mdi:on {% else %} mdi:off {% endif %}' + '{% if states("switch.test")=="on" %} mdi:on {% else %} mdi:off {% endif %}' ), "name": "Test2", }, From 40e866f221edd50eab63fdfb63659cd54d35eca0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 11 Oct 2024 15:55:56 +0000 Subject: [PATCH 3/5] Fix when state breaks to stringify --- homeassistant/helpers/trigger_template_entity.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index 24e66a30c45..eafebc82cc0 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, MAX_LENGTH_STATE_STATE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import TemplateError @@ -239,12 +240,18 @@ class ManualTriggerEntity(TriggerBaseEntity): attr.update(state_attributes) if extra_state_attributes := self.extra_state_attributes: attr.update(extra_state_attributes) - entity_state = str(self.state) + try: + entity_state = str(self.state) + except ValueError: + # Catch value errors e.g. sensor state expecting a number + # but the value is not a number. + # Return unknown state to not mix with availability template. + entity_state = STATE_UNKNOWN if len(entity_state) > MAX_LENGTH_STATE_STATE: entity_state = entity_state[:MAX_LENGTH_STATE_STATE] state = State( self.entity_id, - str(entity_state), + entity_state, attr, now, now, From d9e35ac38515ed9294bd02cb373ec07a55b8d380 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 26 Oct 2024 13:42:50 +0000 Subject: [PATCH 4/5] Add test --- tests/components/rest/test_sensor.py | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 2e02063b215..31ce261e312 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -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 From d9eada044ccf28b6d8ebea0db637cae3b827ccc7 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 6 Nov 2024 12:49:52 +0100 Subject: [PATCH 5/5] Try a different approach --- .../helpers/trigger_template_entity.py | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index eafebc82cc0..9df263207eb 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -23,12 +23,9 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, - MAX_LENGTH_STATE_STATE, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import TemplateError -from homeassistant.util import dt as dt_util from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import config_validation as cv @@ -179,18 +176,43 @@ 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.""" + rendered = dict(self._static_rendered) + self._rendered = self._static_rendered + try: + key = CONF_AVAILABILITY + if key in self._to_render_simple: + rendered[key] = self._config[key].async_render( + variables, + parse_result=key in self._parse_result, + ) + elif key in self._to_render_complex: + rendered[key] = render_complex( + self._config[key], + variables, + ) + 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 = rendered + def _render_templates(self, variables: dict[str, Any]) -> None: """Render templates.""" + rendered = dict(self._rendered) 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, ) for key in self._to_render_complex: + if key == CONF_AVAILABILITY: + continue rendered[key] = render_complex( self._config[key], variables, @@ -207,7 +229,6 @@ class TriggerBaseEntity(Entity): 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 class ManualTriggerEntity(TriggerBaseEntity): @@ -234,43 +255,24 @@ class ManualTriggerEntity(TriggerBaseEntity): Ex: self._process_manual_data(payload) """ - now = dt_util.utcnow() - attr = {} - if state_attributes := self.state_attributes: - attr.update(state_attributes) - if extra_state_attributes := self.extra_state_attributes: - attr.update(extra_state_attributes) - try: - entity_state = str(self.state) - except ValueError: - # Catch value errors e.g. sensor state expecting a number - # but the value is not a number. - # Return unknown state to not mix with availability template. - entity_state = STATE_UNKNOWN - if len(entity_state) > MAX_LENGTH_STATE_STATE: - entity_state = entity_state[:MAX_LENGTH_STATE_STATE] - state = State( - self.entity_id, - entity_state, - attr, - now, - now, - now, - None, - True, - None, - now.timestamp(), - ) - this = state.as_dict() - run_variables: dict[str, Any] = {"value": value} + this = None + if state := self.hass.states.get(self.entity_id): + this = state.as_dict() # 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_availability_template(variables) + + self.async_write_ha_state() + this = None + if state := self.hass.states.get(self.entity_id): + this = state.as_dict() + + variables["this"] = this self._render_templates(variables) - self.async_write_ha_state() class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity):