Files
core/AGENTS.md
2026-01-23 15:22:18 -05:00

12 KiB

GitHub Copilot & Claude Code Instructions

This repository contains the core of Home Assistant, a Python 3 based home automation application.

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)

Git commit practices during review:

  • Do NOT amend, squash, or rebase commits after review has started - Reviewers need to see what changed since their last review

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
  • Lint/Type/Format Fixes: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding # type: ignore, or skipping a formatter. Treat suppressions and noqa comments as a last resort once no compliant fix exists
  • 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

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

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 of time.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

Error Handling

  • Exception Types: Choose most specific exception available
    • ServiceValidationError: User input errors (preferred over ValueError)
    • HomeAssistantError: Device communication failures
    • ConfigEntryNotReady: Temporary setup issues (device offline)
    • ConfigEntryAuthFailed: Authentication problems
    • ConfigEntryError: 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: or except Exception:
      • Allowed in config flows to ensure robustness
      • Allowed in functions/methods that run in background tasks
    • 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
    

Development Commands

Code Quality & Linting

  • Run all linters on all files: prek run --all-files
  • Run linters on staged files only: prek 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

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

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
        )