Skip to content

Developer Architecture

Jason Rhubottom edited this page May 25, 2026 · 7 revisions

Developer Architecture

Overview

Adaptive Cover Pro is a Home Assistant custom integration that automatically controls blinds, awnings, and venetian blinds based on sun position. The integration uses a layered architecture that separates HA state access, pure calculation logic, an override priority pipeline, and focused manager classes from a thin coordinator orchestrator.

Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Home Assistant Core                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Config Flow (UI Setup)                         β”‚
β”‚  - Multi-step wizard for vertical/horizontal/tilt covers        β”‚
β”‚  - Options flow for configuration updates                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              AdaptiveDataUpdateCoordinator                      β”‚
β”‚  - Thin orchestrator: runs update cycle, routes events          β”‚
β”‚  - Schedules refreshes (end-time, timed)                        β”‚
β”‚  - Manages toggle properties (automatic_control, etc.)          β”‚
β”‚  - Reads options once per cycle into a typed RuntimeConfig      β”‚
β”‚  - Delegates to managers, providers, pipeline, diagnostics      β”‚
β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚          β”‚           β”‚            β”‚
   β–Ό          β–Ό           β–Ό            β–Ό
State      Managers    Pipeline    Diagnostics
Providers  (7 classes) (10 handlers) Builder

Core Components

1. Coordinator (coordinator.py)

Role: Thin orchestrator: delegates, does not implement

Responsibilities:

  • Runs _async_update_data() update cycle
  • Routes entity state changes (sun, cover, motion)
  • Schedules timed and end-time refreshes
  • Manages toggle properties (automatic_control, switch_mode, manual_override_mode)
  • Wires together providers, managers, pipeline, and diagnostics builder

2. State Providers (state/)

All Home Assistant state reads are isolated here. The rest of the codebase has zero HA imports for data access.

File Class Reads
climate_provider.py ClimateProvider Temp/weather/presence/lux/irradiance entities β†’ ClimateReadings frozen dataclass
sun_provider.py SunProvider Astral location from HA β†’ pure SunData instance
cover_provider.py CoverProvider Cover entity state from HA (position, state)
snapshot.py SunSnapshot, CoverStateSnapshot Frozen dataclasses holding unified state for each update cycle

Benefit: Calculation engine and pipeline handlers are fully testable without HA mocks.

3. Pure Calculation Engine (engine/covers/, sun.py)

Zero homeassistant imports. Receives pre-computed data from providers.

Note: calculation.py is now a thin re-export shim β€” the real classes live in engine/covers/. New code should import from there directly. The shim exists for backward compatibility with consumers that still reference from .calculation import AdaptiveVerticalCover.

AdaptiveGeneralCover (Base Class β€” engine/covers/base.py)

  • Receives SunData (not hass)
  • Shared sun position calculations, FOV validation, elevation limits, blind spot detection

AdaptiveVerticalCover (engine/covers/vertical.py)

  • Up/down blinds
  • Projects sun rays to calculate required blind height
  • Enhanced geometric accuracy: safety margins, edge case handling, optional window depth and sill height support
  • Output: blind height in meters β†’ percentage

AdaptiveHorizontalCover (engine/covers/horizontal.py)

  • In/out awnings
  • Uses vertical calculation + trigonometry for horizontal extension
  • Output: extension length β†’ percentage

AdaptiveTiltCover (engine/covers/tilt.py)

  • Slat rotation for venetian/tilt covers
  • Calculates optimal slat angle to block sun while allowing light
  • Output: degrees β†’ percentage clamped to [0, max_degrees]

Climate Strategy

  • ClimateCoverState (in pipeline/handlers/climate.py): receives pre-read ClimateReadings from ClimateProvider (no direct HA reads)
    • Winter: open for solar heating
    • Summer: close for heat blocking
    • Presence-aware strategies

4. Manager Classes (managers/)

Focused classes extracted from the coordinator, each owning one responsibility:

