evcc-io/docs/agents/core-domain.md
2026-04-19 14:26:07 +02:00

147 lines
5.6 KiB
Markdown

# Core Domain: Site, Loadpoint, and the Control Loop
## Object Hierarchy
```
Site (orchestrator — core/site.go)
├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]
├── Tariffs: Grid, FeedIn, CO2, Solar
├── Coordinator (vehicle <-> loadpoint assignment)
├── Prioritizer (power allocation fairness)
└── Loadpoints[] (core/loadpoint.go)
├── Charger (api.Charger — hardware controller)
├── Vehicle (api.Vehicle — EV battery state via cloud API)
├── ChargeMeter (api.Meter — AC power at charger)
└── Circuit (optional — electrical domain limits)
```
## Key Interfaces (api/api.go)
### Meter
- `Meter` — `CurrentPower() (float64, error)` — watts
- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh
- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings
### Battery
- `Battery` — `Soc() (float64, error)` — 0-100%
- `BatteryCapacity` — kWh
- `BatteryController` — set charge/discharge/hold mode
### Charger
- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)`
- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)`
- `PhaseSwitcher` — `Phases1p3p(int) error`
- `ChargeRater` — `ChargedEnergy() (float64, error)`
- `ChargeTimer` — `ChargeDuration() (time.Duration, error)`
### Vehicle
- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()`
- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehicleFinishTimer`, `VehiclePosition`
- `ChargeController` — remote start/stop on vehicle
- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits
- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side
## Charge Modes
| Mode | Behavior |
|------|----------|
| `OFF` | Disabled (unless welcome charge) |
| `NOW` | Max current immediately |
| `MINPV` | Min current when PV surplus; fast if cheap tariff |
| `PV` | Ramp current proportional to available solar |
## Charge States (IEC 61851)
- `A` — not connected
- `B` — connected, not charging
- `C` — connected, charging
## The Control Loop (Site.update — runs every N seconds)
```
1. Update all meters (grid, PV, battery, aux)
2. For each loadpoint: UpdateChargePowerAndCurrents()
3. Calculate site power balance:
sitePower = gridPower + batteryPower + excessDCPower
+ residualPower - auxPower - flexiblePower
4. Apply battery priority rules (prioritySoc, bufferSoc)
5. Get tariff rates
6. For EACH loadpoint: Update(sitePower, ...)
├── Read charger status
├── Detect/identify vehicle
├── Check plan requirements (minSOC, target time)
├── Check limits (limitSOC, limitEnergy)
├── MODE switch -> calculate target current
├── Cap at maxCurrent, respect circuit limits
├── Send MaxCurrent() to charger
└── Record metrics
7. Push updates to WebSocket + metrics
```
The loop is stateless per cycle: always re-reads actual state, calculates
optimal current, sends single command. Resilient to restarts and missed updates.
## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)
```
1. Read effective min/max current limits
2. Reduce sitePower by battery boost power
3. Consider phase switching (1p <-> 3p) if supported
4. deltaCurrent = powerToCurrent(-sitePower, activePhases)
targetCurrent = effectiveCurrent + deltaCurrent
5. Below minCurrent -> start disable timer (default 3 min)
6. Surplus returns -> start enable timer (default 1 min)
7. Cap at maxCurrent
```
## Battery Priority Rules
| Setting | Effect |
|---------|--------|
| `prioritySoc` | Below this: battery charges first, EV gets 0 |
| `bufferSoc` | Above this: EV can draw from battery reserves |
| `bufferStartSoc` | Above this: EV charging can begin even if importing |
## Effective Price Calculation
```
greenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower
effectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare
```
## Concurrency Model
- **Site** owns `RWMutex` for its state (meters, battery, tariffs)
- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current)
- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking
- No global locks — ordering prevents deadlocks
### Channels
| Channel | Scope | Buffer | Purpose |
|---------|-------|--------|---------|
| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) |
| `lpUpdateChan` | Site | 1 | Early loadpoint update requests |
| `pushChan` | Loadpoint | Buffered | User notifications |
## Tariff Integration
Types: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar`
### Smart Features
- **Cheap-tariff override** — rate below threshold -> fast charge
- **Smart feed-in** — feed-in rate above threshold -> prioritize export
- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline
- `optimalPlan()` — cheapest non-contiguous slots
- `continuousPlan()` — cheapest continuous window (fallback)
## Key File Locations
- `api/api.go` — all core interfaces
- `core/site.go` — Site orchestrator + control loop
- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch)
- `core/site_battery.go` — battery priority logic
- `core/site_tariffs.go` — tariff integration
- `core/planner/planner.go` — charge time optimization
- `core/prioritizer/prioritizer.go` — power allocation across loadpoints
- `core/circuit/circuit.go` — electrical domain limits