From bdc5af8dd271d368fed547fda5ebbe2986a49516 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Aug 2020 20:01:27 +0200 Subject: [PATCH] Add support for attributes in (numeric) state conditions (#39050) --- homeassistant/const.py | 3 +- homeassistant/helpers/condition.py | 28 ++++++--- homeassistant/helpers/config_validation.py | 3 + tests/helpers/test_condition.py | 66 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7ef23fa6903..c72974cf478 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -37,9 +37,10 @@ CONF_API_KEY = "api_key" CONF_API_VERSION = "api_version" CONF_ARMING_TIME = "arming_time" CONF_AT = "at" -CONF_AUTHENTICATION = "authentication" +CONF_ATTRIBUTE = "attribute" CONF_AUTH_MFA_MODULES = "auth_mfa_modules" CONF_AUTH_PROVIDERS = "auth_providers" +CONF_AUTHENTICATION = "authentication" CONF_BASE = "base" CONF_BEFORE = "before" CONF_BELOW = "below" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 5c7313f6716..b2385d827a1 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta import functools as ft import logging import sys -from typing import Callable, Container, List, Optional, Set, Union, cast +from typing import Any, Callable, Container, List, Optional, Set, Union, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_ABOVE, CONF_AFTER, + CONF_ATTRIBUTE, CONF_BEFORE, CONF_BELOW, CONF_CONDITION, @@ -191,16 +192,21 @@ def async_numeric_state( above: Optional[float] = None, value_template: Optional[Template] = None, variables: TemplateVarsType = None, + attribute: Optional[str] = None, ) -> bool: """Test a numeric state condition.""" if isinstance(entity, str): entity = hass.states.get(entity) - if entity is None: + if entity is None or (attribute is not None and attribute not in entity.attributes): return False + value: Any = None if value_template is None: - value = entity.state + if attribute is None: + value = entity.state + else: + value = entity.attributes.get(attribute) else: variables = dict(variables or {}) variables["state"] = entity @@ -239,6 +245,7 @@ def async_numeric_state_from_config( if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) entity_ids = config.get(CONF_ENTITY_ID, []) + attribute = config.get(CONF_ATTRIBUTE) below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -252,7 +259,7 @@ def async_numeric_state_from_config( return all( async_numeric_state( - hass, entity_id, below, above, value_template, variables + hass, entity_id, below, above, value_template, variables, attribute ) for entity_id in entity_ids ) @@ -265,6 +272,7 @@ def state( entity: Union[None, str, State], req_state: Union[str, List[str]], for_period: Optional[timedelta] = None, + attribute: Optional[str] = None, ) -> bool: """Test if state matches requirements. @@ -273,14 +281,18 @@ def state( if isinstance(entity, str): entity = hass.states.get(entity) - if entity is None: + if entity is None or (attribute is not None and attribute not in entity.attributes): return False + assert isinstance(entity, State) if isinstance(req_state, str): req_state = [req_state] - is_state = entity.state in req_state + if attribute is None: + is_state = entity.state in req_state + else: + is_state = str(entity.attributes.get(attribute)) in req_state if for_period is None or not is_state: return is_state @@ -297,6 +309,7 @@ def state_from_config( entity_ids = config.get(CONF_ENTITY_ID, []) req_states: Union[str, List[str]] = config.get(CONF_STATE, []) for_period = config.get("for") + attribute = config.get(CONF_ATTRIBUTE) if not isinstance(req_states, list): req_states = [req_states] @@ -304,7 +317,8 @@ def state_from_config( def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" return all( - state(hass, entity_id, req_states, for_period) for entity_id in entity_ids + state(hass, entity_id, req_states, for_period, attribute) + for entity_id in entity_ids ) return if_state diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 3b8988b325b..7144aad6bbc 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -37,6 +37,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ABOVE, CONF_ALIAS, + CONF_ATTRIBUTE, CONF_BELOW, CONF_CHOOSE, CONF_CONDITION, @@ -868,6 +869,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( { vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Optional(CONF_ATTRIBUTE): str, CONF_BELOW: vol.Coerce(float), CONF_ABOVE: vol.Coerce(float), vol.Optional(CONF_VALUE_TEMPLATE): template, @@ -881,6 +883,7 @@ STATE_CONDITION_SCHEMA = vol.All( { vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Optional(CONF_ATTRIBUTE): str, vol.Required(CONF_STATE): vol.Any(str, [str]), vol.Optional(CONF_FOR): positive_time_period, # To support use_trigger_value in automation diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1a575d1eff7..0ce1b786d54 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -323,6 +323,39 @@ async def test_multiple_states(hass): assert not test(hass) +async def test_state_attribute(hass): + """Test with state attribute in condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "attribute": "attribute1", + "state": "200", + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200}) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": 200}) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": "200"}) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": 201}) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": None}) + assert not test(hass) + + async def test_numeric_state_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( @@ -352,6 +385,39 @@ async def test_numeric_state_multiple_entities(hass): assert not test(hass) +async def test_numberic_state_attribute(hass): + """Test with numeric state attribute in condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "and", + "conditions": [ + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "attribute": "attribute1", + "below": 50, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10}) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": 49}) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": "49"}) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": 51}) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100, {"attribute1": None}) + assert not test(hass) + + async def test_zone_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config(