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.
Config entries are the modern way to configure integrations in Home Assistant. They provide a user-friendly UI for setting up and managing integrations, replacing the older YAML-based configuration.
Overview
A config entry consists of:
- Config Flow: Handles the setup process through the UI
- Config Entry: Stores the configuration data
- Options Flow: Allows users to modify settings after setup
Config Entry Basics
The ConfigEntry Object
A ConfigEntry object stores configuration data and has several key properties:
class ConfigEntry:
"""Hold a configuration entry."""
entry_id: str # Unique identifier
domain: str # Integration domain
title: str # Display name
data: MappingProxyType[str, Any] # Configuration data
options: MappingProxyType[str, Any] # Optional settings
state: ConfigEntryState # Current state
source: str # How it was created
unique_id: str | None # Unique identifier for the device
Config Entry States
Config entries have different states:
class ConfigEntryState(Enum):
"""Config entry state."""
LOADED = "loaded" # Successfully set up
SETUP_ERROR = "setup_error" # Setup failed
SETUP_RETRY = "setup_retry" # Will retry setup
NOT_LOADED = "not_loaded" # Not yet loaded
FAILED_UNLOAD = "failed_unload" # Unload failed
SETUP_IN_PROGRESS = "setup_in_progress"
Implementing a Config Flow
The config flow is the UI wizard that guides users through setting up your integration.
Basic Config Flow
"""Config flow for Your Integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN
class YourIntegrationConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Your Integration."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
# Validate the user input
info = await self._async_validate_input(user_input)
# Set a unique ID to prevent duplicate entries
await self.async_set_unique_id(info["serial_number"])
self._abort_if_unique_id_configured()
# Create the config entry
return self.async_create_entry(
title=info["title"],
data=user_input,
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
# Show the form to the user
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}),
errors=errors,
)
async def _async_validate_input(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Validate the user input allows us to connect."""
# Initialize your API client
# client = YourAPIClient(
# user_input[CONF_HOST],
# user_input[CONF_USERNAME],
# user_input[CONF_PASSWORD],
# )
#
# # Test the connection
# await client.authenticate()
# info = await client.get_info()
return {
"title": "Your Device Name",
"serial_number": "unique_device_id",
}
Config Flow Steps
Each step in the config flow is a method named async_step_<step_name>. Common steps include:
async_step_user: Initial user-initiated setup
async_step_import: Import from YAML configuration
async_step_discovery: Handle discovered devices
async_step_zeroconf: Handle Zeroconf/mDNS discovery
async_step_bluetooth: Handle Bluetooth discovery
Multi-Step Config Flows
For complex setups, you can chain multiple steps:
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
}),
)
# Store the host for use in the next step
self.context["host"] = user_input[CONF_HOST]
# Move to authentication step
return await self.async_step_auth()
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the authentication step."""
if user_input is None:
return self.async_show_form(
step_id="auth",
data_schema=vol.Schema({
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}),
)
# Combine data from both steps
data = {
CONF_HOST: self.context["host"],
**user_input,
}
return self.async_create_entry(title="Device", data=data)
Discovery Flows
Integrations can be automatically discovered through various methods:
Zeroconf Discovery
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> FlowResult:
"""Handle zeroconf discovery."""
# Extract information from discovery
host = discovery_info.host
properties = discovery_info.properties
# Set unique ID to prevent duplicate entries
await self.async_set_unique_id(properties["serial"])
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Store discovery info and ask user to confirm
self.context["title_placeholders"] = {
"name": properties.get("name", host)
}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={CONF_HOST: self.context["host"]},
)
return self.async_show_form(step_id="discovery_confirm")
Declare Zeroconf discovery in your manifest:
{
"zeroconf": [
{
"type": "_your-service._tcp.local.",
"name": "your-device*"
}
]
}
Bluetooth Discovery
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle bluetooth discovery."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
# Store the address for later use
self.context["address"] = discovery_info.address
self.context["title_placeholders"] = {
"name": discovery_info.name or discovery_info.address
}
return await self.async_step_bluetooth_confirm()
Options Flow
Options flows allow users to modify integration settings after initial setup:
class YourIntegrationConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> YourIntegrationOptionsFlow:
"""Get the options flow for this handler."""
return YourIntegrationOptionsFlow(config_entry)
class YourIntegrationOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Optional(
"scan_interval",
default=self.config_entry.options.get("scan_interval", 60),
): int,
}),
)
Handling Config Entry Updates
You can listen for config entry changes:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# Set up your integration...
# Register update listener
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
# Reload the config entry when options change
await hass.config_entries.async_reload(entry.entry_id)
Reauthentication Flow
If credentials expire, trigger a reauth flow:
# In your integration code when auth fails:
await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
},
data=entry.data,
)
# In your config flow:
async def async_step_reauth(
self, user_input: dict[str, Any]
) -> FlowResult:
"""Handle reauth upon an API authentication error."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm reauth dialog."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({
vol.Required(CONF_PASSWORD): str,
}),
)
# Update the entry with new credentials
self.hass.config_entries.async_update_entry(
self.entry,
data={**self.entry.data, **user_input},
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
Allow users to reconfigure an existing entry:
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reconfiguration."""
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
if user_input is not None:
self.hass.config_entries.async_update_entry(
entry,
data={**entry.data, **user_input},
)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reconfigure_successful")
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=entry.data[CONF_HOST]): str,
}),
)
Translations
Provide translations for your config flow in strings.json:
{
"config": {
"step": {
"user": {
"title": "Connect to Your Device",
"description": "Enter the connection details",
"data": {
"host": "Host",
"username": "Username",
"password": "Password"
}
}
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
}
},
"options": {
"step": {
"init": {
"title": "Options",
"data": {
"scan_interval": "Update interval (seconds)"
}
}
}
}
}
Best Practices
Use Unique IDs
Always set a unique ID to prevent duplicate entries:
await self.async_set_unique_id(device_serial)
self._abort_if_unique_id_configured()
Validate Early
Validate user input as soon as possible to provide quick feedback.
Handle Errors Gracefully
Provide clear error messages for common failure scenarios.
Update Existing Entries
When rediscovering a device, update the existing entry:
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info.host}
)
Store Minimal Data
Only store essential configuration in data. Use runtime_data for temporary data.
Common Patterns
Single Config Entry
Prevent multiple config entries:
async def async_step_user(self, user_input=None):
"""Handle user step."""
# Check if already configured
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
# Continue with setup...
Or use in manifest:
{
"single_config_entry": true
}
Selectors
Use modern selectors for better UX:
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
data_schema=vol.Schema({
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
})
Next Steps