Add domain knowledge docs for AI agents (#29228)

This commit is contained in:
Michael Heß 2026-04-19 14:26:07 +02:00 • committed by GitHub
parent cd176a4399
commit c968d68947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 736 additions and 0 deletions

View file

@ -24,6 +24,30 @@ This file provides guidance to AI coding agents when working with code in this r
- `evcc --template-type [type] --template [file]` - test device templates
- `make docs` - generate template documentation
## Domain Knowledge
Deep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:
| File | When to load |
|------|-------------|
| [Core Domain](docs/agents/core-domain.md) | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, interfaces |
| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices |
| [Easee Architecture](docs/agents/easee-architecture.md) | Easee charger (REST+SignalR, async correlation, concurrency) |
| [Plugin System](docs/agents/plugin-system.md) | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS) |
| [Web UI & API](docs/agents/web-ui-api.md) | REST API, WebSocket, Vue frontend, authentication |
### Loading guide by task type
- **Charger implementation** — hardware-integrations + core-domain
- **Easee charger work** — easee-architecture + core-domain
- **Meter implementation** — hardware-integrations + plugin-system
- **Vehicle implementation** — hardware-integrations
- **UI/frontend work** — web-ui-api
- **API endpoint work** — web-ui-api + core-domain
- **Config/template work** — plugin-system
- **Control loop / charging logic** — core-domain
- **Bug in any area** — core-domain + relevant topic file(s)
## Architecture Guidelines
### Core Components

147
docs/agents/core-domain.md Normal file
View file

@ -0,0 +1,147 @@
# 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

View file

