-
Notifications
You must be signed in to change notification settings - Fork 14
Developer Architecture
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.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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
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
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.
Zero homeassistant imports. Receives pre-computed data from providers.
Note:
calculation.pyis now a thin re-export shim β the real classes live inengine/covers/. New code should import from there directly. The shim exists for backward compatibility with consumers that still referencefrom .calculation import AdaptiveVerticalCover.
- Receives
SunData(nothass) - Shared sun position calculations, FOV validation, elevation limits, blind spot detection
- 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
- In/out awnings
- Uses vertical calculation + trigonometry for horizontal extension
- Output: extension length β percentage
- Slat rotation for venetian/tilt covers
- Calculates optimal slat angle to block sun while allowing light
- Output: degrees β percentage clamped to [0, max_degrees]
-
ClimateCoverState (in
pipeline/handlers/climate.py): receives pre-readClimateReadingsfromClimateProvider(no direct HA reads)- Winter: open for solar heating
- Summer: close for heat blocking
- Presence-aware strategies
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 |
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
ClimateCoverStateuse these; do not inline these patterns in new handlers
Registry (pipeline/registry.py):
-
PipelineRegistryevaluates handlers in descending priority order - Returns the first
PipelineResultthat 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().
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
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) |
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_positionvsset_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 viapolicy.position_for_intent(sun_through=...) - Glare zones support β
policy.supports_glare_zonesClassVar - 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 incover_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.
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.
| 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
|
-
AdaptiveCoverBaseEntity: shareddevice_info, coordinator handling -
AdaptiveCoverSensorBase: base for sensors -
AdaptiveCoverDiagnosticSensorBase: base for diagnostic sensors
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
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
-
name: instance name -
cover_type:cover_blind/cover_awning/cover_tilt/cover_venetian(renamed fromsensor_typein v4)
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
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:
NumberSelectorwith 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-translateskill on maintainer request; see the Translations wiki page
Configuration steps:
- Initial setup: choose cover type (vertical/horizontal/tilt)
- Cover-specific settings: dimensions, orientation, tracking parameters
- Automation settings: delta position/time, manual override, start/end times
- Climate mode (optional): temperature, presence, weather, lux/irradiance sensors
- Weather conditions (if climate mode enabled)
- Blind spot (optional): define obstacles that block sun
- Interpolation (optional): custom position mapping for non-standard covers
Best practices for config flow changes:
- Always add
data_descriptionfor new fields intranslations/en.json, then run theacp-translateskill to propagate to DE/FR - Use
NumberSelectorwithunit_of_measurementfor 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)
CRITICAL: do not change this behavior without careful consideration.
The inverse_state feature handles covers that don't follow Home Assistant guidelines:
- Calculate position (0β100)
- Apply inverse if enabled:
state = 100 - state - For open/close-only covers: compare inverted state to threshold
- Send command to cover
The order of inversion and threshold checking must never change.
- Create handler in
pipeline/handlers/implementingOverrideHandler - Set priority relative to existing handlers
- Register in coordinator
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:
- (Optional) Add a calculation class under
engine/covers/β reuse an existing one if the geometry matches (VenetianPolicyreusesAdaptiveVerticalCover). - Create a
CoverTypePolicysubclass undercover_types/. Declareaxes, set thesupports_*/exposes_*/custom_position_*ClassVarflags relevant to the new type, overridewiki_anchor()anddisplay_label(), implementbuild_calc_engine. - Register in
cover_types/POLICY_REGISTRYand add a row totests/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.
- Create provider in
state/returning a frozen dataclass - Inject into coordinator and pass to calculation engine or pipeline context
- ~2,876 tests (run
venv/bin/python -m pytest tests/ -n auto -qfor 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
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
- User Documentation: README
- Home Assistant Docs: https://developers.home-assistant.io/
- Python Async Guide: https://docs.python.org/3/library/asyncio.html
- Ruff Documentation: https://docs.astral.sh/ruff/
- Issues: https://github.com/jrhubott/adaptive-cover-pro/issues
- Discussions: https://github.com/jrhubott/adaptive-cover-pro/discussions
- Home Assistant Community: https://community.home-assistant.io/
π Home Β· β¨ Features Β· π° What's New
π Getting Started
- Installation
- Migrating from Custom Repository
- Migrating from Adaptive Cover
- First-Time Setup
- Cover Types
π§ Core Concepts
π Cover Types
βοΈ Configuration
- Sun Tracking
- Position
- Glare Zones
- Automation
- Custom Position
- Force Override
- Weather Safety
- Climate
- Blindspot
- Summary Screen
- Debug & Diagnostics
π Entities & Services
- Entities
- Proxy Cover Entity
- Position Verification
- My Position Support (Somfy RTS)
- Runtime Configuration Services
π οΈ Operations
π§ Advanced Use Cases
- Dynamic Temperature Thresholds
- Dynamic Tracking Window
- Bedroom Sleep Mode
- Handling Variable Cloud Cover
- Venetian Tilt-Only on Overcast Days
π¨ Dashboard
π§ͺ Testing & Simulation
π Reference
π©βπ» For Developers