Teach state and numeric_state conditions about entity registry ids (#60841)

This commit is contained in:
Erik Montnemery 2021-12-02 23:55:12 +01:00 committed by GitHub
parent a07f75c6b0
commit 0e3bc21d54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 236 additions and 31 deletions

View file

@ -27,7 +27,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import condition, config_validation as cv, entity_registry
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from homeassistant.helpers.entity import get_supported_features
@ -104,7 +104,10 @@ async def async_get_conditions(
return conditions
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
@callback
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == CONDITION_TRIGGERED:
state = STATE_ALARM_TRIGGERED

View file

@ -264,7 +264,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
condition_type = config[CONF_TYPE]
if condition_type in IS_ON:
@ -279,6 +281,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]
state_config = cv.STATE_CONDITION_SCHEMA(state_config)
state_config = condition.state_validate_config(hass, state_config)
return condition.state_from_config(state_config)

View file

@ -70,7 +70,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_hvac_mode":
attribute = const.ATTR_HVAC_MODE

View file

@ -119,7 +119,9 @@ async def async_get_condition_capabilities(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] in STATE_CONDITION_TYPES:
if config[CONF_TYPE] == "is_open":

View file

@ -127,7 +127,9 @@ async def async_call_action_from_config(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
if config[CONF_TYPE] == CONF_IS_ON:
stat = "on"
@ -142,6 +144,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke
state_config[CONF_FOR] = config[CONF_FOR]
state_config = cv.STATE_CONDITION_SCHEMA(state_config)
state_config = condition.state_validate_config(hass, state_config)
return condition.state_from_config(state_config)

View file

@ -55,7 +55,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
reverse = config[CONF_TYPE] == "is_not_home"

View file

@ -55,7 +55,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_on":
state = STATE_ON

View file

@ -65,12 +65,14 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_mode":
attribute = ATTR_MODE
else:
return toggle_entity.async_condition_from_config(config)
return toggle_entity.async_condition_from_config(hass, config)
def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if an entity is a certain state."""

View file

@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend(
@callback
def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
"""Evaluate state based on configuration."""
return toggle_entity.async_condition_from_config(config)
return toggle_entity.async_condition_from_config(hass, config)
async def async_get_conditions(

View file

@ -67,7 +67,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_jammed":
state = STATE_JAMMED

View file

@ -59,7 +59,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_playing":
state = STATE_PLAYING

View file

@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend(
@callback
def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
"""Evaluate state based on configuration."""
return toggle_entity.async_condition_from_config(config)
return toggle_entity.async_condition_from_config(hass, config)
async def async_get_conditions(

View file

@ -52,7 +52,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
@callback

View file

@ -186,7 +186,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
numeric_state_config = {
condition.CONF_CONDITION: "numeric_state",
@ -198,6 +200,9 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke
numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW]
numeric_state_config = cv.NUMERIC_STATE_CONDITION_SCHEMA(numeric_state_config)
numeric_state_config = condition.numeric_state_validate_config(
hass, numeric_state_config
)
return condition.async_numeric_state_from_config(numeric_state_config)

View file

@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend(
@callback
def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
"""Evaluate state based on configuration."""
return toggle_entity.async_condition_from_config(config)
return toggle_entity.async_condition_from_config(hass, config)
async def async_get_conditions(

View file

@ -53,7 +53,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_docked":
test_states = [STATE_DOCKED]

View file

@ -147,7 +147,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
condition_type = config[CONF_TYPE]
device_id = config[CONF_DEVICE_ID]

View file

@ -51,7 +51,7 @@ from homeassistant.exceptions import (
HomeAssistantError,
TemplateError,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
@ -71,8 +71,9 @@ from .trace import (
# mypy: disallow-any-generics
FROM_CONFIG_FORMAT = "{}_from_config"
ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
FROM_CONFIG_FORMAT = "{}_from_config"
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
_LOGGER = logging.getLogger(__name__)
@ -885,7 +886,7 @@ async def async_device_from_config(
return trace_condition_function(
cast(
ConditionCheckerType,
platform.async_condition_from_config(config), # type: ignore
platform.async_condition_from_config(hass, config), # type: ignore
)
)
@ -908,6 +909,30 @@ async def async_trigger_from_config(
return trigger_if
def numeric_state_validate_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate numeric_state condition config."""
registry = er.async_get(hass)
config = dict(config)
config[CONF_ENTITY_ID] = er.async_resolve_entity_ids(
registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID])
)
return config
def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
"""Validate state condition config."""
registry = er.async_get(hass)
config = dict(config)
config[CONF_ENTITY_ID] = er.async_resolve_entity_ids(
registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID])
)
return config
async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType | Template
) -> ConfigType | Template:
@ -933,6 +958,12 @@ async def async_validate_condition_config(
return await platform.async_validate_condition_config(hass, config) # type: ignore
return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore
if condition in ("numeric_state", "state"):
validator = getattr(
sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition)
)
return validator(hass, config) # type: ignore
return config

