40 KiB
40 KiB
GitHub Copilot & Claude Code Instructions
This repository contains the core of Home Assistant, a Python 3 based home automation application.
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
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 implementedexempt
: Rule doesn't apply (with reason in comment)todo
: Rule needs implementation
Example quality_scale.yaml
Structure
rules:
# Bronze (mandatory)
config-flow: done
entity-unique-id: done
action-setup:
status: exempt
comment: Integration does not register custom actions.
# Silver (if targeting Silver+)
entity-unavailable: done
parallel-updates: done
# Gold (if targeting Gold+)
devices: done
diagnostics: done
# Platinum (if targeting Platinum)
strict-typing: done
When Reviewing/Creating Code: Always check the integration's quality scale level and exemption status before applying rules.
Code Review Guidelines
When reviewing code, do NOT comment on:
- Missing imports - We use static analysis tooling to catch that
- Code formatting - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
Python Requirements
- Compatibility: Python 3.13+
- Language Features: Use the newest features when possible:
- Pattern matching
- Type hints
- f-strings (preferred over
%
or.format()
) - Dataclasses
- Walrus operator
Strict Typing (Platinum)
- Comprehensive Type Hints: Add type hints to all functions, methods, and variables
- Custom Config Entry Types: When using runtime_data:
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
- Library Requirements: Include
py.typed
file for PEP-561 compliance
Code Quality Standards
- Formatting: Ruff
- Linting: PyLint and Ruff
- Type Checking: MyPy
- Testing: pytest with plain functions and fixtures
- Language: American English for all code, comments, and documentation (use sentence case, including titles)
Writing Style Guidelines
- Tone: Friendly and informative
- Perspective: Use second-person ("you" and "your") for user-facing messages
- Inclusivity: Use objective, non-discriminatory language
- Clarity: Write for non-native English speakers
- Formatting in Messages:
- Use backticks for: file paths, filenames, variable names, field entries
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
- Avoid abbreviations when possible
Code Organization
Core Locations
- Shared constants:
homeassistant/const.py
(use these instead of hardcoding) - Integration structure:
homeassistant/components/{domain}/const.py
- Constantshomeassistant/components/{domain}/models.py
- Data modelshomeassistant/components/{domain}/coordinator.py
- Update coordinatorhomeassistant/components/{domain}/config_flow.py
- Configuration flowhomeassistant/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, # ✅ Pass config_entry - it's accepted and recommended )
- entity.py: Base entity definitions to reduce duplication
class MyEntity(CoordinatorEntity[MyCoordinator]): _attr_has_entity_name = True
Runtime Data Storage
- Use ConfigEntry.runtime_data: Store non-persistent runtime data
type MyIntegrationConfigEntry = ConfigEntry[MyClient] async def async_setup_entry(hass: HomeAssistant, entry: MyIntegrationConfigEntry) -> bool: client = MyClient(entry.data[CONF_HOST]) entry.runtime_data = client
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
- Version Control: Always set
VERSION = 1
andMINOR_VERSION = 1
- Unique ID Management:
await self.async_set_unique_id(device_unique_id) self._abort_if_unique_id_configured()
- Error Handling: Define errors in
strings.json
underconfig.error
- Step Methods: Use standard naming (
async_step_user
,async_step_discovery
, etc.)
Integration Ownership
- manifest.json: Add GitHub usernames to
codeowners
:{ "domain": "my_integration", "name": "My Integration", "codeowners": ["@me"] }
Documentation Standards
- File Headers: Short and concise
"""Integration for Peblar EV chargers."""
- Method/Function Docstrings: Required for all
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool: """Set up Peblar from a config entry."""
- Comment Style:
- Use clear, descriptive comments
- Explain the "why" not just the "what"
- Keep code block lines under 80 characters when possible
- Use progressive disclosure (simple explanation first, complex details later)
Async Programming
- All external I/O operations must be async
- Best Practices:
- Avoid sleeping in loops
- Avoid awaiting in loops - use
gather
instead - No blocking calls
- Group executor jobs when possible - switching between event loop and executor is expensive
Async Dependencies (Platinum)
- Requirement: All dependencies must use asyncio
- Ensures efficient task handling without thread context switching
WebSession Injection (Platinum)
- Pass WebSession: Support passing web sessions to dependencies
async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool: """Set up integration from config entry.""" client = MyClient(entry.data[CONF_HOST], async_get_clientsession(hass))
- For cookies: Use
async_create_clientsession
(aiohttp) orcreate_async_httpx_client
(httpx)
Blocking Operations
- Use Executor: For blocking I/O operations
result = await hass.async_add_executor_job(blocking_function, args)
- Never Block Event Loop: Avoid file operations,
time.sleep()
, blocking HTTP calls - Replace with Async: Use
asyncio.sleep()
instead oftime.sleep()
Thread Safety
- @callback Decorator: For event loop safe functions
@callback def async_update_callback(self, event): """Safe to run in event loop.""" self.async_write_ha_state()
- Sync APIs from Threads: Use sync versions when calling from non-event loop threads
- Registry Changes: Must be done in event loop thread
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, # ✅ Pass config_entry - it's accepted and recommended ) 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
inmanifest.json
- Data Storage:
- Connection-critical config: Store in
ConfigEntry.data
- Non-critical settings: Store in
ConfigEntry.options
- Connection-critical config: Store in
- 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:
# Using unique ID await self.async_set_unique_id(identifier) self._abort_if_unique_id_configured() # Using unique data self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
Reauthentication Support
- Required Method: Implement
async_step_reauth
in config flow - Credential Updates: Allow users to update credentials without re-adding
- Validation: Verify account matches existing unique ID:
await self.async_set_unique_id(user_id) self._abort_if_unique_id_mismatch(reason="wrong_account") return self.async_update_reload_and_abort( self._get_reauth_entry(), data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]} )
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 failureConfigEntryAuthFailed
: Authentication issuesConfigEntryError
: 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() # Clean up resources return unload_ok
Service Actions
- Registration: Register all service actions in
async_setup
, NOT inasync_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:
# For invalid input if end_date < start_date: raise ServiceValidationError("End date must be after start date") # For service errors 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
- Use update coordinator pattern when possible
- Polling intervals are NOT user-configurable: Never add scan_interval, update_interval, or polling frequency options to config flows or config entries
- Integration determines intervals: Set
update_interval
programmatically based on integration logic, not user input - Minimum Intervals:
- Local network: 5 seconds
- Cloud services: 60 seconds
- Parallel Updates: Specify number of concurrent updates:
PARALLEL_UPDATES = 1 # Serialize updates to prevent overwhelming device # OR PARALLEL_UPDATES = 0 # Unlimited (for coordinator-based or read-only)
Error Handling
- Exception Types: Choose most specific exception available
ServiceValidationError
: User input errors (preferred overValueError
)HomeAssistantError
: Device communication failuresConfigEntryNotReady
: Temporary setup issues (device offline)ConfigEntryAuthFailed
: Authentication problemsConfigEntryError
: Permanent setup issues
- Try/Catch Best Practices:
- Only wrap code that can throw exceptions
- Keep try blocks minimal - process data after the try/catch
- Avoid bare exceptions except in specific cases:
- ❌ Generally not allowed:
except:
orexcept Exception:
- ✅ Allowed in config flows to ensure robustness
- ✅ Allowed in functions/methods that run in background tasks
- ❌ Generally not allowed:
- Bad pattern:
try: data = await device.get_data() # Can throw # ❌ Don't process data inside try block processed = data.get("value", 0) * 100 self._attr_native_value = processed except DeviceError: _LOGGER.error("Failed to get data")
- Good pattern:
try: data = await device.get_data() # Can throw except DeviceError: _LOGGER.error("Failed to get data") return # ✅ Process data outside try block processed = data.get("value", 0) * 100 self._attr_native_value = processed
- Bare Exception Usage:
# ❌ Not allowed in regular code try: data = await device.get_data() except Exception: # Too broad _LOGGER.error("Failed") # ✅ Allowed in config flow for robustness async def async_step_user(self, user_input=None): try: await self._test_connection(user_input) except Exception: # Allowed here errors["base"] = "unknown" # ✅ Allowed in background tasks async def _background_refresh(): try: await coordinator.async_refresh() except Exception: # Allowed in task _LOGGER.exception("Unexpected error in background task")
- Setup Failure Patterns:
try: await device.async_setup() except (asyncio.TimeoutError, TimeoutException) as ex: raise ConfigEntryNotReady(f"Timeout connecting to {device.host}") from ex except AuthFailed as ex: raise ConfigEntryAuthFailed(f"Credentials expired for {device.name}") from ex
Logging
- Format Guidelines:
- No periods at end of messages
- No integration names/domains (added automatically)
- No sensitive data (keys, tokens, passwords)
- Use debug level for non-user-facing messages
- Use Lazy Logging:
_LOGGER.debug("This is a log message with %s", variable)
Unavailability Logging
- Log Once: When device/service becomes unavailable (info level)
- Log Recovery: When device/service comes back online
- Implementation Pattern:
_unavailable_logged: bool = False if not self._unavailable_logged: _LOGGER.info("The sensor is unavailable: %s", ex) self._unavailable_logged = True # On recovery: if self._unavailable_logged: _LOGGER.info("The sensor is back online") self._unavailable_logged = False
Entity Development
Unique IDs
- Required: Every entity must have a unique ID for registry tracking
- Must be unique per platform (not per integration)
- Don't include integration domain or platform in ID
- Implementation:
class MySensor(SensorEntity): def __init__(self, device_id: str) -> None: self._attr_unique_id = f"{device_id}_temperature"
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, # ❌ Too long )
- Good pattern:
SensorEntityDescription( key="temperature", name="Temperature", value_fn=lambda data: ( # ✅ Parenthesis on same line as lambda 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 # e.g., "temperature", "humidity"
- For device itself: Set
_attr_name = None
Event Lifecycle Management
- Subscribe in
async_added_to_hass
:async def async_added_to_hass(self) -> None: """Subscribe to events.""" self.async_on_remove( self.client.events.subscribe("my_event", self._handle_event) )
- Unsubscribe in
async_will_remove_from_hass
if not usingasync_on_remove
- Never subscribe in
__init__
or other methods
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
- Auto-remove: When devices disappear from hub/account
- Device Registry Update:
device_registry.async_update_device( device_id=device.id, remove_config_entry_id=self.config_entry.entry_id, )
- Manual Deletion: Implement
async_remove_config_entry_device
when needed
Diagnostics and Repairs
Integration Diagnostics
- Required: Implement diagnostic data collection
- Implementation:
TO_REDACT = [CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE] async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: MyConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" return { "entry_data": async_redact_data(entry.data, TO_REDACT), "data": entry.runtime_data.data, }
- Security: Never expose passwords, tokens, or sensitive coordinates
Repair Issues
- Actionable Issues Required: All repair issues must be actionable for end users
- Issue Content Requirements:
- Clearly explain what is happening
- Provide specific steps users need to take to resolve the issue
- Use friendly, helpful language
- Include relevant context (device names, error details, etc.)
- Implementation:
ir.async_create_issue( hass, DOMAIN, "outdated_version", is_fixable=False, issue_domain=DOMAIN, severity=ir.IssueSeverity.ERROR, translation_key="outdated_version", )
- Translation Strings Requirements: Must contain user-actionable text in
strings.json
:{ "issues": { "outdated_version": { "title": "Device firmware is outdated", "description": "Your device firmware version {current_version} is below the minimum required version {min_version}. To fix this issue: 1) Open the manufacturer's mobile app, 2) Navigate to device settings, 3) Select 'Update Firmware', 4) Wait for the update to complete, then 5) Restart Home Assistant." } } }
- String Content Must Include:
- What the problem is
- Why it matters
- Exact steps to resolve (numbered list when multiple steps)
- What to expect after following the steps
- Avoid Vague Instructions: Don't just say "update firmware" - provide specific steps
- Severity Guidelines:
CRITICAL
: Reserved for extreme scenarios onlyERROR
: Requires immediate user attentionWARNING
: Indicates future potential breakage
- Additional Attributes:
ir.async_create_issue( hass, DOMAIN, "issue_id", breaks_in_ha_version="2024.1.0", is_fixable=True, is_persistent=True, severity=ir.IssueSeverity.ERROR, translation_key="issue_description", )
- Only create issues for problems users can potentially resolve
Entity Categories
- Required: Assign appropriate category to entities
- Implementation: Set
_attr_entity_category
class MySensor(SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC
- Categories include:
DIAGNOSTIC
for system/technical information
Device Classes
- Use When Available: Set appropriate device class for entity type
class MyTemperatureSensor(SensorEntity): _attr_device_class = SensorDeviceClass.TEMPERATURE
- Provides context for: unit conversion, voice control, UI representation
Disabled by Default
- Disable Noisy/Less Popular Entities: Reduce resource usage
class MySignalStrengthSensor(SensorEntity): _attr_entity_registry_enabled_default = False
- Target: frequently changing states, technical diagnostics
Entity Translations
- Required with has_entity_name: Support international users
- Implementation:
class MySensor(SensorEntity): _attr_has_entity_name = True _attr_translation_key = "phase_voltage"
- Create
strings.json
with translations:{ "entity": { "sensor": { "phase_voltage": { "name": "Phase voltage" } } } }
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
- Use pytest fixtures from
Config Flow Testing
- 100% Coverage Required: All config flow paths must be tested
- Test Scenarios:
- All flow initiation methods (user, discovery, import)
- Successful configuration paths
- Error recovery scenarios
- Prevention of duplicate entries
- Flow completion after errors
Development Commands
Code Quality & Linting
- Run all linters on all files:
pre-commit run --all-files
- Run linters on staged files only:
pre-commit run
- PyLint on everything (slow):
pylint homeassistant
- PyLint on specific folder:
pylint homeassistant/components/my_integration
- MyPy type checking (whole project):
mypy homeassistant/
- MyPy on specific integration:
mypy homeassistant/components/my_integration
Testing
- Integration-specific tests (recommended):
pytest ./tests/components/<integration_domain> \ --cov=homeassistant.components.<integration_domain> \ --cov-report term-missing \ --durations-min=1 \ --durations=0 \ --numprocesses=auto
- Quick test of changed files:
pytest --timeout=10 --picked
- Update test snapshots: Add
--snapshot-update
to pytest command- ⚠️ Omit test results after using
--snapshot-update
- Always run tests again without the flag to verify snapshots
- ⚠️ Omit test results after using
- Full test suite (AVOID - very slow):
pytest ./tests
Dependencies & Requirements
- Update generated files after dependency changes:
python -m script.gen_requirements_all
- Install all Python requirements:
uv pip install -r requirements_all.txt -r requirements.txt -r requirements_test.txt
- Install test requirements only:
uv pip install -r requirements_test_all.txt -r requirements.txt
Translations
- Update translations after strings.json changes:
python -m script.translations develop --all
Project Validation
- Run hassfest (checks project structure and updates generated files):
python -m script.hassfest
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
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
Minimal Integration Checklist
manifest.json
with required fields (domain, name, codeowners, etc.)__init__.py
withasync_setup_entry
andasync_unload_entry
config_flow.py
with UI configuration supportconst.py
withDOMAIN
constantstrings.json
with at least config flow text- Platform files (
sensor.py
, etc.) as needed quality_scale.yaml
with rule status tracking
Common Anti-Patterns & Best Practices
❌ Avoid These Patterns
# Blocking operations in event loop
data = requests.get(url) # ❌ Blocks event loop
time.sleep(5) # ❌ Blocks event loop
# Reusing BleakClient instances
self.client = BleakClient(address)
await self.client.connect()
# Later...
await self.client.connect() # ❌ Don't reuse
# Hardcoded strings in code
self._attr_name = "Temperature Sensor" # ❌ Not translatable
# Missing error handling
data = await self.api.get_data() # ❌ No exception handling
# Storing sensitive data in diagnostics
return {"api_key": entry.data[CONF_API_KEY]} # ❌ Exposes secrets
# Accessing hass.data directly in tests
coordinator = hass.data[DOMAIN][entry.entry_id] # ❌ Don't access hass.data
# User-configurable polling intervals
# In config flow
vol.Optional("scan_interval", default=60): cv.positive_int # ❌ Not allowed
# In coordinator
update_interval = timedelta(minutes=entry.data.get("scan_interval", 1)) # ❌ Not allowed
# User-configurable config entry names (non-helper integrations)
vol.Optional("name", default="My Device"): cv.string # ❌ Not allowed in regular integrations
# Too much code in try block
try:
response = await client.get_data() # Can throw
# ❌ Data processing should be outside try block
temperature = response["temperature"] / 10
humidity = response["humidity"]
self._attr_native_value = temperature
except ClientError:
_LOGGER.error("Failed to fetch data")
# Bare exceptions in regular code
try:
value = await sensor.read_value()
except Exception: # ❌ Too broad - catch specific exceptions
_LOGGER.error("Failed to read sensor")
✅ Use These Patterns Instead
# Async operations with executor
data = await hass.async_add_executor_job(requests.get, url)
await asyncio.sleep(5) # ✅ Non-blocking
# Fresh BleakClient instances
client = BleakClient(address) # ✅ New instance each time
await client.connect()
# Translatable entity names
_attr_translation_key = "temperature_sensor" # ✅ Translatable
# Proper error handling
try:
data = await self.api.get_data()
except ApiException as err:
raise UpdateFailed(f"API error: {err}") from err
# Redacted diagnostics data
return async_redact_data(data, {"api_key", "password"}) # ✅ Safe
# Test through proper integration setup and fixtures
@pytest.fixture
async def init_integration(hass, mock_config_entry, mock_api):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) # ✅ Proper setup
# Integration-determined polling intervals (not user-configurable)
SCAN_INTERVAL = timedelta(minutes=5) # ✅ Common pattern: constant in const.py
class MyCoordinator(DataUpdateCoordinator[MyData]):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
# ✅ Integration determines interval based on device capabilities, connection type, etc.
interval = timedelta(minutes=1) if client.is_local else SCAN_INTERVAL
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=interval,
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
)
Entity Performance Optimization
# Use __slots__ for memory efficiency
class MySensor(SensorEntity):
__slots__ = ("_attr_native_value", "_attr_available")
@property
def should_poll(self) -> bool:
"""Disable polling when using coordinator."""
return False # ✅ Let coordinator handle updates
Testing Patterns
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"
# Test form submission
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.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True)
@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)
# Ensure entities are correctly assigned to device
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
# Modern integration fixture setup
@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
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_device_api: MagicMock,
) -> MockConfigEntry:
"""Set up the integration for testing."""
mock_config_entry.add_to_hass(hass)
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
andhas_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
# Enable debug logging in tests
caplog.set_level(logging.DEBUG, logger="my_integration")
# In integration code - use proper logging
_LOGGER = logging.getLogger(__name__)
_LOGGER.debug("Processing data: %s", data) # Use lazy logging
Validation Commands
# Check specific integration
python -m script.hassfest --integration-path homeassistant/components/my_integration
# Validate quality scale
# Check quality_scale.yaml against current rules
# Run integration tests with coverage
pytest ./tests/components/my_integration \
--cov=homeassistant.components.my_integration \
--cov-report term-missing