Skip to main content
Device automation allows users to create automations based on device events, states, and actions without needing to know entity IDs. This guide covers how to implement device triggers, conditions, and actions for your integration.

Overview

Device automation consists of three components:
  • Triggers: Events that start an automation (e.g., “button pressed”)
  • Conditions: States that must be true for automation to run (e.g., “light is on”)
  • Actions: Things the automation can do (e.g., “turn on light”)

Benefits of Device Automation

  1. User-friendly: Users select devices instead of entity IDs
  2. Discovery: Automations are suggested based on available devices
  3. Type-safe: Structured configuration prevents errors
  4. UI-first: Fully integrated with the automation UI

File Structure

Device automation implementations use separate files:
homeassistant/components/your_integration/
├── device_trigger.py      # Trigger implementation
├── device_condition.py    # Condition implementation
└── device_action.py       # Action implementation

Implementing Device Triggers

Device triggers fire when specific events occur on a device.

Basic Trigger Implementation

device_trigger.py
"""Provides device triggers for Your Integration."""
from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import (
    CONF_DEVICE_ID,
    CONF_DOMAIN,
    CONF_PLATFORM,
    CONF_TYPE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN

# Define trigger types
TRIGGER_TYPE_BUTTON_PRESSED = "button_pressed"
TRIGGER_TYPE_BUTTON_RELEASED = "button_released"

TRIGGER_TYPES = {
    TRIGGER_TYPE_BUTTON_PRESSED,
    TRIGGER_TYPE_BUTTON_RELEASED,
}

# Schema for trigger configuration
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
    {
        vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
    }
)


async def async_get_triggers(
    hass: HomeAssistant, device_id: str
) -> list[dict[str, Any]]:
    """List device triggers for a device."""
    device_registry = dr.async_get(hass)
    device = device_registry.async_get(device_id)
    
    if not device:
        return []
    
    # Return available triggers for this device
    triggers = []
    
    # Example: Button device supports both press and release
    if is_button_device(device):
        triggers.extend(
            [
                {
                    CONF_PLATFORM: "device",
                    CONF_DOMAIN: DOMAIN,
                    CONF_DEVICE_ID: device_id,
                    CONF_TYPE: TRIGGER_TYPE_BUTTON_PRESSED,
                },
                {
                    CONF_PLATFORM: "device",
                    CONF_DOMAIN: DOMAIN,
                    CONF_DEVICE_ID: device_id,
                    CONF_TYPE: TRIGGER_TYPE_BUTTON_RELEASED,
                },
            ]
        )
    
    return triggers


async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: TriggerActionType,
    trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
    """Attach a trigger."""
    # Map the device trigger to an event trigger
    event_config = event_trigger.TRIGGER_SCHEMA(
        {
            event_trigger.CONF_PLATFORM: "event",
            event_trigger.CONF_EVENT_TYPE: f"{DOMAIN}_event",
            event_trigger.CONF_EVENT_DATA: {
                CONF_DEVICE_ID: config[CONF_DEVICE_ID],
                CONF_TYPE: config[CONF_TYPE],
            },
        }
    )
    
    return await event_trigger.async_attach_trigger(
        hass, event_config, action, trigger_info, platform_type="device"
    )


