mirror of
https://github.com/home-assistant/core.git
synced 2026-08-20 21:31:17 +01:00
Log warning when event triggers filter on composite device (#179490)
This commit is contained in:
@@ -1,16 +1,29 @@
|
||||
"""Offer event listening automation rules."""
|
||||
|
||||
from collections.abc import ItemsView, Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM, EVENT_STATE_REPORTED
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_EVENT_DATA,
|
||||
CONF_PLATFORM,
|
||||
EVENT_STATE_REPORTED,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import yaml as yaml_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
CONF_EVENT_CONTEXT = "context"
|
||||
@@ -39,6 +52,58 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate trigger config.
|
||||
|
||||
Warn if the trigger filters event_data.device_id on a pre-migration composite device
|
||||
id - a device that was split into one device per config entry.
|
||||
A templated device id is a Template (not a plain string) and is left alone.
|
||||
"""
|
||||
validated_config: ConfigType = TRIGGER_SCHEMA(config)
|
||||
if (
|
||||
CONF_EVENT_DATA in validated_config
|
||||
and isinstance(
|
||||
device_id := validated_config[CONF_EVENT_DATA].get(CONF_DEVICE_ID), str
|
||||
)
|
||||
and (
|
||||
split_devices := dr.async_get(
|
||||
hass
|
||||
).async_get_devices_for_composite_device_id(device_id)
|
||||
)
|
||||
):
|
||||
_log_composite_device_id_warning(hass, config, device_id, split_devices)
|
||||
return validated_config
|
||||
|
||||
|
||||
@callback
|
||||
def _log_composite_device_id_warning(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
device_id: str,
|
||||
split_devices: list[dr.DeviceEntry],
|
||||
) -> None:
|
||||
"""Warn that an event trigger filters on a split (pre-migration) device id."""
|
||||
|
||||
device_summaries: list[str] = []
|
||||
for device in split_devices:
|
||||
entry = hass.config_entries.async_get_entry(device.config_entry_id)
|
||||
domain = entry.domain if entry else "unknown"
|
||||
name = device.name_by_user or device.name or device.id
|
||||
device_summaries.append(f"{name} ({device.id}) from the {domain} integration")
|
||||
|
||||
_LOGGER.warning(
|
||||
"Event trigger filters on device '%s', which was split into one device per "
|
||||
"integration and no longer exists, so the trigger can no longer fire. Update the "
|
||||
"automation, script or template entity to filter on one of these devices instead: "
|
||||
"%s.\nThe affected trigger is configured as:\n%s",
|
||||
device_id,
|
||||
", ".join(device_summaries),
|
||||
yaml_util.dump(config),
|
||||
)
|
||||
|
||||
|
||||
def _schema_value(value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return vol.In(value)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""The tests for the Event automation."""
|
||||
|
||||
import logging
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr, script, trigger
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import mock_component
|
||||
from tests.common import MockConfigEntry, mock_component
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -629,3 +633,109 @@ async def test_templated_state_reported_event(
|
||||
"Got error 'Can't listen to state_reported in event trigger' "
|
||||
"when setting up triggers for automation 0" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
COMPOSITE_ID = "composite00000000000000000000ab"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def split_devices(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> tuple[dr.DeviceEntry, dr.DeviceEntry]:
|
||||
"""Create two devices which are splits of a pre-migration composite device."""
|
||||
entry_1 = MockConfigEntry(domain="itg1")
|
||||
entry_1.add_to_hass(hass)
|
||||
entry_2 = MockConfigEntry(domain="itg2")
|
||||
entry_2.add_to_hass(hass)
|
||||
device_1 = device_registry.async_get_or_create(
|
||||
config_entry_id=entry_1.entry_id,
|
||||
identifiers={("itg1", "1")},
|
||||
name="Split device 1",
|
||||
)
|
||||
device_2 = device_registry.async_get_or_create(
|
||||
config_entry_id=entry_2.entry_id,
|
||||
identifiers={("itg2", "1")},
|
||||
name="Split device 2",
|
||||
)
|
||||
device_registry.devices[device_1.id] = attr.evolve(
|
||||
device_1, composite_device_id=COMPOSITE_ID
|
||||
)
|
||||
device_registry.devices[device_2.id] = attr.evolve(
|
||||
device_2, composite_device_id=COMPOSITE_ID
|
||||
)
|
||||
return device_registry.devices[device_1.id], device_registry.devices[device_2.id]
|
||||
|
||||
|
||||
_EVENT_TRIGGER = {
|
||||
"platform": "event",
|
||||
"event_type": "my_event",
|
||||
"event_data": {"device_id": COMPOSITE_ID},
|
||||
}
|
||||
|
||||
|
||||
def _expected_composite_warning(
|
||||
device_1: dr.DeviceEntry, device_2: dr.DeviceEntry
|
||||
) -> str:
|
||||
"""Return the exact warning the event validator logs for a composite device id."""
|
||||
return (
|
||||
f"Event trigger filters on device '{COMPOSITE_ID}', which was split into one "
|
||||
"device per integration and no longer exists, so the trigger can no longer fire. "
|
||||
"Update the automation, script or template entity to filter on one of these "
|
||||
"devices instead: "
|
||||
f"Split device 1 ({device_1.id}) from the itg1 integration, "
|
||||
f"Split device 2 ({device_2.id}) from the itg2 integration.\n"
|
||||
"The affected trigger is configured as:\n"
|
||||
"platform: event\n"
|
||||
"event_type: my_event\n"
|
||||
"event_data:\n"
|
||||
f" device_id: {COMPOSITE_ID}\n"
|
||||
)
|
||||
|
||||
|
||||
async def test_composite_device_id_logs_warning(
|
||||
hass: HomeAssistant,
|
||||
split_devices: tuple[dr.DeviceEntry, dr.DeviceEntry],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a composite event_data.device_id filter logs the full warning."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await trigger.async_validate_trigger_config(hass, [_EVENT_TRIGGER])
|
||||
assert caplog.messages == [_expected_composite_warning(*split_devices)]
|
||||
|
||||
|
||||
async def test_live_device_id_no_warning(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a live event_data.device_id filter does not warn."""
|
||||
entry = MockConfigEntry(domain="itg")
|
||||
entry.add_to_hass(hass)
|
||||
live_device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id, identifiers={("itg", "1")}
|
||||
)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await trigger.async_validate_trigger_config(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
"platform": "event",
|
||||
"event_type": "my_event",
|
||||
"event_data": {"device_id": live_device.id},
|
||||
}
|
||||
],
|
||||
)
|
||||
assert caplog.messages == []
|
||||
|
||||
|
||||
async def test_wait_for_trigger_composite_device_id_logs_warning(
|
||||
hass: HomeAssistant,
|
||||
split_devices: tuple[dr.DeviceEntry, dr.DeviceEntry],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a composite device_id in a wait_for_trigger event trigger warns too."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await script.async_validate_actions_config(
|
||||
hass, [{"wait_for_trigger": [_EVENT_TRIGGER]}]
|
||||
)
|
||||
assert caplog.messages == [_expected_composite_warning(*split_devices)]
|
||||
|
||||
Reference in New Issue
Block a user