mcpField
H
ha-integration

ha-integration

github.com/nodnarbnitramDevelopment1672 installsUpdated 27w ago
Repository

About ha-integration

ha-integration is a development skill with patterns, references, and templates for building custom Home Assistant integrations. It helps developers implement config flows and entities while addressing common setup, async, and registration pitfalls.

Key features

  1. 01Integration manifest and setup guidance
  2. 02Config flow validation patterns
  3. 03Entity and device registry patterns
  4. 04DataUpdateCoordinator examples
  5. 05Templates and reference documents

Use cases

  1. 01Scaffold a custom integration
  2. 02Implement and validate a config flow
  3. 03Register sensor entities and devices
  4. 04Handle asynchronous updates and failures

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/nodnarbnitram/claude-code-extensions --skill ha-integration

After installing, type/ha-integrationin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

README.md
assets/
__init__.py
config_flow.py
const.py
coordinator.py
manifest.json
sensor.py
references/
config-flow-patterns.md
entity-base-classes.md
manifest-reference.md

Details

From the project README

Home Assistant Integration Development

Create professional-grade custom Home Assistant integrations with complete config flows and entity implementations.

⚠️ BEFORE YOU START

This skill prevents 8 common integration errors and saves ~40% implementation time.

MetricWithout SkillWith Skill
Setup Time45 minutes12 minutes
Common Errors80
Config Flow Issues5+0
Entity Registration Bugs4+0
Known Issues This Skill Prevents
  1. Missing manifest.json dependencies - Forgetting to declare required Home Assistant components
  2. Async/await issues - Not properly awaiting coordinator updates and entity initialization
  3. Entity state class mismatches - Using wrong STATE_CLASS (measurement vs total) for sensor platforms
  4. Config flow schema errors - Invalid vol.Schema definitions causing validation failures
  5. Device info not linked - Entities created without proper device registry connections
  6. Coordinator errors - Not handling data update failures gracefully
  7. Platform import timing - Loading platform files before component initialization
  8. Missing unique ID generation - Creating duplicate entities across restarts

Quick Start

Step 1: Create manifest.json
{
  "domain": "my_integration",
  "name": "My Integration",
  "codeowners": ["@username"],
  "config_flow": true,
  "documentation": "https://github.com/username/ha-my-integration",
  "requirements": [],
  "version": "0.0.1"
}

Why this matters: The manifest.json defines integration metadata, declares dependencies, and enables config flow UI in Home Assistant.

Step 2: Create init.py with async setup
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .coordinator import MyDataUpdateCoordinator

DOMAIN = "my_integration"

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up the integration from config entry."""
    hass.data.setdefault(DOMAIN, {})

    # Create coordinator
    coordinator = MyDataUpdateCoordinator(hass, entry)
    await coordinator.async_config_entry_first_refresh()

    hass.data[DOMAIN][entry.entry_id] = coordinator

    # Forward setup to platforms
    await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])

    return True

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload the integration."""
    unload_ok = await hass.config_entries.async_unload_platforms(entry, ["sensor"])
    if unload_ok:
        hass.data[DOMAIN].pop(entry.entry_id)
    return unload_ok

Why this matters: Proper async initialization ensures Home Assistant waits for data loading and platform setup completes before continuing.

Step 3: Create config_flow.py with validation
from typing import Any, Dict, Optional
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigEntry
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult

from .const import DOMAIN

class MyIntegrationConfigFlow(ConfigFlow, domain=DOMAIN):
    """Handle config flow for my_integration."""

    async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
        """Handle user initiation of config flow."""
        errors = {}

        if user_input is not None:
            # Validate user input
            try:
                # Validate connection or API call
                pass
            except Exception as exc:
                errors["base"] = "invalid_auth"

            if not errors:
                # Create unique entry
                await self.async_set_unique_id(user_input.get("host"))
                self._abort_if_unique_id_configured()

                return self.async_create_entry(
                    title=user_input.get("name"),
                    data=user_input
                )

        # Show form
        return self.async_show_form(
            step_id="user",
            data_schema=vol.Schema({
                vol.Required("name"): str,
                vol.Required("host"): str,
            }),
            errors=errors
        )

    @staticmethod
    @callback
    def async_get_options_flow(config_entry: ConfigEntry):
        """Return options flow for this integration."""
        return MyIntegrationOptionsFlow(config_entry)

Why this matters: Config flows provide user-friendly setup UI and validate input before creating config entries.

Critical Rules

✅ Always Do
  • ✅ Use async/await throughout (async_setup_entry, async_added_to_hass, async_update_data)
  • ✅ Generate unique_id for each entity (prevents duplicates on restart)
  • ✅ Link entities to devices via device_info property
  • ✅ Handle coordinator update failures gracefully (log, mark unavailable)
  • ✅ Declare all external dependencies in manifest.json requirements
  • ✅ Use type hints for better IDE support and Home Assistant compliance
  • ✅ Register entities via coordinator patterns (DataUpdateCoordinator)