async def async_get_trigger_capabilities(
    hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
    """List trigger capabilities."""
    # Return additional fields that can be configured
    return {
        "extra_fields": vol.Schema(
            {
                # Add optional fields like button number, etc.
            }
        )
    }


def is_button_device(device: dr.DeviceEntry) -> bool:
    """Check if device is a button."""
    # Implement logic to determine device type
    return True

Firing Device Triggers

In your integration code, fire events when triggers occur:
__init__.py
# When a button is pressed:
hass.bus.async_fire(
    f"{DOMAIN}_event",
    {
        CONF_DEVICE_ID: device_id,
        CONF_TYPE: "button_pressed",
    },
)

Trigger Types Examples

Common trigger types:
# Button triggers
TRIGGER_TYPE_BUTTON_SHORT_PRESS = "button_short_press"
TRIGGER_TYPE_BUTTON_LONG_PRESS = "button_long_press"
TRIGGER_TYPE_BUTTON_DOUBLE_PRESS = "button_double_press"

# Motion sensor triggers
TRIGGER_TYPE_MOTION_DETECTED = "motion_detected"
TRIGGER_TYPE_MOTION_CLEARED = "motion_cleared"

# Door/window sensor triggers
TRIGGER_TYPE_OPENED = "opened"
TRIGGER_TYPE_CLOSED = "closed"

Implementing Device Conditions

Device conditions check device state in automations.

Basic Condition Implementation

device_condition.py
"""Provides device conditions for Your Integration."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.const import (
    CONF_CONDITION,
    CONF_DEVICE_ID,
    CONF_DOMAIN,
    CONF_ENTITY_ID,
    CONF_TYPE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
    condition,
    config_validation as cv,
    device_registry as dr,
    entity_registry as er,
)
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN

# Define condition types
CONDITION_TYPE_IS_ON = "is_on"
CONDITION_TYPE_IS_OFF = "is_off"

CONDITION_TYPES = {
    CONDITION_TYPE_IS_ON,
    CONDITION_TYPE_IS_OFF,
}

# Schema for condition configuration
CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
    {
        vol.Required(CONF_ENTITY_ID): cv.entity_id,
        vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES),
    }
)


async def async_get_conditions(
    hass: HomeAssistant, device_id: str
) -> list[dict[str, Any]]:
    """List device conditions for a device."""
    conditions = []
    device_registry = dr.async_get(hass)
    entity_registry = er.async_get(hass)
    
    # Get all entities for this device
    entries = er.async_entries_for_device(entity_registry, device_id)
    
    for entry in entries:
        # Only include switch entities
        if entry.domain != "switch":
            continue
        
        conditions.extend(
            [
                {
                    CONF_CONDITION: "device",
                    CONF_DEVICE_ID: device_id,
                    CONF_DOMAIN: DOMAIN,
                    CONF_ENTITY_ID: entry.entity_id,
                    CONF_TYPE: CONDITION_TYPE_IS_ON,
                },
                {
                    CONF_CONDITION: "device",
                    CONF_DEVICE_ID: device_id,
                    CONF_DOMAIN: DOMAIN,
                    CONF_ENTITY_ID: entry.entity_id,
                    CONF_TYPE: CONDITION_TYPE_IS_OFF,
                },
            ]
        )
    
    return conditions


@callback
def async_condition_from_config(
    hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
    """Create a condition checker from config."""
    condition_type = config[CONF_TYPE]
    entity_id = config[CONF_ENTITY_ID]
    
    if condition_type == CONDITION_TYPE_IS_ON:
        state = "on"
    else:
        state = "off"
    
    @callback
    def test_is_state(hass: HomeAssistant, variables: dict = None) -> bool:
        """Test if condition is true."""
        return condition.state(hass, entity_id, state)
    
    return test_is_state


async def async_get_condition_capabilities(
    hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
    """List condition capabilities."""
    return {}

Implementing Device Actions

Device actions perform operations in automations.

Basic Action Implementation

device_action.py
"""Provides device actions for Your Integration."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.const import (
    CONF_DEVICE_ID,
    CONF_DOMAIN,
    CONF_ENTITY_ID,
    CONF_TYPE,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN

# Define action types
ACTION_TYPE_TURN_ON = "turn_on"
ACTION_TYPE_TURN_OFF = "turn_off"
ACTION_TYPE_TOGGLE = "toggle"

ACTION_TYPES = {
    ACTION_TYPE_TURN_ON,
    ACTION_TYPE_TURN_OFF,
    ACTION_TYPE_TOGGLE,
}

# Schema for action configuration
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
    {
        vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
        vol.Required(CONF_ENTITY_ID): cv.entity_domain("switch"),
    }
)


