Add support for attributes in (numeric) state conditions (#39050)

pull/39057/head
Franck Nijhof 2020-08-19 20:01:27 +02:00 committed by GitHub
parent 5f10c55303
commit bdc5af8dd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 8 deletions

View File

@ -37,9 +37,10 @@ CONF_API_KEY = "api_key"
CONF_API_VERSION = "api_version" CONF_API_VERSION = "api_version"
CONF_ARMING_TIME = "arming_time" CONF_ARMING_TIME = "arming_time"
CONF_AT = "at" CONF_AT = "at"
CONF_AUTHENTICATION = "authentication" CONF_ATTRIBUTE = "attribute"
CONF_AUTH_MFA_MODULES = "auth_mfa_modules" CONF_AUTH_MFA_MODULES = "auth_mfa_modules"
CONF_AUTH_PROVIDERS = "auth_providers" CONF_AUTH_PROVIDERS = "auth_providers"
CONF_AUTHENTICATION = "authentication"
CONF_BASE = "base" CONF_BASE = "base"
CONF_BEFORE = "before" CONF_BEFORE = "before"
CONF_BELOW = "below" CONF_BELOW = "below"

View File

@ -5,7 +5,7 @@ from datetime import datetime, timedelta
import functools as ft import functools as ft
import logging import logging
import sys 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 import zone as zone_cmp
from homeassistant.components.device_automation import ( from homeassistant.components.device_automation import (
@ -17,6 +17,7 @@ from homeassistant.const import (
ATTR_LONGITUDE, ATTR_LONGITUDE,
CONF_ABOVE, CONF_ABOVE,
CONF_AFTER, CONF_AFTER,
CONF_ATTRIBUTE,
CONF_BEFORE, CONF_BEFORE,
CONF_BELOW, CONF_BELOW,
CONF_CONDITION, CONF_CONDITION,
@ -191,16 +192,21 @@ def async_numeric_state(
above: Optional[float] = None, above: Optional[float] = None,
value_template: Optional[Template] = None, value_template: Optional[Template] = None,
variables: TemplateVarsType = None, variables: TemplateVarsType = None,
attribute: Optional[str] = None,
) -> bool: ) -> bool:
"""Test a numeric state condition.""" """Test a numeric state condition."""
if isinstance(entity, str): if isinstance(entity, str):
entity = hass.states.get(entity) 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 return False
value: Any = None
if value_template is None: if value_template is None:
value = entity.state if attribute is None:
value = entity.state
else:
value = entity.attributes.get(attribute)
else: else:
variables = dict(variables or {}) variables = dict(variables or {})
variables["state"] = entity variables["state"] = entity
@ -239,6 +245,7 @@ def async_numeric_state_from_config(
if config_validation: if config_validation:
config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config)
entity_ids = config.get(CONF_ENTITY_ID, []) entity_ids = config.get(CONF_ENTITY_ID, [])
attribute = config.get(CONF_ATTRIBUTE)
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
@ -252,7 +259,7 @@ def async_numeric_state_from_config(
return all( return all(
async_numeric_state( 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 for entity_id in entity_ids
) )
@ -265,6 +272,7 @@ def state(
entity: Union[None, str, State], entity: Union[None, str, State],
req_state: Union[str, List[str]], req_state: Union[str, List[str]],
for_period: Optional[timedelta] = None, for_period: Optional[timedelta] = None,
attribute: Optional[str] = None,
) -> bool: ) -> bool:
"""Test if state matches requirements. """Test if state matches requirements.
@ -273,14 +281,18 @@ def state(
if isinstance(entity, str): if isinstance(entity, str):
entity = hass.states.get(entity) 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 return False
assert isinstance(entity, State) assert isinstance(entity, State)
if isinstance(req_state, str): if isinstance(req_state, str):
req_state = [req_state] 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: if for_period is None or not is_state:
return is_state return is_state
@ -297,6 +309,7 @@ def state_from_config(
entity_ids = config.get(CONF_ENTITY_ID, []) entity_ids = config.get(CONF_ENTITY_ID, [])
req_states: Union[str, List[str]] = config.get(CONF_STATE, []) req_states: Union[str, List[str]] = config.get(CONF_STATE, [])
for_period = config.get("for") for_period = config.get("for")
attribute = config.get(CONF_ATTRIBUTE)
if not isinstance(req_states, list): if not isinstance(req_states, list):
req_states = [req_states] req_states = [req_states]
@ -304,7 +317,8 @@ def state_from_config(
def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Test if condition.""" """Test if condition."""
return all( 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 return if_state

View File

@ -37,6 +37,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_ABOVE, CONF_ABOVE,
CONF_ALIAS, CONF_ALIAS,
CONF_ATTRIBUTE,
CONF_BELOW, CONF_BELOW,
CONF_CHOOSE, CONF_CHOOSE,
CONF_CONDITION, CONF_CONDITION,
@ -868,6 +869,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
{ {
vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_CONDITION): "numeric_state",
vol.Required(CONF_ENTITY_ID): entity_ids, vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Optional(CONF_ATTRIBUTE): str,
CONF_BELOW: vol.Coerce(float), CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float), CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): template, vol.Optional(CONF_VALUE_TEMPLATE): template,
@ -881,6 +883,7 @@ STATE_CONDITION_SCHEMA = vol.All(
{ {
vol.Required(CONF_CONDITION): "state", vol.Required(CONF_CONDITION): "state",
vol.Required(CONF_ENTITY_ID): entity_ids, vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Optional(CONF_ATTRIBUTE): str,
vol.Required(CONF_STATE): vol.Any(str, [str]), vol.Required(CONF_STATE): vol.Any(str, [str]),
vol.Optional(CONF_FOR): positive_time_period, vol.Optional(CONF_FOR): positive_time_period,
# To support use_trigger_value in automation # To support use_trigger_value in automation

View File

@ -323,6 +323,39 @@ async def test_multiple_states(hass):
assert not test(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): async def test_numeric_state_multiple_entities(hass):
"""Test with multiple entities in condition.""" """Test with multiple entities in condition."""
test = await condition.async_from_config( test = await condition.async_from_config(
@ -352,6 +385,39 @@ async def test_numeric_state_multiple_entities(hass):
assert not test(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): async def test_zone_multiple_entities(hass):
"""Test with multiple entities in condition.""" """Test with multiple entities in condition."""
test = await condition.async_from_config( test = await condition.async_from_config(