Skip to content
Merged
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
20 changes: 14 additions & 6 deletions homeassistant/components/battery/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,27 @@
}

TRIGGERS: dict[str, type[Trigger]] = {
"low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_ON),
"not_low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_OFF),
"low": make_entity_target_state_trigger(
BATTERY_LOW_DOMAIN_SPECS, STATE_ON, primary_entities_only=False
),
"not_low": make_entity_target_state_trigger(
BATTERY_LOW_DOMAIN_SPECS, STATE_OFF, primary_entities_only=False
),
"started_charging": make_entity_target_state_trigger(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, primary_entities_only=False
),
"stopped_charging": make_entity_target_state_trigger(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, primary_entities_only=False
),
"level_changed": make_entity_numerical_state_changed_trigger(
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
BATTERY_PERCENTAGE_DOMAIN_SPECS,
valid_unit="%",
primary_entities_only=False,
),
"level_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
BATTERY_PERCENTAGE_DOMAIN_SPECS,
valid_unit="%",
primary_entities_only=False,
),
Comment thread
abmantis marked this conversation as resolved.
}

Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/battery/triggers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@
entity:
- domain: binary_sensor
device_class: battery
primary_entities_only: false

.trigger_target_charging: &trigger_target_charging
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false

.trigger_target_percentage: &trigger_target_percentage
entity:
- domain: sensor
device_class: battery
primary_entities_only: false

low:
fields:
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/helpers/selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,7 @@ class TargetSelectorConfig(BaseSelectorConfig, total=False):

entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
primary_entities_only: bool


@SELECTORS.register("target")
Expand All @@ -1899,6 +1900,7 @@ class TargetSelector(Selector[TargetSelectorConfig]):
cv.ensure_list,
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("primary_entities_only"): cv.boolean,
}
Comment thread
abmantis marked this conversation as resolved.
)

Expand Down
19 changes: 18 additions & 1 deletion homeassistant/helpers/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Final,
Literal,
Protocol,
Expand Down Expand Up @@ -357,6 +358,9 @@ class EntityTriggerBase(Trigger):
{STATE_UNAVAILABLE, STATE_UNKNOWN}
)
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST
# When True, indirect target expansion (via device/area/floor) skips
# entities with an entity_category.
_primary_entities_only: ClassVar[bool] = True

@override
@classmethod
Expand Down Expand Up @@ -519,7 +523,11 @@ def call_action() -> None:
)

unsub = async_track_target_selector_state_change_event(
self._hass, self._target, state_change_listener, self.entity_filter
self._hass,
self._target,
state_change_listener,
self.entity_filter,
primary_entities_only=self._primary_entities_only,
)