File Class Responsibility
manual_override.py AdaptiveCoverManager Manual override detection and tracking
grace_period.py GracePeriodManager Per-command and startup grace periods
motion.py MotionManager Motion sensor timeout tracking
time_window.py TimeWindowManager Enforces operational start/end time windows
toggles.py ToggleManager Boolean feature toggles (climate mode, motion control)
weather.py WeatherManager Wind/rain/severe weather evaluation with clear-delay timeout
cover_command.py CoverCommandService Cover service calls, capability detection, delta checks
dual_axis_sequencer.py DualAxisSequencer Position→settle→tilt sequencer for venetian dual-axis covers (owned by VenetianPolicy)
position_verification.py PositionVerificationManager Detects covers that report a position but never reach it

5. Override Pipeline (pipeline/)

A pluggable priority chain replaces the previous if/elif override logic.

Core types (pipeline/types.py):

  • PipelineSnapshot: frozen snapshot of current state passed to all handlers
  • PipelineResult: winning handler's decision + full decision trace
  • DecisionStep: one handler's evaluation record

Pipeline helpers (pipeline/helpers.py):

  • Canonical shared utilities: compute_solar_position(), compute_default_position(), apply_snapshot_limits(), compute_raw_calculated_position()
  • All handlers and ClimateCoverState use these; do not inline these patterns in new handlers

Registry (pipeline/registry.py):

  • PipelineRegistry evaluates handlers in descending priority order
  • Returns the first PipelineResult that takes effect

Handlers (pipeline/handlers/):

Handler Priority Condition
force_override.py 100 Force override sensor(s) active: bypasses automatic control
weather.py 90 Wind/rain/severe weather exceeds thresholds: bypasses automatic control
manual_override.py 80 User manually moved the cover
custom_position.py 1–99 (default 77, configurable) Sensor-driven fixed position; up to 4 slots configured in _build_pipeline()
motion_timeout.py 75 No motion detected within configured timeout
cloud_suppression.py 60 Low light / cloud coverage: no direct sun present
climate.py 50 Climate mode active and triggered
glare_zone.py 45 Active glare zone requires additional blind lowering (vertical covers only)
solar.py 40 Sun in window FOV: pure sun tracking
default.py 0 Final fallback (default or sunset position)

Adding a new override: create one file in pipeline/handlers/, implement OverrideHandler (pipeline/handler.py), register in coordinator's _build_pipeline().

6. Diagnostics Builder (diagnostics/builder.py)

DiagnosticsBuilder with DiagnosticContext, extracted from coordinator.

Builds:

  • Solar position diagnostics
  • Position and time window diagnostics
  • Sun validity diagnostics
  • Climate diagnostics
  • Action diagnostics
  • Configuration diagnostics
  • Decision trace from PipelineResult

7. Engine Modules (engine/)

The home of all pure calculation classes. calculation.py re-exports the cover types from here for backward compatibility β€” new code imports from engine.covers.* directly.

File Class Purpose
engine/sun_geometry.py SunGeometry Pure sun angle math (gamma, elevation, azimuth relationships)
engine/covers/base.py AdaptiveGeneralCover Common cover-calculation base (SunData injection, FOV, blind spots)
engine/covers/vertical.py AdaptiveVerticalCover Up/down blind calculation with safety margins / window-depth / sill-height
engine/covers/horizontal.py AdaptiveHorizontalCover Awning extension length
engine/covers/tilt.py AdaptiveTiltCover Slat tilt angle (clamped to mode max)
engine/covers/venetian.py VenetianCoverCalculation Dual-axis venetian blind calculations (position + tilt)

8. Cover-Type Polymorphism (cover_types/)

The integration supports four cover types today (cover_blind, cover_awning, cover_tilt, cover_venetian). All cover-type-specific behaviour lives behind CoverTypePolicy β€” code outside cover_types/ never branches on the cover-type string.

File Purpose
cover_types/base.py CoverTypePolicy ABC, CoverAxis dataclass, CAP_* constants, caps_get() accessor
cover_types/blind.py BlindPolicy (vertical)
cover_types/awning.py AwningPolicy (open=blocks-sun)
cover_types/tilt.py TiltPolicy (slat-only)
cover_types/venetian.py VenetianPolicy (dual-axis, owns DualAxisSequencer)