@ -0,0 +1,269 @@
# Easee Charger Architecture
The Easee integration communicates with the Easee cloud over two distinct channels:
1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration.
2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations.
Commands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.
## Authentication
### Flow
Authentication uses username/password credentials against:
```
POST https://api.easee.com/api/accounts/login
```
Returns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`.
### Token Lifecycle
Wrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls:
```
POST https://api.easee.com/api/accounts/refresh_token
```
Falls back to full re-login if refresh fails.
### Token Caching
`TokenSource` is shared per user via `cache.New[oauth2.TokenSource]()`. Multiple `Easee` instances with the same user email share a single token source, preventing redundant re-authentication.
## Initialization Sequence
`NewEasee` performs these steps in order:
1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger.
2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control.
3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates).
4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change.
5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`).
6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds.
## SignalR Back-Channel
### Why SignalR is Required
1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`.
2. **State is event-driven, not pollable.** No REST endpoint streams charger state.
3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out.
### Server -> Client Methods
#### `ProductUpdate(json.RawMessage)`
Primary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`.
- **Timestamp deduplication**: older timestamps for the same ID are silently dropped.
- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select.
#### `CommandResponse(json.RawMessage)`
Async acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID).
Routes through three maps in order:
1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands
2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch
3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse
Unmatched responses are logged as WARN (rogue response from external system).
#### `ChargerUpdate` / `SubscribeToMyProduct`
Logged at TRACE, not processed further.
## Command Flow and Async Correlation
### REST Command Endpoints
```
POST /api/chargers/{chargerID}/commands/{action} (start/stop/pause/resume)
POST /api/chargers/{chargerID}/settings (enable, DCC, PhaseMode, SmartCharging)
POST /api/sites/{siteID}/circuits/{circuitID}/settings (dynamic circuit currents)
```
### Response Handling
| HTTP Status | Meaning | Behavior |
|-------------|---------|----------|
| `200` | Synchronous / already applied | Returns immediately |
| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse |
| other | Error | Returns error |
### Ticks Correlation
On 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op.
Each in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`.
### The Sync/Async Mismatch
Some endpoints return HTTP `200` but still fire a `CommandResponse` via SignalR. The observed case is circuit settings (`POST /api/sites/{siteID}/circuits/{circuitID}/settings`) which returns `200` but generates `CommandResponse` with `ID=22` (`CIRCUIT_MAX_CURRENT_P1`).
Handled via the **expected-orphan counter**:
```go
expectedOrphans map[easee.ObservationID]int // protected by cmdMu
```
Before a POST to a known 200-returning endpoint, increment the counter. When `CommandResponse` arrives with no pending match, decrement and silently consume. Counter at 0 means genuinely rogue.
## State Management
### Internal State Fields (all protected by `sync.RWMutex`)
| Field | Observation | Notes |
|-------|------------|-------|
| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine |
| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state |
| `smartCharging` | `SMART_CHARGING` (102) | LED color mode |
| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) |
| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling |
| `totalEnergy` | `LIFETIME_ENERGY` (124) | kWh, updated ~hourly |
| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A |
| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase |
| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit |
| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) |
| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit |
| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum |
| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F |
| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token |
### Session Energy Zero-value Protection
`sessionEnergy` is never set to `0` from a `ProductUpdate` — the API sends spurious zeros erratically. Session reset is driven by op-mode transition: when `CHARGER_OP_MODE` transitions from disconnected to awaiting-start, `sessionEnergy` resets to `0` with a fresh timestamp.
## Charger Operation Modes
```
0 = Offline — no cloud connection
1 = Disconnected — no car plugged in
2 = AwaitingStart — car plugged, waiting for authorization/start
3 = Charging — actively charging
4 = Completed — car full or finished, cable still plugged
5 = Error — fault condition
6 = ReadyToCharge — ready, current available
7 = AwaitingAuthentication — RFID auth required
8 = Deauthenticating — finishing authentication teardown
```
### Mapping to evcc Status
| opMode | evcc Status |
|--------|------------|
| 1 (Disconnected) | A |
| 2, 4, 6, 7, 8 | B |
| 3 (Charging) | C |
| 0, 5 and others | error |
## Enable/Disable Flow
### Enable = true
1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait.
2. If `opMode == Disconnected`: return (no cable).
3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`.
4. Otherwise: action = `resume_charging`.
5. POST `/commands/{action}` and wait.
6. Wait for `opMode` to reach enabled state.
7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume).
8. Call `MaxCurrent(c.current)` to restore previous setpoint.
### Enable = false
1. If disconnected or (awaiting auth && !authorize): return.
2. POST `/commands/pause_charging` and wait.
3. Wait for `opMode` to reach disabled state.
4. Wait for `dynamicChargerCurrent` to reach `0`.
### State Waiting Pattern
Both `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use:
1. Short-circuit check: if already in target state, return immediately.
2. Open a timer.
3. Loop on `obsC` channel.
4. On timer expiry: **one final check** before returning `api.ErrTimeout`.
The final check handles the race where the state update arrived between the last channel read and the timer fire.
## Phase Control
### Circuit-Level (preferred, when circuit is known)
Phase switching by zeroing dynamic circuit current on unused phases:
```
POST /api/sites/{siteID}/circuits/{circuitID}/settings
```
For 1-phase: set P2=0, P3=0. For 3-phase: restore all three.
This POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan).
### Charger-Level (fallback)
Uses `PhaseMode` setting: `1` for single-phase, `2` (auto) for 3-phase.
After changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.
## Authorization Mode (`authorize`)
When `authorize: true`, evcc sends `start_charging` to authorize sessions when the charger enters `ModeAwaitingAuthentication`. This enables fully unattended operation but is incompatible with RFID-based vehicle identification.
When `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).
Setting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.
## Concurrency Model
### Mutexes
| Mutex | Type | Protects |
|-------|------|----------|
| `c.mux` | `sync.RWMutex` | All charger state fields |
| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) |
Command dispatch was extracted into `charger/easee/dispatcher.go` (`CommandDispatcher` struct). The two mutexes are intentionally separate to prevent the SignalR receive loop from blocking on command dispatch operations.
### Observation Channel
`obsC chan Observation` is unbuffered. `ProductUpdate` sends via non-blocking select — if no waiter is listening, the notification is dropped. The authoritative state is always in the struct fields; the channel is only a notification mechanism.
**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`.
## Known Design Concerns
1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified.
2. **LIFETIME_ENERGY** — inaccurate by design, API pushes updates ~hourly.
3. **current vs dynamicChargerCurrent drift** — evcc's desired setpoint and charger's confirmed value can drift around pause/resume cycles. Resynced via `MaxCurrent(c.current)` after resume.
4. **Multi-charger circuits** — only circuit-level phase control when charger is alone on its circuit. Multi-charger circuits fall back to less precise charger-level control.
5. **Stale CommandResponses after reconnect** — if SignalR drops mid-command, the response may arrive after reconnect with no pending entry, triggering a false-positive rogue WARN. Acceptable trade-off.
## API Endpoints Summary
| Method | Endpoint | Used For |
|--------|----------|----------|
| `POST` | `/accounts/login` | Initial authentication |
| `POST` | `/accounts/refresh_token` | Token refresh |
| `GET` | `/chargers` | Auto-discover charger ID |
| `GET` | `/chargers/{id}/site` | Discover site and circuit |
| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging |
| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging |
| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents |
| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) |
### SignalR Hub
| Endpoint | `https://streams.easee.com/hubs/chargers` |
|----------|------------------------------------------|
| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` |
| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` |
## Configuration
| Parameter | Required | Default | Notes |
|-----------|----------|---------|-------|
| `user` | yes | | Easee account email |
| `password` | yes | | Easee account password |
| `charger` | no | | Charger serial; auto-detected if exactly one on account |
| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits |
| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions |
Supported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.
Declared capabilities: `1p3p` (phase switching), `rfid` (RFID identification).
Requires evcc sponsorship.