@callback
Expand Down Expand Up @@ -892,6 +900,8 @@ def _normalize_domain_specs(
def make_entity_target_state_trigger(
domain_specs: Mapping[str, DomainSpec] | str,
to_states: str | set[str],
*,
primary_entities_only: bool = True,
) -> type[EntityTargetStateTriggerBase]:
Comment thread
abmantis marked this conversation as resolved.
"""Create a trigger for entity state changes to specific state(s).

Expand All @@ -910,6 +920,7 @@ class CustomTrigger(EntityTargetStateTriggerBase):

_domain_specs = specs
_to_states = to_states_set
_primary_entities_only = primary_entities_only

return CustomTrigger

Expand Down Expand Up @@ -961,6 +972,8 @@ class CustomTrigger(EntityOriginStateTriggerBase):
def make_entity_numerical_state_changed_trigger(
domain_specs: Mapping[str, DomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
*,
primary_entities_only: bool = True,
) -> type[EntityNumericalStateChangedTriggerBase]:
"""Create a trigger for numerical state value change."""

Expand All @@ -969,13 +982,16 @@ class CustomTrigger(EntityNumericalStateChangedTriggerBase):

_domain_specs = domain_specs
_valid_unit = valid_unit
_primary_entities_only = primary_entities_only

return CustomTrigger


def make_entity_numerical_state_crossed_threshold_trigger(
domain_specs: Mapping[str, DomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
*,
primary_entities_only: bool = True,
) -> type[EntityNumericalStateCrossedThresholdTriggerBase]:
"""Create a trigger for numerical state value crossing a threshold."""

Expand All @@ -984,6 +1000,7 @@ class CustomTrigger(EntityNumericalStateCrossedThresholdTriggerBase):

_domain_specs = domain_specs
_valid_unit = valid_unit
_primary_entities_only = primary_entities_only

return CustomTrigger

Expand Down
9 changes: 7 additions & 2 deletions tests/components/battery/test_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF,
STATE_ON,
EntityCategory,
)
from homeassistant.core import HomeAssistant

Expand All @@ -31,13 +32,17 @@
@pytest.fixture
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple binary sensor entities associated with different targets."""
return await target_entities(hass, "binary_sensor")
return await target_entities(
hass, "binary_sensor", entity_category=EntityCategory.DIAGNOSTIC
)


@pytest.fixture
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple sensor entities associated with different targets."""
return await target_entities(hass, "sensor")
return await target_entities(
hass, "sensor", entity_category=EntityCategory.DIAGNOSTIC
)


@pytest.mark.parametrize(
Expand Down
19 changes: 18 additions & 1 deletion tests/components/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CONF_TARGET,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
)
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import (
Expand All @@ -48,13 +49,22 @@


async def target_entities(
hass: HomeAssistant, domain: str, *, domain_excluded: str | None = None
hass: HomeAssistant,
domain: str,
*,
domain_excluded: str | None = None,
entity_category: EntityCategory | None = None,
) -> dict[str, list[str]]:
"""Create multiple entities associated with different targets.

If `domain_excluded` is provided, entities in excluded_entities will have this
domain, otherwise they will have the same domain as included_entities.

If `entity_category` is provided, all created registry entities (i.e. the
area-, device-, and label-associated entities) are created with that
entity category. Standalone entities are referenced directly by entity_id
and are unaffected.

Returns a dict with the following keys:
- included_entities: List of entity_ids meant to be targeted.
- excluded_entities: List of entity_ids not meant to be targeted.
Expand Down Expand Up @@ -89,13 +99,15 @@ async def target_entities(
platform="test",
unique_id=f"{domain}_area",
suggested_object_id=f"area_{domain}",
entity_category=entity_category,
)
entity_reg.async_update_entity(entity_area.entity_id, area_id=area.id)
entity_area_excluded = entity_reg.async_get_or_create(
domain=domain_excluded,
platform="test",
unique_id=f"{domain_excluded}_area_excluded",
suggested_object_id=f"area_{domain_excluded}_excluded",
entity_category=entity_category,
)
entity_reg.async_update_entity(entity_area_excluded.entity_id, area_id=area.id)

Expand All @@ -106,20 +118,23 @@ async def target_entities(
unique_id=f"{domain}_device",
suggested_object_id=f"device_{domain}",
device_id=device.id,
entity_category=entity_category,
)
entity_reg.async_get_or_create(
domain=domain,
platform="test",
unique_id=f"{domain}_device2",
suggested_object_id=f"device2_{domain}",
device_id=device.id,
entity_category=entity_category,
)
entity_reg.async_get_or_create(
domain=domain_excluded,
platform="test",
unique_id=f"{domain_excluded}_device_excluded",
suggested_object_id=f"device_{domain_excluded}_excluded",
device_id=device.id,
entity_category=entity_category,
)

# Entities associated with label
Expand All @@ -128,13 +143,15 @@ async def target_entities(
platform="test",
unique_id=f"{domain}_label",
suggested_object_id=f"label_{domain}",
entity_category=entity_category,
)
entity_reg.async_update_entity(entity_label.entity_id, labels={label.label_id})
entity_label_excluded = entity_reg.async_get_or_create(
domain=domain_excluded,
platform="test",
unique_id=f"{domain_excluded}_label_excluded",
suggested_object_id=f"label_{domain_excluded}_excluded",
entity_category=entity_category,
)
entity_reg.async_update_entity(
entity_label_excluded.entity_id, labels={label.label_id}
Expand Down
18 changes: 18 additions & 0 deletions tests/helpers/test_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,24 @@ def test_serial_port_selector_schema(
(),
(),
),
(
{"primary_entities_only": True},
(),
(),
),
(
{"primary_entities_only": False},
(),
(),
),
(
{
"entity": {"domain": "light"},
"primary_entities_only": True,
},
(),
(),
),
],
)
def test_target_selector_schema(schema, valid_selections, invalid_selections) -> None:
Expand Down