Documentation Index
Fetch the complete documentation index at: https://mintlify.com/home-assistant/core/llms.txt
Use this file to discover all available pages before exploring further.
Services are the primary way to trigger actions in Home Assistant. Understanding how to register and handle services properly is essential for creating integrations that users can control through automations, scripts, and the UI.
Service Registry
The ServiceRegistry manages all available services in Home Assistant:
from homeassistant.core import HomeAssistant, ServiceCall
def example(hass: HomeAssistant):
# Access service registry
service_registry = hass.services
# Check if service exists
if service_registry.has_service("light", "turn_on"):
# Call service
await service_registry.async_call(
"light",
"turn_on",
{"entity_id": "light.living_room"}
)
Reference: homeassistant/core.py:2477
Registering Services
Basic Service Registration
from homeassistant.core import HomeAssistant, ServiceCall, callback
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
DOMAIN = "my_integration"
# Define service schema for validation
SERVICE_MY_ACTION_SCHEMA = vol.Schema({
vol.Required("target"): cv.string,
vol.Optional("intensity", default=50): vol.All(
vol.Coerce(int),
vol.Range(min=0, max=100)
),
})
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the integration."""
async def handle_my_action(call: ServiceCall) -> None:
"""Handle the service call."""
target = call.data["target"]
intensity = call.data["intensity"]
_LOGGER.info(f"Executing action on {target} with intensity {intensity}")
# Perform action
await perform_action(target, intensity)
# Register the service
hass.services.async_register(
DOMAIN,
"my_action",
handle_my_action,
schema=SERVICE_MY_ACTION_SCHEMA
)
return True
Reference: homeassistant/core.py:2570
Service with Response Data
Services can return data to callers:
from homeassistant.core import SupportsResponse, ServiceResponse
import voluptuous as vol
SERVICE_GET_INFO_SCHEMA = vol.Schema({
vol.Required("device_id"): cv.string,
})
async def handle_get_info(call: ServiceCall) -> ServiceResponse:
"""Handle service that returns data."""
device_id = call.data["device_id"]
# Fetch device information
device_info = await fetch_device_info(device_id)
# Return response data
return {
"device_id": device_id,
"name": device_info["name"],
"status": device_info["status"],
"temperature": device_info["temp"],
}
hass.services.async_register(
DOMAIN,
"get_info",
handle_get_info,
schema=SERVICE_GET_INFO_SCHEMA,
supports_response=SupportsResponse.ONLY # Requires return_response=True
)
Reference: homeassistant/core.py:2533
Service Response Types
from homeassistant.core import SupportsResponse
# NONE - Service does not return data (default)
supports_response=SupportsResponse.NONE
# OPTIONAL - Service can return data if requested
supports_response=SupportsResponse.OPTIONAL
# ONLY - Service must be called with return_response=True
supports_response=SupportsResponse.ONLY
Service Handler Types
Callback Handler
For synchronous operations:
from homeassistant.core import callback, HassJobType
@callback
def sync_service_handler(call: ServiceCall) -> None:
"""Handle service synchronously."""
# Fast, synchronous work only
state = hass.states.get(call.data["entity_id"])
_LOGGER.info(f"Current state: {state.state}")
hass.services.async_register(
DOMAIN,
"sync_action",
sync_service_handler,
job_type=HassJobType.Callback # Optional: pre-specify job type
)
Reference: homeassistant/core.py:287
Async Handler
For async operations (most common):
async def async_service_handler(call: ServiceCall) -> None:
"""Handle service asynchronously."""
# Can perform async operations
device_id = call.data["device_id"]
await control_device(device_id)
await asyncio.sleep(1)
_LOGGER.info("Action completed")
hass.services.async_register(
DOMAIN,
"async_action",
async_service_handler
)
Executor Handler
For blocking operations:
import time
def blocking_service_handler(call: ServiceCall) -> None:
"""Handle service with blocking code."""
# This will automatically run in executor
time.sleep(5) # Blocking is OK here
# Perform blocking I/O
with open("/path/to/file") as f:
data = f.read()
hass.services.async_register(
DOMAIN,
"blocking_action",
blocking_service_handler
# HassJobType.Executor detected automatically
)
Reference: homeassistant/core.py:347
Service Call Object
ServiceCall Properties
def handle_service(call: ServiceCall) -> None:
"""Handle service call."""
# Service identification
domain: str = call.domain # "my_integration"
service: str = call.service # "my_action"
# Service data (validated by schema)
data: dict = call.data
entity_id = call.data.get("entity_id")
# Context (who triggered it)
context: Context = call.context
user_id = call.context.user_id
# Return response indicator
return_response: bool = call.return_response
Calling Services
Basic Service Call
# Call service and wait for completion
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.living_room", "brightness": 255},
blocking=True # Wait for completion
)
Reference: homeassistant/core.py:2712
Fire and Forget
# Call service without waiting
await hass.services.async_call(
"notify",
"send_message",
{"message": "Hello"},
blocking=False # Don't wait (default)
)
Service Call with Response
# Call service and get response data
response = await hass.services.async_call(
"my_integration",
"get_info",
{"device_id": "abc123"},
blocking=True,
return_response=True
)
if response:
device_name = response["name"]
device_status = response["status"]
Reference: homeassistant/core.py:2752
Service Call with Target
Target allows specifying entities, devices, or areas:
# Target specific entities
await hass.services.async_call(
"light",
"turn_on",
service_data={"brightness": 200},
target={
"entity_id": ["light.living_room", "light.bedroom"]
}
)
# Target devices
await hass.services.async_call(
"homeassistant",
"reload_config_entry",
target={
"device_id": ["device_abc", "device_xyz"]
}
)
# Target areas
await hass.services.async_call(
"light",
"turn_off",
target={
"area_id": "living_room"
}
)
Entity Services
Entity services automatically route calls to entity methods:
from homeassistant.helpers.service import async_register_entity_service
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
# Define service schema
SERVICE_SET_SPEED_SCHEMA = vol.Schema({
vol.Required("speed"): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=10)
)
})
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up platform with entity service."""
# Register service that calls entity method
await async_register_entity_service(
hass,
DOMAIN,
"set_speed",
SERVICE_SET_SPEED_SCHEMA,
"async_set_speed", # Method name on entity
)
class MyEntity(Entity):
"""Entity with service handler."""
async def async_set_speed(self, speed: int) -> None:
"""Handle set_speed service call."""
self._speed = speed
await self.async_update_ha_state()
Reference: homeassistant/helpers/service.py
Service Validation
Schema Validation
Use voluptuous schemas for robust validation:
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
SERVICE_SCHEMA = vol.Schema({
vol.Required("entity_id"): cv.entity_id,
vol.Optional("temperature"): vol.All(
vol.Coerce(float),
vol.Range(min=10.0, max=30.0)
),
vol.Optional("mode"): vol.In(["auto", "cool", "heat"]),
vol.Optional("duration", default=3600): cv.positive_int,
})
Common Validation Helpers
from homeassistant.helpers import config_validation as cv
# Entity validation
entity_id = cv.entity_id # Validates format
entity_ids = cv.entity_ids # List of entity IDs
entity_domain = cv.entity_domain("light") # Must be in domain
# String validation
string = cv.string # Basic string
slug = cv.slug # Lowercase alphanumeric + underscore
template = cv.template # Template string
# Numeric validation
positive_int = cv.positive_int # > 0
port = cv.port # 1-65535
percentage = cv.percentage # 0-100
# Time validation
time_period = cv.time_period # timedelta
time = cv.time # time object
# Boolean
boolean = cv.boolean # Flexible bool parsing
Service Events
Listening for Service Calls
from homeassistant.const import EVENT_CALL_SERVICE
@callback
def handle_service_call(event: Event) -> None:
"""Monitor all service calls."""
domain = event.data["domain"]
service = event.data["service"]
service_data = event.data["service_data"]
_LOGGER.debug(f"Service called: {domain}.{service}")
hass.bus.async_listen(EVENT_CALL_SERVICE, handle_service_call)
Service Registration Events
from homeassistant.const import (
EVENT_SERVICE_REGISTERED,
EVENT_SERVICE_REMOVED
)
@callback
def handle_service_registered(event: Event) -> None:
"""Handle new service registration."""
domain = event.data["domain"]
service = event.data["service"]
_LOGGER.info(f"Service registered: {domain}.{service}")
hass.bus.async_listen(EVENT_SERVICE_REGISTERED, handle_service_registered)
hass.bus.async_listen(EVENT_SERVICE_REMOVED, handle_service_removed)
Reference: homeassistant/core.py:2644
Removing Services
# Remove service when no longer needed
hass.services.async_remove(DOMAIN, "my_action")
# In cleanup/unload
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# Remove services
hass.services.async_remove(DOMAIN, "my_action")
hass.services.async_remove(DOMAIN, "another_action")
return True
Reference: homeassistant/core.py:2655
Advanced Patterns
Service with Context Propagation
async def handle_complex_action(call: ServiceCall) -> None:
"""Handle service with context propagation."""
# Propagate context to state changes
hass.states.async_set(
"sensor.triggered_by_service",
"active",
{},
context=call.context # Propagate context
)
# Call another service with same context
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.related"},
context=call.context # Chain context
)
Dynamic Service Registration
class DynamicServiceManager:
"""Manage services dynamically."""
def __init__(self, hass: HomeAssistant):
self._hass = hass
self._registered_services: set[str] = set()
async def register_device_service(self, device_id: str):
"""Register service for specific device."""
service_name = f"control_{device_id}"
if service_name in self._registered_services:
return
async def handle_device_service(call: ServiceCall):
await self.control_device(device_id, call.data)
self._hass.services.async_register(
DOMAIN,
service_name,
handle_device_service
)
self._registered_services.add(service_name)
async def cleanup(self):
"""Remove all registered services."""
for service_name in self._registered_services:
self._hass.services.async_remove(DOMAIN, service_name)
self._registered_services.clear()
Service with Multiple Response Types
from homeassistant.core import SupportsResponse
async def flexible_service(call: ServiceCall) -> ServiceResponse:
"""Service that optionally returns data."""
device_id = call.data["device_id"]
device_info = await get_device_info(device_id)
# Always perform action
await control_device(device_id, call.data.get("action"))
# Only return data if requested
if call.return_response:
return {
"device_id": device_id,
"status": device_info["status"],
"timestamp": dt_util.utcnow().isoformat()
}
return None
hass.services.async_register(
DOMAIN,
"flexible_action",
flexible_service,
supports_response=SupportsResponse.OPTIONAL
)
Best Practices
# GOOD - Schema validation
SERVICE_SCHEMA = vol.Schema({
vol.Required("target"): cv.string,
})
hass.services.async_register(
DOMAIN, "action", handler, schema=SERVICE_SCHEMA
)
# BAD - No validation
hass.services.async_register(
DOMAIN, "action", handler # Missing schema
)
2. Use Appropriate Handler Types
# GOOD - Callback for fast sync work
@callback
def quick_handler(call: ServiceCall):
state = hass.states.get(call.data["entity_id"])
# GOOD - Async for I/O
async def async_handler(call: ServiceCall):
await external_api_call()
# BAD - Async without awaiting
async def wasteful_handler(call: ServiceCall):
state = hass.states.get(call.data["entity_id"]) # Wasteful
3. Handle Errors Gracefully
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
async def safe_handler(call: ServiceCall) -> None:
"""Service handler with error handling."""
try:
device_id = call.data["device_id"]
result = await control_device(device_id)
if not result:
raise ServiceValidationError("Device control failed")
except KeyError as err:
raise ServiceValidationError(f"Missing required field: {err}")
except asyncio.TimeoutError:
raise HomeAssistantError("Device communication timeout")
except Exception:
_LOGGER.exception("Unexpected error in service")
raise
4. Document Services
Create services.yaml in your integration:
my_action:
name: My Action
description: Performs a custom action on the device.
fields:
target:
name: Target
description: Target device identifier.
required: true
example: "device_123"
selector:
text:
intensity:
name: Intensity
description: Action intensity level.
required: false
default: 50
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
5. Clean Up on Unload
async def async_unload_entry(
hass: HomeAssistant,
entry: ConfigEntry
) -> bool:
"""Unload a config entry."""
# Remove all services
for service in ["action1", "action2", "action3"]:
hass.services.async_remove(DOMAIN, service)
return True
Common Pitfalls
- Don’t forget schema validation (security and UX)
- Don’t block the event loop in service handlers
- Always handle missing entities gracefully
- Clean up services when integration unloads
- Use context propagation for state changes
- Document all services in services.yaml
- Use @callback for fast handlers - Avoid task creation overhead
- Batch operations - Group multiple entity updates
- Validate early - Reject invalid calls quickly
- Use entity services - More efficient for entity operations
- Limit blocking operations - Use executor for I/O