❌ Never Do
  • ❌ Use synchronous network calls (requests library) - use aiohttp
  • ❌ Import platform files at component level - let Home Assistant forward setup
  • ❌ Create entities without unique_id - causes duplicates on restart
  • ❌ Ignore coordinator update failures - mark entities unavailable
  • ❌ Hardcode API endpoints - use config flow to store them
  • ❌ Forget device_info when implementing multi-device integrations
  • ❌ Use STATE_CLASS incorrectly (measurement vs total vs total_increasing)
Common Mistakes

❌ Wrong:

# Synchronous network call - blocks event loop
import requests
data = requests.get("https://api.example.com/data").json()

# No unique_id - duplicate entities on restart
class MySensor(SensorEntity):
    pass

# Missing await
coordinator.async_refresh()

✅ Correct:

# Async network call - doesn't block
async with aiohttp.ClientSession() as session:
    async with session.get("https://api.example.com/data") as resp:
        data = await resp.json()

# Proper unique_id generation
class MySensor(SensorEntity):
    @property
    def unique_id(self) -> str:
        return f"{self.coordinator.data['id']}_sensor"

# Proper await
await coordinator.async_request_refresh()

Why: Synchronous calls block Home Assistant's event loop, causing UI freezes. Missing unique_id causes entity duplicates. Missing await means code continues before async operation completes.

Known Issues Prevention

IssueRoot CauseSolution
Duplicate entities on restartNo unique_id setImplement unique_id property with stable identifier
Config flow validation fails silentlyMissing error handling in async_step_userWrap validation in try/except, set errors dict
Entity state doesn't updateCoordinator not refreshing or entity not subscribedUse @callback decorator for update listeners
Device not appearingMissing device_info or device_identifier mismatchSet device_info with identifiers matching registry
UI freezes during setupSynchronous network calls in async_setup_entryUse aiohttp for all async network operations
Platform imports failImporting platform files in init.pyLet Home Assistant handle via async_forward_entry_setups

Manifest Configuration Reference

manifest.json
{
  "domain": "integration_name",
  "name": "Integration Display Name",
  "codeowners": ["@github_username"],
  "config_flow": true,
  "documentation": "https://github.com/username/repo",
  "homeassistant": "2024.1.0",
  "requirements": ["requests>=2.25.0"],
  "version": "1.0.0",
  "issue_tracker": "https://github.com/username/repo/issues"
}

Key settings:

  • domain: Unique identifier (alphanumeric, underscores, lowercase)
  • config_flow: Set to true to enable config UI
  • requirements: List of PyPI packages needed (e.g., ["requests>=2.25.0"])
  • homeassistant: Minimum Home Assistant version required

Config Flow Patterns

Schema with vol.All for validation
vol.Schema({
    vol.Required("host"): vol.All(str, vol.Length(min=5)),
    vol.Required("port", default=8080): int,
    vol.Optional("api_key"): str,
})
Reauth flow for expired credentials
async def async_step_reauth(self, user_input: Dict[str, Any] | None = None) -> FlowResult:
    """Handle reauth upon an API authentication error."""
    config_entry = self.hass.config_entries.async_get_entry(
        self.context["entry_id"]
    )

    if user_input is not None:
        config_entry.data = {**config_entry.data, **user_input}
        self.hass.config_entries.async_update_entry(config_entry)
        return self.async_abort(reason="reauth_successful")

    return self.async_show_form(
        step_id="reauth",
        data_schema=vol.Schema({vol.Required("api_key"): str})
    )

Entity Implementation Patterns

Sensor with State Class
from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.const import UnitOfTemperature

class TemperatureSensor(SensorEntity):
    """Temperature sensor entity."""

    _attr_device_class = "temperature"
    _attr_state_class = SensorStateClass.MEASUREMENT
    _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS

    def __init__(self, coordinator, idx):
        """Initialize sensor."""
        self.coordinator = coordinator
        self._idx = idx

    @property
    def unique_id(self) -> str:
        """Return unique ID."""
        return f"{self.coordinator.data['id']}_temp_{self._idx}"

    @property
    def device_info(self) -> DeviceInfo:
        """Return device information."""
        return DeviceInfo(
            identifiers={(DOMAIN, self.coordinator.data['id'])},
            name=self.coordinator.data['name'],
            manufacturer="My Company",
        )

    @property
    def native_value(self) -> float | None:
        """Return sensor value."""
        try:
            return float(self.coordinator.data['temperature'])
        except (KeyError, TypeError):
            return None

    async def async_added_to_hass(self) -> None:
        """Connect to coordinator when added."""
        await super().async_added_to_hass()
        self.async_on_remove(
            self.coordinator.async_add_listener(self._handle_coordinator_update)
        )

    @callback
    def _handle_coordinator_update(self) -> None:
        """Update when coordinator updates."""
        self.async_write_ha_state()
Binary Sensor
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass

class MotionSensor(BinarySensorEntity):
    """Motion detection sensor."""

    _attr_device_class = BinarySensorDeviceClass.MOTION

    @property
    def is_on(self) -> bool | None:
        """Return True if motion detected."""
        return self.coordinator.data.get('motion', False)

DataUpdateCoordinator Pattern

from datetime import timedelta
from homeassistant.helpers.update_coordinator import (
    DataUpdateCoordinator,
    UpdateFailed,
)
import logging

_LOGGER = logging.getLogger(__name__)

class MyDataUpdateCoordinator(DataUpdateCoordinator):
    """Coordinator for fetching data."""

    def __init__(self, hass, entry):
        """Initialize coordinator."""
        super().__init__(
            hass,
            _LOGGER,
            name="My Integration",
            update_interval=timedelta(minutes=5),
        )
        self.entry = entry

    async def _async_update_data(self):
        """Fetch data from API."""
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"https://api.example.com/data",
                    headers={"Authorization": f"Bearer {self.entry.data['api_key']}"}
                ) as resp:
                    if resp.status == 401:
                        raise ConfigEntryAuthFailed("Invalid API key")
                    return await resp.json()
        except asyncio.TimeoutError as err:
            raise UpdateFailed("API timeout") from err
        except Exception as err:
            raise UpdateFailed(f"API error: {err}") from err

Device Registry Patterns

Creating device with identifiers
from homeassistant.helpers.device_registry import DeviceInfo

device_info = DeviceInfo(
    identifiers={(DOMAIN, "device_unique_id")},
    name="Device Name",
    manufacturer="Manufacturer",
    model="Model Name",
    sw_version="1.0.0",
    via_device=(DOMAIN, "parent_device_id"),  # For child devices
)
Serial number and connections
device_info = DeviceInfo(
    identifiers={(DOMAIN, device_id)},
    serial_number="SERIAL123",
    connections={(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")},
)

Common Patterns

Loading config from config entry
class MyIntegration:
    def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
        self.hass = hass
        self.entry = entry
        self.api_key = entry.data.get("api_key")
        self.host = entry.data.get("host")
Handling options flow
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
    """Manage integration options."""
    if user_input is not None:
        return self.async_create_entry(
            title="",
            data=user_input
        )

    current_options = self.config_entry.options
    return self.async_show_form(
        step_id="init",
        data_schema=vol.Schema({
            vol.Optional("refresh_rate", default=current_options.get("refresh_rate", 5)): int,
        })
    )

Bundled Resources

References

Located in references/:

Templates

Located in assets/:

Note: For deep dives on specific topics, see the reference files above.

Dependencies

Required
PackageVersionPurpose
homeassistant>=2024.1.0Home Assistant core
voluptuous>=0.13.0Config validation schemas
Optional
PackageVersionPurpose
aiohttp>=3.8.0Async HTTP requests (for API integrations)
pyyaml>=5.4YAML parsing (for config file integrations)

Official Documentation

Troubleshooting

Entity appears multiple times after restart

Symptoms: Same sensor/switch/light appears 2+ times in Home Assistant after reboot

Solution:

# Add unique_id property to entity class
@property
def unique_id(self) -> str:
    return f"{self.coordinator.data['id']}_{self.platform}_{self._attr_name}"
Config flow validation never completes

Symptoms: Form hangs when submitting, no error displayed

Solution:

# Ensure all async operations are awaited and errors caught
async def async_step_user(self, user_input=None):
    errors = {}
    if user_input is not None:
        try:
            await self._validate_input(user_input)  # ← Add await
        except Exception as e:
            errors["base"] = "validation_error"  # ← Set error

        if not errors:
            return self.async_create_entry(...)
Entities show unavailable after update

Symptoms: All entities turn unavailable after coordinator update

Solution:

# Handle coordinator errors gracefully
async def _async_update_data(self):
    try:
        return await self.api.fetch_data()
    except Exception as err:
        raise UpdateFailed(f"Error: {err}") from err  # ← Raises UpdateFailed, not Exception
Device doesn't appear in device registry

Symptoms: Device created but not visible in Home Assistant devices

Solution:

# Ensure device_info is returned by ALL entities for the device
@property
def device_info(self) -> DeviceInfo:
    return DeviceInfo(
        identifiers={(DOMAIN, self.coordinator.data['id'])},  # ← Must be consistent
        name=self.coordinator.data['name'],
        manufacturer="Manufacturer",
    )

Setup Checklist

Before implementing a new integration, verify:

  • Domain name is unique and follows lowercase-with-underscores convention
  • manifest.json created with domain, name, and codeowners
  • Config flow or manual configuration method implemented
  • All async functions properly awaited
  • Unique IDs generated for all entities (prevents duplicates)
  • Device info linked if multi-device integration
  • DataUpdateCoordinator or equivalent polling pattern
  • Error handling with UpdateFailed exceptions
  • Type hints on all function signatures
  • Tests written for config flow validation
  • Documentation URL in manifest points to valid location

Comments

Markdown supported