mirror of
https://github.com/home-assistant/core.git
synced 2026-03-18 17:02:25 +01:00
Compare commits
13 Commits
dev
...
todo_trigg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8b2a9b82 | ||
|
|
12930a6670 | ||
|
|
0709e053c0 | ||
|
|
df9cb4d35d | ||
|
|
44dcdc53b5 | ||
|
|
8f62e2334e | ||
|
|
692265cec3 | ||
|
|
dd437bf822 | ||
|
|
32a5d8965c | ||
|
|
cbe1ec6f3e | ||
|
|
d5a6283f4f | ||
|
|
ebdbe25751 | ||
|
|
33355a1bf1 |
@@ -20,5 +20,16 @@
|
||||
"update_item": {
|
||||
"service": "mdi:clipboard-edit"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"item_added": {
|
||||
"trigger": "mdi:clipboard-plus"
|
||||
},
|
||||
"item_completed": {
|
||||
"trigger": "mdi:clipboard-check"
|
||||
},
|
||||
"item_removed": {
|
||||
"trigger": "mdi:clipboard-minus"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,5 +98,19 @@
|
||||
"name": "Update item"
|
||||
}
|
||||
},
|
||||
"title": "To-do list"
|
||||
"title": "To-do list",
|
||||
"triggers": {
|
||||
"item_added": {
|
||||
"description": "Triggers when a to-do item is added to a list.",
|
||||
"name": "To-do item added"
|
||||
},
|
||||
"item_completed": {
|
||||
"description": "Triggers when a to-do item is marked as done.",
|
||||
"name": "To-do item completed"
|
||||
},
|
||||
"item_removed": {
|
||||
"description": "Triggers when a to-do item is removed from a list.",
|
||||
"name": "To-do item removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
263
homeassistant/components/todo/trigger.py
Normal file
263
homeassistant/components/todo/trigger.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""Provides triggers for todo platform."""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import functools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import TodoItem, TodoListEntity
|
||||
from .const import DATA_COMPONENT, DOMAIN, TodoItemStatus
|
||||
|
||||
ITEM_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_entity(hass: HomeAssistant, entity_id: str) -> TodoListEntity:
|
||||
"""Get the todo entity for the provided entity_id."""
|
||||
component: EntityComponent[TodoListEntity] = hass.data[DATA_COMPONENT]
|
||||
if not (entity := component.get_entity(entity_id)) or not isinstance(
|
||||
entity, TodoListEntity
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Entity does not exist {entity_id} or is not a todo entity"
|
||||
)
|
||||
return entity
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TodoItemChangeEvent:
|
||||
"""Data class for todo item change event."""
|
||||
|
||||
entity_id: str
|
||||
items: list[TodoItem]
|
||||
|
||||
|
||||
class ItemChangeListener(TargetEntityChangeTracker):
|
||||
"""Helper class to listen to todo item changes for target entities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
target_selection: TargetSelection,
|
||||
listener: Callable[[TodoItemChangeEvent], None],
|
||||
) -> None:
|
||||
"""Initialize the item change tracker."""
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
super().__init__(hass, target_selection, entity_filter)
|
||||
self._listener = listener
|
||||
|
||||
self._pending_listener_task: asyncio.Task[None] | None = None
|
||||
self._unsubscribe_listeners: list[CALLBACK_TYPE] = []
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
|
||||
"""Restart the listeners when the list of entities of the tracked targets is updated."""
|
||||
if self._pending_listener_task:
|
||||
self._pending_listener_task.cancel()
|
||||
self._pending_listener_task = self._hass.async_create_task(
|
||||
self._start_listening(tracked_entities)
|
||||
)
|
||||
|
||||
async def _start_listening(self, tracked_entities: set[str]) -> None:
|
||||
"""Start listening for todo item changes."""
|
||||
_LOGGER.debug("Tracking items for todos: %s", tracked_entities)
|
||||
for unsub in self._unsubscribe_listeners:
|
||||
unsub()
|
||||
|
||||
def _listener_wrapper(entity_id: str, items: list[TodoItem]) -> None:
|
||||
self._listener(TodoItemChangeEvent(entity_id=entity_id, items=items))
|
||||
|
||||
self._unsubscribe_listeners = []
|
||||
for entity_id in tracked_entities:
|
||||
entity = get_entity(self._hass, entity_id)
|
||||
unsub = entity.async_subscribe_updates(
|
||||
functools.partial(_listener_wrapper, entity_id)
|
||||
)
|
||||
self._unsubscribe_listeners.append(unsub)
|
||||
|
||||
def unsubscribe(self) -> None:
|
||||
"""Unsubscribe from all events."""
|
||||
super()._unsubscribe()
|
||||
if self._pending_listener_task:
|
||||
self._pending_listener_task.cancel()
|
||||
self._pending_listener_task = None
|
||||
for unsub in self._unsubscribe_listeners:
|
||||
unsub()
|
||||
|
||||
|
||||
class ItemTriggerBase(Trigger, abc.ABC):
|
||||
"""todo item trigger base."""
|
||||
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, ITEM_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
|
||||
target_selection = TargetSelection(self._target)
|
||||
if not target_selection.has_any_target:
|
||||
raise HomeAssistantError(f"No target defined in {self._target}")
|
||||
listener = ItemChangeListener(
|
||||
self._hass,
|
||||
target_selection,
|
||||
functools.partial(self.handle_item_change_event, run_action=run_action),
|
||||
)
|
||||
return listener.async_setup()
|
||||
|
||||
@callback
|
||||
@abc.abstractmethod
|
||||
def handle_item_change_event(
|
||||
self, event: TodoItemChangeEvent, run_action: TriggerActionRunner
|
||||
) -> None:
|
||||
"""Handle todo item change event."""
|
||||
|
||||
|
||||
class ItemAddedTrigger(ItemTriggerBase):
|
||||
"""todo item added trigger."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._entity_item_ids: dict[str, set[str]] = {}
|
||||
|
||||
@override
|
||||
@callback
|
||||
def handle_item_change_event(
|
||||
self, event: TodoItemChangeEvent, run_action: TriggerActionRunner
|
||||
) -> None:
|
||||
"""Listen for todo item changes."""
|
||||
old_item_ids = self._entity_item_ids.get(event.entity_id, set())
|
||||
current_item_ids = {item.uid for item in event.items if item.uid is not None}
|
||||
added_item_ids = current_item_ids - old_item_ids
|
||||
self._entity_item_ids[event.entity_id] = current_item_ids
|
||||
if added_item_ids:
|
||||
_LOGGER.debug(
|
||||
"Detected added items with ids %s for entity %s",
|
||||
added_item_ids,
|
||||
event.entity_id,
|
||||
)
|
||||
payload = {
|
||||
ATTR_ENTITY_ID: event.entity_id,
|
||||
"item_ids": list(added_item_ids),
|
||||
}
|
||||
run_action(payload, description="todo item added trigger")
|
||||
|
||||
|
||||
class ItemRemovedTrigger(ItemTriggerBase):
|
||||
"""todo item removed trigger."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._entity_item_ids: dict[str, set[str]] = {}
|
||||
|
||||
@override
|
||||
@callback
|
||||
def handle_item_change_event(
|
||||
self, event: TodoItemChangeEvent, run_action: TriggerActionRunner
|
||||
) -> None:
|
||||
"""Listen for todo item changes."""
|
||||
old_item_ids = self._entity_item_ids.get(event.entity_id, set())
|
||||
current_item_ids = {item.uid for item in event.items if item.uid is not None}
|
||||
removed_item_ids = old_item_ids - current_item_ids
|
||||
self._entity_item_ids[event.entity_id] = current_item_ids
|
||||
if removed_item_ids:
|
||||
_LOGGER.debug(
|
||||
"Detected removed items with ids %s for entity %s",
|
||||
removed_item_ids,
|
||||
event.entity_id,
|
||||
)
|
||||
payload = {
|
||||
ATTR_ENTITY_ID: event.entity_id,
|
||||
"item_ids": list(removed_item_ids),
|
||||
}
|
||||
run_action(payload, description="todo item removed trigger")
|
||||
|
||||
|
||||
class ItemCompletedTrigger(ItemTriggerBase):
|
||||
"""todo item completed trigger."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._entity_completed_item_ids: dict[str, set[str]] = {}
|
||||
|
||||
@override
|
||||
@callback
|
||||
def handle_item_change_event(
|
||||
self, event: TodoItemChangeEvent, run_action: TriggerActionRunner
|
||||
) -> None:
|
||||
"""Listen for todo item changes."""
|
||||
old_item_ids = self._entity_completed_item_ids.get(event.entity_id, set())
|
||||
current_item_ids = {
|
||||
item.uid
|
||||
for item in event.items
|
||||
if item.uid is not None and item.status == TodoItemStatus.COMPLETED
|
||||
}
|
||||
new_completed_item_ids = current_item_ids - old_item_ids
|
||||
self._entity_completed_item_ids[event.entity_id] = current_item_ids
|
||||
if new_completed_item_ids:
|
||||
_LOGGER.debug(
|
||||
"Detected new completed items with ids %s for entity %s",
|
||||
new_completed_item_ids,
|
||||
event.entity_id,
|
||||
)
|
||||
payload = {
|
||||
ATTR_ENTITY_ID: event.entity_id,
|
||||
"item_ids": list(new_completed_item_ids),
|
||||
}
|
||||
run_action(payload, description="todo item completed trigger")
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"item_added": ItemAddedTrigger,
|
||||
"item_completed": ItemCompletedTrigger,
|
||||
"item_removed": ItemRemovedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for todo platform."""
|
||||
return TRIGGERS
|
||||
8
homeassistant/components/todo/triggers.yaml
Normal file
8
homeassistant/components/todo/triggers.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: todo
|
||||
|
||||
item_added: *trigger_common
|
||||
item_completed: *trigger_common
|
||||
item_removed: *trigger_common
|
||||
@@ -165,8 +165,6 @@ ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional("supported_features"): [
|
||||
vol.All(cv.ensure_list, [str], _validate_supported_features)
|
||||
],
|
||||
# Unit of measurement of the entity
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.ensure_list, [str]),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,7 +190,6 @@ class EntityFilterSelectorConfig(TypedDict, total=False):
|
||||
domain: str | list[str]
|
||||
device_class: str | list[str]
|
||||
supported_features: list[str]
|
||||
unit_of_measurement: str | list[str]
|
||||
|
||||
|
||||
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the To-do integration."""
|
||||
|
||||
import uuid
|
||||
|
||||
from homeassistant.components.todo import DOMAIN, TodoItem, TodoListEntity
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,7 +30,15 @@ class MockTodoListEntity(TodoListEntity):
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
self._attr_todo_items.append(item)
|
||||
self._attr_todo_items.append(
|
||||
TodoItem(
|
||||
summary=item.summary,
|
||||
uid=item.uid or uuid.uuid4().hex,
|
||||
status=item.status,
|
||||
due=item.due,
|
||||
description=item.description,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete an item in the To-do list."""
|
||||
|
||||
406
tests/components/todo/test_trigger.py
Normal file
406
tests/components/todo/test_trigger.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""Tests for the todo item_added trigger."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.todo import (
|
||||
DOMAIN,
|
||||
TodoItem,
|
||||
TodoItemStatus,
|
||||
TodoListEntityFeature,
|
||||
)
|
||||
from homeassistant.components.todo.const import ATTR_ITEM, ATTR_STATUS, TodoServices
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FLOOR_ID,
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
label_registry as lr,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MockTodoListEntity, create_mock_platform
|
||||
|
||||
from tests.common import async_mock_service, mock_device_registry
|
||||
|
||||
TODO_ENTITY_ID1 = "todo.list_one"
|
||||
TODO_ENTITY_ID2 = "todo.list_two"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def todo_lists(
|
||||
hass: HomeAssistant,
|
||||
) -> tuple[MockTodoListEntity, MockTodoListEntity]:
|
||||
"""Create two todo list entities via the mock platform."""
|
||||
entity1 = _make_entity(TODO_ENTITY_ID1, unique_id="list_one")
|
||||
entity2 = _make_entity(TODO_ENTITY_ID2, unique_id="list_two")
|
||||
await create_mock_platform(hass, [entity1, entity2])
|
||||
return entity1, entity2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def target_todo_lists(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
label_registry: lr.LabelRegistry,
|
||||
) -> None:
|
||||
"""Associate todo list entities with different targets.
|
||||
|
||||
Sets up the following target structure (per entity):
|
||||
- floor_list_one / area_list_one: floor and area for list_one only
|
||||
- floor_list_two / area_list_two: floor and area for list_two only
|
||||
- label_both: label shared by both entities
|
||||
- label_list_one / label_list_two: labels for one entity only
|
||||
- device_list_one / device_list_two: devices for one entity only
|
||||
"""
|
||||
floor_list_one = floor_registry.async_create("floor_list_one")
|
||||
area_list_one = area_registry.async_create(
|
||||
"area_list_one", floor_id=floor_list_one.floor_id
|
||||
)
|
||||
floor_list_two = floor_registry.async_create("floor_list_two")
|
||||
area_list_two = area_registry.async_create(
|
||||
"area_list_two", floor_id=floor_list_two.floor_id
|
||||
)
|
||||
|
||||
label_both = label_registry.async_create("label_both_lists")
|
||||
label_list_one = label_registry.async_create("label_list_one")
|
||||
label_list_two = label_registry.async_create("label_list_two")
|
||||
|
||||
device_list_one = dr.DeviceEntry(id="device_list_one")
|
||||
device_list_two = dr.DeviceEntry(id="device_list_two")
|
||||
mock_device_registry(
|
||||
hass,
|
||||
{
|
||||
device_list_one.id: device_list_one,
|
||||
device_list_two.id: device_list_two,
|
||||
},
|
||||
)
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
TODO_ENTITY_ID1,
|
||||
area_id=area_list_one.id,
|
||||
labels={label_both.label_id, label_list_one.label_id},
|
||||
device_id=device_list_one.id,
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
TODO_ENTITY_ID2,
|
||||
area_id=area_list_two.id,
|
||||
labels={label_both.label_id, label_list_two.label_id},
|
||||
device_id=device_list_two.id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_calls(hass: HomeAssistant) -> list[ServiceCall]:
|
||||
"""Track calls to a mock service."""
|
||||
return async_mock_service(hass, "test", "item_added")
|
||||
|
||||
|
||||
def _assert_service_calls(
|
||||
service_calls: list[ServiceCall], expected_calls: list[dict[str, Any]]
|
||||
) -> None:
|
||||
"""Assert that the service calls match the expected calls."""
|
||||
assert len(service_calls) == len(expected_calls), (
|
||||
f"Expected {len(expected_calls)} calls, got {len(service_calls)}"
|
||||
)
|
||||
for call, expected in zip(service_calls, expected_calls, strict=True):
|
||||
for key, value in expected.items():
|
||||
assert call.data.get(key) == value, (
|
||||
f"Expected call data[{key}] to be {value}, got {call.data.get(key)}"
|
||||
)
|
||||
|
||||
|
||||
async def _setup_automation(hass: HomeAssistant, target: dict[str, Any]) -> None:
|
||||
"""Set up an automation with the todo.item_added trigger."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": [
|
||||
{
|
||||
CONF_PLATFORM: "todo.item_added",
|
||||
CONF_TARGET: target,
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "todo.item_completed",
|
||||
CONF_TARGET: target,
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "todo.item_removed",
|
||||
CONF_TARGET: target,
|
||||
},
|
||||
],
|
||||
"action": {
|
||||
"service": "test.item_added",
|
||||
"data_template": {
|
||||
"platform": "{{ trigger.platform }}",
|
||||
"entity_id": "{{ trigger.entity_id }}",
|
||||
"item_ids": "{{ trigger.item_ids }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def _make_entity(
|
||||
entity_id: str,
|
||||
items: list[TodoItem] | None = None,
|
||||
unique_id: str | None = None,
|
||||
) -> MockTodoListEntity:
|
||||
"""Create a mock todo entity with the given items."""
|
||||
entity = MockTodoListEntity(items or [])
|
||||
entity.entity_id = entity_id
|
||||
entity._attr_unique_id = unique_id
|
||||
entity._attr_supported_features = (
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
return entity
|
||||
|
||||
|
||||
async def _add_item(hass: HomeAssistant, entity_id: str, item: str) -> None:
|
||||
"""Add an item to the entity."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
TodoServices.ADD_ITEM,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_ITEM: item},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def _remove_item(hass: HomeAssistant, entity_id: str, item: str) -> None:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
TodoServices.REMOVE_ITEM,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_ITEM: [item]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def _complete_item(hass: HomeAssistant, entity_id: str, item: str) -> None:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
TodoServices.UPDATE_ITEM,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_ITEM: item,
|
||||
ATTR_STATUS: TodoItemStatus.COMPLETED,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_item_change_triggers(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test item change triggers fire."""
|
||||
entity = _make_entity(TODO_ENTITY_ID1)
|
||||
await create_mock_platform(hass, [entity])
|
||||
|
||||
await _setup_automation(hass, {CONF_ENTITY_ID: TODO_ENTITY_ID1})
|
||||
|
||||
item1 = "item_id"
|
||||
await _add_item(hass, TODO_ENTITY_ID1, item1)
|
||||
await _add_item(hass, TODO_ENTITY_ID1, "other_item")
|
||||
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{"platform": "todo.item_added", "entity_id": TODO_ENTITY_ID1},
|
||||
{"platform": "todo.item_added", "entity_id": TODO_ENTITY_ID1},
|
||||
],
|
||||
)
|
||||
assert len(service_calls[0].data["item_ids"]) == 1
|
||||
assert len(service_calls[1].data["item_ids"]) == 1
|
||||
item1_id = service_calls[0].data["item_ids"][0]
|
||||
item2_id = service_calls[1].data["item_ids"][0]
|
||||
assert item1_id != item2_id
|
||||
service_calls.clear()
|
||||
|
||||
await _complete_item(hass, TODO_ENTITY_ID1, item1)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_completed",
|
||||
"entity_id": TODO_ENTITY_ID1,
|
||||
"item_ids": [item1_id],
|
||||
},
|
||||
],
|
||||
)
|
||||
service_calls.clear()
|
||||
|
||||
await _remove_item(hass, TODO_ENTITY_ID1, item1)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_removed",
|
||||
"entity_id": TODO_ENTITY_ID1,
|
||||
"item_ids": [item1_id],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features", "target_todo_lists")
|
||||
@pytest.mark.parametrize(
|
||||
"included_target",
|
||||
[
|
||||
{CONF_ENTITY_ID: TODO_ENTITY_ID1},
|
||||
{ATTR_AREA_ID: "area_list_one"},
|
||||
{ATTR_FLOOR_ID: "floor_list_one"},
|
||||
{ATTR_LABEL_ID: "label_list_one"},
|
||||
{ATTR_DEVICE_ID: "device_list_one"},
|
||||
],
|
||||
)
|
||||
async def test_item_change_trigger_does_not_fire_for_other_entity(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
included_target: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test item_added trigger only fires for the targeted entity."""
|
||||
included_entity = TODO_ENTITY_ID1
|
||||
excluded_entity = TODO_ENTITY_ID2
|
||||
|
||||
await _setup_automation(hass, included_target)
|
||||
|
||||
# Add item to excluded entity (not targeted)
|
||||
await _add_item(hass, excluded_entity, "Untargeted item")
|
||||
_assert_service_calls(service_calls, [])
|
||||
|
||||
# Add item to included entity (targeted)
|
||||
await _add_item(hass, included_entity, "Targeted item")
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[{"platform": "todo.item_added", "entity_id": included_entity}],
|
||||
)
|
||||
targeted_item_id = service_calls[0].data["item_ids"][0]
|
||||
service_calls.clear()
|
||||
|
||||
# Complete item on excluded entity (not targeted) - should not fire
|
||||
await _complete_item(hass, excluded_entity, "Untargeted item")
|
||||
_assert_service_calls(service_calls, [])
|
||||
|
||||
# Complete item on included entity (targeted) - should fire
|
||||
await _complete_item(hass, included_entity, targeted_item_id)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_completed",
|
||||
"entity_id": included_entity,
|
||||
"item_ids": [targeted_item_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
service_calls.clear()
|
||||
|
||||
# Remove item on excluded entity (not targeted) - should not fire
|
||||
await _remove_item(hass, excluded_entity, "Untargeted item")
|
||||
_assert_service_calls(service_calls, [])
|
||||
|
||||
# Remove item on included entity (targeted) - should fire
|
||||
await _remove_item(hass, included_entity, targeted_item_id)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_removed",
|
||||
"entity_id": included_entity,
|
||||
"item_ids": [targeted_item_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features", "target_todo_lists")
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_target",
|
||||
[
|
||||
{CONF_ENTITY_ID: [TODO_ENTITY_ID1, TODO_ENTITY_ID2]},
|
||||
{ATTR_AREA_ID: ["area_list_one", "area_list_two"]},
|
||||
{ATTR_FLOOR_ID: ["floor_list_one", "floor_list_two"]},
|
||||
{ATTR_LABEL_ID: "label_both_lists"},
|
||||
{ATTR_DEVICE_ID: ["device_list_one", "device_list_two"]},
|
||||
],
|
||||
ids=["entity_id", "area", "floor", "label", "device"],
|
||||
)
|
||||
async def test_item_change_trigger_with_multiple_target_entities(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger_target: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test item_added trigger fires for multiple targeted entities."""
|
||||
await _setup_automation(hass, target=trigger_target)
|
||||
|
||||
await _add_item(hass, TODO_ENTITY_ID1, "Item on list one")
|
||||
await _add_item(hass, TODO_ENTITY_ID2, "Item on list two")
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{"platform": "todo.item_added", "entity_id": TODO_ENTITY_ID1},
|
||||
{"platform": "todo.item_added", "entity_id": TODO_ENTITY_ID2},
|
||||
],
|
||||
)
|
||||
item_one_id = service_calls[0].data["item_ids"][0]
|
||||
item_two_id = service_calls[1].data["item_ids"][0]
|
||||
service_calls.clear()
|
||||
|
||||
await _complete_item(hass, TODO_ENTITY_ID1, item_one_id)
|
||||
await _complete_item(hass, TODO_ENTITY_ID2, item_two_id)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_completed",
|
||||
"entity_id": TODO_ENTITY_ID1,
|
||||
"item_ids": [item_one_id],
|
||||
},
|
||||
{
|
||||
"platform": "todo.item_completed",
|
||||
"entity_id": TODO_ENTITY_ID2,
|
||||
"item_ids": [item_two_id],
|
||||
},
|
||||
],
|
||||
)
|
||||
service_calls.clear()
|
||||
|
||||
await _remove_item(hass, TODO_ENTITY_ID1, item_one_id)
|
||||
await _remove_item(hass, TODO_ENTITY_ID2, item_two_id)
|
||||
_assert_service_calls(
|
||||
service_calls,
|
||||
[
|
||||
{
|
||||
"platform": "todo.item_removed",
|
||||
"entity_id": TODO_ENTITY_ID1,
|
||||
"item_ids": [item_one_id],
|
||||
},
|
||||
{
|
||||
"platform": "todo.item_removed",
|
||||
"entity_id": TODO_ENTITY_ID2,
|
||||
"item_ids": [item_two_id],
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -297,24 +297,6 @@ def test_device_selector_schema_error(schema) -> None:
|
||||
("light.abc123", "blah.blah", FAKE_UUID),
|
||||
(None,),
|
||||
),
|
||||
(
|
||||
{
|
||||
"filter": [
|
||||
{"unit_of_measurement": "baguette"},
|
||||
]
|
||||
},
|
||||
("light.abc123", "blah.blah", FAKE_UUID),
|
||||
(None,),
|
||||
),
|
||||
(
|
||||
{
|
||||
"filter": [
|
||||
{"unit_of_measurement": ["currywurst", "bratwurst"]},
|
||||
]
|
||||
},
|
||||
("light.abc123", "blah.blah", FAKE_UUID),
|
||||
(None,),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> None:
|
||||
@@ -337,10 +319,6 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) ->
|
||||
{"filter": [{"supported_features": ["light.LightEntityFeature.blah"]}]},
|
||||
# supported_features should be used under the filter key
|
||||
{"supported_features": ["light.LightEntityFeature.EFFECT"]},
|
||||
# unit_of_measurement should be used under the filter key
|
||||
{"unit_of_measurement": ["currywurst", "bratwurst"]},
|
||||
# Invalid unit_of_measurement
|
||||
{"filter": [{"unit_of_measurement": 42}]},
|
||||
# reorder can only be used when multiple is true
|
||||
{"reorder": True},
|
||||
{"reorder": True, "multiple": False},
|
||||
|
||||
Reference in New Issue
Block a user