Add support for attributes in (numeric) state conditions (#39050)
parent
5f10c55303
commit
bdc5af8dd2
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
if attribute is None:
|
||||||
value = entity.state
|
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]
|
||||||
|
|
||||||
|
if attribute is None:
|
||||||
is_state = entity.state in req_state
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue