-
Notifications
You must be signed in to change notification settings - Fork 0
Development Guide
Chris Purcell edited this page Feb 7, 2026
·
2 revisions
Complete guide to developing Home Assistant integrations with this template.
Before writing code, answer these questions:
What does it integrate?
- Device (single device like a thermostat)
- Hub (gateway with multiple devices)
- Service (cloud API like weather service)
- Helper (utility integration like template)
What data does it provide?
- Sensors (read-only values)
- Binary sensors (on/off states)
- Switches (controllable on/off)
- Lights (controllable with brightness/color)
- Climate (thermostats)
- Other platforms?
How does it communicate?
- Local polling (fetch data periodically)
- Local push (device sends updates)
- Cloud polling (API requests)
- Cloud push (webhooks/websockets)
# Copy example integration
cp -r custom_components/example_integration custom_components/my_integration
# Update manifest.jsonmanifest.json required fields:
{
"domain": "my_integration",
"name": "My Integration",
"version": "1.0.0",
"codeowners": ["@yourusername"],
"config_flow": true,
"documentation": "https://github.com/yourusername/my-integration",
"integration_type": "device",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/yourusername/my-integration/issues",
"requirements": []
}"""Constants for My Integration."""
from typing import Final
DOMAIN: Final = "my_integration"
DEFAULT_SCAN_INTERVAL: Final[int] = 30 # seconds
CONF_API_KEY: Final = "api_key""""Config flow for My Integration."""
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
class MyIntegrationConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
# Validate user input here
info = await self._async_validate_input(user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
# Set unique ID
await self.async_set_unique_id(info["unique_id"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info["title"],
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
}),
errors=errors,
)
async def _async_validate_input(
self, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate user input."""
# Connect to device/service and validate
# Return dict with title and unique_id
return {"title": "My Device", "unique_id": "12345"}"""DataUpdateCoordinator for My Integration."""
from datetime import timedelta
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN, DEFAULT_SCAN_INTERVAL
class MyIntegrationCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""My Integration coordinator."""
def __init__(
self,
hass: HomeAssistant,
client: MyApiClient,
) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self.client = client
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from API endpoint."""
try:
return await self.client.async_get_data()
except AuthenticationError as err:
# Authentication failed
raise ConfigEntryAuthFailed from err
except ConnectionError as err:
# Connection failed (device offline, network issue)
raise UpdateFailed(f"Error communicating: {err}") from err"""The My Integration integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import MyIntegrationCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# Create API client
client = MyApiClient(entry.data)
# Create coordinator
coordinator = MyIntegrationCoordinator(hass, client)
# Fetch initial data
await coordinator.async_config_entry_first_refresh()
# Store coordinator
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
# Forward entry setup to platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# Unload platforms
unload_ok = await hass.config_entries.async_unload_platforms(
entry, PLATFORMS
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok"""Sensor platform for My Integration."""
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import MyIntegrationCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensor platform."""
coordinator: MyIntegrationCoordinator = hass.data[DOMAIN][entry.entry_id]
# Create sensor entities
async_add_entities([
MyTemperatureSensor(coordinator, "device_1"),
])
class MyTemperatureSensor(CoordinatorEntity[MyIntegrationCoordinator], SensorEntity):
"""Temperature sensor."""
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = "°C"
def __init__(
self,
coordinator: MyIntegrationCoordinator,
device_id: str,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator)
self._device_id = device_id
self._attr_unique_id = f"{DOMAIN}_{device_id}_temperature"
@property
def native_value(self) -> float | None:
"""Return sensor value."""
return self.coordinator.data[self._device_id].get("temperature")
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self._device_id in self.coordinator.data
)See Testing Guide for detailed testing instructions.
Minimum tests required (Bronze tier):
- Config flow tests
- Setup/unload tests
- Platform tests
# Lint
ruff check custom_components/my_integration/
# Format
ruff format custom_components/my_integration/
# Type check
mypy custom_components/my_integration/
# Test
pytest tests/ -v
# Coverage
pytest tests/ --cov=custom_components.my_integrationgit add custom_components/my_integration/
git add tests/
git commit -m "feat: add My Integration"- Use DataUpdateCoordinator for all polling
- Type hint everything - modern Python 3.14.2+ syntax
- Handle errors properly - ConfigEntryAuthFailed, UpdateFailed
- Provide unique IDs for all entities
- Write tests - minimum 90% coverage for Gold tier
- Use async - all I/O operations must be async
- Follow naming conventions - _attr_has_entity_name = True
- Document your code - docstrings for classes and methods
- Validate user input - in config flow
- Log appropriately - errors as errors, debug info as debug
- Block the event loop - no sync I/O in async functions
- Use YAML configuration - config flow only
- Poll in init.py - use coordinator instead
- Skip unique IDs - entities won't be editable
- Ignore errors - handle and report properly
- Use old type hints - no List[], Dict[], use list[], dict[]
- Mix sync and async - be consistent
- Forget availability - entities should show unavailable when offline
- Hardcode values - use constants
- Skip testing - tests are required for Bronze tier
Already configured in pyproject.toml:
- Targets Python 3.14.2
- Line length: 88 characters
- Enables pycodestyle, pyflakes, isort, comprehensions, bugbear, pyupgrade
Strict mode enabled in mypy.ini:
- All functions must have type hints
- No implicit optionals
- Strict equality checks
- Warning on unused ignores
Install with: pre-commit install
Runs automatically before each commit:
- Trailing whitespace removal
- End of file fixer
- YAML/JSON/TOML validation
- Ruff linting and formatting
- mypy type checking
See Quality Scale Guide for detailed tier requirements.
Quick reference:
- Bronze: Config flow, basic tests, linting passes
- Silver: Error handling, availability, troubleshooting docs
- Gold: Full async, 90%+ coverage, type hints
- Platinum: Code excellence, performance, maintainability
See Common Patterns for code examples of:
- OAuth2 authentication
- Polling strategies
- State restoration
- Error recovery
- Device grouping
Continue to:
- Testing Guide - Write comprehensive tests
- Quality Scale Guide - Achieve higher tiers
- Publishing Guide - Publish to HACS