| name | Home Assistant Integration knowledge |
| description | Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference. |
File Locations
- Integration code:
./homeassistant/components/<integration_domain>/
- Integration tests:
./tests/components/<integration_domain>/
Integration Templates
Standard Integration Structure
homeassistant/components/my_integration/
├── __init__.py # Entry point with async_setup_entry
├── manifest.json # Integration metadata and dependencies
├── const.py # Domain and constants
├── config_flow.py # UI configuration flow
├── coordinator.py # Data update coordinator (if needed)
├── entity.py # Base entity class (if shared patterns)
├── sensor.py # Sensor platform
├── strings.json # User-facing text and translations
├── services.yaml # Service definitions (if applicable)
└── quality_scale.yaml # Quality scale rule status
An integration can have platforms as needed (e.g., sensor.py, switch.py, etc.). The following platforms have extra guidelines:
Minimal Integration Checklist
Integration Quality Scale
Home Assistant uses an Integration Quality Scale to ensure code quality and consistency. The quality level determines which rules apply:
Quality Scale Levels
- Bronze: Basic requirements (ALL Bronze rules are mandatory)
- Silver: Enhanced functionality
- Gold: Advanced features
- Platinum: Highest quality standards
Quality Scale Progression
- Bronze → Silver: Add entity unavailability, parallel updates, auth flows
- Silver → Gold: Add device management, diagnostics, translations
- Gold → Platinum: Add strict typing, async dependencies, websession injection
How Rules Apply
- Check
manifest.json: Look for "quality_scale" key to determine integration level
- Bronze Rules: Always required for any integration with quality scale
- Higher Tier Rules: Only apply if integration targets that tier or higher
- Rule Status: Check
quality_scale.yaml in integration folder for:
done: Rule implemented
exempt: Rule doesn't apply (with reason in comment)
todo: Rule needs implementation
Example quality_scale.yaml Structure
rules:
config-flow: done
entity-unique-id: done
action-setup:
status: exempt
comment: Integration does not register custom actions.
entity-unavailable: done
parallel-updates: done
devices: done
diagnostics: done
strict-typing: done
When Reviewing/Creating Code: Always check the integration's quality scale level and exemption status before applying rules.
Code Organization
Core Locations
- Shared constants:
homeassistant/const.py (use these instead of hardcoding)
- Integration structure:
homeassistant/components/{domain}/const.py - Constants
homeassistant/components/{domain}/models.py - Data models
homeassistant/components/{domain}/coordinator.py - Update coordinator
homeassistant/components/{domain}/config_flow.py - Configuration flow
homeassistant/components/{domain}/{platform}.py - Platform implementations
Common Modules
- coordinator.py: Centralize data fetching logic
class MyCoordinator(DataUpdateCoordinator[MyData]):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(minutes=1),
config_entry=config_entry,
)
- entity.py: Base entity definitions to reduce duplication
class MyEntity(CoordinatorEntity[MyCoordinator]):
_attr_has_entity_name = True
Runtime Data Storage
Manifest Requirements
- Required Fields:
domain, name, codeowners, integration_type, documentation, requirements
- Integration Types:
device, hub, service, system, helper
- IoT Class: Always specify connectivity method (e.g.,
cloud_polling, local_polling, local_push)
- Discovery Methods: Add when applicable:
zeroconf, dhcp, bluetooth, ssdp, usb
- Dependencies: Include platform dependencies (e.g.,
application_credentials, bluetooth_adapters)
Config Flow Patterns
Integration Ownership
Async Dependencies (Platinum)
- Requirement: All dependencies must use asyncio
- Ensures efficient task handling without thread context switching
WebSession Injection (Platinum)
Data Update Coordinator
- Standard Pattern: Use for efficient data management
class MyCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(minutes=5),
config_entry=config_entry,
)
self.client = client
async def _async_update_data(self):
try:
return await self.client.fetch_data()
except ApiError as err:
raise UpdateFailed(f"API communication error: {err}")
- Error Types: Use
UpdateFailed for API errors, ConfigEntryAuthFailed for auth issues
- Config Entry: Always pass
config_entry parameter to coordinator - it's accepted and recommended
Integration Guidelines
Configuration Flow
- UI Setup Required: All integrations must support configuration via UI
- Manifest: Set
"config_flow": true in manifest.json
- Data Storage:
- Connection-critical config: Store in
ConfigEntry.data
- Non-critical settings: Store in
ConfigEntry.options
- Validation: Always validate user input before creating entries
- Config Entry Naming:
- ❌ Do NOT allow users to set config entry names in config flows
- Names are automatically generated or can be customized later in UI
- ✅ Exception: Helper integrations MAY allow custom names in config flow
- Connection Testing: Test device/service connection during config flow:
try:
await client.get_data()
except MyException:
errors["base"] = "cannot_connect"
- Duplicate Prevention: Prevent duplicate configurations:
await self.async_set_unique_id(identifier)
self._abort_if_unique_id_configured()
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
Reauthentication Support
Reconfiguration Flow
- Purpose: Allow configuration updates without removing device
- Implementation: Add
async_step_reconfigure method
- Validation: Prevent changing underlying account with
_abort_if_unique_id_mismatch
Device Discovery
- Manifest Configuration: Add discovery method (zeroconf, dhcp, etc.)
{
"zeroconf": ["_mydevice._tcp.local."]
}
- Discovery Handler: Implement appropriate
async_step_* method:
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
await self.async_set_unique_id(discovery_info.properties["serialno"])
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
- Network Updates: Use discovery to update dynamic IP addresses
Network Discovery Implementation
- Zeroconf/mDNS: Use async instances
aiozc = await zeroconf.async_get_async_instance(hass)
- SSDP Discovery: Register callbacks with cleanup
entry.async_on_unload(
ssdp.async_register_callback(
hass, _async_discovered_device,
{"st": "urn:schemas-upnp-org:device:ZonePlayer:1"}
)
)
Bluetooth Integration
- Manifest Dependencies: Add
bluetooth_adapters to dependencies
- Connectable: Set
"connectable": true for connection-required devices
- Scanner Usage: Always use shared scanner instance
scanner = bluetooth.async_get_scanner()
entry.async_on_unload(
bluetooth.async_register_callback(
hass, _async_discovered_device,
{"service_uuid": "example_uuid"},
bluetooth.BluetoothScanningMode.ACTIVE
)
)
- Connection Handling: Never reuse
BleakClient instances, use 10+ second timeouts
Setup Validation
- Test Before Setup: Verify integration can be set up in
async_setup_entry
- Exception Handling:
ConfigEntryNotReady: Device offline or temporary failure
ConfigEntryAuthFailed: Authentication issues
ConfigEntryError: Unresolvable setup problems
Config Entry Unloading
- Required: Implement
async_unload_entry for runtime removal/reload
- Platform Unloading: Use
hass.config_entries.async_unload_platforms
- Cleanup: Register callbacks with
entry.async_on_unload:
async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
entry.runtime_data.listener()
return unload_ok
Service Actions
- Registration: Register all service actions in
async_setup, NOT in async_setup_entry
- Validation: Check config entry existence and loaded state:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def service_action(call: ServiceCall) -> ServiceResponse:
if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])):
raise ServiceValidationError("Entry not found")
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError("Entry not loaded")
- Exception Handling: Raise appropriate exceptions:
if end_date < start_date:
raise ServiceValidationError("End date must be after start date")
try:
await client.set_schedule(start_date, end_date)
except MyConnectionError as err:
raise HomeAssistantError("Could not connect to the schedule") from err
Service Registration Patterns
- Entity Services: Register on platform setup
platform.async_register_entity_service(
"my_entity_service",
{vol.Required("parameter"): cv.string},
"handle_service_method"
)
- Service Schema: Always validate input
SERVICE_SCHEMA = vol.Schema({
vol.Required("entity_id"): cv.entity_ids,
vol.Required("parameter"): cv.string,
vol.Optional("timeout", default=30): cv.positive_int,
})
- Services File: Create
services.yaml with descriptions and field definitions
Polling
Entity Development
Unique IDs
Acceptable ID Sources:
- Device serial numbers
- MAC addresses (formatted using
format_mac from device registry)
- Physical identifiers (printed/EEPROM)
- Config entry ID as last resort:
f"{entry.entry_id}-battery"
Never Use:
- IP addresses, hostnames, URLs
- Device names
- Email addresses, usernames
Entity Descriptions
- Lambda/Anonymous Functions: Often used in EntityDescription for value transformation
- Multiline Lambdas: When lambdas exceed line length, wrap in parentheses for readability
- Bad pattern:
SensorEntityDescription(
key="temperature",
name="Temperature",
value_fn=lambda data: round(data["temp_value"] * 1.8 + 32, 1) if data.get("temp_value") is not None else None,
)
- Good pattern:
SensorEntityDescription(
key="temperature",
name="Temperature",
value_fn=lambda data: (
round(data["temp_value"] * 1.8 + 32, 1)
if data.get("temp_value") is not None
else None
),
)
Entity Naming
- Use has_entity_name: Set
_attr_has_entity_name = True
- For specific fields:
class MySensor(SensorEntity):
_attr_has_entity_name = True
def __init__(self, device: Device, field: str) -> None:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
name=device.name,
)
self._attr_name = field
- For device itself: Set
_attr_name = None
Event Lifecycle Management
State Handling
- Unknown values: Use
None (not "unknown" or "unavailable")
- Availability: Implement
available() property instead of using "unavailable" state
Entity Availability
- Mark Unavailable: When data cannot be fetched from device/service
- Coordinator Pattern:
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.identifier in self.coordinator.data
- Direct Update Pattern:
async def async_update(self) -> None:
"""Update entity."""
try:
data = await self.client.get_data()
except MyException:
self._attr_available = False
else:
self._attr_available = True
self._attr_native_value = data.value
Extra State Attributes
- All attribute keys must always be present
- Unknown values: Use
None
- Provide descriptive attributes
Device Management
Device Registry
- Create Devices: Group related entities under devices
- Device Info: Provide comprehensive metadata:
_attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, device.mac)},
identifiers={(DOMAIN, device.id)},
name=device.name,
manufacturer="My Company",
model="My Sensor",
sw_version=device.version,
)
- For services: Add
entry_type=DeviceEntryType.SERVICE
Dynamic Device Addition
- Auto-detect New Devices: After initial setup
- Implementation Pattern:
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities([MySensor(coordinator, device_id) for device_id in new_devices])
entry.async_on_unload(coordinator.async_add_listener(_check_device))
Stale Device Removal
Entity Categories
Device Classes
Disabled by Default
Entity Translations
Exception Translations (Gold)
- Translatable Errors: Use translation keys for user-facing exceptions
- Implementation:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="end_date_before_start_date",
)
- Add to
strings.json:
{
"exceptions": {
"end_date_before_start_date": {
"message": "The end date cannot be before the start date."
}
}
}
Icon Translations (Gold)
- Dynamic Icons: Support state and range-based icon selection
- State-based Icons:
{
"entity": {
"sensor": {
"tree_pollen": {
"default": "mdi:tree",
"state": {
"high": "mdi:tree-outline"
}
}
}
}
}
- Range-based Icons (for numeric values):
{
"entity": {
"sensor": {
"battery_level": {
"default": "mdi:battery-unknown",
"range": {
"0": "mdi:battery-outline",
"90": "mdi:battery-90",
"100": "mdi:battery"
}
}
}
}
}
Testing Requirements
- Location:
tests/components/{domain}/
- Coverage Requirement: Above 95% test coverage for all modules
- Best Practices:
- Use pytest fixtures from
tests.common
- Mock all external dependencies
- Use snapshots for complex data structures
- Follow existing test patterns
Config Flow Testing
- 100% Coverage Required: All config flow paths must be tested
- Patch Boundaries: Only patch library or client methods when testing config flows. Do not patch methods defined in
config_flow.py; exercise the flow logic end-to-end.
- Test Scenarios:
- All flow initiation methods (user, discovery, import)
- Successful configuration paths
- Error recovery scenarios
- Prevention of duplicate entries
- Flow completion after errors
- Reauthentication/reconfigure flows
Testing
Testing Best Practices
- Never access
hass.data directly - Use fixtures and proper integration setup instead
- Use snapshot testing - For verifying entity states and attributes
- Test through integration setup - Don't test entities in isolation
- Mock external APIs - Use fixtures with realistic JSON data
- Verify registries - Ensure entities are properly registered with devices
Config Flow Testing Template
async def test_user_flow_success(hass, mock_api):
"""Test successful user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=TEST_USER_INPUT
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "My Device"
assert result["data"] == TEST_USER_INPUT
async def test_flow_connection_error(hass, mock_api_error):
"""Test connection error handling."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=TEST_USER_INPUT
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
Entity Testing Patterns
@pytest.fixture
def platforms() -> list[Platform]:
"""Overridden fixture to specify platforms to test."""
return [Platform.SENSOR]
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the sensor entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "device_unique_id")}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
for entity_entry in entity_entries:
assert entity_entry.device_id == device_entry.id
Mock Patterns
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="My Integration",
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_API_KEY: "test_key"},
unique_id="device_unique_id",
)
@pytest.fixture
def mock_device_api() -> Generator[MagicMock]:
"""Return a mocked device API."""
with patch("homeassistant.components.my_integration.MyDeviceAPI", autospec=True) as api_mock:
api = api_mock.return_value
api.get_data.return_value = MyDeviceData.from_json(
load_fixture("device_data.json", DOMAIN)
)
yield api
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return PLATFORMS
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_device_api: MagicMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the integration for testing."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
Debugging & Troubleshooting
Common Issues & Solutions
- Integration won't load: Check
manifest.json syntax and required fields
- Entities not appearing: Verify
unique_id and has_entity_name implementation
- Config flow errors: Check
strings.json entries and error handling
- Discovery not working: Verify manifest discovery configuration and callbacks
- Tests failing: Check mock setup and async context
Debug Logging Setup
caplog.set_level(logging.DEBUG, logger="my_integration")
_LOGGER = logging.getLogger(__name__)
_LOGGER.debug("Processing data: %s", data)
Validation Commands
python -m script.hassfest --integration-path homeassistant/components/my_integration
pytest ./tests/components/my_integration \
--cov=homeassistant.components.my_integration \
--cov-report term-missing