async def async_get_actions(
    hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
    """List device actions for a device."""
    actions = []
    entity_registry = er.async_get(hass)
    
    # Get all entities for this device
    entries = er.async_entries_for_device(entity_registry, device_id)
    
    for entry in entries:
        # Only include switch entities
        if entry.domain != "switch":
            continue
        
        actions.extend(
            [
                {
                    CONF_DEVICE_ID: device_id,
                    CONF_DOMAIN: DOMAIN,
                    CONF_ENTITY_ID: entry.entity_id,
                    CONF_TYPE: ACTION_TYPE_TURN_ON,
                },
                {
                    CONF_DEVICE_ID: device_id,
                    CONF_DOMAIN: DOMAIN,
                    CONF_ENTITY_ID: entry.entity_id,
                    CONF_TYPE: ACTION_TYPE_TURN_OFF,
                },
                {
                    CONF_DEVICE_ID: device_id,
                    CONF_DOMAIN: DOMAIN,
                    CONF_ENTITY_ID: entry.entity_id,
                    CONF_TYPE: ACTION_TYPE_TOGGLE,
                },
            ]
        )
    
    return actions


async def async_call_action_from_config(
    hass: HomeAssistant,
    config: ConfigType,
    variables: dict,
    context: Context | None,
) -> None:
    """Execute a device action."""
    action_type = config[CONF_TYPE]
    entity_id = config[CONF_ENTITY_ID]
    
    service_data = {CONF_ENTITY_ID: entity_id}
    
    if action_type == ACTION_TYPE_TURN_ON:
        service = "turn_on"
    elif action_type == ACTION_TYPE_TURN_OFF:
        service = "turn_off"
    else:  # ACTION_TYPE_TOGGLE
        service = "toggle"
    
    await hass.services.async_call(
        "switch",
        service,
        service_data,
        blocking=True,
        context=context,
    )


async def async_get_action_capabilities(
    hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
    """List action capabilities."""
    # Return additional fields that can be configured
    return {
        "extra_fields": vol.Schema(
            {
                # Add optional fields like brightness, color, etc.
            }
        )
    }

Advanced: Entity-Based Device Automation

For simple entity-based device automation, you can use the built-in helpers:
device_trigger.py
"""Simplified entity-based device triggers."""
from homeassistant.components.device_automation import toggle_entity

# For binary sensors with simple on/off triggers
async def async_get_triggers(hass, device_id):
    """List device triggers."""
    return await toggle_entity.async_get_triggers(hass, device_id, "binary_sensor")

async def async_attach_trigger(hass, config, action, trigger_info):
    """Attach a trigger."""
    return await toggle_entity.async_attach_trigger(
        hass, config, action, trigger_info
    )

Translations

Provide translations for device automation UI:
strings.json
{
  "device_automation": {
    "trigger_type": {
      "button_pressed": "Button pressed",
      "button_released": "Button released"
    },
    "condition_type": {
      "is_on": "{entity_name} is on",
      "is_off": "{entity_name} is off"
    },
    "action_type": {
      "turn_on": "Turn on {entity_name}",
      "turn_off": "Turn off {entity_name}",
      "toggle": "Toggle {entity_name}"
    }
  }
}

Testing Device Automation

Test your device automation implementation:
test_device_trigger.py
"""Test device triggers."""
import pytest
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.setup import async_setup_component


async def test_get_triggers(hass, device_registry, entity_registry):
    """Test we get the expected triggers."""
    # Create a test device
    config_entry = MockConfigEntry(domain="your_integration")
    device_entry = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        identifiers={("your_integration", "test-device")},
    )
    
    # Get triggers
    triggers = await async_get_device_automations(
        hass, DeviceAutomationType.TRIGGER, device_entry.id
    )
    
    # Verify triggers
    assert len(triggers) == 2
    assert triggers[0]["type"] == "button_pressed"

Best Practices

Use Clear Type Names

Choose descriptive trigger/condition/action types:
  • button_short_press instead of btn_sp
  • motion_detected instead of motion

Provide Good Translations

Translations should be clear and use entity names when available:
"is_on": "{entity_name} is on"

Handle Entity Registry

Always check if entities exist and handle missing entities gracefully. If a device supports multiple similar actions, group them logically.

Test Thoroughly

Write tests for all trigger types, conditions, and actions.

Common Patterns

Multi-Button Devices

For devices with multiple buttons:
CONF_SUBTYPE = "subtype"

TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
    {
        vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
        vol.Required(CONF_SUBTYPE): vol.In(["button_1", "button_2", "button_3"]),
    }
)

Brightness/Value Actions

For actions that accept values:
from homeassistant.components.light import ATTR_BRIGHTNESS

ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
    {
        vol.Required(CONF_TYPE): "set_brightness",
        vol.Required(ATTR_BRIGHTNESS): vol.All(
            vol.Coerce(int), vol.Range(min=0, max=255)
        ),
    }
)

Next Steps

Build docs developers (and LLMs) love