Polymorphism boundaries:

  • HA service to call (set_cover_position vs set_cover_tilt_position) β†’ policy.select_default_axis(caps).service
  • HA state attribute to read β†’ policy.read_axis_value(...)
  • "Open lets sun in" semantic (awning differs) β†’ axis.open_blocks_sun, exposed via policy.position_for_intent(sun_through=...)
  • Glare zones support β†’ policy.supports_glare_zones ClassVar
  • Post-position-command settle/tilt sequence β†’ policy.after_position_command(...)
  • Capability flag reads (has_open, has_close, has_stop) β†’ caps_get(caps, CAP_HAS_*) (named constants in cover_types/base.py, no string literals at use sites)

The tests/test_cover_types/test_axes.py module includes a regression guard that fails CI if any production file outside cover_types/ reintroduces a caps.get("has_*") literal.

9. Configuration Types (config_types.py)

Typed dataclasses replacing raw dict lookups throughout the codebase:

Class Purpose
CoverConfig Common cover geometry options (window azimuth, FOV, sunset offsets, position limits). Built via CoverConfig.from_options().
VerticalConfig / HorizontalConfig / TiltConfig Cover-type-specific geometry.
RuntimeConfig All operational options the coordinator reads each cycle (motion, weather, time-window, manual-override, tracking thresholds). Built via RuntimeConfig.from_options() β€” single source of truth for DEFAULT_* values consumed by _update_options.

const.OPTION_RANGES: dict[str, tuple[float, float]] is the single source of truth for (min, max) numeric bounds. Both services/options_service.FIELD_VALIDATORS (programmatic validators) and config_flow.py (UI selectors) read from it; the contract test in tests/test_option_ranges.py enforces drift-free coverage.

10. Utility Modules

File Purpose
position_utils.py PositionConverter: percentage conversion and limit application
geometry.py SafetyMarginCalculator, EdgeCaseHandler for geometric accuracy
enums.py Type-safe enumerations (CoverType, TiltMode, ClimateStrategy, etc.)
const.py Named constants for thresholds, multipliers, defaults; OPTION_RANGES (numeric option bounds)
helpers.py General utility functions; should_use_tilt, check_cover_features, state_attr
services/configuration_service.py Config entry parsing, parameter extraction
services/options_service.py Programmatic option mutation; FIELD_VALIDATORS consume const.OPTION_RANGES

11. Entity Base Classes (entity_base.py)

  • AdaptiveCoverBaseEntity: shared device_info, coordinator handling
  • AdaptiveCoverSensorBase: base for sensors
  • AdaptiveCoverDiagnosticSensorBase: base for diagnostic sensors

12. Platform Entities

Sensor Platform (sensor.py): Cover Position (consolidated, includes position explanation and control method as attributes), Start/End Sun Times, diagnostic sensors (sun azimuth/elevation, control status, decision trace, and more)

Switch Platform (switch.py): Automatic Control, Climate Mode, Manual Override

Binary Sensor Platform (binary_sensor.py): Sun Visibility, Position Mismatch

Button Platform (button.py): Manual Override Reset

Data Flow

1. Entity state change (sun / cover / motion)
        β”‚
        β–Ό
2. Coordinator event handler
        β”‚
        β–Ό
3. State providers build snapshot
   SunProvider  β†’ SunData
   ClimateProvider β†’ ClimateReadings
        β”‚
        β–Ό
4. Pure calculation engine
   AdaptiveXXXCover.calculate_position()
        β”‚
        β–Ό
5. Override pipeline
   PipelineRegistry.evaluate() β†’ PipelineResult
        β”‚
        β–Ό
6. Post-processing
   Interpolation, inverse state, position limits (PositionConverter)
        β”‚
        β–Ό
7. Cover commands
   CoverCommandService β†’ cover.set_cover_position
        β”‚
        β–Ό
8. DiagnosticsBuilder
   Produces diagnostic data + decision trace from PipelineResult
        β”‚
        β–Ό
9. coordinator.data updated β†’ entities refresh

Configuration Structure

config_entry.data (Setup Phase)

  • name: instance name
  • cover_type: cover_blind / cover_awning / cover_tilt / cover_venetian (renamed from sensor_type in v4)

config_entry.options (User Configurable)

