Actions in integrations
Register actions that users call from automations, scripts, and dashboards, describe them in services.yaml, and return response data.
An action is something users can call from an automation, a script, or the Actions tab of the developer tools, like light.turn_on. Entity integrations, like light, provide the common actions. Your integration can register its own, for things that are specific to it, like sonos.set_sleep_timer.
This page is about adding actions to your integration. How the action registry works underneath, what a handler gets, and how to call actions from code, is on how core works: actions.
Add an action to your own integration first, rather than changing a shared one. When a pattern shows up across integrations, it can become a shared action later.
In code, actions are still called services: hass.services, ServiceCall, and services.yaml. It is the same thing.
Register actions in async_setup of your integration, not in async_setup_entry and not in a platform. That way they exist even when no config entry is loaded, so automations that use them can be edited and validated, and users get a clear error instead of an unknown action. That is a rule of the Bronze tier of the quality scale.
Since your integration now has an async_setup, it needs a CONFIG_SCHEMA. See integrations without YAML.
Target the thing the action acts on, at the right level. Do not use a higher or lower level, even if one can be resolved to the other.
- Entity: the action controls or needs a specific entity, like setting a sleep timer on a media player. Register an entity action.
- Device: the action needs a device as a whole, like rebooting it. Use a
device_idfield. Do not use an entity ID as a stand-in for its device. - Config entry: the action needs the integration instance, like creating something in the account at a cloud service. Use a
config_entry_idfield. Do not use a device or entity ID as a stand-in for its config entry.
When an action needs a target, make it required. Do not fall back to a default target: an automation that works today would silently do something else when the user adds a second device.
import probatio
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.typing import ConfigType
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
GET_FORECAST_SCHEMA = probatio.Schema(
{
probatio.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
probatio.Required("days"): cv.positive_int,
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Example integration."""
async def async_get_forecast(call: ServiceCall) -> ServiceResponse:
"""Return the forecast for the coming days."""
entry: ExampleConfigEntry = service.async_get_config_entry(
hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
)
forecast = await entry.runtime_data.client.get_forecast(
call.data["days"]
)
return {"forecast": [day.as_dict() for day in forecast]}
hass.services.async_register(
DOMAIN,
"get_forecast",
async_get_forecast,
schema=GET_FORECAST_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
return Trueservice.async_get_config_entry checks that the config entry exists, belongs to your integration, and is loaded. If not, it raises a ServiceValidationError with a translated message. For device targets, service.async_get_device_and_config_entry(hass, DOMAIN, device_id) does the same for a device and the config entry that owns it.
Entity actions run on the entities the user targets. Users can target them by entity, device, area, floor, or label, and Home Assistant resolves that to your entities. Register them with async_register_platform_entity_service, under your own domain (like sonos), not under the entity domain (like media_player):
import probatio
from homeassistant.components.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN,
)
from homeassistant.helpers import config_validation as cv, service
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Sonos integration."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
"set_sleep_timer",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={probatio.Required("sleep_time"): cv.positive_int},
func="set_sleep_timer",
)
return TrueThis calls the set_sleep_timer method of every targeted entity with sleep_time as keyword argument. Instead of a method name, func can be an async function that receives the entity and the ServiceCall. The schema can be a dictionary, which is turned into an entity action schema for you. To only allow entities with certain features, pass required_features.
Actions that can change configuration, or have other security implications, should only be available to administrators. Register them with async_register_admin_service from homeassistant.helpers.service, instead of hass.services.async_register. It takes hass first, then the same arguments, and checks that the user of the call is an admin before your handler runs.
When an action fails, raise an exception, so the user sees what went wrong: ServiceValidationError when the user did something wrong, HomeAssistantError for anything else. Use translated messages. See exceptions in actions.
Actions can return data, for automations that need more than the state of an entity, like a list of calendar events or search results. Do not use it for data that fits entity state: a temperature is a sensor, not a response.
Response data is a dict that can be serialized to JSON. Pass supports_response when you register the action:
| Value | Meaning |
|---|---|
SupportsResponse.NONE |
The action returns nothing. The default. |
SupportsResponse.OPTIONAL |
The action does something, and can also return data. Check call.return_response to see if the caller wants it, and return None if not. |
SupportsResponse.ONLY |
The action does nothing but return data |
Errors are always exceptions, never error codes in the response. Users should not need error handling in their automations.
Users only use actions they can find. Describe every action in services.yaml, so the UI can show the right fields and selectors. The key is the name of the action, without your domain:
set_speed:
target:
entity:
domain: fan
supported_features:
- fan.FanEntityFeature.SET_SPEED
fields:
speed:
required: true
example: "low"
selector:
select:
translation_key: "fan_speed"
options:
- "off"
- "low"
- "medium"
- "high"
advanced_options:
collapsed: true
fields:
speed_pct:
selector:
number:
min: 0
max: 100target: for actions that take entities. It lets users pick entities, devices, and areas. Without a filter it offers every entity, so narrow it with theentityselector. Hassfest refuses adevicefilter on a target: use a device selector in a field for that. Withtarget, do not add anentity_idfield.supported_features: only offer entities that support at least one of these features. To require several features together, put them in a nested list.fields: one entry per field, withrequired, anexample, adefault, and aselectorthat decides the input in the UI.- Sections: a key with
collapsedand its ownfieldsgroups fields in a collapsible section. It only changes how the fields look: the data is not nested, so the action above gets{"speed_pct": 50}.
A filter on a field shows it only when at least one selected entity supports it. Filters only work in actions with a target, because they look at the selected entities: hassfest refuses them in other actions. Use either supported_features or attribute, not both:
temperature:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE
color_temp:
filter:
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HSWith an attribute filter, the field is shown when the attribute of a selected entity has one of the listed values. If the attribute is a list, one matching item is enough.
The name and description of each action and each field are not in services.yaml: they are in strings.json, under services, and every action and field needs one. Keep URLs out of translations with description placeholders, which you pass when you register the action:
hass.services.async_register(
DOMAIN,
"get_forecast",
async_get_forecast,
schema=GET_FORECAST_SCHEMA,
supports_response=SupportsResponse.ONLY,
description_placeholders={"docs_url": "https://example.com/forecast"},
)See translations for the format, and make your strings translatable.
Give each action an icon in icons.json, under services. Sections can have an icon too:
{
"services": {
"start_brewing": {
"service": "mdi:flask",
"sections": {
"advanced_options": "mdi:test-tube"
}
}
}
}Open the Actions tab of the developer tools, pick your action, fill in the fields, and select Perform action.