Skip to content

Development Guide

Chris Purcell edited this page Feb 7, 2026 · 2 revisions

Development Guide

Complete guide to developing Home Assistant integrations with this template.

Development Workflow

Step 1: Plan Your Integration

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)

Step 2: Create Integration Structure

# Copy example integration
cp -r custom_components/example_integration custom_components/my_integration

# Update manifest.json

manifest.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": []
}

Step 3: Implement Core Components

3.1 Constants (const.py)

"""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"

3.2 Config Flow (config_flow.py)

"""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"}

3.3 DataUpdateCoordinator (coordinator.py)

"""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

3.4 Integration Setup (init.py)

"""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

3.5 Entity Platforms (sensor.py)

"""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
        )

Step 4: Write Tests

See Testing Guide for detailed testing instructions.

Minimum tests required (Bronze tier):

  • Config flow tests
  • Setup/unload tests
  • Platform tests

Step 5: Run Quality Checks

# 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_integration

Step 6: Commit Changes

git add custom_components/my_integration/
git add tests/
git commit -m "feat: add My Integration"

Best Practices

DO ✅

  1. Use DataUpdateCoordinator for all polling
  2. Type hint everything - modern Python 3.14.2+ syntax
  3. Handle errors properly - ConfigEntryAuthFailed, UpdateFailed
  4. Provide unique IDs for all entities
  5. Write tests - minimum 90% coverage for Gold tier
  6. Use async - all I/O operations must be async
  7. Follow naming conventions - _attr_has_entity_name = True
  8. Document your code - docstrings for classes and methods
  9. Validate user input - in config flow
  10. Log appropriately - errors as errors, debug info as debug

DON'T ❌

  1. Block the event loop - no sync I/O in async functions
  2. Use YAML configuration - config flow only
  3. Poll in init.py - use coordinator instead
  4. Skip unique IDs - entities won't be editable
  5. Ignore errors - handle and report properly
  6. Use old type hints - no List[], Dict[], use list[], dict[]
  7. Mix sync and async - be consistent
  8. Forget availability - entities should show unavailable when offline
  9. Hardcode values - use constants
  10. Skip testing - tests are required for Bronze tier

Code Quality Standards

Ruff Configuration

Already configured in pyproject.toml:

  • Targets Python 3.14.2
  • Line length: 88 characters
  • Enables pycodestyle, pyflakes, isort, comprehensions, bugbear, pyupgrade

Mypy Configuration

Strict mode enabled in mypy.ini:

  • All functions must have type hints
  • No implicit optionals
  • Strict equality checks
  • Warning on unused ignores

Pre-commit Hooks

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

Integration Quality Scale

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

Common Patterns

See Common Patterns for code examples of:

  • OAuth2 authentication
  • Polling strategies
  • State restoration
  • Error recovery
  • Device grouping

Next Steps

Continue to:

Resources