Skip to content

Configuration Flow Development

Guide to HAEO's ConfigSubentry-based configuration flow implementation.

Overview

HAEO uses Home Assistant's ConfigSubentry architecture where each element is managed as a subentry:

  1. Hub flow (in custom_components/haeo/flows/hub.py): Creates main hub entry with optimization settings
  2. Element flows (in custom_components/haeo/flows/element.py): Creates element ConfigSubentries using ConfigSubentryFlow
  3. Network subentry: Automatically created representing the optimization network itself

This architecture follows Home Assistant's native subentry pattern. Elements appear as subentries under the main hub in the UI with proper parent-child management.

For general information on config flows, see the Home Assistant Config Flow documentation and Data Entry Flow.

Architecture Benefits

  • Native Home Assistant subentry UI integration
  • Automatic parent-child relationship management
  • Independent configuration of each element
  • Proper device registry association via config_subentry_id
  • Easy addition/removal through native UI
  • No manual parent_entry_id tracking required

Architecture

graph TD
    A[Add Integration] --> B{Entry Type?}
    B -->|First Time| C[Hub Flow]
    B -->|Hub Exists| D[Element Type Selection]
    C --> E[Optimization Settings]
    E --> F[Create Hub Entry]
    D --> G{Element Type}
    G -->|Battery| H[Battery Flow]
    G -->|Grid| I[Grid Flow]
    G -->|PV| J[PV Flow]
    G -->|Load| K[Load Flow]
    H --> L[Select Parent Hub]
    I --> L
    J --> L
    K --> L
    L --> M[Configure Element]
    M --> N[Create Subentry]

Hub Flow

The hub flow creates the main integration entry that acts as a parent for element subentries and hosts the optimization coordinator.

Hub entry structure

Hub entries are identified by the presence of integration_type: "hub" in their data:

{
    "entry_id": "abc123...",
    "domain": "haeo",
    "title": "Home Energy System",
    "data": {
        "integration_type": "hub"  # Marker to identify hub entries
    },
    "options": {
        "horizon_hours": 48,
        "period_minutes": 5,
    },
}

Optimization settings are stored in options (user-editable), while the hub marker is stored in data (immutable). The hub flow implementation is in custom_components/haeo/flows/hub.py.

Key implementation points

  • Hub flow uses standard config flow pattern with user step
  • Prevents duplicate hub names by checking existing entries
  • Stores optimization settings in options for later editing via options flow
  • Hub marker in data allows coordinator to identify hub entries

Element Flows

Element subentries are created through separate config flows, one per element type. All element flows inherit from a common base class that handles parent hub selection and entry creation.

Element entry structure

Element entries link to their parent hub via parent_entry_id:

{
    "entry_id": "def456...",
    "domain": "haeo",
    "title": "Home Battery",
    "data": {
        "element_type": "battery",
        "parent_entry_id": "abc123...",  # Links to hub entry
        "capacity": 13500,
        "charge_power": 5000,
        # ... element-specific configuration
    },
}

Base element flow pattern

All element flows extend ElementConfigFlow which provides:

  • Parent hub selection (auto-selects if only one hub exists)
  • Entry creation with proper parent linkage
  • Duplicate prevention
  • Standard error handling

The element flow base class is in custom_components/haeo/flows/element.py.

Connection endpoint filtering

Connection elements require selecting source and target endpoints from other configured elements. The element flow filters available elements based on connectivity level and Advanced Mode setting.

Each element type in the ELEMENT_TYPES registry defines a connectivity level that controls when it appears in connection selectors. The ConnectivityLevel enum has three values:

  • ALWAYS: Always shown in connection selectors
  • ADVANCED: Only shown when Advanced Mode is enabled
  • NEVER: Never shown in connection selectors

This filtering ensures connection endpoints are appropriate for the user's configuration level. It prevents invalid connection topologies by excluding elements that shouldn't be connection endpoints. See custom_components/haeo/elements/__init__.py for the connectivity level assigned to each element type.

Element-specific implementations

Each element type has its own flow class in custom_components/haeo/flows/:

  • BatteryConfigFlow - Battery element configuration
  • GridConfigFlow - Grid configuration
  • SolarConfigFlow - Solar system configuration
  • ConstantLoadConfigFlow - Constant load configuration
  • ForecastLoadConfigFlow - Forecast-based load configuration
  • NodeConfigFlow - Network node configuration

Each flow defines element-specific schema fields, defaults, and validation logic.

Two-Step Config Flow Pattern

Some element types use a two-step configuration flow that separates mode selection from value entry. This pattern provides a cleaner user experience when fields can be configured in different ways.

Flow Steps

Step 1 (user): User enters the element name, connection target, and selects an input mode for each configurable field.

Step 2 (values): Based on the mode selections, the UI shows appropriate inputs for each field.

Input Modes

The InputMode enum defines how each field receives its value:

Mode Description UI Widget
CONSTANT User enters a fixed numeric value NumberSelector
ENTITY_LINK Value comes from one or more Home Assistant sensors EntitySelector (multi)
NONE Field is disabled (only available for optional fields) No input shown

The NONE option only appears for optional fields (those marked with NotRequired in the TypedDict schema). Required fields only show CONSTANT and ENTITY_LINK options.

Implementation Pattern

The two-step flow utilities are in custom_components/haeo/flows/field_schema.py:

  • build_mode_schema_entry(): Creates the mode selector for step 1
  • build_value_schema_entry(): Creates the value input for step 2 based on selected mode
  • get_mode_defaults(): Provides default mode selections based on field types
  • get_value_defaults(): Extracts current values for reconfigure flows