Window Properties: azimuth, fov_left, fov_right, min_elevation, max_elevation, window_height, distance_shaded_area, window_depth, sill_height

Position Limits: min_position, max_position, apply_limits_during_tracking_only (merged from enable_min_position + enable_max_position in v4)

Cover entities: covers (list of HA cover entity_ids β€” renamed from group in v4)

Automation: delta_position, delta_time, start_time, end_time, manual_override_duration, manual_threshold

Climate Mode: temp_entity, presence_entity, weather_entity, temp_low, temp_high, weather_state, lux_entity, lux_threshold, irradiance_entity, irradiance_threshold

Force Override: force_override_sensors, force_override_position

Motion Control: motion_sensors, motion_timeout

Blind Spots: blind_spot_left, blind_spot_right, blind_spot_elevation

Configuration Flow UI

The integration provides a comprehensive multi-step configuration UI (config_flow.py):

Enhanced user experience:

  • Rich field descriptions: every configuration field includes detailed descriptions with practical examples, recommended values, and context
  • Visual units: all numeric selectors display appropriate units (Β°, %, m, cm, minutes, lux, W/mΒ²)
  • Consistent interface: NumberSelector with sliders for most numeric inputs, providing clear min/max bounds
  • Technical term explanations: concepts like azimuth, FOV, and elevation are explained in user-friendly language

Translation support:

  • English is the single source of truth: translations/en.json
  • Shipped languages: English (en), German (de), French (fr)
  • Additional languages are added via the acp-translate skill on maintainer request; see the Translations wiki page

Configuration steps:

  1. Initial setup: choose cover type (vertical/horizontal/tilt)
  2. Cover-specific settings: dimensions, orientation, tracking parameters
  3. Automation settings: delta position/time, manual override, start/end times
  4. Climate mode (optional): temperature, presence, weather, lux/irradiance sensors
  5. Weather conditions (if climate mode enabled)
  6. Blind spot (optional): define obstacles that block sun
  7. Interpolation (optional): custom position mapping for non-standard covers

Best practices for config flow changes:

  • Always add data_description for new fields in translations/en.json, then run the acp-translate skill to propagate to DE/FR
  • Use NumberSelector with unit_of_measurement for all numeric inputs
  • Provide practical examples and typical values in descriptions
  • Test configuration flow on mobile and desktop interfaces
  • Keep descriptions concise but informative (2–4 sentences ideal)

Inverse State Behavior

CRITICAL: do not change this behavior without careful consideration.

The inverse_state feature handles covers that don't follow Home Assistant guidelines:

  1. Calculate position (0–100)
  2. Apply inverse if enabled: state = 100 - state
  3. For open/close-only covers: compare inverted state to threshold
  4. Send command to cover

The order of inversion and threshold checking must never change.

Extension Points

New override type

  1. Create handler in pipeline/handlers/ implementing OverrideHandler
  2. Set priority relative to existing handlers
  3. Register in coordinator

New cover type

Full checklist lives in CODING_GUIDELINES.md β€Ί When Adding a New Cover Type β€” kept canonical there so the wiki and repo don't drift. Short version:

  1. (Optional) Add a calculation class under engine/covers/ β€” reuse an existing one if the geometry matches (VenetianPolicy reuses AdaptiveVerticalCover).
  2. Create a CoverTypePolicy subclass under cover_types/. Declare axes, set the supports_* / exposes_* / custom_position_* ClassVar flags relevant to the new type, override wiki_anchor() and display_label(), implement build_calc_engine.
  3. Register in cover_types/POLICY_REGISTRY and add a row to tests/test_cover_types/test_axes.py:ALL_COVER_TYPES.

The parametrised invariant tests in tests/test_cover_types/test_invariants.py and the regression scan in test_axes.py pick up the new type automatically. If you find yourself editing coordinator.py, managers/*, sensor.py, switch.py, binary_sensor.py, or any pipeline handler, stop β€” the file is missing a hook on CoverTypePolicy and that's where the change belongs.

New state source

  1. Create provider in state/ returning a frozen dataclass
  2. Inject into coordinator and pass to calculation engine or pipeline context

Testing

  • ~2,876 tests (run venv/bin/python -m pytest tests/ -n auto -q for the current count)
  • Calculation engine tests require no HA mocks (zero HA imports in engine/covers/, sun.py)
  • Each manager, pipeline handler, state provider, and engine module has dedicated test coverage
  • Key test files: tests/test_property_based.py, tests/test_geometric_accuracy.py, tests/test_motion_control.py, tests/test_force_override_sensors.py, tests/test_engine/, tests/test_cover_types/test_axes.py, tests/test_runtime_config.py, tests/test_option_ranges.py

File Organization

custom_components/adaptive_cover_pro/
  __init__.py                        # Integration entry point
  coordinator.py                     # Thin orchestrator
  calculation.py                     # Backward-compat re-export shim for engine.covers.*
  sun.py                             # Pure solar calculations (0 HA imports)
  config_flow.py                     # Configuration UI
  config_types.py                    # CoverConfig + RuntimeConfig typed dataclasses

  engine/                            # Pure calculation engine (0 HA imports)
    sun_geometry.py                  # SunGeometry dataclass
    covers/
      base.py                        # AdaptiveGeneralCover base
      vertical.py                    # AdaptiveVerticalCover
      horizontal.py                  # AdaptiveHorizontalCover
      tilt.py                        # AdaptiveTiltCover
      venetian.py                    # VenetianCoverCalculation

  cover_types/                       # CoverTypePolicy hierarchy (cover-type-specific behaviour)
    base.py                          # CoverTypePolicy ABC, CoverAxis, CAP_* constants, caps_get
    blind.py                         # BlindPolicy
    awning.py                        # AwningPolicy (open=blocks-sun)
    tilt.py                          # TiltPolicy
    venetian.py                      # VenetianPolicy (owns DualAxisSequencer)

  managers/                          # Focused coordinator responsibilities
    manual_override.py               # AdaptiveCoverManager
    grace_period.py                  # GracePeriodManager
    motion.py                        # MotionManager
    time_window.py                   # TimeWindowManager
    toggles.py                       # ToggleManager
    weather.py                       # WeatherManager
    cover_command.py                 # CoverCommandService
    dual_axis_sequencer.py           # DualAxisSequencer (venetian only)
    position_verification.py         # PositionVerificationManager

  state/                             # HA boundary layer (all HA reads)
    climate_provider.py              # ClimateProvider β†’ ClimateReadings
    cover_provider.py                # CoverProvider β†’ cover entity state
    snapshot.py                      # SunSnapshot, CoverStateSnapshot, CoverCapabilities
    sun_provider.py                  # SunProvider β†’ SunData

  pipeline/                          # Override priority chain
    registry.py                      # PipelineRegistry
    types.py                         # PipelineSnapshot, PipelineResult, DecisionStep
    handler.py                       # OverrideHandler abstract base
    helpers.py                       # Shared position helpers
    handlers/
      force_override.py              # Priority 100
      weather.py                     # Priority 90
      manual_override.py             # Priority 80
      custom_position.py             # Priority 1–99 (default 77, up to 4 slots)
      motion_timeout.py              # Priority 75
      cloud_suppression.py           # Priority 60
      climate.py                     # Priority 50
      glare_zone.py                  # Priority 45
      solar.py                       # Priority 40
      default.py                     # Priority 0

  diagnostics/
    builder.py                       # DiagnosticsBuilder, DiagnosticContext
    event_buffer.py                  # EventBuffer (shared diagnostic ring)

  entity_base.py                     # Base entity classes
  sensor.py                          # Sensor platform
  switch.py                          # Switch platform
  binary_sensor.py                   # Binary sensor platform
  button.py                          # Button platform
  helpers.py                         # Utility functions
  const.py                           # Constants + OPTION_RANGES (single source of numeric bounds)
  enums.py                           # Type-safe enumerations
  geometry.py                        # Geometric utilities
  position_utils.py                  # Position conversion utilities
  services/
    configuration_service.py         # Config entry parsing
    options_service.py               # Programmatic option mutation; FIELD_VALIDATORS
    diagnostics_service.py           # Diagnostics entry-point
    export_service.py                # YAML export

Additional Resources

Getting Help

Clone this wiki locally