Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
89a9ab6
Add method to track entity state changes from target selectors
abmantis Jul 3, 2025
5d553e5
Use class to manage subscriptions
abmantis Jul 3, 2025
0ead4c0
Move target selector extractor method to common module
abmantis Jul 3, 2025
0335c9e
Use common method in triggers.py
abmantis Jul 3, 2025
a971313
Merge branch 'dev' of github.com:home-assistant/core into track_entit…
abmantis Jul 3, 2025
695f47c
Add missed components
abmantis Jul 3, 2025
2d931c5
Simplify tracker class
abmantis Jul 3, 2025
72a982b
Merge branch 'dev' of github.com:home-assistant/core into track_entit…
abmantis Jul 7, 2025
7a57ab4
Update import
abmantis Jul 7, 2025
4cc97d2
Implement review suggestion on triggering all test entities
abmantis Jul 7, 2025
67a7cf8
Rename class; add comment
abmantis Jul 7, 2025
9376715
Move to target.py
abmantis Jul 7, 2025
5e47462
Add state trigger to light component
abmantis Jul 8, 2025
ee2c7ca
Merge branch 'dev' of github.com:home-assistant/core into target_trigger
abmantis Jul 14, 2025
1b85a92
Cleanup comments
abmantis Jul 14, 2025
5d792de
Merge fix
abmantis Jul 14, 2025
4a12c18
Remove behavior from yaml
abmantis Jul 14, 2025
b9246c4
Merge branch 'dev' of github.com:home-assistant/core into target_trigger
abmantis Jul 15, 2025
a6d0532
Add test
abmantis Jul 15, 2025
f52a362
Cleanup comments
abmantis Jul 15, 2025
0bd5231
Add any state test
abmantis Jul 16, 2025
3d98bc9
Move target selector to toplevel; add strings and icons
abmantis Jul 16, 2025
93e570a
Merge branch 'dev' of github.com:home-assistant/core into target_trigger
abmantis Jul 21, 2025
09328aa
Add list of targeted entities to target state event
abmantis Jul 21, 2025
35b54b7
Merge tests
abmantis Jul 22, 2025
801591f
Add behavior conf
abmantis Jul 22, 2025
9e8372c
Merge branch 'dev' of github.com:home-assistant/core into target_trigger
abmantis Jul 29, 2025
e2e7c56
Add behavior to strings
abmantis Jul 29, 2025
5257307
Fix translations test
abmantis Jul 29, 2025
13d3401
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Aug 4, 2025
2af652a
Change state to select selector
abmantis Aug 4, 2025
ba79225
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Aug 5, 2025
43c2283
Apply suggestions from code review
abmantis Aug 5, 2025
80cd461
Fix non-light domain entities on trigger
abmantis Aug 5, 2025
7adfb42
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Aug 7, 2025
29fd422
Update trigger method names
abmantis Aug 7, 2025
789e5da
Fix targets key
abmantis Aug 7, 2025
06f1d3b
Type consts
abmantis Aug 7, 2025
e59567d
Use TARGET_SELECTION_SCHEMA
abmantis Aug 7, 2025
05bc556
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Aug 25, 2025
b877471
Fix translations
abmantis Aug 25, 2025
beea8e4
Move reverse state to test params
abmantis Aug 25, 2025
4b77a5b
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Sep 15, 2025
24abe55
Change to options config
abmantis Sep 15, 2025
7dcf015
Use TARGET_FIELDS
abmantis Sep 16, 2025
d323212
Merge branch 'dev_target_triggers_conditions' of github.com:home-assi…
abmantis Sep 18, 2025
df0d907
Update to modernized schema
abmantis Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions homeassistant/components/light/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@
"turn_on": {
"service": "mdi:lightbulb-on"
}
},
"triggers": {
"state": {
"trigger": "mdi:state-machine"
}
}
}
30 changes: 30 additions & 0 deletions homeassistant/components/light/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@
}
},
"selector": {
"behavior": {
"options": {
"first": "First",
"last": "Last",
"any": "Any"
}
},
"color_name": {
"options": {
"homeassistant": "Home Assistant",
Expand Down Expand Up @@ -290,6 +297,12 @@
"short": "Short",
"long": "Long"
}
},
"state": {
"options": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
}
},
"services": {
Expand Down Expand Up @@ -462,5 +475,22 @@
}
}
}
},
"triggers": {
"state": {
"name": "State",
"description": "When the state of a light changes, such as turning on or off.",
"description_configured": "When the state of a light changes",
"fields": {
"state": {
"name": "State",
"description": "The state to trigger on."
},
"behavior": {
"name": "Behavior",
"description": "The behavior of the targeted entities to trigger on."
}
}
}
}
}
165 changes: 165 additions & 0 deletions homeassistant/components/light/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Provides triggers for lights."""

from typing import Final, cast, override

import voluptuous as vol

from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PLATFORM,
CONF_STATE,
CONF_TARGET,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import (
CALLBACK_TYPE,
HassJob,
HomeAssistant,
callback,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import process_state_match
from homeassistant.helpers.target import (
TargetStateChangedData,
async_track_target_selector_state_change_event,
)
from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN

# remove when #151314 is merged
CONF_OPTIONS: Final = "options"

ATTR_BEHAVIOR: Final = "behavior"
BEHAVIOR_FIRST: Final = "first"
BEHAVIOR_LAST: Final = "last"
BEHAVIOR_ANY: Final = "any"

STATE_PLATFORM_TYPE: Final = "state"
STATE_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_STATE): vol.In([STATE_ON, STATE_OFF]),
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
),
},
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
Comment thread
arturpragacz marked this conversation as resolved.
}
)


class StateTrigger(Trigger):
"""Trigger for state changes."""

@override
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, STATE_TRIGGER_SCHEMA(config))
Comment thread
arturpragacz marked this conversation as resolved.

def __init__(self, hass: HomeAssistant, config: dict) -> None:
"""Initialize the state trigger."""
self.hass = hass
self.config = config

@override
async def async_attach(
self, action: TriggerActionType, trigger_info: TriggerInfo
) -> CALLBACK_TYPE:
"""Attach the trigger."""
job = HassJob(action, f"light state trigger {trigger_info}")
trigger_data = trigger_info["trigger_data"]
config_options = self.config[CONF_OPTIONS]

match_config_state = process_state_match(config_options.get(CONF_STATE))

def check_all_match(entity_ids: set[str]) -> bool:
"""Check if all entity states match."""
return all(
match_config_state(state.state)
for entity_id in entity_ids
if (state := self.hass.states.get(entity_id)) is not None
)

def check_one_match(entity_ids: set[str]) -> bool:
"""Check that only one entity state matches."""
return (
sum(
match_config_state(state.state)
for entity_id in entity_ids
if (state := self.hass.states.get(entity_id)) is not None
)
== 1
)

behavior = config_options.get(ATTR_BEHAVIOR)

@callback
def state_change_listener(
target_state_change_data: TargetStateChangedData,
) -> None:
"""Listen for state changes and call action."""
event = target_state_change_data.state_change_event
entity_id = event.data["entity_id"]
from_state = event.data["old_state"]
to_state = event.data["new_state"]

if to_state is None:
return

# This check is required for "first" behavior, to check that it went from zero
# entities matching the state to one. Otherwise, if previously there were two
# entities on CONF_STATE and one changed, this would trigger.
# For "last" behavior it is not required, but serves as a quicker fail check.
if not match_config_state(to_state.state):
return
if behavior == BEHAVIOR_LAST:
if not check_all_match(target_state_change_data.targeted_entity_ids):
return
elif behavior == BEHAVIOR_FIRST:
if not check_one_match(target_state_change_data.targeted_entity_ids):
return

self.hass.async_run_hass_job(
job,
{
"trigger": {
**trigger_data,
CONF_PLATFORM: self.config[CONF_PLATFORM],
ATTR_ENTITY_ID: entity_id,
"from_state": from_state,
"to_state": to_state,
"description": f"state of {entity_id}",
Comment thread
arturpragacz marked this conversation as resolved.
}
},
event.context,
)

def entity_filter(entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
return {
entity_id
for entity_id in entities
if split_entity_id(entity_id)[0] == DOMAIN
}

target_config = self.config[CONF_TARGET]
return async_track_target_selector_state_change_event(
self.hass, target_config, state_change_listener, entity_filter
)


TRIGGERS: dict[str, type[Trigger]] = {
STATE_PLATFORM_TYPE: StateTrigger,
}


async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for lights."""
return TRIGGERS
22 changes: 22 additions & 0 deletions homeassistant/components/light/triggers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
state:
target:
entity:
domain: light
fields:
state:
required: true
default: "on"
selector:
select:
options:
- "off"
- "on"
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
Loading
Loading