Exception Hierarchy
Home Assistant defines specialized exceptions inhomeassistant/exceptions.py:
Base Exception
from homeassistant.exceptions import HomeAssistantError
class HomeAssistantError(Exception):
"""General Home Assistant exception occurred."""
HomeAssistantError.
Integration Exceptions
ConfigEntryNotReady
Raise when setup fails due to temporary conditions:from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
try:
client = await create_client(entry.data["host"])
await client.connect()
except ConnectionError as err:
# Temporary failure - Home Assistant will retry
raise ConfigEntryNotReady(
f"Unable to connect to {entry.data['host']}"
) from err
# Setup successful
return True
ConfigEntryAuthFailed
Raise when authentication fails:from homeassistant.exceptions import ConfigEntryAuthFailed
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
try:
client = await authenticate(
entry.data["username"],
entry.data["password"]
)
except AuthenticationError as err:
# Invalid credentials - user must reauthenticate
raise ConfigEntryAuthFailed(
"Invalid credentials, please reauthenticate"
) from err
return True
ConfigEntryError
Raise for permanent configuration errors:from homeassistant.exceptions import ConfigEntryError
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
if not entry.data.get("api_key"):
# Permanent configuration error
raise ConfigEntryError(
"API key is required but not configured"
)
try:
client = APIClient(entry.data["api_key"])
info = await client.get_account_info()
except InvalidAPIKeyError as err:
# Invalid API key - can't be fixed by retrying
raise ConfigEntryError(
"API key is invalid or has been revoked"
) from err
return True
Service Exceptions
ServiceValidationError
Raise when service call parameters are invalid:from homeassistant.exceptions import ServiceValidationError
async def async_set_temperature(self, temperature: float) -> None:
"""Set target temperature."""
if not self.min_temp <= temperature <= self.max_temp:
raise ServiceValidationError(
f"Temperature {temperature} is outside valid range "
f"{self.min_temp}-{self.max_temp}",
translation_domain="my_integration",
translation_key="invalid_temperature",
translation_placeholders={
"temperature": str(temperature),
"min_temp": str(self.min_temp),
"max_temp": str(self.max_temp),
},
)
await self.device.set_temperature(temperature)
Translation Support
Exceptions can include translations for better user experience:from homeassistant.exceptions import HomeAssistantError
raise HomeAssistantError(
translation_domain="my_integration",
translation_key="connection_failed",
translation_placeholders={
"device_name": device.name,
"error": str(err),
},
)
strings.json:
{
"exceptions": {
"connection_failed": {
"message": "Failed to connect to {device_name}: {error}"
}
}
}
Error Handling Patterns
Setup Error Handling
Handle errors during integration setup:from homeassistant.exceptions import ConfigEntryNotReady, ConfigEntryAuthFailed
import asyncio
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
try:
# Test connection with timeout
async with asyncio.timeout(10):
client = await create_client(entry.data)
await client.connect()
except asyncio.TimeoutError as err:
# Timeout - temporary issue
raise ConfigEntryNotReady(
"Timeout connecting to device"
) from err
except AuthenticationError as err:
# Authentication failed - need user action
raise ConfigEntryAuthFailed(
"Authentication failed, please reauthenticate"
) from err
except ConnectionRefusedError as err:
# Device not ready - temporary issue
raise ConfigEntryNotReady(
"Device refused connection, may be starting up"
) from err
except InvalidConfigError as err:
# Configuration is wrong - permanent error
raise ConfigEntryError(
"Invalid configuration"
) from err
# Store client for later use
entry.runtime_data = client
# Forward setup to platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
Coordinator Error Handling
Handle errors in data update coordinators:from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.exceptions import ConfigEntryAuthFailed
import logging
_LOGGER = logging.getLogger(__name__)
class MyCoordinator(DataUpdateCoordinator):
"""Coordinator for fetching data."""
async def _async_update_data(self):
"""Fetch data from API."""
try:
return await self.client.fetch_data()
except AuthenticationError as err:
# Authentication failed - trigger reauthentication
raise ConfigEntryAuthFailed(
"Token expired, please reauthenticate"
) from err
except ConnectionError as err:
# Temporary network issue
raise UpdateFailed(
f"Failed to fetch data: {err}"
) from err
except APIError as err:
# Log the error but don't fail completely
_LOGGER.warning("API error: %s", err)
# Return previous data if available
return self.data
Entity Error Handling
Handle errors in entity methods:from homeassistant.exceptions import ServiceValidationError, HomeAssistantError
from homeassistant.helpers.entity import Entity
import logging
_LOGGER = logging.getLogger(__name__)
class MyEntity(Entity):
"""Example entity with error handling."""
async def async_turn_on(self, **kwargs):
"""Turn on the device."""
try:
await self.device.turn_on()
except DeviceOfflineError as err:
# Device is offline - mark unavailable
self._attr_available = False
raise HomeAssistantError(
f"Device {self.name} is offline"
) from err
except InvalidCommandError as err:
# Invalid command - user error
raise ServiceValidationError(
f"Invalid command: {err}",
translation_domain=DOMAIN,
translation_key="invalid_command",
) from err
except Exception as err:
# Unexpected error - log and raise
_LOGGER.exception("Unexpected error turning on %s", self.name)
raise HomeAssistantError(
f"Failed to turn on {self.name}"
) from err
# Success - update state
self._attr_is_on = True
self.async_write_ha_state()
API Client Error Handling
Handle errors in API clients:from aiohttp import ClientError, ClientResponseError
import asyncio
import logging
_LOGGER = logging.getLogger(__name__)
class MyAPIClient:
"""API client with error handling."""
async def fetch_data(self, endpoint: str) -> dict:
"""Fetch data from API endpoint."""
try:
async with asyncio.timeout(30):
response = await self.session.get(f"{self.base_url}/{endpoint}")
response.raise_for_status()
return await response.json()
except asyncio.TimeoutError as err:
_LOGGER.warning("Timeout fetching %s", endpoint)
raise ConnectionError("Request timed out") from err
except ClientResponseError as err:
if err.status == 401:
raise AuthenticationError("Unauthorized") from err
elif err.status == 404:
raise ValueError(f"Endpoint {endpoint} not found") from err
elif err.status >= 500:
raise ConnectionError(f"Server error: {err.status}") from err
else:
raise ValueError(f"HTTP error {err.status}") from err
except ClientError as err:
_LOGGER.warning("Client error: %s", err)
raise ConnectionError("Network error") from err
Logging Best Practices
Use appropriate log levels:import logging
_LOGGER = logging.getLogger(__name__)
# DEBUG: Detailed diagnostic information
_LOGGER.debug("Fetching data from %s", url)
# INFO: General informational messages
_LOGGER.info("Successfully connected to device %s", device_name)
# WARNING: Something unexpected but recoverable
_LOGGER.warning("Device %s is offline, will retry", device_name)
# ERROR: Error occurred but integration continues
_LOGGER.error("Failed to update sensor %s: %s", sensor_name, err)
# EXCEPTION: Error with full traceback
try:
await some_operation()
except Exception:
_LOGGER.exception("Unexpected error in some_operation")
# Don't log sensitive information
_LOGGER.debug("Authenticated with token: %s", token[:8] + "...")
Testing Error Handling
Test error scenarios:import pytest
from homeassistant.exceptions import ConfigEntryNotReady, ConfigEntryAuthFailed
async def test_setup_connection_error(hass, config_entry):
"""Test setup with connection error."""
with patch(
"custom_components.my_integration.create_client",
side_effect=ConnectionError("Network error"),
):
with pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, config_entry)
async def test_setup_auth_error(hass, config_entry):
"""Test setup with authentication error."""
with patch(
"custom_components.my_integration.create_client",
side_effect=AuthenticationError("Invalid token"),
):
with pytest.raises(ConfigEntryAuthFailed):
await async_setup_entry(hass, config_entry)
Common Patterns
Retry Logic
import asyncio
async def async_retry_operation(operation, max_attempts=3):
"""Retry operation with exponential backoff."""
for attempt in range(max_attempts):
try:
return await operation()
except TemporaryError as err:
if attempt == max_attempts - 1:
raise
delay = 2 ** attempt
_LOGGER.debug("Retry %s/%s after %ss", attempt + 1, max_attempts, delay)
await asyncio.sleep(delay)
Graceful Degradation
async def async_update(self):
"""Update entity state."""
try:
# Try to get full data
data = await self.coordinator.client.get_full_data()
self._attr_extra_state_attributes = data.attributes
except FeatureNotSupportedError:
# Fall back to basic data
_LOGGER.debug("Full data not supported, using basic data")
data = await self.coordinator.client.get_basic_data()
self._attr_state = data.state
self.async_write_ha_state()
Context Managers
from contextlib import asynccontextmanager
@asynccontextmanager
async def handle_device_errors(device_name: str):
"""Context manager for device operations."""
try:
yield
except DeviceOfflineError as err:
_LOGGER.warning("Device %s is offline", device_name)
raise HomeAssistantError(f"{device_name} is offline") from err
except Exception as err:
_LOGGER.exception("Error with device %s", device_name)
raise HomeAssistantError(f"Error with {device_name}") from err
# Usage
async with handle_device_errors(self.name):
await self.device.some_operation()