Flow handlers store step 1 data and use it to build the step 2 schema dynamically. When mode is NONE, no value entry is shown and no input entity is created for that field.

Entity Creation

Input entities are only created for fields that are actually configured. If a user selects NONE for an optional field, no entity is created for that field. This keeps the entity list clean and focused on configured functionality.

Field Schema System

HAEO uses a typed schema system to define element configuration fields. The schema system provides type safety, validation, and data loading from Home Assistant entities.

ELEMENT_TYPES Registry

All element types are registered in custom_components/haeo/elements/__init__.py:

ELEMENT_TYPES: dict[ElementType, ElementRegistryEntry] = {
    "battery": ElementRegistryEntry(
        schema=BatteryConfigSchema,  # TypedDict for UI configuration
        data=BatteryConfigData,  # TypedDict for loaded values
        defaults={...},  # Default field values
        translation_key="battery",  # For UI localization
        adapter=create_battery,  # Creates model elements
        extract=extract_battery,  # Extracts optimization results
    ),
    # ... other element types
}

The registry provides:

  • Schema/Data class pairs: Dual TypedDict pattern for type safety
  • Default values: Pre-populated fields in the UI
  • Adapter functions: Convert loaded data to model elements
  • Result extractors: Convert optimization results to sensor data

Schema vs Data Mode

Each element type has two TypedDict definitions:

Schema mode (*ConfigSchema): What the user enters in the UI

  • Contains entity IDs as strings
  • Used during config flow form display and validation

Data mode (*ConfigData): What the optimizer uses

  • Contains loaded numeric values
  • Produced by the load() function during coordinator updates
# Schema mode: entity IDs with Annotated metadata
class BatteryConfigSchema(TypedDict):
    capacity: EnergySensorFieldSchema  # Annotated[str, EntitySelect(...), TimeSeries(...)]


# Data mode: loaded values
class BatteryConfigData(TypedDict):
    capacity: EnergySensorFieldData  # Annotated[list[float], ...]

Field Metadata with Annotated

Fields use Annotated types with composable metadata markers:

from typing import Annotated

# Define field type with composed metadata
EnergySensorFieldSchema = Annotated[
    str,
    EntitySelect(accepted_units=ENERGY_UNITS),
    TimeSeries(accepted_units=ENERGY_UNITS),
]


class BatteryConfigSchema(TypedDict):
    capacity: EnergySensorFieldSchema  # Entity ID with attached metadata

The composable metadata types are:

  • Validator subclasses: Define schema validation and UI selectors (e.g., PositiveKW, Percentage, EntitySelect)
  • LoaderMeta subclasses: Specify how values are loaded at runtime (e.g., ConstantFloat, TimeSeries)
  • Default: Provides default values for config flow UI forms

Available Field Types

Field types are defined in custom_components/haeo/schema/fields.py:

Validator Class Purpose Base Type
PositiveKW Positive power values in kW float
AnyKW Power flow (pos or neg) in kW float
PositiveKWH Positive energy values in kWh float
Price Price values in $/kWh float
Percentage Percentage values (0-100) float
BatterySOC Battery SOC percentage float
Boolean Boolean flags bool
Name Free-form text names str
ElementName References to other elements str
EntitySelect Entity sensor references str

Data Loading Flow

The schema system integrates with data loading:

  1. User enters entity IDs in config flow (Schema mode)
  2. Voluptuous validators ensure valid entity selection
  3. On coordinator update, load() converts Schema → Data mode
  4. LoaderMeta markers determine which loader extracts values from Home Assistant
  5. Adapter functions receive Data mode config to create model elements

For details on loaders, see Data Loading.

Options Flow

The options flow allows users to edit hub optimization settings after initial setup. Elements are managed as separate config entries (added/edited/removed through the main integration flow), not through the options flow.

The options flow implementation is in custom_components/haeo/flows/hub.py.

Key points

  • Options flow only edits hub-level optimization settings (horizon, period, solver)
  • Element configuration happens via separate config entries
  • Settings stored in config_entry.options (not data)
  • Changes trigger coordinator reload to apply new parameters

Element Management

Elements are not managed through the hub's options flow. Instead, users add/edit/remove elements as independent config entries through the main "Add Integration" flow.

User workflow

Adding elements:

  1. Navigate to SettingsDevices & Services
  2. Click Add Integration
  3. Search for "HAEO"
  4. Select element type (battery, grid, etc.)
  5. Choose parent hub
  6. Configure element parameters

Editing elements:

  1. Find the element entry in Devices & Services
  2. Click Configure on the element entry
  3. Modify parameters
  4. Submit changes

Removing elements:

  1. Find the element entry in Devices & Services
  2. Click the three-dot menu
  3. Select Delete

The hub coordinator automatically detects element changes on the next update cycle. See user configuration guide for end-user instructions.

Testing Config Flow

Config flow testing uses Home Assistant's testing fixtures and follows standard patterns.

Comprehensive test coverage is in tests/test_config_flow.py, including:

  • Hub flow success and duplicate prevention
  • Element flow with hub selection
  • Options flow for editing settings
  • Error handling scenarios
  • Validation logic

Example test pattern:

async def test_hub_flow_success(hass: HomeAssistant) -> None:
    """Test successful hub creation."""
    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={CONF_NAME: "Test Hub", CONF_HORIZON_HOURS: 48},
    )

    assert result["type"] == FlowResultType.CREATE_ENTRY
    assert result["data"] == {INTEGRATION_TYPE: INTEGRATION_TYPE_HUB}