Allow triggering on all state changes, ignoring attributes (#59713)

* Allow triggering on all state changes, ignoring attributes

* Add comment

* Apply suggestions from code review

Co-authored-by: Franck Nijhof <git@frenck.dev>

Co-authored-by: Franck Nijhof <git@frenck.dev>
pull/59768/head
Erik Montnemery 2021-11-16 08:35:52 +01:00 committed by GitHub
parent 9256a033a6
commit 4f01631bd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 146 additions and 6 deletions

View File

@ -39,8 +39,8 @@ BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
TRIGGER_STATE_SCHEMA = BASE_SCHEMA.extend(
{
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): vol.Any(str, [str]),
vol.Optional(CONF_TO): vol.Any(str, [str]),
vol.Optional(CONF_FROM): vol.Any(str, [str], None),
vol.Optional(CONF_TO): vol.Any(str, [str], None),
}
)
@ -75,11 +75,15 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
if (from_state := config.get(CONF_FROM)) is None:
from_state = MATCH_ALL
if (to_state := config.get(CONF_TO)) is None:
to_state = MATCH_ALL
time_delta = config.get(CONF_FOR)
template.attach(hass, time_delta)
match_all = from_state == MATCH_ALL and to_state == MATCH_ALL
# If neither CONF_FROM or CONF_TO are specified,
# fire on all changes to the state or an attribute
match_all = CONF_FROM not in config and CONF_TO not in config
unsub_track_same = {}
period: dict[str, timedelta] = {}
match_from_state = process_state_match(from_state)

View File

@ -106,7 +106,7 @@ async def test_if_fires_on_entity_change_with_from_filter(hass, calls):
async def test_if_fires_on_entity_change_with_to_filter(hass, calls):
"""Test for firing on entity change with no filter."""
"""Test for firing on entity change with to filter."""
assert await async_setup_component(
hass,
automation.DOMAIN,
@ -128,6 +128,54 @@ async def test_if_fires_on_entity_change_with_to_filter(hass, calls):
assert len(calls) == 1
async def test_if_fires_on_entity_change_with_from_filter_all(hass, calls):
"""Test for firing on entity change with filter."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "state",
"entity_id": "test.entity",
"from": None,
},
"action": {"service": "test.automation"},
}
},
)
await hass.async_block_till_done()
hass.states.async_set("test.entity", "world")
hass.states.async_set("test.entity", "world", {"attribute": 5})
await hass.async_block_till_done()
assert len(calls) == 1
async def test_if_fires_on_entity_change_with_to_filter_all(hass, calls):
"""Test for firing on entity change with to filter."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "state",
"entity_id": "test.entity",
"to": None,
},
"action": {"service": "test.automation"},
}
},
)
await hass.async_block_till_done()
hass.states.async_set("test.entity", "world")
hass.states.async_set("test.entity", "world", {"attribute": 5})
await hass.async_block_till_done()
assert len(calls) == 1
async def test_if_fires_on_attribute_change_with_to_filter(hass, calls):
"""Test for not firing on attribute change."""
assert await async_setup_component(
@ -1217,6 +1265,94 @@ async def test_attribute_if_fires_on_entity_where_attr_stays_constant(hass, call
assert len(calls) == 1
async def test_attribute_if_fires_on_entity_where_attr_stays_constant_filter(
hass, calls
):
"""Test for firing if attribute stays the same."""
hass.states.async_set("test.entity", "bla", {"name": "other_name"})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "state",
"entity_id": "test.entity",
"attribute": "name",
"to": "best_name",
},
"action": {"service": "test.automation"},
}
},
)
await hass.async_block_till_done()
# Leave all attributes the same
hass.states.async_set(
"test.entity", "bla", {"name": "best_name", "other": "old_value"}
)
await hass.async_block_till_done()
assert len(calls) == 1
# Change the untracked attribute
hass.states.async_set(
"test.entity", "bla", {"name": "best_name", "other": "new_value"}
)
await hass.async_block_till_done()
assert len(calls) == 1
# Change the tracked attribute
hass.states.async_set(
"test.entity", "bla", {"name": "other_name", "other": "old_value"}
)
await hass.async_block_till_done()
assert len(calls) == 1
async def test_attribute_if_fires_on_entity_where_attr_stays_constant_all(hass, calls):
"""Test for firing if attribute stays the same."""
hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "state",
"entity_id": "test.entity",
"attribute": "name",
"to": None,
},
"action": {"service": "test.automation"},
}
},
)
await hass.async_block_till_done()
# Leave all attributes the same
hass.states.async_set(
"test.entity", "bla", {"name": "name_1", "other": "old_value"}
)
await hass.async_block_till_done()
assert len(calls) == 1
# Change the untracked attribute
hass.states.async_set(
"test.entity", "bla", {"name": "name_1", "other": "new_value"}
)
await hass.async_block_till_done()
assert len(calls) == 1
# Change the tracked attribute
hass.states.async_set(
"test.entity", "bla", {"name": "name_2", "other": "old_value"}
)
await hass.async_block_till_done()
assert len(calls) == 2
async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop(
hass, calls
):