View file

@ -1028,7 +1028,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
{
**CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "numeric_state",
vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids,
vol.Optional(CONF_ATTRIBUTE): str,
CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA,
CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA,
@ -1041,7 +1041,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
STATE_CONDITION_BASE_SCHEMA = {
**CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "state",
vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids,
vol.Optional(CONF_ATTRIBUTE): str,
vol.Optional(CONF_FOR): positive_time_period,
# To support use_trigger_value in automation

View file

@ -58,7 +58,9 @@ async def async_get_conditions(
@callback
def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType:
def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_on":
state = STATE_ON

View file

@ -552,7 +552,7 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration):
with pytest.raises(HomeAssistantError):
await device_condition.async_condition_from_config(
{"type": "failed.test", "device_id": device.id}
hass, {"type": "failed.test", "device_id": device.id}
)
with patch(

View file

@ -16,7 +16,12 @@ from homeassistant.const import (
SUN_EVENT_SUNSET,
)
from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition, config_validation as cv, trace
from homeassistant.helpers import (
condition,
config_validation as cv,
entity_registry as er,
trace,
)
from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@ -1107,6 +1112,29 @@ async def test_state_attribute_boolean(hass):
assert test(hass)
async def test_state_entity_registry_id(hass):
"""Test with entity specified by entity registry id."""
registry = er.async_get(hass)
entry = registry.async_get_or_create(
"switch", "hue", "1234", suggested_object_id="test"
)
assert entry.entity_id == "switch.test"
config = {
"condition": "state",
"entity_id": entry.id,
"state": "on",
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
hass.states.async_set("switch.test", "on")
assert test(hass)
hass.states.async_set("switch.test", "off")
assert not test(hass)
async def test_state_using_input_entities(hass):
"""Test state conditions using input_* entities."""
await async_setup_component(
@ -1419,6 +1447,29 @@ async def test_numeric_state_attribute(hass):
assert not test(hass)
async def test_numeric_state_entity_registry_id(hass):
"""Test with entity specified by entity registry id."""
registry = er.async_get(hass)
entry = registry.async_get_or_create(
"sensor", "hue", "1234", suggested_object_id="test"
)
assert entry.entity_id == "sensor.test"
config = {
"condition": "numeric_state",
"entity_id": entry.id,
"above": 100,
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
hass.states.async_set("sensor.test", "110")
assert test(hass)
hass.states.async_set("sensor.test", "90")
assert not test(hass)
async def test_numeric_state_using_input_number(hass):
"""Test numeric_state conditions using input_number entities."""
hass.states.async_set("number.low", 10)

View file

@ -25,7 +25,13 @@ from homeassistant.const import (
)
from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback
from homeassistant.exceptions import ConditionError, ServiceNotFound
from homeassistant.helpers import config_validation as cv, script, template, trace
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
script,
template,
trace,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@ -1492,6 +1498,81 @@ async def test_condition_basic(hass, caplog):
)
async def test_condition_validation(hass, caplog):
"""Test if we can use conditions which validate late in a script."""
registry = er.async_get(hass)
entry = registry.async_get_or_create(
"test", "hue", "1234", suggested_object_id="entity"
)
assert entry.entity_id == "test.entity"
event = "test_event"
events = async_capture_events(hass, event)
alias = "condition step"
sequence = cv.SCRIPT_SCHEMA(
[
{"event": event},
{
"alias": alias,
"condition": "state",
"entity_id": entry.id,
"state": "hello",
},
{"event": event},
]
)
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
hass.states.async_set("test.entity", "hello")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert f"Test condition {alias}: True" in caplog.text
caplog.clear()
assert len(events) == 2
assert_action_trace(
{
"0": [{"result": {"event": "test_event", "event_data": {}}}],
"1": [{"result": {"result": True}}],
"1/entity_id/0": [
{"result": {"result": True, "state": "hello", "wanted_state": "hello"}}
],
"2": [{"result": {"event": "test_event", "event_data": {}}}],
}
)
hass.states.async_set("test.entity", "goodbye")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert f"Test condition {alias}: False" in caplog.text
assert len(events) == 3
assert_action_trace(
{
"0": [{"result": {"event": "test_event", "event_data": {}}}],
"1": [
{
"error_type": script._StopScript,
"result": {"result": False},
}
],
"1/entity_id/0": [
{
"result": {
"result": False,
"state": "goodbye",
"wanted_state": "hello",
}
}
],
},
expected_script_execution="aborted",
)
@patch("homeassistant.helpers.script.condition.async_from_config")
async def test_condition_created_once(async_from_config, hass):
"""Test that the conditions do not get created multiple times."""