View file

@ -0,0 +1,148 @@
# Hardware Integrations: Chargers, Meters, Vehicles
## Integration Pattern
All device types use a registry-based factory pattern:
```go
// Self-registration in init()
func init() {
registry.AddCtx("typename", NewFromConfig)
}
func NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
// Parse config, create client, return implementation
}
```
Optional interfaces are added via the decorator pattern:
```go
//go:generate decorate -f decorateXxx -b *Xxx -t "api.PhaseSwitcher,Phases1p3p,func(int) error"
```
## Charger Implementations
### By Protocol
| Protocol | Examples |
|----------|---------|
| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |
| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |
| OCPP 1.6 | Generic charge point server |
| EEBus/ISO 15118 | EEBus SPINE protocol |
| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |
| MQTT | OpenWB, Tasmota, Shelly |
| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |
### Required Charger Interface
```go
type Charger interface {
ChargeState
Enabled() (bool, error)
Enable(enable bool) error
CurrentController
}
```
Where `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and
`CurrentController` provides `MaxCurrent(current int64) error`.
### Optional Charger Interfaces
- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision
- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p
- `Meter` / `MeterEnergy` — built-in power/energy measurement
- `PhaseCurrents` / `PhaseVoltages` — per-phase readings
- `ChargeRater` — `ChargedEnergy()` for session energy
- `ChargeTimer` — `ChargeDuration()` for session time
- `Identifier` — `Identify()` for RFID/vehicle identification
### Key Implementations
- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail
- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management
- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2
- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC)
- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template
### Adding a New Charger
1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`)
2. Implement `NewXxxFromConfig()` returning `api.Charger`
3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()`
4. Register: `registry.AddCtx("xxx", factory)` in `init()`
5. Optional: add decorator for PhaseSwitcher, Meter, etc.
6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata
## Meter Implementations
### By Category
| Category | Examples |
|----------|---------|
| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |
| HTTP/REST | Homewizard, Shelly Gen3, E3DC |
| Smart Home | HomeAssistant entities, Homematic |
| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |
### Required Meter Interface
```go
type Meter interface {
CurrentPower() (float64, error) // watts
}
```
### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity`
### Key Implementations
- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection
- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries
- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources)
## Vehicle Integrations
| Manufacturer | API Type |
|-------------|----------|
| Tesla | Fleet API + vehicle-command proxy |
| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |
| Hyundai/Kia | BlueLink (regional variants) |
| BMW/Mini | ConnectedDrive v2 |
| Mercedes | Official API |
| Renault/Nissan | Renault API + Carwings |
| Ford | FordConnect (US/EU) |
| Porsche | Porsche Connect |
| PSA Group | Peugeot, Citroen, DS, Opel |
| Generic | OVMS, Tronity |
### Required Vehicle Interface
```go
type Vehicle interface {
Battery // Soc() (float64, error)
BatteryCapacity // Capacity() float64
IconDescriber // Icon() string
FeatureDescriber // Features() []Feature
PhaseDescriber // Phases() int
TitleDescriber // GetTitle() string
SetTitle(string)
Identifiers() []string
OnIdentified() ActionConfig
}
```
### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehicleFinishTimer`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector`
### Polling Strategy
Configurable: always / while charging / while connected. Interval-based caching
to avoid excessive cloud API calls. OAuth2 token handling built into each provider.
## Auto-Detection (`cmd/detect/`)
Task-based parallel IP scanning:
`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes
Detects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,
Tasmota, Shelly, Phoenix, and many more.

View file

@ -0,0 +1,61 @@
# Plugin System
The plugin system (`plugin/`) provides protocol-level abstraction for device
communication. Plugins implement typed getter/setter interfaces and are composed
into charger, meter, or vehicle implementations via configuration.
## Plugin Types
| Plugin | Protocol | Key Config |
|--------|----------|------------|
| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` |
| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` |
| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` |
| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree |
| `js` | JavaScript/WASM | Inline script evaluation |
| `go` | Go runtime | Dynamic Go code |
| `gpio` | Linux GPIO | Digital I/O for relays |
## Getter/Setter Interfaces
```go
type StringGetter func() (string, error)
type FloatGetter func() (float64, error)
type IntGetter func() (int64, error)
type BoolGetter func() (bool, error)
// + corresponding Setter types
```
## Pipeline Transforms
Plugins support chained transforms: `scale`, `offset`, `lookup`, `regex`.
## Template-Based Device Configuration
Devices can be defined entirely via YAML templates using plugins:
```yaml
# templates/definition/charger/example.yaml
status:
source: http
uri: http://{{ .host }}/status
enable:
source: http
uri: http://{{ .host }}/enable
method: POST
maxcurrent:
source: http
uri: http://{{ .host }}/current/{{ .maxcurrent }}
```
The generic configurable charger (`charger/charger.go`) wires these plugin
configs into the `api.Charger` interface at runtime.
## Key Files
- `plugin/config.go` — plugin registry and config types
- `plugin/http.go` — HTTP plugin
- `plugin/mqtt.go` — MQTT plugin
- `plugin/modbus.go` — Modbus plugin
- `plugin/sunspec.go` — SunSpec plugin
- `charger/charger.go` — generic configurable charger using plugins

