-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add occupancy binary sensor triggers #157631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
932480b
25179b7
492bc7f
a0db660
ba71105
4213736
2bb71ec
297be0b
81cbea2
8c015d7
b689310
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jlpouffier Please check the text and also the trigger keys (
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the cover one, we used that convention: When it comes to the something something 😅 we can either be generic or find something that suits each device class best.
It's both past tense, so it fits our model, and it's simpler to understand. When it comes to the strings, I would align all of them to this. Title: Presence Detected Title: Presence Cleared (I struggled a bit to write something with the word "Cleared".
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jlpouffier can you check again please? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| """Provides triggers for lights.""" | ||
|
emontnemery marked this conversation as resolved.
Outdated
|
||
|
|
||
| from homeassistant.const import STATE_OFF, STATE_ON | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import HomeAssistantError | ||
| from homeassistant.helpers.entity import get_device_class | ||
| from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger | ||
| from homeassistant.helpers.typing import UNDEFINED, UndefinedType | ||
|
|
||
| from . import DOMAIN, BinarySensorDeviceClass | ||
|
|
||
|
|
||
| def get_device_class_or_undefined( | ||
| hass: HomeAssistant, entity_id: str | ||
| ) -> str | None | UndefinedType: | ||
| """Get the device class of an entity or UNDEFINED if not found.""" | ||
| try: | ||
| return get_device_class(hass, entity_id) | ||
| except HomeAssistantError: | ||
| return UNDEFINED | ||
|
emontnemery marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class BinarySensorOnOffTrigger(EntityStateTriggerBase): | ||
| """Class for cover opened and closed triggers.""" | ||
|
emontnemery marked this conversation as resolved.
Outdated
|
||
|
|
||
| _device_class: BinarySensorDeviceClass | None | ||
| _domain: str = DOMAIN | ||
|
|
||
|
emontnemery marked this conversation as resolved.
|
||
|
|
||
| def make_binary_sensor_trigger( | ||
| device_class: BinarySensorDeviceClass | None, | ||
| to_state: str, | ||
| ) -> type[BinarySensorOnOffTrigger]: | ||
| """Create an entity state trigger class.""" | ||
|
|
||
| class CustomTrigger(BinarySensorOnOffTrigger): | ||
| """Trigger for entity state changes.""" | ||
|
|
||
| _device_class = device_class | ||
| _to_state = to_state | ||
|
|
||
| return CustomTrigger | ||
|
|
||
|
|
||
| TRIGGERS: dict[str, type[Trigger]] = { | ||
| "started_detecting_presence": make_binary_sensor_trigger( | ||
| BinarySensorDeviceClass.PRESENCE, STATE_ON | ||
| ), | ||
| "stopped_detecting_presence": make_binary_sensor_trigger( | ||
| BinarySensorDeviceClass.PRESENCE, STATE_OFF | ||
| ), | ||
| } | ||
|
|
||
|
|
||
| async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]: | ||
| """Return the triggers for lights.""" | ||
|
frenck marked this conversation as resolved.
Outdated
|
||
| return TRIGGERS | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| .trigger_common_fields: &trigger_common_fields | ||
| behavior: | ||
| required: true | ||
| default: any | ||
| selector: | ||
| select: | ||
| translation_key: trigger_behavior | ||
| options: | ||
| - first | ||
| - last | ||
| - any | ||
|
|
||
| started_detecting_presence: | ||
| fields: *trigger_common_fields | ||
| target: | ||
| entity: | ||
| domain: binary_sensor | ||
| device_class: presence | ||
|
|
||
| stopped_detecting_presence: | ||
| fields: *trigger_common_fields | ||
| target: | ||
| entity: | ||
| domain: binary_sensor | ||
| device_class: presence |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| """Test binary sensor trigger.""" | ||
|
|
||
| from collections.abc import Generator | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
|
|
||
| from homeassistant.const import ( | ||
| ATTR_DEVICE_CLASS, | ||
| ATTR_LABEL_ID, | ||
| CONF_ENTITY_ID, | ||
| STATE_OFF, | ||
| STATE_ON, | ||
| ) | ||
| from homeassistant.core import HomeAssistant, ServiceCall | ||
| from homeassistant.setup import async_setup_component | ||
|
|
||
| from tests.components import ( | ||
| StateDescription, | ||
| arm_trigger, | ||
| parametrize_target_entities, | ||
| parametrize_trigger_states, | ||
| set_or_remove_state, | ||
| target_entities, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True, name="stub_blueprint_populate") | ||
| def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: | ||
| """Stub copying the blueprints to the config folder.""" | ||
|
|
||
|
|
||
| @pytest.fixture(name="enable_experimental_triggers_conditions") | ||
| def enable_experimental_triggers_conditions() -> Generator[None]: | ||
| """Enable experimental triggers and conditions.""" | ||
| with patch( | ||
| "homeassistant.components.labs.async_is_preview_feature_enabled", | ||
| return_value=True, | ||
| ): | ||
| yield | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| async def target_binary_sensors(hass: HomeAssistant) -> list[str]: | ||
| """Create multiple binary sensor entities associated with different targets.""" | ||
| return await target_entities(hass, "binary_sensor") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "trigger_key", | ||
| [ | ||
| "binary_sensor.started_detecting_presence", | ||
| "binary_sensor.stopped_detecting_presence", | ||
| ], | ||
| ) | ||
| async def test_binary_sensor_triggers_gated_by_labs_flag( | ||
| hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str | ||
| ) -> None: | ||
| """Test the binary sensor triggers are gated by the labs flag.""" | ||
| await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"}) | ||
| assert ( | ||
| "Unnamed automation failed to setup triggers and has been disabled: Trigger " | ||
| f"'{trigger_key}' requires the experimental 'New triggers and conditions' " | ||
| "feature to be enabled in Home Assistant Labs settings (feature flag: " | ||
| "'new_triggers_conditions')" | ||
| ) in caplog.text | ||
|
|
||
|
|
||
| @pytest.mark.usefixtures("enable_experimental_triggers_conditions") | ||
| @pytest.mark.parametrize( | ||
| ("trigger_target_config", "entity_id", "entities_in_target"), | ||
| parametrize_target_entities("binary_sensor"), | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| ("trigger", "states"), | ||
| [ | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.started_detecting_presence", | ||
| target_states=[STATE_ON], | ||
| other_states=[STATE_OFF], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.stopped_detecting_presence", | ||
| target_states=[STATE_OFF], | ||
| other_states=[STATE_ON], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| ], | ||
| ) | ||
| async def test_binary_sensor_state_attribute_trigger_behavior_any( | ||
| hass: HomeAssistant, | ||
| service_calls: list[ServiceCall], | ||
| target_binary_sensors: list[str], | ||
| trigger_target_config: dict, | ||
| entity_id: str, | ||
| entities_in_target: int, | ||
| trigger: str, | ||
| states: list[StateDescription], | ||
| ) -> None: | ||
| """Test that the binary sensor state trigger fires when any binary sensor state changes to a specific state.""" | ||
| await async_setup_component(hass, "binary_sensor", {}) | ||
|
|
||
| other_entity_ids = set(target_binary_sensors) - {entity_id} | ||
|
|
||
| # Set all binary sensors, including the tested binary sensor, to the initial state | ||
| for eid in target_binary_sensors: | ||
| set_or_remove_state(hass, eid, states[0]) | ||
| await hass.async_block_till_done() | ||
|
|
||
| await arm_trigger(hass, trigger, {}, trigger_target_config) | ||
|
|
||
| for state in states[1:]: | ||
| set_or_remove_state(hass, entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == state["count"] | ||
| for service_call in service_calls: | ||
| assert service_call.data[CONF_ENTITY_ID] == entity_id | ||
| service_calls.clear() | ||
|
|
||
| # Check if changing other binary sensors also triggers | ||
| for other_entity_id in other_entity_ids: | ||
| set_or_remove_state(hass, other_entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == (entities_in_target - 1) * state["count"] | ||
| service_calls.clear() | ||
|
|
||
|
emontnemery marked this conversation as resolved.
|
||
|
|
||
| @pytest.mark.usefixtures("enable_experimental_triggers_conditions") | ||
| @pytest.mark.parametrize( | ||
| ("trigger_target_config", "entity_id", "entities_in_target"), | ||
| parametrize_target_entities("binary_sensor"), | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| ("trigger", "states"), | ||
| [ | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.started_detecting_presence", | ||
| target_states=[STATE_ON], | ||
| other_states=[STATE_OFF], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.stopped_detecting_presence", | ||
| target_states=[STATE_OFF], | ||
| other_states=[STATE_ON], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| ], | ||
| ) | ||
| async def test_binary_sensor_state_attribute_trigger_behavior_first( | ||
| hass: HomeAssistant, | ||
| service_calls: list[ServiceCall], | ||
| target_binary_sensors: list[str], | ||
| trigger_target_config: dict, | ||
| entity_id: str, | ||
| entities_in_target: int, | ||
| trigger: str, | ||
| states: list[StateDescription], | ||
| ) -> None: | ||
| """Test that the binary sensor state trigger fires when the first binary sensor state changes to a specific state.""" | ||
| await async_setup_component(hass, "binary_sensor", {}) | ||
|
|
||
| other_entity_ids = set(target_binary_sensors) - {entity_id} | ||
|
|
||
| # Set all binary sensors, including the tested binary sensor, to the initial state | ||
| for eid in target_binary_sensors: | ||
| set_or_remove_state(hass, eid, states[0]) | ||
| await hass.async_block_till_done() | ||
|
|
||
| await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config) | ||
|
|
||
| for state in states[1:]: | ||
| set_or_remove_state(hass, entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == state["count"] | ||
| for service_call in service_calls: | ||
| assert service_call.data[CONF_ENTITY_ID] == entity_id | ||
| service_calls.clear() | ||
|
|
||
| # Triggering other binary sensors should not cause the trigger to fire again | ||
| for other_entity_id in other_entity_ids: | ||
| set_or_remove_state(hass, other_entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == 0 | ||
|
|
||
|
|
||
| @pytest.mark.usefixtures("enable_experimental_triggers_conditions") | ||
| @pytest.mark.parametrize( | ||
| ("trigger_target_config", "entity_id", "entities_in_target"), | ||
| parametrize_target_entities("binary_sensor"), | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| ("trigger", "states"), | ||
| [ | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.started_detecting_presence", | ||
| target_states=[STATE_ON], | ||
| other_states=[STATE_OFF], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| *parametrize_trigger_states( | ||
| trigger="binary_sensor.stopped_detecting_presence", | ||
| target_states=[STATE_OFF], | ||
| other_states=[STATE_ON], | ||
| additional_attributes={ATTR_DEVICE_CLASS: "presence"}, | ||
| ), | ||
| ], | ||
| ) | ||
| async def test_binary_sensor_state_attribute_trigger_behavior_last( | ||
| hass: HomeAssistant, | ||
| service_calls: list[ServiceCall], | ||
| target_binary_sensors: list[str], | ||
| trigger_target_config: dict, | ||
| entity_id: str, | ||
| entities_in_target: int, | ||
| trigger: str, | ||
| states: list[StateDescription], | ||
| ) -> None: | ||
| """Test that the binary sensor state trigger fires when the last binary sensor state changes to a specific state.""" | ||
| await async_setup_component(hass, "binary_sensor", {}) | ||
|
|
||
| other_entity_ids = set(target_binary_sensors) - {entity_id} | ||
|
|
||
| # Set all binary sensors, including the tested binary sensor, to the initial state | ||
| for eid in target_binary_sensors: | ||
| set_or_remove_state(hass, eid, states[0]) | ||
| await hass.async_block_till_done() | ||
|
|
||
| await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config) | ||
|
|
||
| for state in states[1:]: | ||
| for other_entity_id in other_entity_ids: | ||
| set_or_remove_state(hass, other_entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == 0 | ||
|
|
||
| set_or_remove_state(hass, entity_id, state) | ||
| await hass.async_block_till_done() | ||
| assert len(service_calls) == state["count"] | ||
| for service_call in service_calls: | ||
| assert service_call.data[CONF_ENTITY_ID] == entity_id | ||
| service_calls.clear() | ||
Uh oh!
There was an error while loading. Please reload this page.