Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"binary_sensor",
"climate",
"cover",
"device_tracker",
"fan",
"lawn_mower",
"light",
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/device_tracker/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@
"see": {
"service": "mdi:account-eye"
}
},
"triggers": {
"entered_home": {
"trigger": "mdi:account-arrow-left"
},
"left_home": {
"trigger": "mdi:account-arrow-right"
}
}
}
37 changes: 36 additions & 1 deletion homeassistant/components/device_tracker/strings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted device trackers to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_home": "{entity_name} is home",
Expand Down Expand Up @@ -44,6 +48,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"see": {
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
Expand Down Expand Up @@ -80,5 +93,27 @@
"name": "See"
}
},
"title": "Device tracker"
"title": "Device tracker",
"triggers": {
"entered_home": {
"description": "Triggers when one or more device trackers enter home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
}
},
"name": "Entered home"
},
"left_home": {
"description": "Triggers when one or more device trackers leave home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
}
},
"name": "Left home"
}
}
}
21 changes: 21 additions & 0 deletions homeassistant/components/device_tracker/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Provides triggers for device_trackers."""

from homeassistant.const import STATE_HOME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import (
Trigger,
make_entity_state_trigger,
make_from_entity_state_trigger,
)

from .const import DOMAIN

TRIGGERS: dict[str, type[Trigger]] = {
"entered_home": make_entity_state_trigger(DOMAIN, STATE_HOME),
"left_home": make_from_entity_state_trigger(DOMAIN, from_state=STATE_HOME),
}


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

entered_home: *trigger_common
left_home: *trigger_common
38 changes: 34 additions & 4 deletions homeassistant/helpers/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,12 @@ def state_change_listener(
if not from_state or from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return

# The trigger should never fire if the previous state was not the from state
if not to_state or not self.is_from_state(from_state, to_state):
# The trigger should never fire if the new state is not the to state
if not to_state or not self.is_to_state(to_state):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just moved the is_to_state up to keep the not to_state check on the same line.
it was weird to see not to_state on a line that is checking the "from_state"

return

# The trigger should never fire if the new state is not the to state
if not self.is_to_state(to_state):
# The trigger should never fire if the previous state was not the from state
if not self.is_from_state(from_state, to_state):
return

if behavior == BEHAVIOR_LAST:
Expand Down Expand Up @@ -453,6 +453,22 @@ def is_to_state(self, state: State) -> bool:
return state.state in self._to_states


class FromEntityStateTriggerBase(EntityTriggerBase):
"""Class for entity state changes from a specific state."""

_from_state: str

def is_from_state(self, from_state: State, to_state: State) -> bool:
"""Check if the state matches the origin state."""
return (
from_state.state == self._from_state and to_state.state != self._from_state
)

def is_to_state(self, state: State) -> bool:
"""Check if the state matches the target state."""
return state.state != self._from_state


class EntityStateAttributeTriggerBase(EntityTriggerBase):
"""Trigger for entity state attribute changes."""

Expand Down Expand Up @@ -493,6 +509,20 @@ class CustomTrigger(ConditionalEntityStateTriggerBase):
return CustomTrigger


def make_from_entity_state_trigger(
domain: str, *, from_state: str
) -> type[FromEntityStateTriggerBase]:
"""Create a "from" entity state trigger class."""

class CustomTrigger(FromEntityStateTriggerBase):
"""Trigger for "from" entity state changes."""

_domain = domain
_from_state = from_state

return CustomTrigger


def make_entity_state_attribute_trigger(
domain: str, attribute: str, to_state: str
) -> type[EntityStateAttributeTriggerBase]:
Expand Down
Loading
Loading