87
docs/agents/web-ui-api.md Normal file
View file

@ -0,0 +1,87 @@
# Web UI & REST API
## Server Architecture
- **Router:** gorilla/mux, strict slash
- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth
- **Timeouts:** Read 5s, Write 10s, Idle 120s
- **Static assets:** embedded in binary (`fs.FS`)
- **Default port:** 7070
## REST API (base `/api/`)
### Site-level
- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc.
- `GET /tariff/{tariff}` — tariff rates
- `GET /sessions` — charging history
- `GET /state` — complete system state (supports jq filtering)
### Per-loadpoint (`/loadpoints/{id}/...`)
- `POST mode/{value}` — off/now/minpv/pv
- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits
- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits
- `POST phases/{value}` — phase config
- `POST priority/{value}`, `batteryboost/{value}`
- `POST plan/energy/{value}/{time}` — schedule plan
- `POST vehicle/{name}` — select vehicle
- `POST smartcostlimit/{value}` — smart cost threshold
### Configuration (`/config/...`, auth required)
- CRUD for devices (chargers, meters, vehicles, tariffs)
- Template browsing and testing
- Site, loadpoint, circuit, HEMS, messaging config
- `GET /config/evcc.yaml` — YAML export
### System (`/system/...`, auth required)
- Log viewing, cache clear, DB backup/restore/reset, shutdown
### Handler Pattern
Generic `handler[T]` with type conversion, setter, getter.
Specialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`.
## WebSocket (`/ws`)
- `coder/websocket` (RFC 6455)
- Pub/sub via `SocketHub`
- Buffered channels (1024 per subscriber)
- Welcome message with full state snapshot
- Incremental updates as JSON key-value pairs with dot-notation keys:
```json
{"loadpoints.1.mode": "solar", "site.gridPower": 1234}
```
- Write timeout: 10s, compression (disabled for Safari)
## State Flow
1. WS connects -> receives welcome with full state
2. App emits `util.Param` on changes
3. Hub broadcasts to subscribers
4. Frontend `store.update(msg)` merges via dot-notation
5. Components reactively re-render
## Authentication
- JWT, 90-day lifetime
- HttpOnly cookie (`auth`) with `SameSite=Strict`
- Also accepts `Authorization: Bearer <token>` header
- Modes: Disabled, Locked (demo), Configured (password)
- Protects `/api/config` and `/api/system`
## MQTT Integration
- Publishes state changes to configurable broker
- Subscribes to control topics
- Retained messages for state persistence
## Key Files
- `server/http.go` — router setup
- `server/http_auth.go` — authentication
- `server/http_site_handler.go` — state + request handlers
- `server/http_config_*.go` — config endpoints
- `server/http_loadpoint_handler.go` — per-loadpoint endpoints
- `server/socket.go` — WebSocket pub/sub
- `assets/js/app.ts` — Vue app entry
- `assets/js/store.ts` — reactive state store
- `assets/js/api.ts` — Axios clients
- `assets/js/router.ts` — route definitions