Add domain knowledge docs for AI agents (#29228)
This commit is contained in:
parent
cd176a4399
commit
c968d68947
6 changed files with 736 additions and 0 deletions
24
AGENTS.md
24
AGENTS.md
|
|
@ -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
147
docs/agents/core-domain.md
Normal 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
|
||||
269
docs/agents/easee-architecture.md
Normal file
269
docs/agents/easee-architecture.md
Normal 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.
|
||||
148
docs/agents/hardware-integrations.md
Normal file
148
docs/agents/hardware-integrations.md
Normal 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.
|
||||
61
docs/agents/plugin-system.md
Normal file
61
docs/agents/plugin-system.md
Normal 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
87
docs/agents/web-ui-api.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue