Model layer development

SkillProductivity

Linear programming model layer standards — constraints, costs, decision variables, connection segments, reactive parameter tracking, shadow prices, and the HiGHS solve strategy. Use when working under custom_components/haeo/core/model/, when optimization results look wrong, when the model is infeasible, or when solve time regresses.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Model layer development skill

What this skill tells your AI

The instructions your AI receives, as published by hass-energy/haeo in .agents/skills/model/SKILL.md and read by ahel’s review.

core/model/ is a standalone linear programming library. It has no Home Assistant imports and import-linter enforces that: it may import only highspy, numpy, and typing_extensions. Anything needing Home Assistant belongs in the adapter layer.

Mathematical background lives in docs/modeling/. Read tagged power and shadow prices before touching policies or dual values. When model behavior or formulation changes, update the corresponding page in docs/modeling/.

Units

Use SI-derived units scaled for numerical stability throughout all model calculations:

  • Power: kilowatts (kW)
  • Energy: kilowatt-hours (kWh)
  • Time: hours (model layer uses hours; rest of codebase uses seconds)
  • Price: dollars per kilowatt-hour ($/kWh)

This scaling keeps values in the ideal range for LP solvers. The adapter layer handles conversion between model time (hours) and Home Assistant time (seconds). See units.md for rationale and conversion utilities.

periods is an array of variable-width interval durations, because the planning horizon is tiered. Never assume a constant period length — multiply by periods element-wise.

Model elements

The device layer's element types compile down to a small fixed set of model elements: power balance nodes, directed connections, batteries, and the policy pricing elements generated by policy compilation. A single device element expands into several of these through its adapter's model_elements(), with sub-elements named {main_element}:{subname}.

Adding a physical behavior rarely means a new model element type. Connections are composed of segments, so a new kind of loss, limit, or charge is a new Segment subclass added to the chain rather than a branch inside Connection.

Constraints and costs

  • Express all constraints as linear inequalities or equalities. No products of variables, no conditionals on variable values, no absolute values without the standard two-variable split.
  • Document the physical meaning of each constraint.
  • Costs must be linear in the decision variables, with positive meaning cost and negative meaning revenue.
  • Document the economic interpretation of each cost.
  • Use descriptive variable names that reflect physical quantities.

Reactive parameter tracking

core/model/reactive/ is what makes re-optimization cheap. Understand it before editing any element:

  • TrackedParam — a descriptor. Assigning a new value invalidates exactly the constraints and costs that read it. Declare parameters as TrackedParam class attributes; values stashed in plain instance attributes are invisible to invalidation.
  • @constraint — declares a constraint method that the network discovers and aggregates. @constraint(output=True, unit=...) also surfaces the constraint's dual as a shadow-price output.
  • @cost — declares a linear cost contribution.
  • @output — declares a named output the adapter layer can map to sensors.

When a forecast updates, only affected constraints are rebuilt and the solver warm-starts. A change that makes every constraint depend on every parameter fails no test — it just makes optimization slow. The period update and warm start tests are the ones that notice.

Optimization

Network.optimize() runs a calibrated scheme: a lexicographic solve first (minimize cost, then minimize a time-preference secondary objective to break ties deterministically), then a calibrated blend weight, then single blended solves. This exists so repeated solves warm-start cleanly and still produce meaningful shadow prices. Degenerate LPs have many optimal solutions; the secondary objective is what stops sensor values jittering between equally optimal plans.

  • Catch optimization failures gracefully and return appropriate status indicators.
  • Log solver diagnostics at debug level.

Policy compilation

core/adapters/policy_compilation.py implements source-to-destination pricing by tagging power with VLAN-like tags. compile_policies() mutates element configs in place, adding tag fields, and generates policy pricing elements.

It is a required step, not an optional one: connections take their tags as a required argument, so anything building a network must run compile_policies() before adding elements. Both the coordinator and the diagnostics CLI do this; any new network builder must too.

Testing

Model tests are data-driven. Add cases to core/model/tests/test_data/ rather than writing per-element test functions:

  • VALID_CASES — inputs plus expected_outputs, with LP variables fixed to known values by the factory
  • INVALID_CASES — inputs plus an expected_error regex

Test each element type independently, verify constraints are satisfied in solutions, and cover edge cases such as zero power and full or empty batteries. If a line cannot be covered by any input case, that is evidence it is unreachable — delete it rather than testing it.

For end-to-end verification of an optimizer change, run the scenario suite and read the snapshot diff.

Signals

GitHub stars
65
Forks
21
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
model
Source
github.com/hass-energy/haeo