{"owner":"evcc-io","repo":"evcc","hasSkills":true,"totalSkillsCount":9,"totalTokensCount":16269,"categories":["subagent-persona","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["docs/agents/easee-architecture.md",".github/agents/template.agent.md","docs/agents/ocpp-forwarder.md","docs/agents/hardware-integrations.md","docs/agents/api-security.md","docs/agents/core-domain.md","AGENTS.md","docs/agents/web-ui-api.md","docs/agents/plugin-system.md"],"skills":{"docs/agents/easee-architecture.md":"# Easee Charger Architecture\n\nThe Easee integration communicates with the Easee cloud over two distinct channels:\n\n1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration.\n2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations.\n\nCommands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.\n\n## Authentication\n\n### Flow\n\nAuthentication uses username/password credentials against:\n```\nPOST https://api.easee.com/api/accounts/login\n```\n\nReturns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`.\n\n### Token Lifecycle\n\nWrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls:\n```\nPOST https://api.easee.com/api/accounts/refresh_token\n```\nFalls back to full re-login if refresh fails.\n\n### Token Caching\n\n`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.\n\n## Initialization Sequence\n\n`NewEasee` performs these steps in order:\n\n1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger.\n2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control.\n3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates).\n4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change.\n5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`).\n6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds.\n\n## SignalR Back-Channel\n\n### Why SignalR is Required\n\n1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`.\n2. **State is event-driven, not pollable.** No REST endpoint streams charger state.\n3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out.\n\n### Server -> Client Methods\n\n#### `ProductUpdate(json.RawMessage)`\nPrimary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`.\n\n- **Timestamp deduplication**: older timestamps for the same ID are silently dropped.\n- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select.\n\n#### `CommandResponse(json.RawMessage)`\nAsync acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID).\n\nRoutes through three maps in order:\n1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands\n2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch\n3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse\n\nUnmatched responses are logged as WARN (rogue response from external system).\n\n#### `ChargerUpdate` / `SubscribeToMyProduct`\nLogged at TRACE, not processed further.\n\n## Command Flow and Async Correlation\n\n### REST Command Endpoints\n\n```\nPOST /api/chargers/{chargerID}/commands/{action}     (start/stop/pause/resume)\nPOST /api/chargers/{chargerID}/settings              (enable, DCC, PhaseMode, SmartCharging)\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings  (dynamic circuit currents)\n```\n\n### Response Handling\n\n| HTTP Status | Meaning | Behavior |\n|-------------|---------|----------|\n| `200` | Synchronous / already applied | Returns immediately |\n| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse |\n| other | Error | Returns error |\n\n### Ticks Correlation\n\nOn 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op.\n\nEach in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`.\n\n### The Sync/Async Mismatch\n\nSome 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`).\n\nHandled via the **expected-orphan counter**:\n```go\nexpectedOrphans map[easee.ObservationID]int  // protected by cmdMu\n```\n\nBefore 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.\n\n## State Management\n\n### Internal State Fields (all protected by `sync.RWMutex`)\n\n| Field | Observation | Notes |\n|-------|------------|-------|\n| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine |\n| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state |\n| `smartCharging` | `SMART_CHARGING` (102) | LED color mode |\n| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) |\n| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling |\n| `totalEnergy` | `LIFETIME_ENERGY` (124), `CHARGE_SESSION_START` (223), `CHARGING_SESSION` (129) | kWh, monotonic max, see Logical Energy Meter |\n| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A |\n| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase |\n| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit |\n| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) |\n| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit |\n| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum |\n| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F |\n| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token |\n\n### Session Energy Zero-value Protection\n\n`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. The first op mode after startup is replayed state and does not reset, so replayed session energy survives a restart. `CHARGE_SESSION_START` and a zero `SESSION_ENERGY` arrive before the mode change, so the reset is skipped when the cached session started after the previous mode observation.\n\n### Logical Energy Meter\n\n`LIFETIME_ENERGY` is pushed only ~hourly. `TotalEnergy()` therefore keeps a monotonic max of the lifetime reading, the session meter values from `CHARGE_SESSION_START` and `CHARGING_SESSION`, and `CHARGE_SESSION_START.MeterValue + sessionEnergy`. The session term is only applied while the last nonzero `SESSION_ENERGY` (or session completion) is not older than the session start, so a stale previous-session value cannot inflate the total. Ignored zero observations do not count as fresh readings.\n\n## Charger Operation Modes\n\n```\n0 = Offline               — no cloud connection\n1 = Disconnected          — no car plugged in\n2 = AwaitingStart         — car plugged, waiting for authorization/start\n3 = Charging              — actively charging\n4 = Completed             — car full or finished, cable still plugged\n5 = Error                 — fault condition\n6 = ReadyToCharge         — ready, current available\n7 = AwaitingAuthentication — RFID auth required\n8 = Deauthenticating      — finishing authentication teardown\n```\n\n### Mapping to evcc Status\n\n| opMode | evcc Status |\n|--------|------------|\n| 1 (Disconnected) | A |\n| 2, 4, 6, 7, 8 | B |\n| 3 (Charging) | C |\n| 0, 5 and others | error |\n\n## Enable/Disable Flow\n\n### Enable = true\n\n1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait.\n2. If `opMode == Disconnected`: return (no cable).\n3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`.\n4. Otherwise: action = `resume_charging`.\n5. POST `/commands/{action}` and wait.\n6. Wait for `opMode` to reach enabled state.\n7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume).\n8. Call `MaxCurrent(c.current)` to restore previous setpoint.\n\n### Enable = false\n\n1. If disconnected or (awaiting auth && !authorize): return.\n2. POST `/commands/pause_charging` and wait.\n3. Wait for `opMode` to reach disabled state.\n4. Wait for `dynamicChargerCurrent` to reach `0`.\n\n### State Waiting Pattern\n\nBoth `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use:\n1. Short-circuit check: if already in target state, return immediately.\n2. Open a timer.\n3. Loop on `obsC` channel.\n4. On timer expiry: **one final check** before returning `api.ErrTimeout`.\n\nThe final check handles the race where the state update arrived between the last channel read and the timer fire.\n\n## Phase Control\n\n### Circuit-Level (preferred, when circuit is known)\n\nPhase switching by zeroing dynamic circuit current on unused phases:\n```\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings\n```\n\nFor 1-phase: set P2=0, P3=0. For 3-phase: restore all three.\n\nThis POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan).\n\n### Charger-Level (fallback)\n\nUses `PhaseMode` setting: `1` locked single-phase, `3` locked 3-phase. `2` (auto, set by earlier versions) is reported as unknown phases so the loadpoint locks it.\nAfter changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.\n\n## Authorization Mode (`authorize`)\n\nWhen `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.\n\nWhen `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).\n\nSetting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.\n\n## Concurrency Model\n\n### Mutexes\n\n| Mutex | Type | Protects |\n|-------|------|----------|\n| `c.mux` | `sync.RWMutex` | All charger state fields |\n| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) |\n\nCommand 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.\n\n### Observation Channel\n\n`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.\n\n**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`.\n\n## Known Design Concerns\n\n1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified.\n2. **LIFETIME_ENERGY** — API pushes updates ~hourly; bridged by the logical energy meter, so accuracy between readings depends on `SESSION_ENERGY` and the session start meter value.\n3. **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.\n4. **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.\n5. **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.\n\n## API Endpoints Summary\n\n| Method | Endpoint | Used For |\n|--------|----------|----------|\n| `POST` | `/accounts/login` | Initial authentication |\n| `POST` | `/accounts/refresh_token` | Token refresh |\n| `GET` | `/chargers` | Auto-discover charger ID |\n| `GET` | `/chargers/{id}/site` | Discover site and circuit |\n| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging |\n| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging |\n| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents |\n| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) |\n\n### SignalR Hub\n\n| Endpoint | `https://streams.easee.com/hubs/chargers` |\n|----------|------------------------------------------|\n| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` |\n| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` |\n\n## Configuration\n\n| Parameter | Required | Default | Notes |\n|-----------|----------|---------|-------|\n| `user` | yes | | Easee account email |\n| `password` | yes | | Easee account password |\n| `charger` | no | | Charger serial; auto-detected if exactly one on account |\n| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits |\n| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions |\n\nSupported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.\nDeclared capabilities: `1p3p` (phase switching), `rfid` (RFID identification).\nRequires evcc sponsorship.\n",".github/agents/template.agent.md":"---\ndescription: 'Tidy templates'\ntools: [\"read\", \"edit\", \"search\",\"shell\"]\n---\n\n## Cleanse parameter defaults\n\nValidate templates files inside `templates/definition/**` against default parameters in `util/templates/defaults.yaml`.\nMake sure that templates don't repeat parts of the default parameter definition.\n\n## Reorder parameters after presets\n\nValidate templates files inside `templates/definition/**`. If the template contains parameters and presets, ensure that any parameter contained in a `preset` appears in the template after the `preset`. Reorder such parameters after the presets.\n","docs/agents/ocpp-forwarder.md":"# OCPP Forwarder Architecture\n\nThe OCPP forwarder (`charger/ocpp/forwarder.go`) is a hybrid proxy that lets a charger talk to evcc and an upstream OCPP server at the same time. Chargers connect directly to evcc's central system on the normal port. For each charger with a matching `ForwarderRule`, a \"sidecar\" WebSocket connection to the upstream server is opened and kept in parallel for the lifetime of the charger connection.\n\nThe forwarder is opt-in: hooks (`chargerConnectHook`, `chargerDisconnectHook`, `chargerMessageHook` in `instance.go`) are nil unless a rule matches, so a charger without a rule behaves exactly as before.\n\n## Forwarding modes\n\nTwo modes apply at the same time, selected per message by its action.\n\n### Transparent relay (billing-critical)\n\nFor the actions in `actionsRelayedToUpstream` (`Authorize`, `StartTransaction`, `StopTransaction`, `DataTransfer`), upstream is the authoritative Central System:\n\n1. Charger sends the Call to evcc.\n2. The message hook forwards it to the upstream sidecar and bypasses evcc's OCPP handler.\n3. Upstream's `CallResult`/`CallError` is relayed back to the charger.\n\nevcc's handler is never invoked for these. This lets the pay backend control authorization, issue its own transaction IDs, and see consistent Start/Stop pairs.\n\n### Sidecar observation (informational)\n\nFor all other messages (`BootNotification`, `StatusNotification`, `MeterValues`, `Heartbeat`, etc.):\n\n1. Charger sends the Call to evcc, which processes it normally.\n2. The same frame is also mirrored to the upstream sidecar.\n\nUpstream observes the session while evcc manages the charger as usual.\n\n## Upstream to charger (commands)\n\nCalls (type 2) initiated by upstream are injected into the charger via `CS.Write`. The charger's `CallResult`/`CallError` is routed back to upstream. Examples: `RemoteStartTransaction`, `RemoteStopTransaction`, `GetConfiguration`, `ChangeConfiguration`, `TriggerMessage`, `SetChargingProfile`.\n\n`ChangeConfiguration` for `MeterValueSampleInterval` is intercepted: the forwarder absorbs it as a local throttle on `MeterValues` forwarded to upstream and replies `Accepted` without touching the charger's own config. evcc still processes every `MeterValues` frame for energy management.\n\n## Read-only mode\n\nWhen a rule sets `ReadOnly`, upstream may observe but cannot control the charger. Any incoming Call from upstream is answered with a `SecurityError` and not forwarded. `ReadOnly` is applied live per message, so toggling it does not require reconnecting the sidecar.\n\n## Connection lifecycle\n\nFrames that arrive from a charger before its sidecar finishes dialling are buffered (`pendingMsgs`) and flushed in order once the sidecar connects, so early messages such as `BootNotification` still reach upstream. If the dial fails or upstream drops mid-session, any buffered or in-flight relay Calls are answered to the charger with a `CallError` so it is not left hanging, and the failure is surfaced to the UI via `forwarderErrors`.\n\nWhen the upstream connection fails while the charger stays connected, the sidecar is re-dialled automatically with exponential backoff (`runUpstreamSidecar`, 5s up to 5min; a successful session resets the backoff). Between attempts the relay actions fall back to evcc's local handler, so charging continues but upstream misses those transactions. The reconnect loop ends when the charger disconnects, the rule is removed, or its connection parameters change (`ApplyForwarderRules` dials its own sidecar in that case).\n\nThe last `BootNotification` of each connected charger is cached (`lastBoot`). When a sidecar connects mid-session (after an upstream reconnect or a rule added at runtime) and the pending buffer does not already carry a boot frame, the cached frame is replayed to upstream with a fresh message id, since many backends expect a boot before accepting transactions. Upstream's reply to the replay is discarded; evcc answered the charger's original boot long ago.\n\nRules can be changed at runtime through `ApplyForwarderRules`. Sidecars for removed rules are closed, rules with changed connection parameters are re-dialled, and rules for chargers that are not connected are test-dialled to surface unreachable hosts immediately.\n","docs/agents/hardware-integrations.md":"# Hardware Integrations: Chargers, Meters, Vehicles\n\n## Integration Pattern\n\nAll device types use a registry-based factory pattern:\n\n```go\n// Self-registration in init()\nfunc init() {\n    registry.AddCtx(\"typename\", NewFromConfig)\n}\n\nfunc NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {\n    // Parse config, create client, return implementation\n}\n```\n\nOptional interfaces are added via the decorator pattern:\n```go\n//go:generate decorate -f decorateXxx -b *Xxx -t \"api.PhaseSwitcher,Phases1p3p,func(int) error\"\n```\n\n## Charger Implementations\n\n### By Protocol\n\n| Protocol | Examples |\n|----------|---------|\n| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |\n| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |\n| OCPP 1.6 | Generic charge point server |\n| EEBus/ISO 15118 | EEBus SPINE protocol |\n| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |\n| MQTT | OpenWB, Tasmota, Shelly |\n| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |\n\n### Required Charger Interface\n\n```go\ntype Charger interface {\n    ChargeState\n    Enabled() (bool, error)\n    Enable(enable bool) error\n    CurrentController\n}\n```\n\nWhere `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and\n`CurrentController` provides `MaxCurrent(current int64) error`.\n\n### Optional Charger Interfaces\n\n- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision\n- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p\n- `Meter` / `MeterEnergy` — built-in power/energy measurement\n- `PhaseCurrents` / `PhaseVoltages` — per-phase readings\n- `ChargeRater` — `ChargedEnergy()` for session energy\n- `ChargeTimer` — `ChargeDuration()` for session time\n- `Identifier` — `Identify()` for RFID/vehicle identification\n\n### Key Implementations\n\n- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail\n- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management\n- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2\n- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC)\n- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template\n\n### Adding a New Charger\n\n1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`)\n2. Implement `NewXxxFromConfig()` returning `api.Charger`\n3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()`\n4. Register: `registry.AddCtx(\"xxx\", factory)` in `init()`\n5. Optional: add decorator for PhaseSwitcher, Meter, etc.\n6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata\n\n## Meter Implementations\n\n### By Category\n\n| Category | Examples |\n|----------|---------|\n| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |\n| HTTP/REST | Homewizard, Shelly Gen3, E3DC |\n| Smart Home | HomeAssistant entities, Homematic |\n| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |\n\n### Required Meter Interface\n\n```go\ntype Meter interface {\n    CurrentPower() (float64, error)  // watts\n}\n```\n\n### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity`\n\n### Key Implementations\n- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection\n- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries\n- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources)\n\n## Vehicle Integrations\n\n| Manufacturer | API Type |\n|-------------|----------|\n| Tesla | Fleet API + vehicle-command proxy |\n| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |\n| Hyundai/Kia | BlueLink (regional variants) |\n| BMW/Mini | ConnectedDrive v2 |\n| Mercedes | Official API |\n| Renault/Nissan | Renault API + Carwings |\n| Ford | FordConnect (US/EU) |\n| Porsche | Porsche Connect |\n| PSA Group | Peugeot, Citroen, DS, Opel |\n| Generic | OVMS, Tronity |\n\n### Required Vehicle Interface\n\n```go\ntype Vehicle interface {\n    Battery          // Soc() (float64, error)\n    BatteryCapacity  // Capacity() float64\n    IconDescriber    // Icon() string\n    FeatureDescriber // Features() []Feature\n    PhaseDescriber   // Phases() int\n    TitleDescriber   // GetTitle() string\n    SetTitle(string)\n    Identifiers() []string\n    OnIdentified() ActionConfig\n}\n```\n\n### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector`\n\n### Polling Strategy\n\nConfigurable: always / while charging / while connected. Interval-based caching\nto avoid excessive cloud API calls. OAuth2 token handling built into each provider.\n\n## Auto-Detection (`cmd/detect/`)\n\nTask-based parallel IP scanning:\n`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes\n\nDetects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,\nTasmota, Shelly, Phoenix, and many more.\n","docs/agents/api-security.md":"# API Security & Authentication\n\nHow evcc authenticates HTTP requests and how endpoints are classified by\nsensitivity.\n\n## Threat Model\n\nevcc is designed for use within a trusted home network. The auth layer\nprotects credential management, configuration changes, and system operations\n(logs, backup/restore/reset, shutdown). Read-only state and basic charging\ncontrols are intentionally unauthenticated.\n\n## Auth Modes\n\n| Mode       | Trigger               | Behavior                                          |\n|------------|-----------------------|---------------------------------------------------|\n| `Enabled`  | default               | password required; JWT or API key accepted        |\n| `Disabled` | `--disable-auth` flag | all auth checks skipped                           |\n| `Locked`   | demo mode             | mutating endpoints return 403; reads still work   |\n\nMode is fixed at startup. The frontend mirrors the mode so admin actions can\nbe greyed out and a banner shown.\n\n## Endpoint Sensitivity Tiers\n\nThree tiers, by what the caller has to prove:\n\n**Public.** No auth. State, loadpoint controls, login. Anyone on the\nnetwork can read and operate.\n\n**Secure.** Requires a valid session: either the auth cookie (browser, JWT)\nor an API key in the `Authorization: Bearer …` header (automation). Used\nfor configuration and system administration.\n\n**Critical.** Secure plus an additional admin-password check inside the\nhandler. Used for destructive or credential-scoped operations.\n\nFor some Critical endpoints (backup, restore, reset) the password check is\n**skipped when the caller is authenticated via API key**, so unattended automation\ndoesn't need to embed the admin password. For credential-management\nendpoints (rotate API key, change admin password) the password check is\n**strict**: a leaked API key must not be able to rotate itself or change\nthe admin password.\n\nDisabling auth short-circuits all checks.\n\n## Sessions\n\nTwo transports, no overlap:\n\n- Browsers use a session cookie (JWT, 90-day TTL, issued on login).\n- Automation uses an API key in the `Authorization: Bearer …` header.\n\nAPI keys are random alphanumeric strings prefixed `evcc_`. The prefix makes\nleaked keys recognizable to secret-scanning tools.\n\nA single API key per installation; regenerating replaces the previous one.\nPlaintext is shown to the user **once** at generation time and cannot be\nretrieved afterwards.\n\n## Credential Storage\n\nAdmin password and API key are stored as bcrypt hashes. The JWT signing\nsecret is a per-installation random value. Plaintext credentials are never\npersisted.\n\nRemoving the admin password (CLI recovery) also clears the JWT secret and the\nAPI key, which invalidates all outstanding sessions and any previously-issued\nAPI key. Regenerating the API key replaces the stored hash; the previous key\nstops working immediately.\n\n## API Key Lifecycle\n\nTwo operations:\n\n- **Status.** Whether a key is configured. Secure tier; never returns\n  plaintext.\n- **Regenerate.** Critical tier, strict password check. Returns the new\n  plaintext key exactly once.\n\nThere is no delete operation: regenerating and discarding the new key\nachieves the same effect (the previous key stops working immediately).\n\n## Endpoint Matrix\n\n| Endpoint category                            | Tier      | Additional Requirements            |\n|----------------------------------------------|-----------|------------------------------------|\n| State / read-only / basic charging control   | Public    |                                    |\n| Set or update admin password                 | Public    | admin password                     |\n| Configuration                                | Secure    |                                    |\n| Configuration embedding a script plugin       | Critical  | api key or admin password          |\n| System: logs, cache, shutdown                | Secure    |                                    |\n| API key status                               | Secure    |                                    |\n| System: backup / restore / reset             | Critical  | api key or admin password          |\n| API key regenerate                           | Critical  | admin password                     |\n\nDevice test, create, and update (`/api/config/test/{class}` and `/api/config/devices/{class}`)\ninstantiate a config immediately, so a `script` plugin in the payload runs a shell command on the\nserver. Because that command could read credentials a session is not otherwise allowed to see (for\nexample the contents of the database), these requests are treated as Critical when the config embeds\na script plugin, at any nesting depth. A session caller must supply the admin password in the\n`X-Admin-Password` header; an API-key caller passes without it. The `go` (yaegi) and `js` (otto)\nplugins are excluded: their interpreters are sandboxed to pure computation and cannot read files,\nspawn processes, or open network connections.\n\n**Public** endpoints accept any caller. **Secure** endpoints require a\nvalid session (cookie or API key). **Critical** endpoints require extra\nauthentication in the form of an admin password (or, for some, an API\nkey).\n\n## Frontend\n\nAuth management lives under **General Config → Security**, which links to\ntwo sub-flows: change admin password, and manage the API key. The API key\nflow has a reveal view that shows the plaintext exactly once with a\ncopy-to-clipboard link.\n\nWhen auth is disabled, the security modals show a warning banner and\ndisable all action buttons. This is UI-only; the backend still accepts the\nunderlying calls so legitimate automation against a disabled-auth instance\nkeeps working.\n\n## OpenAPI\n\nThe OpenAPI spec declares two security schemes (cookie and bearer);\nprotected operations accept either.\n","docs/agents/core-domain.md":"# Core Domain: Site, Loadpoint, and the Control Loop\n\n## Object Hierarchy\n\n```\nSite (orchestrator — core/site.go)\n├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]\n├── Tariffs: Grid, FeedIn, CO2, Solar\n├── Coordinator (vehicle <-> loadpoint assignment)\n├── Prioritizer (power allocation fairness)\n└── Loadpoints[] (core/loadpoint.go)\n    ├── Charger      (api.Charger — hardware controller)\n    ├── Vehicle      (api.Vehicle — EV battery state via cloud API)\n    ├── ChargeMeter  (api.Meter — AC power at charger)\n    └── Circuit      (optional — electrical domain limits)\n```\n\n## Key Interfaces (api/api.go)\n\n### Meter\n- `Meter` — `CurrentPower() (float64, error)` — watts\n- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh\n- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings\n\n### Battery\n- `Battery` — `Soc() (float64, error)` — 0-100%\n- `BatteryCapacity` — kWh\n- `BatteryController` — set charge/discharge/hold mode\n\n### Charger\n- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)`\n- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)`\n- `PhaseSwitcher` — `Phases1p3p(int) error`\n- `ChargeRater` — `ChargedEnergy() (float64, error)`\n- `ChargeTimer` — `ChargeDuration() (time.Duration, error)`\n\n### Vehicle\n- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()`\n- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`\n- `ChargeController` — remote start/stop on vehicle\n- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits\n- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side\n\n## Charge Modes\n\n| Mode | Behavior |\n|------|----------|\n| `OFF` | Disabled (unless welcome charge) |\n| `SMART` | Ramp current with solar surplus; fast if cheap tariff or plan requires |\n| `NOW` | Max current immediately |\n\nAlways charge (`alwaysCharge`: off/on/once) is a per-loadpoint option for smart mode: never pause, keep at least min current. `once` resets on disconnect. `pv` and `minpv` are deprecated write-only aliases for smart without/with always charge.\n\n## Charge States (IEC 61851)\n\n- `A` — not connected\n- `B` — connected, not charging\n- `C` — connected, charging\n\n## The Control Loop (Site.update — runs every N seconds)\n\n```\n1. Update all meters (grid, PV, battery, aux)\n2. For each loadpoint: UpdateChargePowerAndCurrents()\n3. Calculate site power balance:\n   sitePower = gridPower + batteryPower + excessDCPower\n             + residualPower - auxPower - flexiblePower\n4. Apply battery priority rules (prioritySoc, bufferSoc)\n5. Get tariff rates\n6. For EACH loadpoint: Update(sitePower, ...)\n   ├── Read charger status\n   ├── Detect/identify vehicle\n   ├── Check plan requirements (minSOC, target time)\n   ├── Check limits (limitSOC, limitEnergy)\n   ├── MODE switch -> calculate target current\n   ├── Cap at maxCurrent, respect circuit limits\n   ├── Send MaxCurrent() to charger\n   └── Record metrics\n7. Push updates to WebSocket + metrics\n```\n\nThe loop is stateless per cycle: always re-reads actual state, calculates\noptimal current, sends single command. Resilient to restarts and missed updates.\n\n## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)\n\n```\n1. Read effective min/max current limits\n2. Reduce sitePower by battery boost power\n3. Consider phase switching (1p <-> 3p) if supported\n4. deltaCurrent = powerToCurrent(-sitePower, activePhases)\n   targetCurrent = effectiveCurrent + deltaCurrent\n5. Below minCurrent -> start disable timer (default 3 min)\n6. Surplus returns -> start enable timer (default 1 min)\n7. Cap at maxCurrent\n```\n\n## Battery Priority Rules\n\n| Setting | Effect |\n|---------|--------|\n| `prioritySoc` | Below this: battery charges first, EV gets 0 |\n| `bufferSoc` | Above this: EV can draw from battery reserves |\n| `bufferStartSoc` | Above this: EV charging can begin even if importing |\n\nNone of these apply to a loadpoint the optimizer controls: the optimizer plans the home battery, so a stop gives the battery priority and a charge takes the surplus (`loadpointSitePower`, `#34606`). A charge the home battery covers is pinned to its setpoint instead, the pv loop would count the discharge as deficit (`surplusRegime`, `#34809`).\n\n## Effective Price Calculation\n\n```\ngreenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower\neffectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare\n```\n\n## Concurrency Model\n\n- **Site** owns `RWMutex` for its state (meters, battery, tariffs)\n- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current)\n- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking\n- No global locks — ordering prevents deadlocks\n\n### Channels\n\n| Channel | Scope | Buffer | Purpose |\n|---------|-------|--------|---------|\n| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) |\n| `lpUpdateChan` | Site | 1 | Early loadpoint update requests |\n| `pushChan` | Loadpoint | 16 | User notifications, queued via `valueChan` so the message renders the state at event time |\n\n## Tariff Integration\n\nTypes: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar`\n\n### Smart Features\n- **Cheap-tariff override** — rate below threshold -> fast charge\n- **Smart feed-in** — feed-in rate above threshold -> prioritize export\n- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline\n  - `optimalPlan()` — cheapest non-contiguous slots\n  - `continuousPlan()` — cheapest continuous window (fallback)\n\n## Optimizer Suggestions (core/site_optimizer.go)\n\nThe optimizer (`optimizer.evcc.io`, sponsor-gated) solves a linear program over tariff slots. Every solved \"battery\" (home battery, vehicle/loadpoint) gets a `types.Suggestion` for the slot covering now. In `Automatic` mode these suggestions drive control; otherwise they are advisory (UI + notifications).\n\n### Request side: what becomes a battery\n\n- **Home battery meter**: `batteryRequest`. `controllable` = `api.HasCap[api.BatteryController]`. Only controllable batteries get `charge_from_grid` / `discharge_to_grid`.\n- **Loadpoint with vehicle**: `loadpointRequest`, `detail.Type = vehicle`, `detail.loadpoint = &id`, always `controllable`. Needs vehicle capacity (or a session energy limit) to model as storage. `ModeOff` sets `CMax = 0`; `ModeNow` and plans produce a `PDemand` / `SGoal`.\n- **Unmodellable loadpoints** (no vehicle, no capacity and no energy limit, or heaters) are added to the home load (`addUnmodelledLoad`) and get no suggestion.\n- Disconnected loadpoints are skipped entirely.\n\nDevice key (`batteryDetail.key()`): `battery:<name>` or `loadpoint:<id>`; empty key = cannot act.\n\n### Result side: `applyOptimizerResult`\n\nRuns after each solve, and again every cycle via `reapplySuggestions` when the active slot changes (re-derived from the cached solve, no new request). Expiry is only evaluated on that slot change: if the horizon is passed or no solve completed for `2 * tariff.SlotDuration`, `clearSuggestions()` drops suggestions, battery forecast and notification state. Within an unchanged slot the cached advice stays active regardless of age. A failed solve keeps the previous advice so controlled devices are not released for one cycle.\n\n1. `slot = schedule.activeSlot(now)`, `slotHours` = its duration. Grid import/export and all home-battery discharge for that slot are summed.\n2. `currentSlotSuggestion(detail, req, res, slot, gridImport, gridExport, slotHours)` per battery. Because the LP is linear, the slot result sits at an operating-range extreme and maps cleanly onto discrete modes. All thresholds use `suggestionThreshold = 50 W` to ignore solver noise.\n\n**Home battery** (`Action` is an `api.BatteryMode` string):\n\n| Condition (slot values, W)                         | Action       | Meaning                             |\n| -------------------------------------------------- | ------------ | ----------------------------------- |\n| charge > thr and grid importing                    | `charge`     | grid charging                       |\n| idle and grid importing and soc > `SMin`           | `hold`       | discharge deliberately withheld     |\n| idle and grid exporting and soc < `SMax`           | `holdcharge` | surplus exported instead of charged |\n| discharge > thr and grid exporting                 | `discharge`  | battery-to-grid                     |\n| otherwise (incl. idle at a soc bound, PV charging) | `normal`     | inverter self-consumption logic     |\n\n`idle` = charge and discharge both <= thr. At a soc bound nothing is withheld (the bound does it), hence `normal`.\n\n**Loadpoint/vehicle**: `charge` if charge > thr, else `stop`. `Suggestion.Battery` is set to the planned home-battery discharge so the loadpoint can tell surplus from battery-fed charging.\n\n3. Suggestions are stored only for controllable devices with a key (`site.suggestions`). Loadpoints with a plan goal (`SGoal`) also receive `optimizerPlan` (the remaining charging slots priced at `PN`) via `setOptimizerPlan`, used by `OptimizerPlan()` for the planner UI.\n4. `publishBattery` / `publishSuggestions` publish to UI and hand loadpoint suggestions to `lp.setSuggestion`. `Actionable` is evaluated on every read (`site.suggestion`) as `Action != currentAction`, where the current action is `site.batteryAction()` for batteries (the battery mode, `unknown` reads as `normal`) and `loadpointCurrentAction` (`charge` when enabled and goal not reached, else `stop`).\n5. `diffSuggestions` emits one `suggestion` messenger event per device whose actionable action changed since the last run; non-actionable or vanished devices are pruned so a later change re-notifies.\n\n### Control side: where suggestions take effect\n\nGated by `OptimizerAutomatic()`: `off` (advisory only), battery-only, or `full` (also loadpoints). `site.Automatic()` additionally requires optimizer enabled + sponsor.\n\n**Home battery** (`requiredBatteryMode`, `core/site_battery.go`), in priority order:\n\n- external mode (API) wins.\n- `Automatic()` and `unmodelledCharging(rate)`: a loadpoint the optimizer does not control is fast charging or on the smart cost limit. The suggestion did not account for that load, so force `hold`.\n- `Automatic()`: `batterySuggestionMode()` takes the first controllable battery's suggestion (`api.BatteryModeString(s.Action)`); an unknown action releases to `normal`; no suggestion releases to `normal` only if evcc had modified the mode (`batteryModeModified`), otherwise the mode is left untouched. This replaces the grid charge limit and discharge control. TODO: per-battery modes.\n- not automatic: legacy grid charge / discharge control / feed-in discharge rules.\n\n**Loadpoint** (`activeSuggestion`, `core/loadpoint_optimizer.go`): non-nil only if `optimizerControlled()` (full automatic, not a heater or integrated device, vehicle with capacity > 0) and the suggestion is younger than `suggestionMaxAge = 2 * SlotDuration` (a stalled optimizer must not keep the loadpoint gated). With an active suggestion:\n\n- `ModeSmart` strategy switch calls `optimizerCharging(s)` and skips the smart cost / feed-in price limits.\n  - `surplusRegime` (charge below max power, no grid flow, no battery discharge): not handled, the PV loop tracks measured surplus instead of the forecast.\n  - `stop` without always-charge: not handled, PV loop may still enable on measured surplus.\n  - `charge` at full power or above what the active phases deliver: `fastCharging()`.\n  - `charge` at a limited setpoint: `setLimit(PowerToCurrent(s.Charge))`, or `minCharging()` below min current.\n  - `stop` with always-charge: `minCharging()`.\n- Planner is reduced to a backstop: `plannerActive = planDeadlineCritical()`.\n- Session `LimitEnergy` / `LimitSoc` do not stop charging while the optimizer is driving a plan goal above them.\n- `loadpointSitePower`: priority soc adjustment is applied, and the battery's discharge is subtracted from the surplus unless the action is `charge` (the optimizer plans the home battery, so `bufferSoc` / `bufferStartSoc` are also disabled).\n\nEvery cycle ends with `site.publishSuggestions()` so `Actionable` reflects the updated loadpoint state.\n\n## Key File Locations\n\n- `api/api.go` — all core interfaces\n- `core/site.go` — Site orchestrator + control loop\n- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch)\n- `core/site_battery.go` — battery priority logic, battery mode from suggestion\n- `core/site_optimizer.go` — optimizer request, suggestion mapping, reapply/expiry\n- `core/loadpoint_optimizer.go` — loadpoint suggestion gate and charging strategy\n- `core/site_tariffs.go` — tariff integration\n- `core/planner/planner.go` — charge time optimization\n- `core/prioritizer/prioritizer.go` — power allocation across loadpoints\n- `core/circuit/circuit.go` — electrical domain limits\n","AGENTS.md":"# Agent Rules for evcc Project\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\n- evcc is an extensible EV Charge Controller and home energy management system written in Go with a Vue.js frontend\n- The system manages electric vehicle charging, integrates with solar systems, and provides local energy management without cloud dependencies\n- Architecture follows a plugin-based approach for device integrations\n\n## Essential Commands\n\n- `make` - build full application (UI + Go binary)\n- `make build` - build Go binary only\n- `make ui` - build UI assets only\n- `make install` - install Go tools and dependencies\n- `make install-ui` - install Node.js dependencies (`vp install`)\n- `make test` - run Go tests\n- `make test-ui` - run frontend tests\n- `make lint` - run Go linting (golangci-lint)\n- `make lint-ui` - run frontend linting\n- `vp run dev` - start Vue dev server (http://127.0.0.1:7071)\n- `vp run playwright` - run integration tests\n- `build`, `openapi` and `test` are cached tasks in `vite.config.ts`, run through `vp run`\n- `evcc --config [file] --disable-auth` - run a throw-away instance for UI checks without password setup\n- `evcc --template-type [type] --template [file]` - test device templates\n- `make docs` - generate template documentation\n\n## Domain Knowledge\n\nDeep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:\n\n| File                                                          | When to load                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| [Core Domain](docs/agents/core-domain.md)                     | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, optimizer  |\n| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices                    |\n| [Easee Architecture](docs/agents/easee-architecture.md)       | Easee charger (REST+SignalR, async correlation, concurrency)                 |\n| [OCPP Forwarder](docs/agents/ocpp-forwarder.md)               | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) |\n| [Plugin System](docs/agents/plugin-system.md)                 | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS)                               |\n| [Web UI & API](docs/agents/web-ui-api.md)                     | REST API, WebSocket, Vue frontend, authentication                            |\n| [API Security](docs/agents/api-security.md)                   | Auth modes, JWT/API key/session, two-tier checks, credential storage         |\n| [Provider Auth](docs/agents/provider-auth.md)                 | Third-party logins (OAuth, device code, tokens), template `auth` block, UI   |\n| [Chart Conventions](docs/agents/charts.md)                    | ECharts charts: axis units and scale rules, tooltips, layout                 |\n| [Energy Analytics](docs/agents/energy-analytics.md)           | Energy page metrics: attribution, cost/CO2, forecast accuracy, missing data  |\n| [Discovery Hints](docs/agents/discovery-hints.md)             | Vendor registry update, adding and reviewing template `discovery` hints      |\n\n### Loading guide by task type\n\n- **Charger implementation** — hardware-integrations + core-domain\n- **Easee charger work** — easee-architecture + core-domain\n- **Meter implementation** — hardware-integrations + plugin-system\n- **Vehicle implementation** — hardware-integrations\n- **UI/frontend work** — web-ui-api\n- **Chart work (ECharts, axes, tooltips)** — charts + web-ui-api\n- **API endpoint work** — web-ui-api + core-domain\n- **Auth / login / API key / permissions** — api-security + web-ui-api\n- **Vehicle or device cloud login (OAuth, tokens, captcha)** — provider-auth + hardware-integrations\n- **Config/template work** — plugin-system\n- **Discovery hints / vendor registry update** — discovery-hints + plugin-system\n- **Control loop / charging logic** — core-domain\n- **Energy page metrics (attribution, savings, accuracy)** — energy-analytics + charts\n- **Bug in any area** — core-domain + relevant topic file(s)\n\n## Architecture Guidelines\n\n### Core Components\n\n- **main.go** serves as entry point and embeds web assets and i18n files\n- **cmd/** contains CLI commands, application setup, and various utility commands (configure, detect, migrate, etc.)\n- **core/** contains core business logic with main files (loadpoint.go, site.go) and subdirectories:\n  - **loadpoint/** - EV charging point management modules\n  - **planner/** - Smart charging planning algorithms\n  - **coordinator/** - Multi-loadpoint coordination logic\n  - **session/** - Charging session management\n  - **vehicle/** - Vehicle-specific core logic\n  - **soc/** - State of charge handling\n- **api/** contains API definitions and types\n- **server/** handles HTTP server, WebSocket, MQTT, database operations, and various handlers\n- **charger/**, **meter/**, **vehicle/** contain device integrations\n- **tariff/** contains tariff integrations\n- **plugin/** implements plugin system for device and tariff communication\n- **assets/** contains Vue.js frontend application\n\n### Frontend Structure\n\n- **assets/js/** contains the main TypeScript/Vue.js application with:\n  - **views/** - Vue page components (App.vue, Config.vue, Sessions.vue, etc.)\n  - **components/** - Reusable Vue components\n  - **composables/** - Vue utility functions\n  - **types/** - TypeScript type definitions\n  - **utils/** - Utility functions\n  - **mixins/** - Vue mixins\n- **assets/css/** contains application stylesheets\n- **assets/public/** contains static assets and metadata\n- **i18n/** contains internationalization files\n- **tests/** contains Playwright integration tests and test configuration files\n- **dist/** contains built frontend assets (generated)\n\n## Writing Style\n\n- No em dashes (—) in comments, commit messages, or docs. Use periods, commas, or colons\n- Project name is `evcc`, always lowercase\n- In user-facing strings, only mention `evcc` when needed to understand the context. Inside evcc's own UI the self-reference is usually redundant\n- Acronyms uppercase in prose: OCPP, MQTT, HEMS, SoC\n- Terminology: agreed terms per language with explanations live in `i18n/glossary/` (`en.json` + `<lang>.json`, rules in `README.md`). Check it before wording user-facing strings (including API descriptions in `assets/js/types/evcc.ts` and `server/openapi.yaml`), and extend it when a new concept gets a name\n- Commit subjects: `Component: short description`, no trailing period. Sub-scope in parens: `Meter (Home Assistant): ...`. Use `chore:`/`fix:`/`docs:` only for non-feature changes\n\n## Comment Style\n\n- Prefer self-documenting code over comments; comment the _why_, not the _what_\n- Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary\n- Skip refs to the current task, PR, issue, or caller (\"added for X flow\", \"see #1234\"). Git history covers that\n- Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name\n\n## Go Coding Standards\n\n### Core Principles\n\n- Follow Go idioms and conventions (Effective Go)\n- Use `gofmt` for formatting, self-documenting names, early returns\n- Handle all errors explicitly with meaningful messages\n- Use interfaces for behavior contracts (small, focused, single responsibility)\n- Use `context.Context` for I/O, long-running, or cancelable operations\n- Organize code into logical packages with clear responsibilities\n- Prefer composition over inheritance, minimize external dependencies\n- Navigate Go symbols with go-to-definition and find-references rather than text search; reserve text search for comments and string literals\n\n### File Patterns\n\n- `_blueprint.go` - templates for new device implementations\n- `_enumer.go` - generated enum code\n- `*_decorators.go` - generated decorator pattern implementations\n- Validate interface implementations: `var _ Interface = (*Type)(nil)`\n- Capabilities: register via `implement.Has`/`May` only when a capability is _conditional_ (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()`\n\n### Error Handling\n\n- Wrap errors with context: `fmt.Errorf(\"context: %w\", err)`\n- Use `errors.As` and `errors.Is` for type checking\n- Use `errors.Join` for combining errors (prefer custom `joinErrors` helper)\n- Create domain-specific error types (ClassError, DeviceError)\n- Use `backoff.Permanent(err)` for non-retryable errors\n- Implement panic recovery with `defer` and `recover()` in script contexts\n\n### Testing & Code Generation\n\n- Use `testing` package with `testify/assert` and `testify/require`\n- Table-driven tests with struct definitions for multiple cases\n- Use `gomock` for interface mocking, `go:generate mockgen` for generation\n- Test both success and failure scenarios, use `require` for setup, `assert` for tests\n- Use `go:generate` for code generation, regenerate after interface/enum changes\n- Never manually edit generated files\n\n### Context & Concurrency\n\n- Use `context.Context` as first parameter for I/O operations\n- Use `context.WithTimeout`, `context.WithCancel` appropriately\n- Check `ctx.Done()` in long-running loops\n- Propagate context through goroutines for proper cancellation\n- Handle concurrent operations safely with Go's concurrency primitives\n\n### Data Validation\n\n- Filter `NaN` and `Infinity` values using `math.IsNaN()` and `math.IsInf()`\n- Validate numeric inputs from external sources\n- Use helper functions like `parseFloat()` that reject invalid values\n\n## Vue.js/TypeScript Frontend Standards\n\n### Core Architecture\n\n- Use Vue 3 Options API (preferred over Composition API)\n- Use reactive stores without Vuex/Pinia for cross-component state\n- Use global app instance (`window.app`) only for: notifications (`raise()`), offline status (`setOffline()`/`setOnline()`), clearing notifications (`clear()`)\n- Organize components by feature/domain in `assets/js/components/` subdirectories\n\n### Component Development\n\n- Use TypeScript for all new frontend code\n- Use `const` instead of `function` for component methods (e.g., `const updateType = () =>`)\n- Define TypeScript interfaces for component props, data, and API responses\n- Implement accessibility features (tabindex, aria-label, keyboard handlers)\n- Use descriptive names for variables, functions, and event handlers\n- Use early returns for readability\n- Prefer named computed properties over inline template expressions, even for single use. Readability beats saving lines\n- Use configured Axios instance for HTTP communication\n\n### State Management\n\n- Never access the store from sub-components; keep them stateless and pass the values they need as props (emit events back to the parent). Only top-level views read from the store. This keeps components reusable and testable (e.g. Storybook should never mock the store).\n- Use `reactive()` from Vue for simple global state\n- Implement property setters for nested object updates using helper functions\n- Use localStorage with reactive wrappers for persistent settings\n- Use Vue `watch()` for automatic persistence of settings changes\n- Separate concerns with dedicated stores (settings, application state)\n\n### TypeScript Patterns\n\n- Define comprehensive interfaces for API responses and application state\n- Use enums for constants (e.g., `THEME`, `CURRENCY`)\n- Extend global interfaces for window object augmentation\n- Use union types for flexible but type-safe configurations\n- Use generic types for reusable utility functions\n- Handle type assertions carefully with proper error handling\n- Create focused utility functions with proper TypeScript typing\n\n### Styling & Internationalization\n\n- Use CSS Custom Properties for theming (semantic names: `--evcc-green`, `--evcc-battery`)\n- No inline `style` attributes; use Bootstrap utility classes or scoped CSS (inline only for truly dynamic values)\n- Use the custom media queries from `assets/css/breakpoints.css` (`@media (--sm-and-up)`) for responsive breakpoints, never raw px `min-width`/`max-width`. Write mobile-first and override upwards instead of adding max-width queries\n- Use `$t()` function for all user-facing strings\n- Update both `i18n/en.json` and `i18n/de.json` for new strings\n- Use hierarchical namespace: `{section}.{component}.{purpose}`\n- Examples: `config.vehicle.titleAdd`, `main.vehicleStatus.charging`\n- Action patterns: `titleAdd`, `titleEdit`, `save`, `cancel`, `delete`, `validateSave`\n- Use placeholders for dynamic content: `{soc}`, `{duration}`, `{value}`\n- Prefer context-specific keys over generic ones\n- Test with German translations (20-40% longer text)\n- Keep separators and trailing punctuation (`: `, `…`, `—`) in the template, not in the translation value.\n\n### Testing\n\n- Write integration tests using Playwright for user workflows\n- Use Storybook for component development and visual testing\n- Use semantic selectors (roles, labels, button text); `data-testid` only when necessary\n- Test error states and loading states\n\n## Playwright Integration Testing\n\n### Test Organization\n\n- **Location**: `tests/` directory with `.spec.ts` files\n- **Configuration**: `.evcc.yaml` files for different test scenarios\n- **Utilities**: `tests/utils.ts` for common helpers, `tests/evcc.ts` for binary management\n- **Categories**: `config-*.spec.ts` (UI config), `sessions.spec.ts`/`plan.spec.ts` (workflows), `smart-cost.spec.ts`/`limits.spec.ts` (features), `backup-restore.spec.ts`/`auth.spec.ts` (integration)\n\n### Test Configuration\n\n- Base URL: `http://127.0.0.1:7070`\n- Parallel execution with different ports per worker for isolation\n- Uses `./evcc` binary with test-specific configuration files\n- Each worker uses isolated temporary database files\n- Always runs with English UI language\n\n### Essential Commands\n\n- Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev).\n- Run tests: `vp run playwright` or `vpx playwright test`\n- Debug: `vpx playwright test --debug`\n- Specific test: `vpx playwright test tests/config-loadpoint.spec.ts`\n\n### Selector Strategy\n\n- **Preferred**: Semantic selectors using `getByRole()`, `getByLabel()`, `getByText()`\n- **Fallback**: `data-testid` only when semantic selectors aren't available\n- **Examples**:\n  - `page.getByRole(\"button\", { name: \"Add charger\" })`\n  - `page.getByLabel(\"Manufacturer\").selectOption(\"Demo charger\")`\n  - `page.getByRole(\"listitem\", { name: \"Draggable: First Loadpoint\" })` (using aria-label)\n  - `page.getByTestId(\"loadpoint\")` (fallback only)\n- never use `.locator()` or `class` and `id`-based selectors\n\n### Test Patterns\n\n- Use test-specific `.evcc.yaml` configurations\n- Import utilities from `tests/utils.ts` for common operations\n- Focus on complete user journeys rather than isolated interactions\n- Use `expectModalVisible()` and `expectModalHidden()` helpers\n- Test configuration persistence across application restarts\n- Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)`\n- Never use fixed timeouts. Wait on element state (visibility, count, value) instead.\n- Never use `page.waitForLoadState(\"networkidle\")`. SPAs keep emitting requests (websockets, polling), so it either races or hangs. Wait for the specific element / value you need instead.\n- Keep test names and describe titles short and concrete. They should complement each other, not repeat. Prefer `describe(\"aux meter\") test(\"create\")` over `describe(\"aux meter\") test(\"create aux meter and verify it appears\")`. Drop scenario filler like \"and lands in section\", \"appears correctly\", \"ensure\".\n\n## Device Integration & Configuration\n\n### Plugin System\n\n- Device types: chargers, meters, vehicles, tariffs\n- Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go\n- Define device capabilities and configuration in templates at `templates/definition/[type]/`\n- Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a _different_ value. Restating the same value is redundant duplication: reference the param by `name` alone.\n- Test templates: `evcc --template-type [type] --template [file]`\n- Update docs after template changes: `make docs`\n- When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details\n\n### Configuration\n\n- Use YAML format for all configuration files (default: `evcc.yaml`, or specify with `--config`)\n- Provide clear validation and error messages for invalid configurations\n- Support template-based device configurations with meaningful defaults\n- Use SQLite as default database (default: `evcc.db`, or specify with `--database`) with proper migrations and data integrity\n\n## Security & Performance Guidelines\n\n### Security\n\n- Validate all user inputs and sanitize data before database storage\n- Use secure protocols (TLS) for external integrations\n- Implement proper authentication and authorization\n- Never log sensitive information (passwords, tokens, personal data)\n\n### Performance\n\n- Optimize database queries with appropriate indexes\n- Handle concurrent operations safely with Go's concurrency primitives\n- Implement proper caching strategies and connection pooling\n- Avoid blocking operations in main application loop\n\n## Pull Request Descriptions\n\nStructure PR descriptions in this order. No headlines. Be concise.\n\n1. **References first line**: link related issues or PRs (`fixes #1123`, `replaces #222`, `pairs with org/repo#345`). PRs should almost always reference an issue or related PR — only skip in rare exceptions (e.g. trivial typo fixes).\n2. **Intro**: one or a few concise sentences framing what the PR does and why it was created this way. The full problem description belongs in the linked issue, not here.\n3. **Bullet list**: most significant changes or user-facing implications. Lead with the most significant.\n4. **TODO section** (only if open points remain):\n\n   ```\n   **TODO**\n   - [ ] item a\n   - [ ] item b\n   ```\n\nAvoid file paths, line numbers, or code listings reproduced from the diff. Include a code snippet only when it conveys the contract (event shape, API signature) more clearly than prose. No testing checklists, no co-author footers.\n\nNever state that `go build`, `go vet`, `go test -race`, or `gofmt` pass (or any \"all checks/tests green\" phrasing). These are non-negotiable givens that must already be fulfilled, not noteworthy results.\n\n## Pull Request CI and Reviews\n\n- After opening or updating a pull request, watch CI until every check has finished. Work is not done while checks are still running. Fix failures on the same branch and keep watching until the run is green.\n- Do not argue with automated review bots such as Sourcery. Either implement the suggestion or resolve the thread. No rebuttal comments.\n\n## AI Attribution\n\nWork produced by an AI agent must be attributable as such on GitHub. Append the tool's attribution footer to every PR description, issue body, review, and comment written by an agent, for example:\n\n```\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n```\n\nCommit messages are the exception: no footer, and no `Co-Authored-By` trailer. Never dress agent work up as human review (e.g. \"PR by an agent but looks good to me\") in place of the footer.\n","docs/agents/web-ui-api.md":"# Web UI & REST API\n\n## Server Architecture\n\n- **Router:** gorilla/mux, strict slash\n- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth\n- **Timeouts:** Read 5s, Write 10s, Idle 120s\n- **Static assets:** embedded in binary (`fs.FS`)\n- **Default port:** 7070\n\n## REST API (base `/api/`)\n\n### Site-level\n- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc.\n- `GET /tariff/{tariff}` — tariff rates\n- `GET /sessions` — charging history\n- `GET /state` — complete system state (supports jq filtering)\n\n### Per-loadpoint (`/loadpoints/{id}/...`)\n- `POST mode/{value}` — off/smart/now (deprecated aliases pv/minpv)\n- `POST alwayscharge/{value}` — off/on/once\n- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits\n- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits\n- `POST phases/{value}` — phase config\n- `POST priority/{value}`, `batteryboost/{value}`\n- `POST plan/energy/{value}/{time}` — schedule plan\n- `POST vehicle/{name}` — select vehicle\n- `POST smartcostlimit/{value}` — smart cost threshold\n- `POST solarshare/{value}` — required solar share of min power (default 1)\n\n### Configuration (`/config/...`, auth required)\n- CRUD for devices (chargers, meters, vehicles, tariffs)\n- Template browsing and testing\n- Site, loadpoint, circuit, HEMS, messaging config\n- `GET /config/evcc.yaml` — YAML export\n\n### System (`/system/...`, auth required)\n- Log viewing (`/log`, `/log/areas`), cache clear, shutdown\n\n### Database (`/db/...`, auth + second factor required)\n- Backup download, restore from file, selective reset\n- Second factor: admin password in request body, or API key via Bearer token (bypasses password check)\n\n### Handler Pattern\nGeneric `handler[T]` with type conversion, setter, getter.\nSpecialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`.\n\n## WebSocket (`/ws`)\n\n- `coder/websocket` (RFC 6455)\n- Pub/sub via `SocketHub`\n- Buffered channels (1024 per subscriber)\n- Welcome message with full state snapshot\n- Incremental updates as JSON key-value pairs with dot-notation keys:\n  ```json\n  {\"loadpoints.1.mode\": \"solar\", \"site.gridPower\": 1234}\n  ```\n- Write timeout: 10s, compression (disabled for Safari)\n\n## State Flow\n\n1. WS connects -> receives welcome with full state\n2. App emits `util.Param` on changes\n3. Hub broadcasts to subscribers\n4. Frontend `store.update(msg)` merges via dot-notation\n5. Components reactively re-render\n\n## Authentication\n\n- JWT, 90-day lifetime\n- HttpOnly cookie (`auth`) with `SameSite=Strict`\n- Also accepts `Authorization: Bearer <token>` header\n- Modes: Disabled, Locked (demo), Configured (password)\n- Protects `/api/config`, `/api/system`, and `/api/db`\n\n## MQTT Integration\n\n- Publishes state changes to configurable broker\n- Subscribes to control topics\n- Retained messages for state persistence\n\n## Key Files\n\n- `server/http.go` — router setup\n- `server/http_auth.go` — authentication\n- `server/http_site_handler.go` — state + request handlers\n- `server/http_config_*.go` — config endpoints\n- `server/http_loadpoint_handler.go` — per-loadpoint endpoints\n- `server/socket.go` — WebSocket pub/sub\n- `assets/js/app.ts` — Vue app entry\n- `assets/js/store.ts` — reactive state store\n- `assets/js/api.ts` — Axios clients\n- `assets/js/router.ts` — route definitions\n","docs/agents/plugin-system.md":"# Plugin System\n\nThe plugin system (`plugin/`) provides protocol-level abstraction for device\ncommunication. Plugins implement typed getter/setter interfaces and are composed\ninto charger, meter, or vehicle implementations via configuration.\n\n## Plugin Types\n\n| Plugin | Protocol | Key Config |\n|--------|----------|------------|\n| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` |\n| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` |\n| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` |\n| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree |\n| `js` | JavaScript/WASM | Inline script evaluation |\n| `go` | Go runtime | Dynamic Go code |\n| `gpio` | Linux GPIO | Digital I/O for relays |\n\n## Getter/Setter Interfaces\n\n```go\ntype StringGetter func() (string, error)\ntype FloatGetter  func() (float64, error)\ntype IntGetter    func() (int64, error)\ntype BoolGetter   func() (bool, error)\n// + corresponding Setter types\n```\n\n## Pipeline Transforms\n\nPlugins support chained transforms: `scale`, `offset`, `lookup`, `regex`.\n\n## Template-Based Device Configuration\n\nDevices can be defined entirely via YAML templates using plugins:\n\n```yaml\n# templates/definition/charger/example.yaml\nstatus:\n  source: http\n  uri: http://{{ .host }}/status\nenable:\n  source: http\n  uri: http://{{ .host }}/enable\n  method: POST\nmaxcurrent:\n  source: http\n  uri: http://{{ .host }}/current/{{ .maxcurrent }}\n```\n\nThe generic configurable charger (`charger/charger.go`) wires these plugin\nconfigs into the `api.Charger` interface at runtime.\n\nTemplates render once at config time. HTTP `uri` and `body` are templates\nthemselves, evaluated per request (`util.ReplaceFormatted`: sprig, `addDate`,\n`timeRound`). Defer `now` to request time with a raw string:\n`` {{ `{{ now | date \"2006-01-02\" }}` }} ``.\n\n## Param Suggestions and Discovery\n\nTemplate params get suggestions from services (`service:`), see the `service`\nand `discovery` sections in `templates/README.md` for the contract. Handlers\nregister in `init()` via `service.Register` (`server/service/registry.go`),\nobject responses use `service.Option` (`util/service/helper.go`) and render as\n`Helper/Combobox.vue`.\n\nLocal network discovery lives in `util/discovery` (mDNS, SSDP, neighbor table,\nreverse DNS; no raw sockets, no port scans) and feeds `network/hosts`,\n`network/report` and `network/scan` in `util/service/network*.go`. The vendor\nlist is generated from the IEEE registry (`make oui-update`), see\n[Discovery Hints](discovery-hints.md) for the update routine.\n\n- Scan: 3 s first pass (first request waits), 5 s second pass, repeated when\n  older than a minute, results combined, hosts dropped after 30 minutes.\n- mDNS: browses the types of the template hints plus all types announced in\n  the network (DNS-SD enumeration), so reports show vendor types without a hint.\n- A host keeps all its names (router name first, then mDNS), hostname hints\n  match any of them.\n- `EVCC_DISCOVERY_HOSTS` (JSON list of `discovery.Host`) replaces the scan,\n  used in tests and for environments without network access. On macOS the neighbor table is empty for binaries started with\n  `go run`, test with a built binary.\n\n## Key Files\n\n- `plugin/config.go` — plugin registry and config types\n- `plugin/http.go` — HTTP plugin\n- `plugin/mqtt.go` — MQTT plugin\n- `plugin/modbus.go` — Modbus plugin\n- `plugin/sunspec.go` — SunSpec plugin\n- `charger/charger.go` — generic configurable charger using plugins\n"},"files":{"docs/agents/easee-architecture.md":"# Easee Charger Architecture\n\nThe Easee integration communicates with the Easee cloud over two distinct channels:\n\n1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration.\n2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations.\n\nCommands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.\n\n## Authentication\n\n### Flow\n\nAuthentication uses username/password credentials against:\n```\nPOST https://api.easee.com/api/accounts/login\n```\n\nReturns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`.\n\n### Token Lifecycle\n\nWrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls:\n```\nPOST https://api.easee.com/api/accounts/refresh_token\n```\nFalls back to full re-login if refresh fails.\n\n### Token Caching\n\n`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.\n\n## Initialization Sequence\n\n`NewEasee` performs these steps in order:\n\n1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger.\n2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control.\n3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates).\n4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change.\n5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`).\n6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds.\n\n## SignalR Back-Channel\n\n### Why SignalR is Required\n\n1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`.\n2. **State is event-driven, not pollable.** No REST endpoint streams charger state.\n3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out.\n\n### Server -> Client Methods\n\n#### `ProductUpdate(json.RawMessage)`\nPrimary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`.\n\n- **Timestamp deduplication**: older timestamps for the same ID are silently dropped.\n- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select.\n\n#### `CommandResponse(json.RawMessage)`\nAsync acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID).\n\nRoutes through three maps in order:\n1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands\n2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch\n3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse\n\nUnmatched responses are logged as WARN (rogue response from external system).\n\n#### `ChargerUpdate` / `SubscribeToMyProduct`\nLogged at TRACE, not processed further.\n\n## Command Flow and Async Correlation\n\n### REST Command Endpoints\n\n```\nPOST /api/chargers/{chargerID}/commands/{action}     (start/stop/pause/resume)\nPOST /api/chargers/{chargerID}/settings              (enable, DCC, PhaseMode, SmartCharging)\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings  (dynamic circuit currents)\n```\n\n### Response Handling\n\n| HTTP Status | Meaning | Behavior |\n|-------------|---------|----------|\n| `200` | Synchronous / already applied | Returns immediately |\n| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse |\n| other | Error | Returns error |\n\n### Ticks Correlation\n\nOn 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op.\n\nEach in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`.\n\n### The Sync/Async Mismatch\n\nSome 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`).\n\nHandled via the **expected-orphan counter**:\n```go\nexpectedOrphans map[easee.ObservationID]int  // protected by cmdMu\n```\n\nBefore 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.\n\n## State Management\n\n### Internal State Fields (all protected by `sync.RWMutex`)\n\n| Field | Observation | Notes |\n|-------|------------|-------|\n| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine |\n| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state |\n| `smartCharging` | `SMART_CHARGING` (102) | LED color mode |\n| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) |\n| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling |\n| `totalEnergy` | `LIFETIME_ENERGY` (124), `CHARGE_SESSION_START` (223), `CHARGING_SESSION` (129) | kWh, monotonic max, see Logical Energy Meter |\n| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A |\n| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase |\n| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit |\n| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) |\n| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit |\n| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum |\n| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F |\n| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token |\n\n### Session Energy Zero-value Protection\n\n`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. The first op mode after startup is replayed state and does not reset, so replayed session energy survives a restart. `CHARGE_SESSION_START` and a zero `SESSION_ENERGY` arrive before the mode change, so the reset is skipped when the cached session started after the previous mode observation.\n\n### Logical Energy Meter\n\n`LIFETIME_ENERGY` is pushed only ~hourly. `TotalEnergy()` therefore keeps a monotonic max of the lifetime reading, the session meter values from `CHARGE_SESSION_START` and `CHARGING_SESSION`, and `CHARGE_SESSION_START.MeterValue + sessionEnergy`. The session term is only applied while the last nonzero `SESSION_ENERGY` (or session completion) is not older than the session start, so a stale previous-session value cannot inflate the total. Ignored zero observations do not count as fresh readings.\n\n## Charger Operation Modes\n\n```\n0 = Offline               — no cloud connection\n1 = Disconnected          — no car plugged in\n2 = AwaitingStart         — car plugged, waiting for authorization/start\n3 = Charging              — actively charging\n4 = Completed             — car full or finished, cable still plugged\n5 = Error                 — fault condition\n6 = ReadyToCharge         — ready, current available\n7 = AwaitingAuthentication — RFID auth required\n8 = Deauthenticating      — finishing authentication teardown\n```\n\n### Mapping to evcc Status\n\n| opMode | evcc Status |\n|--------|------------|\n| 1 (Disconnected) | A |\n| 2, 4, 6, 7, 8 | B |\n| 3 (Charging) | C |\n| 0, 5 and others | error |\n\n## Enable/Disable Flow\n\n### Enable = true\n\n1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait.\n2. If `opMode == Disconnected`: return (no cable).\n3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`.\n4. Otherwise: action = `resume_charging`.\n5. POST `/commands/{action}` and wait.\n6. Wait for `opMode` to reach enabled state.\n7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume).\n8. Call `MaxCurrent(c.current)` to restore previous setpoint.\n\n### Enable = false\n\n1. If disconnected or (awaiting auth && !authorize): return.\n2. POST `/commands/pause_charging` and wait.\n3. Wait for `opMode` to reach disabled state.\n4. Wait for `dynamicChargerCurrent` to reach `0`.\n\n### State Waiting Pattern\n\nBoth `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use:\n1. Short-circuit check: if already in target state, return immediately.\n2. Open a timer.\n3. Loop on `obsC` channel.\n4. On timer expiry: **one final check** before returning `api.ErrTimeout`.\n\nThe final check handles the race where the state update arrived between the last channel read and the timer fire.\n\n## Phase Control\n\n### Circuit-Level (preferred, when circuit is known)\n\nPhase switching by zeroing dynamic circuit current on unused phases:\n```\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings\n```\n\nFor 1-phase: set P2=0, P3=0. For 3-phase: restore all three.\n\nThis POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan).\n\n### Charger-Level (fallback)\n\nUses `PhaseMode` setting: `1` locked single-phase, `3` locked 3-phase. `2` (auto, set by earlier versions) is reported as unknown phases so the loadpoint locks it.\nAfter changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.\n\n## Authorization Mode (`authorize`)\n\nWhen `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.\n\nWhen `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).\n\nSetting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.\n\n## Concurrency Model\n\n### Mutexes\n\n| Mutex | Type | Protects |\n|-------|------|----------|\n| `c.mux` | `sync.RWMutex` | All charger state fields |\n| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) |\n\nCommand 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.\n\n### Observation Channel\n\n`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.\n\n**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`.\n\n## Known Design Concerns\n\n1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified.\n2. **LIFETIME_ENERGY** — API pushes updates ~hourly; bridged by the logical energy meter, so accuracy between readings depends on `SESSION_ENERGY` and the session start meter value.\n3. **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.\n4. **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.\n5. **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.\n\n## API Endpoints Summary\n\n| Method | Endpoint | Used For |\n|--------|----------|----------|\n| `POST` | `/accounts/login` | Initial authentication |\n| `POST` | `/accounts/refresh_token` | Token refresh |\n| `GET` | `/chargers` | Auto-discover charger ID |\n| `GET` | `/chargers/{id}/site` | Discover site and circuit |\n| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging |\n| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging |\n| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents |\n| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) |\n\n### SignalR Hub\n\n| Endpoint | `https://streams.easee.com/hubs/chargers` |\n|----------|------------------------------------------|\n| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` |\n| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` |\n\n## Configuration\n\n| Parameter | Required | Default | Notes |\n|-----------|----------|---------|-------|\n| `user` | yes | | Easee account email |\n| `password` | yes | | Easee account password |\n| `charger` | no | | Charger serial; auto-detected if exactly one on account |\n| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits |\n| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions |\n\nSupported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.\nDeclared capabilities: `1p3p` (phase switching), `rfid` (RFID identification).\nRequires evcc sponsorship.\n",".github/agents/template.agent.md":"---\ndescription: 'Tidy templates'\ntools: [\"read\", \"edit\", \"search\",\"shell\"]\n---\n\n## Cleanse parameter defaults\n\nValidate templates files inside `templates/definition/**` against default parameters in `util/templates/defaults.yaml`.\nMake sure that templates don't repeat parts of the default parameter definition.\n\n## Reorder parameters after presets\n\nValidate templates files inside `templates/definition/**`. If the template contains parameters and presets, ensure that any parameter contained in a `preset` appears in the template after the `preset`. Reorder such parameters after the presets.\n","docs/agents/ocpp-forwarder.md":"# OCPP Forwarder Architecture\n\nThe OCPP forwarder (`charger/ocpp/forwarder.go`) is a hybrid proxy that lets a charger talk to evcc and an upstream OCPP server at the same time. Chargers connect directly to evcc's central system on the normal port. For each charger with a matching `ForwarderRule`, a \"sidecar\" WebSocket connection to the upstream server is opened and kept in parallel for the lifetime of the charger connection.\n\nThe forwarder is opt-in: hooks (`chargerConnectHook`, `chargerDisconnectHook`, `chargerMessageHook` in `instance.go`) are nil unless a rule matches, so a charger without a rule behaves exactly as before.\n\n## Forwarding modes\n\nTwo modes apply at the same time, selected per message by its action.\n\n### Transparent relay (billing-critical)\n\nFor the actions in `actionsRelayedToUpstream` (`Authorize`, `StartTransaction`, `StopTransaction`, `DataTransfer`), upstream is the authoritative Central System:\n\n1. Charger sends the Call to evcc.\n2. The message hook forwards it to the upstream sidecar and bypasses evcc's OCPP handler.\n3. Upstream's `CallResult`/`CallError` is relayed back to the charger.\n\nevcc's handler is never invoked for these. This lets the pay backend control authorization, issue its own transaction IDs, and see consistent Start/Stop pairs.\n\n### Sidecar observation (informational)\n\nFor all other messages (`BootNotification`, `StatusNotification`, `MeterValues`, `Heartbeat`, etc.):\n\n1. Charger sends the Call to evcc, which processes it normally.\n2. The same frame is also mirrored to the upstream sidecar.\n\nUpstream observes the session while evcc manages the charger as usual.\n\n## Upstream to charger (commands)\n\nCalls (type 2) initiated by upstream are injected into the charger via `CS.Write`. The charger's `CallResult`/`CallError` is routed back to upstream. Examples: `RemoteStartTransaction`, `RemoteStopTransaction`, `GetConfiguration`, `ChangeConfiguration`, `TriggerMessage`, `SetChargingProfile`.\n\n`ChangeConfiguration` for `MeterValueSampleInterval` is intercepted: the forwarder absorbs it as a local throttle on `MeterValues` forwarded to upstream and replies `Accepted` without touching the charger's own config. evcc still processes every `MeterValues` frame for energy management.\n\n## Read-only mode\n\nWhen a rule sets `ReadOnly`, upstream may observe but cannot control the charger. Any incoming Call from upstream is answered with a `SecurityError` and not forwarded. `ReadOnly` is applied live per message, so toggling it does not require reconnecting the sidecar.\n\n## Connection lifecycle\n\nFrames that arrive from a charger before its sidecar finishes dialling are buffered (`pendingMsgs`) and flushed in order once the sidecar connects, so early messages such as `BootNotification` still reach upstream. If the dial fails or upstream drops mid-session, any buffered or in-flight relay Calls are answered to the charger with a `CallError` so it is not left hanging, and the failure is surfaced to the UI via `forwarderErrors`.\n\nWhen the upstream connection fails while the charger stays connected, the sidecar is re-dialled automatically with exponential backoff (`runUpstreamSidecar`, 5s up to 5min; a successful session resets the backoff). Between attempts the relay actions fall back to evcc's local handler, so charging continues but upstream misses those transactions. The reconnect loop ends when the charger disconnects, the rule is removed, or its connection parameters change (`ApplyForwarderRules` dials its own sidecar in that case).\n\nThe last `BootNotification` of each connected charger is cached (`lastBoot`). When a sidecar connects mid-session (after an upstream reconnect or a rule added at runtime) and the pending buffer does not already carry a boot frame, the cached frame is replayed to upstream with a fresh message id, since many backends expect a boot before accepting transactions. Upstream's reply to the replay is discarded; evcc answered the charger's original boot long ago.\n\nRules can be changed at runtime through `ApplyForwarderRules`. Sidecars for removed rules are closed, rules with changed connection parameters are re-dialled, and rules for chargers that are not connected are test-dialled to surface unreachable hosts immediately.\n","docs/agents/hardware-integrations.md":"# Hardware Integrations: Chargers, Meters, Vehicles\n\n## Integration Pattern\n\nAll device types use a registry-based factory pattern:\n\n```go\n// Self-registration in init()\nfunc init() {\n    registry.AddCtx(\"typename\", NewFromConfig)\n}\n\nfunc NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {\n    // Parse config, create client, return implementation\n}\n```\n\nOptional interfaces are added via the decorator pattern:\n```go\n//go:generate decorate -f decorateXxx -b *Xxx -t \"api.PhaseSwitcher,Phases1p3p,func(int) error\"\n```\n\n## Charger Implementations\n\n### By Protocol\n\n| Protocol | Examples |\n|----------|---------|\n| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |\n| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |\n| OCPP 1.6 | Generic charge point server |\n| EEBus/ISO 15118 | EEBus SPINE protocol |\n| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |\n| MQTT | OpenWB, Tasmota, Shelly |\n| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |\n\n### Required Charger Interface\n\n```go\ntype Charger interface {\n    ChargeState\n    Enabled() (bool, error)\n    Enable(enable bool) error\n    CurrentController\n}\n```\n\nWhere `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and\n`CurrentController` provides `MaxCurrent(current int64) error`.\n\n### Optional Charger Interfaces\n\n- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision\n- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p\n- `Meter` / `MeterEnergy` — built-in power/energy measurement\n- `PhaseCurrents` / `PhaseVoltages` — per-phase readings\n- `ChargeRater` — `ChargedEnergy()` for session energy\n- `ChargeTimer` — `ChargeDuration()` for session time\n- `Identifier` — `Identify()` for RFID/vehicle identification\n\n### Key Implementations\n\n- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail\n- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management\n- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2\n- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC)\n- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template\n\n### Adding a New Charger\n\n1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`)\n2. Implement `NewXxxFromConfig()` returning `api.Charger`\n3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()`\n4. Register: `registry.AddCtx(\"xxx\", factory)` in `init()`\n5. Optional: add decorator for PhaseSwitcher, Meter, etc.\n6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata\n\n## Meter Implementations\n\n### By Category\n\n| Category | Examples |\n|----------|---------|\n| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |\n| HTTP/REST | Homewizard, Shelly Gen3, E3DC |\n| Smart Home | HomeAssistant entities, Homematic |\n| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |\n\n### Required Meter Interface\n\n```go\ntype Meter interface {\n    CurrentPower() (float64, error)  // watts\n}\n```\n\n### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity`\n\n### Key Implementations\n- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection\n- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries\n- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources)\n\n## Vehicle Integrations\n\n| Manufacturer | API Type |\n|-------------|----------|\n| Tesla | Fleet API + vehicle-command proxy |\n| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |\n| Hyundai/Kia | BlueLink (regional variants) |\n| BMW/Mini | ConnectedDrive v2 |\n| Mercedes | Official API |\n| Renault/Nissan | Renault API + Carwings |\n| Ford | FordConnect (US/EU) |\n| Porsche | Porsche Connect |\n| PSA Group | Peugeot, Citroen, DS, Opel |\n| Generic | OVMS, Tronity |\n\n### Required Vehicle Interface\n\n```go\ntype Vehicle interface {\n    Battery          // Soc() (float64, error)\n    BatteryCapacity  // Capacity() float64\n    IconDescriber    // Icon() string\n    FeatureDescriber // Features() []Feature\n    PhaseDescriber   // Phases() int\n    TitleDescriber   // GetTitle() string\n    SetTitle(string)\n    Identifiers() []string\n    OnIdentified() ActionConfig\n}\n```\n\n### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector`\n\n### Polling Strategy\n\nConfigurable: always / while charging / while connected. Interval-based caching\nto avoid excessive cloud API calls. OAuth2 token handling built into each provider.\n\n## Auto-Detection (`cmd/detect/`)\n\nTask-based parallel IP scanning:\n`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes\n\nDetects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,\nTasmota, Shelly, Phoenix, and many more.\n","docs/agents/api-security.md":"# API Security & Authentication\n\nHow evcc authenticates HTTP requests and how endpoints are classified by\nsensitivity.\n\n## Threat Model\n\nevcc is designed for use within a trusted home network. The auth layer\nprotects credential management, configuration changes, and system operations\n(logs, backup/restore/reset, shutdown). Read-only state and basic charging\ncontrols are intentionally unauthenticated.\n\n## Auth Modes\n\n| Mode       | Trigger               | Behavior                                          |\n|------------|-----------------------|---------------------------------------------------|\n| `Enabled`  | default               | password required; JWT or API key accepted        |\n| `Disabled` | `--disable-auth` flag | all auth checks skipped                           |\n| `Locked`   | demo mode             | mutating endpoints return 403; reads still work   |\n\nMode is fixed at startup. The frontend mirrors the mode so admin actions can\nbe greyed out and a banner shown.\n\n## Endpoint Sensitivity Tiers\n\nThree tiers, by what the caller has to prove:\n\n**Public.** No auth. State, loadpoint controls, login. Anyone on the\nnetwork can read and operate.\n\n**Secure.** Requires a valid session: either the auth cookie (browser, JWT)\nor an API key in the `Authorization: Bearer …` header (automation). Used\nfor configuration and system administration.\n\n**Critical.** Secure plus an additional admin-password check inside the\nhandler. Used for destructive or credential-scoped operations.\n\nFor some Critical endpoints (backup, restore, reset) the password check is\n**skipped when the caller is authenticated via API key**, so unattended automation\ndoesn't need to embed the admin password. For credential-management\nendpoints (rotate API key, change admin password) the password check is\n**strict**: a leaked API key must not be able to rotate itself or change\nthe admin password.\n\nDisabling auth short-circuits all checks.\n\n## Sessions\n\nTwo transports, no overlap:\n\n- Browsers use a session cookie (JWT, 90-day TTL, issued on login).\n- Automation uses an API key in the `Authorization: Bearer …` header.\n\nAPI keys are random alphanumeric strings prefixed `evcc_`. The prefix makes\nleaked keys recognizable to secret-scanning tools.\n\nA single API key per installation; regenerating replaces the previous one.\nPlaintext is shown to the user **once** at generation time and cannot be\nretrieved afterwards.\n\n## Credential Storage\n\nAdmin password and API key are stored as bcrypt hashes. The JWT signing\nsecret is a per-installation random value. Plaintext credentials are never\npersisted.\n\nRemoving the admin password (CLI recovery) also clears the JWT secret and the\nAPI key, which invalidates all outstanding sessions and any previously-issued\nAPI key. Regenerating the API key replaces the stored hash; the previous key\nstops working immediately.\n\n## API Key Lifecycle\n\nTwo operations:\n\n- **Status.** Whether a key is configured. Secure tier; never returns\n  plaintext.\n- **Regenerate.** Critical tier, strict password check. Returns the new\n  plaintext key exactly once.\n\nThere is no delete operation: regenerating and discarding the new key\nachieves the same effect (the previous key stops working immediately).\n\n## Endpoint Matrix\n\n| Endpoint category                            | Tier      | Additional Requirements            |\n|----------------------------------------------|-----------|------------------------------------|\n| State / read-only / basic charging control   | Public    |                                    |\n| Set or update admin password                 | Public    | admin password                     |\n| Configuration                                | Secure    |                                    |\n| Configuration embedding a script plugin       | Critical  | api key or admin password          |\n| System: logs, cache, shutdown                | Secure    |                                    |\n| API key status                               | Secure    |                                    |\n| System: backup / restore / reset             | Critical  | api key or admin password          |\n| API key regenerate                           | Critical  | admin password                     |\n\nDevice test, create, and update (`/api/config/test/{class}` and `/api/config/devices/{class}`)\ninstantiate a config immediately, so a `script` plugin in the payload runs a shell command on the\nserver. Because that command could read credentials a session is not otherwise allowed to see (for\nexample the contents of the database), these requests are treated as Critical when the config embeds\na script plugin, at any nesting depth. A session caller must supply the admin password in the\n`X-Admin-Password` header; an API-key caller passes without it. The `go` (yaegi) and `js` (otto)\nplugins are excluded: their interpreters are sandboxed to pure computation and cannot read files,\nspawn processes, or open network connections.\n\n**Public** endpoints accept any caller. **Secure** endpoints require a\nvalid session (cookie or API key). **Critical** endpoints require extra\nauthentication in the form of an admin password (or, for some, an API\nkey).\n\n## Frontend\n\nAuth management lives under **General Config → Security**, which links to\ntwo sub-flows: change admin password, and manage the API key. The API key\nflow has a reveal view that shows the plaintext exactly once with a\ncopy-to-clipboard link.\n\nWhen auth is disabled, the security modals show a warning banner and\ndisable all action buttons. This is UI-only; the backend still accepts the\nunderlying calls so legitimate automation against a disabled-auth instance\nkeeps working.\n\n## OpenAPI\n\nThe OpenAPI spec declares two security schemes (cookie and bearer);\nprotected operations accept either.\n","docs/agents/core-domain.md":"# Core Domain: Site, Loadpoint, and the Control Loop\n\n## Object Hierarchy\n\n```\nSite (orchestrator — core/site.go)\n├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]\n├── Tariffs: Grid, FeedIn, CO2, Solar\n├── Coordinator (vehicle <-> loadpoint assignment)\n├── Prioritizer (power allocation fairness)\n└── Loadpoints[] (core/loadpoint.go)\n    ├── Charger      (api.Charger — hardware controller)\n    ├── Vehicle      (api.Vehicle — EV battery state via cloud API)\n    ├── ChargeMeter  (api.Meter — AC power at charger)\n    └── Circuit      (optional — electrical domain limits)\n```\n\n## Key Interfaces (api/api.go)\n\n### Meter\n- `Meter` — `CurrentPower() (float64, error)` — watts\n- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh\n- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings\n\n### Battery\n- `Battery` — `Soc() (float64, error)` — 0-100%\n- `BatteryCapacity` — kWh\n- `BatteryController` — set charge/discharge/hold mode\n\n### Charger\n- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)`\n- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)`\n- `PhaseSwitcher` — `Phases1p3p(int) error`\n- `ChargeRater` — `ChargedEnergy() (float64, error)`\n- `ChargeTimer` — `ChargeDuration() (time.Duration, error)`\n\n### Vehicle\n- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()`\n- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`\n- `ChargeController` — remote start/stop on vehicle\n- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits\n- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side\n\n## Charge Modes\n\n| Mode | Behavior |\n|------|----------|\n| `OFF` | Disabled (unless welcome charge) |\n| `SMART` | Ramp current with solar surplus; fast if cheap tariff or plan requires |\n| `NOW` | Max current immediately |\n\nAlways charge (`alwaysCharge`: off/on/once) is a per-loadpoint option for smart mode: never pause, keep at least min current. `once` resets on disconnect. `pv` and `minpv` are deprecated write-only aliases for smart without/with always charge.\n\n## Charge States (IEC 61851)\n\n- `A` — not connected\n- `B` — connected, not charging\n- `C` — connected, charging\n\n## The Control Loop (Site.update — runs every N seconds)\n\n```\n1. Update all meters (grid, PV, battery, aux)\n2. For each loadpoint: UpdateChargePowerAndCurrents()\n3. Calculate site power balance:\n   sitePower = gridPower + batteryPower + excessDCPower\n             + residualPower - auxPower - flexiblePower\n4. Apply battery priority rules (prioritySoc, bufferSoc)\n5. Get tariff rates\n6. For EACH loadpoint: Update(sitePower, ...)\n   ├── Read charger status\n   ├── Detect/identify vehicle\n   ├── Check plan requirements (minSOC, target time)\n   ├── Check limits (limitSOC, limitEnergy)\n   ├── MODE switch -> calculate target current\n   ├── Cap at maxCurrent, respect circuit limits\n   ├── Send MaxCurrent() to charger\n   └── Record metrics\n7. Push updates to WebSocket + metrics\n```\n\nThe loop is stateless per cycle: always re-reads actual state, calculates\noptimal current, sends single command. Resilient to restarts and missed updates.\n\n## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)\n\n```\n1. Read effective min/max current limits\n2. Reduce sitePower by battery boost power\n3. Consider phase switching (1p <-> 3p) if supported\n4. deltaCurrent = powerToCurrent(-sitePower, activePhases)\n   targetCurrent = effectiveCurrent + deltaCurrent\n5. Below minCurrent -> start disable timer (default 3 min)\n6. Surplus returns -> start enable timer (default 1 min)\n7. Cap at maxCurrent\n```\n\n## Battery Priority Rules\n\n| Setting | Effect |\n|---------|--------|\n| `prioritySoc` | Below this: battery charges first, EV gets 0 |\n| `bufferSoc` | Above this: EV can draw from battery reserves |\n| `bufferStartSoc` | Above this: EV charging can begin even if importing |\n\nNone of these apply to a loadpoint the optimizer controls: the optimizer plans the home battery, so a stop gives the battery priority and a charge takes the surplus (`loadpointSitePower`, `#34606`). A charge the home battery covers is pinned to its setpoint instead, the pv loop would count the discharge as deficit (`surplusRegime`, `#34809`).\n\n## Effective Price Calculation\n\n```\ngreenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower\neffectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare\n```\n\n## Concurrency Model\n\n- **Site** owns `RWMutex` for its state (meters, battery, tariffs)\n- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current)\n- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking\n- No global locks — ordering prevents deadlocks\n\n### Channels\n\n| Channel | Scope | Buffer | Purpose |\n|---------|-------|--------|---------|\n| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) |\n| `lpUpdateChan` | Site | 1 | Early loadpoint update requests |\n| `pushChan` | Loadpoint | 16 | User notifications, queued via `valueChan` so the message renders the state at event time |\n\n## Tariff Integration\n\nTypes: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar`\n\n### Smart Features\n- **Cheap-tariff override** — rate below threshold -> fast charge\n- **Smart feed-in** — feed-in rate above threshold -> prioritize export\n- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline\n  - `optimalPlan()` — cheapest non-contiguous slots\n  - `continuousPlan()` — cheapest continuous window (fallback)\n\n## Optimizer Suggestions (core/site_optimizer.go)\n\nThe optimizer (`optimizer.evcc.io`, sponsor-gated) solves a linear program over tariff slots. Every solved \"battery\" (home battery, vehicle/loadpoint) gets a `types.Suggestion` for the slot covering now. In `Automatic` mode these suggestions drive control; otherwise they are advisory (UI + notifications).\n\n### Request side: what becomes a battery\n\n- **Home battery meter**: `batteryRequest`. `controllable` = `api.HasCap[api.BatteryController]`. Only controllable batteries get `charge_from_grid` / `discharge_to_grid`.\n- **Loadpoint with vehicle**: `loadpointRequest`, `detail.Type = vehicle`, `detail.loadpoint = &id`, always `controllable`. Needs vehicle capacity (or a session energy limit) to model as storage. `ModeOff` sets `CMax = 0`; `ModeNow` and plans produce a `PDemand` / `SGoal`.\n- **Unmodellable loadpoints** (no vehicle, no capacity and no energy limit, or heaters) are added to the home load (`addUnmodelledLoad`) and get no suggestion.\n- Disconnected loadpoints are skipped entirely.\n\nDevice key (`batteryDetail.key()`): `battery:<name>` or `loadpoint:<id>`; empty key = cannot act.\n\n### Result side: `applyOptimizerResult`\n\nRuns after each solve, and again every cycle via `reapplySuggestions` when the active slot changes (re-derived from the cached solve, no new request). Expiry is only evaluated on that slot change: if the horizon is passed or no solve completed for `2 * tariff.SlotDuration`, `clearSuggestions()` drops suggestions, battery forecast and notification state. Within an unchanged slot the cached advice stays active regardless of age. A failed solve keeps the previous advice so controlled devices are not released for one cycle.\n\n1. `slot = schedule.activeSlot(now)`, `slotHours` = its duration. Grid import/export and all home-battery discharge for that slot are summed.\n2. `currentSlotSuggestion(detail, req, res, slot, gridImport, gridExport, slotHours)` per battery. Because the LP is linear, the slot result sits at an operating-range extreme and maps cleanly onto discrete modes. All thresholds use `suggestionThreshold = 50 W` to ignore solver noise.\n\n**Home battery** (`Action` is an `api.BatteryMode` string):\n\n| Condition (slot values, W)                         | Action       | Meaning                             |\n| -------------------------------------------------- | ------------ | ----------------------------------- |\n| charge > thr and grid importing                    | `charge`     | grid charging                       |\n| idle and grid importing and soc > `SMin`           | `hold`       | discharge deliberately withheld     |\n| idle and grid exporting and soc < `SMax`           | `holdcharge` | surplus exported instead of charged |\n| discharge > thr and grid exporting                 | `discharge`  | battery-to-grid                     |\n| otherwise (incl. idle at a soc bound, PV charging) | `normal`     | inverter self-consumption logic     |\n\n`idle` = charge and discharge both <= thr. At a soc bound nothing is withheld (the bound does it), hence `normal`.\n\n**Loadpoint/vehicle**: `charge` if charge > thr, else `stop`. `Suggestion.Battery` is set to the planned home-battery discharge so the loadpoint can tell surplus from battery-fed charging.\n\n3. Suggestions are stored only for controllable devices with a key (`site.suggestions`). Loadpoints with a plan goal (`SGoal`) also receive `optimizerPlan` (the remaining charging slots priced at `PN`) via `setOptimizerPlan`, used by `OptimizerPlan()` for the planner UI.\n4. `publishBattery` / `publishSuggestions` publish to UI and hand loadpoint suggestions to `lp.setSuggestion`. `Actionable` is evaluated on every read (`site.suggestion`) as `Action != currentAction`, where the current action is `site.batteryAction()` for batteries (the battery mode, `unknown` reads as `normal`) and `loadpointCurrentAction` (`charge` when enabled and goal not reached, else `stop`).\n5. `diffSuggestions` emits one `suggestion` messenger event per device whose actionable action changed since the last run; non-actionable or vanished devices are pruned so a later change re-notifies.\n\n### Control side: where suggestions take effect\n\nGated by `OptimizerAutomatic()`: `off` (advisory only), battery-only, or `full` (also loadpoints). `site.Automatic()` additionally requires optimizer enabled + sponsor.\n\n**Home battery** (`requiredBatteryMode`, `core/site_battery.go`), in priority order:\n\n- external mode (API) wins.\n- `Automatic()` and `unmodelledCharging(rate)`: a loadpoint the optimizer does not control is fast charging or on the smart cost limit. The suggestion did not account for that load, so force `hold`.\n- `Automatic()`: `batterySuggestionMode()` takes the first controllable battery's suggestion (`api.BatteryModeString(s.Action)`); an unknown action releases to `normal`; no suggestion releases to `normal` only if evcc had modified the mode (`batteryModeModified`), otherwise the mode is left untouched. This replaces the grid charge limit and discharge control. TODO: per-battery modes.\n- not automatic: legacy grid charge / discharge control / feed-in discharge rules.\n\n**Loadpoint** (`activeSuggestion`, `core/loadpoint_optimizer.go`): non-nil only if `optimizerControlled()` (full automatic, not a heater or integrated device, vehicle with capacity > 0) and the suggestion is younger than `suggestionMaxAge = 2 * SlotDuration` (a stalled optimizer must not keep the loadpoint gated). With an active suggestion:\n\n- `ModeSmart` strategy switch calls `optimizerCharging(s)` and skips the smart cost / feed-in price limits.\n  - `surplusRegime` (charge below max power, no grid flow, no battery discharge): not handled, the PV loop tracks measured surplus instead of the forecast.\n  - `stop` without always-charge: not handled, PV loop may still enable on measured surplus.\n  - `charge` at full power or above what the active phases deliver: `fastCharging()`.\n  - `charge` at a limited setpoint: `setLimit(PowerToCurrent(s.Charge))`, or `minCharging()` below min current.\n  - `stop` with always-charge: `minCharging()`.\n- Planner is reduced to a backstop: `plannerActive = planDeadlineCritical()`.\n- Session `LimitEnergy` / `LimitSoc` do not stop charging while the optimizer is driving a plan goal above them.\n- `loadpointSitePower`: priority soc adjustment is applied, and the battery's discharge is subtracted from the surplus unless the action is `charge` (the optimizer plans the home battery, so `bufferSoc` / `bufferStartSoc` are also disabled).\n\nEvery cycle ends with `site.publishSuggestions()` so `Actionable` reflects the updated loadpoint state.\n\n## Key File Locations\n\n- `api/api.go` — all core interfaces\n- `core/site.go` — Site orchestrator + control loop\n- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch)\n- `core/site_battery.go` — battery priority logic, battery mode from suggestion\n- `core/site_optimizer.go` — optimizer request, suggestion mapping, reapply/expiry\n- `core/loadpoint_optimizer.go` — loadpoint suggestion gate and charging strategy\n- `core/site_tariffs.go` — tariff integration\n- `core/planner/planner.go` — charge time optimization\n- `core/prioritizer/prioritizer.go` — power allocation across loadpoints\n- `core/circuit/circuit.go` — electrical domain limits\n","AGENTS.md":"# Agent Rules for evcc Project\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\n- evcc is an extensible EV Charge Controller and home energy management system written in Go with a Vue.js frontend\n- The system manages electric vehicle charging, integrates with solar systems, and provides local energy management without cloud dependencies\n- Architecture follows a plugin-based approach for device integrations\n\n## Essential Commands\n\n- `make` - build full application (UI + Go binary)\n- `make build` - build Go binary only\n- `make ui` - build UI assets only\n- `make install` - install Go tools and dependencies\n- `make install-ui` - install Node.js dependencies (`vp install`)\n- `make test` - run Go tests\n- `make test-ui` - run frontend tests\n- `make lint` - run Go linting (golangci-lint)\n- `make lint-ui` - run frontend linting\n- `vp run dev` - start Vue dev server (http://127.0.0.1:7071)\n- `vp run playwright` - run integration tests\n- `build`, `openapi` and `test` are cached tasks in `vite.config.ts`, run through `vp run`\n- `evcc --config [file] --disable-auth` - run a throw-away instance for UI checks without password setup\n- `evcc --template-type [type] --template [file]` - test device templates\n- `make docs` - generate template documentation\n\n## Domain Knowledge\n\nDeep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:\n\n| File                                                          | When to load                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| [Core Domain](docs/agents/core-domain.md)                     | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, optimizer  |\n| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices                    |\n| [Easee Architecture](docs/agents/easee-architecture.md)       | Easee charger (REST+SignalR, async correlation, concurrency)                 |\n| [OCPP Forwarder](docs/agents/ocpp-forwarder.md)               | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) |\n| [Plugin System](docs/agents/plugin-system.md)                 | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS)                               |\n| [Web UI & API](docs/agents/web-ui-api.md)                     | REST API, WebSocket, Vue frontend, authentication                            |\n| [API Security](docs/agents/api-security.md)                   | Auth modes, JWT/API key/session, two-tier checks, credential storage         |\n| [Provider Auth](docs/agents/provider-auth.md)                 | Third-party logins (OAuth, device code, tokens), template `auth` block, UI   |\n| [Chart Conventions](docs/agents/charts.md)                    | ECharts charts: axis units and scale rules, tooltips, layout                 |\n| [Energy Analytics](docs/agents/energy-analytics.md)           | Energy page metrics: attribution, cost/CO2, forecast accuracy, missing data  |\n| [Discovery Hints](docs/agents/discovery-hints.md)             | Vendor registry update, adding and reviewing template `discovery` hints      |\n\n### Loading guide by task type\n\n- **Charger implementation** — hardware-integrations + core-domain\n- **Easee charger work** — easee-architecture + core-domain\n- **Meter implementation** — hardware-integrations + plugin-system\n- **Vehicle implementation** — hardware-integrations\n- **UI/frontend work** — web-ui-api\n- **Chart work (ECharts, axes, tooltips)** — charts + web-ui-api\n- **API endpoint work** — web-ui-api + core-domain\n- **Auth / login / API key / permissions** — api-security + web-ui-api\n- **Vehicle or device cloud login (OAuth, tokens, captcha)** — provider-auth + hardware-integrations\n- **Config/template work** — plugin-system\n- **Discovery hints / vendor registry update** — discovery-hints + plugin-system\n- **Control loop / charging logic** — core-domain\n- **Energy page metrics (attribution, savings, accuracy)** — energy-analytics + charts\n- **Bug in any area** — core-domain + relevant topic file(s)\n\n## Architecture Guidelines\n\n### Core Components\n\n- **main.go** serves as entry point and embeds web assets and i18n files\n- **cmd/** contains CLI commands, application setup, and various utility commands (configure, detect, migrate, etc.)\n- **core/** contains core business logic with main files (loadpoint.go, site.go) and subdirectories:\n  - **loadpoint/** - EV charging point management modules\n  - **planner/** - Smart charging planning algorithms\n  - **coordinator/** - Multi-loadpoint coordination logic\n  - **session/** - Charging session management\n  - **vehicle/** - Vehicle-specific core logic\n  - **soc/** - State of charge handling\n- **api/** contains API definitions and types\n- **server/** handles HTTP server, WebSocket, MQTT, database operations, and various handlers\n- **charger/**, **meter/**, **vehicle/** contain device integrations\n- **tariff/** contains tariff integrations\n- **plugin/** implements plugin system for device and tariff communication\n- **assets/** contains Vue.js frontend application\n\n### Frontend Structure\n\n- **assets/js/** contains the main TypeScript/Vue.js application with:\n  - **views/** - Vue page components (App.vue, Config.vue, Sessions.vue, etc.)\n  - **components/** - Reusable Vue components\n  - **composables/** - Vue utility functions\n  - **types/** - TypeScript type definitions\n  - **utils/** - Utility functions\n  - **mixins/** - Vue mixins\n- **assets/css/** contains application stylesheets\n- **assets/public/** contains static assets and metadata\n- **i18n/** contains internationalization files\n- **tests/** contains Playwright integration tests and test configuration files\n- **dist/** contains built frontend assets (generated)\n\n## Writing Style\n\n- No em dashes (—) in comments, commit messages, or docs. Use periods, commas, or colons\n- Project name is `evcc`, always lowercase\n- In user-facing strings, only mention `evcc` when needed to understand the context. Inside evcc's own UI the self-reference is usually redundant\n- Acronyms uppercase in prose: OCPP, MQTT, HEMS, SoC\n- Terminology: agreed terms per language with explanations live in `i18n/glossary/` (`en.json` + `<lang>.json`, rules in `README.md`). Check it before wording user-facing strings (including API descriptions in `assets/js/types/evcc.ts` and `server/openapi.yaml`), and extend it when a new concept gets a name\n- Commit subjects: `Component: short description`, no trailing period. Sub-scope in parens: `Meter (Home Assistant): ...`. Use `chore:`/`fix:`/`docs:` only for non-feature changes\n\n## Comment Style\n\n- Prefer self-documenting code over comments; comment the _why_, not the _what_\n- Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary\n- Skip refs to the current task, PR, issue, or caller (\"added for X flow\", \"see #1234\"). Git history covers that\n- Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name\n\n## Go Coding Standards\n\n### Core Principles\n\n- Follow Go idioms and conventions (Effective Go)\n- Use `gofmt` for formatting, self-documenting names, early returns\n- Handle all errors explicitly with meaningful messages\n- Use interfaces for behavior contracts (small, focused, single responsibility)\n- Use `context.Context` for I/O, long-running, or cancelable operations\n- Organize code into logical packages with clear responsibilities\n- Prefer composition over inheritance, minimize external dependencies\n- Navigate Go symbols with go-to-definition and find-references rather than text search; reserve text search for comments and string literals\n\n### File Patterns\n\n- `_blueprint.go` - templates for new device implementations\n- `_enumer.go` - generated enum code\n- `*_decorators.go` - generated decorator pattern implementations\n- Validate interface implementations: `var _ Interface = (*Type)(nil)`\n- Capabilities: register via `implement.Has`/`May` only when a capability is _conditional_ (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()`\n\n### Error Handling\n\n- Wrap errors with context: `fmt.Errorf(\"context: %w\", err)`\n- Use `errors.As` and `errors.Is` for type checking\n- Use `errors.Join` for combining errors (prefer custom `joinErrors` helper)\n- Create domain-specific error types (ClassError, DeviceError)\n- Use `backoff.Permanent(err)` for non-retryable errors\n- Implement panic recovery with `defer` and `recover()` in script contexts\n\n### Testing & Code Generation\n\n- Use `testing` package with `testify/assert` and `testify/require`\n- Table-driven tests with struct definitions for multiple cases\n- Use `gomock` for interface mocking, `go:generate mockgen` for generation\n- Test both success and failure scenarios, use `require` for setup, `assert` for tests\n- Use `go:generate` for code generation, regenerate after interface/enum changes\n- Never manually edit generated files\n\n### Context & Concurrency\n\n- Use `context.Context` as first parameter for I/O operations\n- Use `context.WithTimeout`, `context.WithCancel` appropriately\n- Check `ctx.Done()` in long-running loops\n- Propagate context through goroutines for proper cancellation\n- Handle concurrent operations safely with Go's concurrency primitives\n\n### Data Validation\n\n- Filter `NaN` and `Infinity` values using `math.IsNaN()` and `math.IsInf()`\n- Validate numeric inputs from external sources\n- Use helper functions like `parseFloat()` that reject invalid values\n\n## Vue.js/TypeScript Frontend Standards\n\n### Core Architecture\n\n- Use Vue 3 Options API (preferred over Composition API)\n- Use reactive stores without Vuex/Pinia for cross-component state\n- Use global app instance (`window.app`) only for: notifications (`raise()`), offline status (`setOffline()`/`setOnline()`), clearing notifications (`clear()`)\n- Organize components by feature/domain in `assets/js/components/` subdirectories\n\n### Component Development\n\n- Use TypeScript for all new frontend code\n- Use `const` instead of `function` for component methods (e.g., `const updateType = () =>`)\n- Define TypeScript interfaces for component props, data, and API responses\n- Implement accessibility features (tabindex, aria-label, keyboard handlers)\n- Use descriptive names for variables, functions, and event handlers\n- Use early returns for readability\n- Prefer named computed properties over inline template expressions, even for single use. Readability beats saving lines\n- Use configured Axios instance for HTTP communication\n\n### State Management\n\n- Never access the store from sub-components; keep them stateless and pass the values they need as props (emit events back to the parent). Only top-level views read from the store. This keeps components reusable and testable (e.g. Storybook should never mock the store).\n- Use `reactive()` from Vue for simple global state\n- Implement property setters for nested object updates using helper functions\n- Use localStorage with reactive wrappers for persistent settings\n- Use Vue `watch()` for automatic persistence of settings changes\n- Separate concerns with dedicated stores (settings, application state)\n\n### TypeScript Patterns\n\n- Define comprehensive interfaces for API responses and application state\n- Use enums for constants (e.g., `THEME`, `CURRENCY`)\n- Extend global interfaces for window object augmentation\n- Use union types for flexible but type-safe configurations\n- Use generic types for reusable utility functions\n- Handle type assertions carefully with proper error handling\n- Create focused utility functions with proper TypeScript typing\n\n### Styling & Internationalization\n\n- Use CSS Custom Properties for theming (semantic names: `--evcc-green`, `--evcc-battery`)\n- No inline `style` attributes; use Bootstrap utility classes or scoped CSS (inline only for truly dynamic values)\n- Use the custom media queries from `assets/css/breakpoints.css` (`@media (--sm-and-up)`) for responsive breakpoints, never raw px `min-width`/`max-width`. Write mobile-first and override upwards instead of adding max-width queries\n- Use `$t()` function for all user-facing strings\n- Update both `i18n/en.json` and `i18n/de.json` for new strings\n- Use hierarchical namespace: `{section}.{component}.{purpose}`\n- Examples: `config.vehicle.titleAdd`, `main.vehicleStatus.charging`\n- Action patterns: `titleAdd`, `titleEdit`, `save`, `cancel`, `delete`, `validateSave`\n- Use placeholders for dynamic content: `{soc}`, `{duration}`, `{value}`\n- Prefer context-specific keys over generic ones\n- Test with German translations (20-40% longer text)\n- Keep separators and trailing punctuation (`: `, `…`, `—`) in the template, not in the translation value.\n\n### Testing\n\n- Write integration tests using Playwright for user workflows\n- Use Storybook for component development and visual testing\n- Use semantic selectors (roles, labels, button text); `data-testid` only when necessary\n- Test error states and loading states\n\n## Playwright Integration Testing\n\n### Test Organization\n\n- **Location**: `tests/` directory with `.spec.ts` files\n- **Configuration**: `.evcc.yaml` files for different test scenarios\n- **Utilities**: `tests/utils.ts` for common helpers, `tests/evcc.ts` for binary management\n- **Categories**: `config-*.spec.ts` (UI config), `sessions.spec.ts`/`plan.spec.ts` (workflows), `smart-cost.spec.ts`/`limits.spec.ts` (features), `backup-restore.spec.ts`/`auth.spec.ts` (integration)\n\n### Test Configuration\n\n- Base URL: `http://127.0.0.1:7070`\n- Parallel execution with different ports per worker for isolation\n- Uses `./evcc` binary with test-specific configuration files\n- Each worker uses isolated temporary database files\n- Always runs with English UI language\n\n### Essential Commands\n\n- Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev).\n- Run tests: `vp run playwright` or `vpx playwright test`\n- Debug: `vpx playwright test --debug`\n- Specific test: `vpx playwright test tests/config-loadpoint.spec.ts`\n\n### Selector Strategy\n\n- **Preferred**: Semantic selectors using `getByRole()`, `getByLabel()`, `getByText()`\n- **Fallback**: `data-testid` only when semantic selectors aren't available\n- **Examples**:\n  - `page.getByRole(\"button\", { name: \"Add charger\" })`\n  - `page.getByLabel(\"Manufacturer\").selectOption(\"Demo charger\")`\n  - `page.getByRole(\"listitem\", { name: \"Draggable: First Loadpoint\" })` (using aria-label)\n  - `page.getByTestId(\"loadpoint\")` (fallback only)\n- never use `.locator()` or `class` and `id`-based selectors\n\n### Test Patterns\n\n- Use test-specific `.evcc.yaml` configurations\n- Import utilities from `tests/utils.ts` for common operations\n- Focus on complete user journeys rather than isolated interactions\n- Use `expectModalVisible()` and `expectModalHidden()` helpers\n- Test configuration persistence across application restarts\n- Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)`\n- Never use fixed timeouts. Wait on element state (visibility, count, value) instead.\n- Never use `page.waitForLoadState(\"networkidle\")`. SPAs keep emitting requests (websockets, polling), so it either races or hangs. Wait for the specific element / value you need instead.\n- Keep test names and describe titles short and concrete. They should complement each other, not repeat. Prefer `describe(\"aux meter\") test(\"create\")` over `describe(\"aux meter\") test(\"create aux meter and verify it appears\")`. Drop scenario filler like \"and lands in section\", \"appears correctly\", \"ensure\".\n\n## Device Integration & Configuration\n\n### Plugin System\n\n- Device types: chargers, meters, vehicles, tariffs\n- Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go\n- Define device capabilities and configuration in templates at `templates/definition/[type]/`\n- Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a _different_ value. Restating the same value is redundant duplication: reference the param by `name` alone.\n- Test templates: `evcc --template-type [type] --template [file]`\n- Update docs after template changes: `make docs`\n- When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details\n\n### Configuration\n\n- Use YAML format for all configuration files (default: `evcc.yaml`, or specify with `--config`)\n- Provide clear validation and error messages for invalid configurations\n- Support template-based device configurations with meaningful defaults\n- Use SQLite as default database (default: `evcc.db`, or specify with `--database`) with proper migrations and data integrity\n\n## Security & Performance Guidelines\n\n### Security\n\n- Validate all user inputs and sanitize data before database storage\n- Use secure protocols (TLS) for external integrations\n- Implement proper authentication and authorization\n- Never log sensitive information (passwords, tokens, personal data)\n\n### Performance\n\n- Optimize database queries with appropriate indexes\n- Handle concurrent operations safely with Go's concurrency primitives\n- Implement proper caching strategies and connection pooling\n- Avoid blocking operations in main application loop\n\n## Pull Request Descriptions\n\nStructure PR descriptions in this order. No headlines. Be concise.\n\n1. **References first line**: link related issues or PRs (`fixes #1123`, `replaces #222`, `pairs with org/repo#345`). PRs should almost always reference an issue or related PR — only skip in rare exceptions (e.g. trivial typo fixes).\n2. **Intro**: one or a few concise sentences framing what the PR does and why it was created this way. The full problem description belongs in the linked issue, not here.\n3. **Bullet list**: most significant changes or user-facing implications. Lead with the most significant.\n4. **TODO section** (only if open points remain):\n\n   ```\n   **TODO**\n   - [ ] item a\n   - [ ] item b\n   ```\n\nAvoid file paths, line numbers, or code listings reproduced from the diff. Include a code snippet only when it conveys the contract (event shape, API signature) more clearly than prose. No testing checklists, no co-author footers.\n\nNever state that `go build`, `go vet`, `go test -race`, or `gofmt` pass (or any \"all checks/tests green\" phrasing). These are non-negotiable givens that must already be fulfilled, not noteworthy results.\n\n## Pull Request CI and Reviews\n\n- After opening or updating a pull request, watch CI until every check has finished. Work is not done while checks are still running. Fix failures on the same branch and keep watching until the run is green.\n- Do not argue with automated review bots such as Sourcery. Either implement the suggestion or resolve the thread. No rebuttal comments.\n\n## AI Attribution\n\nWork produced by an AI agent must be attributable as such on GitHub. Append the tool's attribution footer to every PR description, issue body, review, and comment written by an agent, for example:\n\n```\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n```\n\nCommit messages are the exception: no footer, and no `Co-Authored-By` trailer. Never dress agent work up as human review (e.g. \"PR by an agent but looks good to me\") in place of the footer.\n","docs/agents/web-ui-api.md":"# Web UI & REST API\n\n## Server Architecture\n\n- **Router:** gorilla/mux, strict slash\n- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth\n- **Timeouts:** Read 5s, Write 10s, Idle 120s\n- **Static assets:** embedded in binary (`fs.FS`)\n- **Default port:** 7070\n\n## REST API (base `/api/`)\n\n### Site-level\n- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc.\n- `GET /tariff/{tariff}` — tariff rates\n- `GET /sessions` — charging history\n- `GET /state` — complete system state (supports jq filtering)\n\n### Per-loadpoint (`/loadpoints/{id}/...`)\n- `POST mode/{value}` — off/smart/now (deprecated aliases pv/minpv)\n- `POST alwayscharge/{value}` — off/on/once\n- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits\n- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits\n- `POST phases/{value}` — phase config\n- `POST priority/{value}`, `batteryboost/{value}`\n- `POST plan/energy/{value}/{time}` — schedule plan\n- `POST vehicle/{name}` — select vehicle\n- `POST smartcostlimit/{value}` — smart cost threshold\n- `POST solarshare/{value}` — required solar share of min power (default 1)\n\n### Configuration (`/config/...`, auth required)\n- CRUD for devices (chargers, meters, vehicles, tariffs)\n- Template browsing and testing\n- Site, loadpoint, circuit, HEMS, messaging config\n- `GET /config/evcc.yaml` — YAML export\n\n### System (`/system/...`, auth required)\n- Log viewing (`/log`, `/log/areas`), cache clear, shutdown\n\n### Database (`/db/...`, auth + second factor required)\n- Backup download, restore from file, selective reset\n- Second factor: admin password in request body, or API key via Bearer token (bypasses password check)\n\n### Handler Pattern\nGeneric `handler[T]` with type conversion, setter, getter.\nSpecialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`.\n\n## WebSocket (`/ws`)\n\n- `coder/websocket` (RFC 6455)\n- Pub/sub via `SocketHub`\n- Buffered channels (1024 per subscriber)\n- Welcome message with full state snapshot\n- Incremental updates as JSON key-value pairs with dot-notation keys:\n  ```json\n  {\"loadpoints.1.mode\": \"solar\", \"site.gridPower\": 1234}\n  ```\n- Write timeout: 10s, compression (disabled for Safari)\n\n## State Flow\n\n1. WS connects -> receives welcome with full state\n2. App emits `util.Param` on changes\n3. Hub broadcasts to subscribers\n4. Frontend `store.update(msg)` merges via dot-notation\n5. Components reactively re-render\n\n## Authentication\n\n- JWT, 90-day lifetime\n- HttpOnly cookie (`auth`) with `SameSite=Strict`\n- Also accepts `Authorization: Bearer <token>` header\n- Modes: Disabled, Locked (demo), Configured (password)\n- Protects `/api/config`, `/api/system`, and `/api/db`\n\n## MQTT Integration\n\n- Publishes state changes to configurable broker\n- Subscribes to control topics\n- Retained messages for state persistence\n\n## Key Files\n\n- `server/http.go` — router setup\n- `server/http_auth.go` — authentication\n- `server/http_site_handler.go` — state + request handlers\n- `server/http_config_*.go` — config endpoints\n- `server/http_loadpoint_handler.go` — per-loadpoint endpoints\n- `server/socket.go` — WebSocket pub/sub\n- `assets/js/app.ts` — Vue app entry\n- `assets/js/store.ts` — reactive state store\n- `assets/js/api.ts` — Axios clients\n- `assets/js/router.ts` — route definitions\n","docs/agents/plugin-system.md":"# Plugin System\n\nThe plugin system (`plugin/`) provides protocol-level abstraction for device\ncommunication. Plugins implement typed getter/setter interfaces and are composed\ninto charger, meter, or vehicle implementations via configuration.\n\n## Plugin Types\n\n| Plugin | Protocol | Key Config |\n|--------|----------|------------|\n| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` |\n| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` |\n| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` |\n| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree |\n| `js` | JavaScript/WASM | Inline script evaluation |\n| `go` | Go runtime | Dynamic Go code |\n| `gpio` | Linux GPIO | Digital I/O for relays |\n\n## Getter/Setter Interfaces\n\n```go\ntype StringGetter func() (string, error)\ntype FloatGetter  func() (float64, error)\ntype IntGetter    func() (int64, error)\ntype BoolGetter   func() (bool, error)\n// + corresponding Setter types\n```\n\n## Pipeline Transforms\n\nPlugins support chained transforms: `scale`, `offset`, `lookup`, `regex`.\n\n## Template-Based Device Configuration\n\nDevices can be defined entirely via YAML templates using plugins:\n\n```yaml\n# templates/definition/charger/example.yaml\nstatus:\n  source: http\n  uri: http://{{ .host }}/status\nenable:\n  source: http\n  uri: http://{{ .host }}/enable\n  method: POST\nmaxcurrent:\n  source: http\n  uri: http://{{ .host }}/current/{{ .maxcurrent }}\n```\n\nThe generic configurable charger (`charger/charger.go`) wires these plugin\nconfigs into the `api.Charger` interface at runtime.\n\nTemplates render once at config time. HTTP `uri` and `body` are templates\nthemselves, evaluated per request (`util.ReplaceFormatted`: sprig, `addDate`,\n`timeRound`). Defer `now` to request time with a raw string:\n`` {{ `{{ now | date \"2006-01-02\" }}` }} ``.\n\n## Param Suggestions and Discovery\n\nTemplate params get suggestions from services (`service:`), see the `service`\nand `discovery` sections in `templates/README.md` for the contract. Handlers\nregister in `init()` via `service.Register` (`server/service/registry.go`),\nobject responses use `service.Option` (`util/service/helper.go`) and render as\n`Helper/Combobox.vue`.\n\nLocal network discovery lives in `util/discovery` (mDNS, SSDP, neighbor table,\nreverse DNS; no raw sockets, no port scans) and feeds `network/hosts`,\n`network/report` and `network/scan` in `util/service/network*.go`. The vendor\nlist is generated from the IEEE registry (`make oui-update`), see\n[Discovery Hints](discovery-hints.md) for the update routine.\n\n- Scan: 3 s first pass (first request waits), 5 s second pass, repeated when\n  older than a minute, results combined, hosts dropped after 30 minutes.\n- mDNS: browses the types of the template hints plus all types announced in\n  the network (DNS-SD enumeration), so reports show vendor types without a hint.\n- A host keeps all its names (router name first, then mDNS), hostname hints\n  match any of them.\n- `EVCC_DISCOVERY_HOSTS` (JSON list of `discovery.Host`) replaces the scan,\n  used in tests and for environments without network access. On macOS the neighbor table is empty for binaries started with\n  `go run`, test with a built binary.\n\n## Key Files\n\n- `plugin/config.go` — plugin registry and config types\n- `plugin/http.go` — HTTP plugin\n- `plugin/mqtt.go` — MQTT plugin\n- `plugin/modbus.go` — Modbus plugin\n- `plugin/sunspec.go` — SunSpec plugin\n- `charger/charger.go` — generic configurable charger using plugins\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Rules for evcc Project\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\n- evcc is an extensible EV Charge Controller and home energy management system written in Go with a Vue.js frontend\n- The system manages electric vehicle charging, integrates with solar systems, and provides local energy management without cloud dependencies\n- Architecture follows a plugin-based approach for device integrations\n\n## Essential Commands\n\n- `make` - build full application (UI + Go binary)\n- `make build` - build Go binary only\n- `make ui` - build UI assets only\n- `make install` - install Go tools and dependencies\n- `make install-ui` - install Node.js dependencies (`vp install`)\n- `make test` - run Go tests\n- `make test-ui` - run frontend tests\n- `make lint` - run Go linting (golangci-lint)\n- `make lint-ui` - run frontend linting\n- `vp run dev` - start Vue dev server (http://127.0.0.1:7071)\n- `vp run playwright` - run integration tests\n- `build`, `openapi` and `test` are cached tasks in `vite.config.ts`, run through `vp run`\n- `evcc --config [file] --disable-auth` - run a throw-away instance for UI checks without password setup\n- `evcc --template-type [type] --template [file]` - test device templates\n- `make docs` - generate template documentation\n\n## Domain Knowledge\n\nDeep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:\n\n| File                                                          | When to load                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| [Core Domain](docs/agents/core-domain.md)                     | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, optimizer  |\n| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices                    |\n| [Easee Architecture](docs/agents/easee-architecture.md)       | Easee charger (REST+SignalR, async correlation, concurrency)                 |\n| [OCPP Forwarder](docs/agents/ocpp-forwarder.md)               | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) |\n| [Plugin System](docs/agents/plugin-system.md)                 | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS)                               |\n| [Web UI & API](docs/agents/web-ui-api.md)                     | REST API, WebSocket, Vue frontend, authentication                            |\n| [API Security](docs/agents/api-security.md)                   | Auth modes, JWT/API key/session, two-tier checks, credential storage         |\n| [Provider Auth](docs/agents/provider-auth.md)                 | Third-party logins (OAuth, device code, tokens), template `auth` block, UI   |\n| [Chart Conventions](docs/agents/charts.md)                    | ECharts charts: axis units and scale rules, tooltips, layout                 |\n| [Energy Analytics](docs/agents/energy-analytics.md)           | Energy page metrics: attribution, cost/CO2, forecast accuracy, missing data  |\n| [Discovery Hints](docs/agents/discovery-hints.md)             | Vendor registry update, adding and reviewing template `discovery` hints      |\n\n### Loading guide by task type\n\n- **Charger implementation** — hardware-integrations + core-domain\n- **Easee charger work** — easee-architecture + core-domain\n- **Meter implementation** — hardware-integrations + plugin-system\n- **Vehicle implementation** — hardware-integrations\n- **UI/frontend work** — web-ui-api\n- **Chart work (ECharts, axes, tooltips)** — charts + web-ui-api\n- **API endpoint work** — web-ui-api + core-domain\n- **Auth / login / API key / permissions** — api-security + web-ui-api\n- **Vehicle or device cloud login (OAuth, tokens, captcha)** — provider-auth + hardware-integrations\n- **Config/template work** — plugin-system\n- **Discovery hints / vendor registry update** — discovery-hints + plugin-system\n- **Control loop / charging logic** — core-domain\n- **Energy page metrics (attribution, savings, accuracy)** — energy-analytics + charts\n- **Bug in any area** — core-domain + relevant topic file(s)\n\n## Architecture Guidelines\n\n### Core Components\n\n- **main.go** serves as entry point and embeds web assets and i18n files\n- **cmd/** contains CLI commands, application setup, and various utility commands (configure, detect, migrate, etc.)\n- **core/** contains core business logic with main files (loadpoint.go, site.go) and subdirectories:\n  - **loadpoint/** - EV charging point management modules\n  - **planner/** - Smart charging planning algorithms\n  - **coordinator/** - Multi-loadpoint coordination logic\n  - **session/** - Charging session management\n  - **vehicle/** - Vehicle-specific core logic\n  - **soc/** - State of charge handling\n- **api/** contains API definitions and types\n- **server/** handles HTTP server, WebSocket, MQTT, database operations, and various handlers\n- **charger/**, **meter/**, **vehicle/** contain device integrations\n- **tariff/** contains tariff integrations\n- **plugin/** implements plugin system for device and tariff communication\n- **assets/** contains Vue.js frontend application\n\n### Frontend Structure\n\n- **assets/js/** contains the main TypeScript/Vue.js application with:\n  - **views/** - Vue page components (App.vue, Config.vue, Sessions.vue, etc.)\n  - **components/** - Reusable Vue components\n  - **composables/** - Vue utility functions\n  - **types/** - TypeScript type definitions\n  - **utils/** - Utility functions\n  - **mixins/** - Vue mixins\n- **assets/css/** contains application stylesheets\n- **assets/public/** contains static assets and metadata\n- **i18n/** contains internationalization files\n- **tests/** contains Playwright integration tests and test configuration files\n- **dist/** contains built frontend assets (generated)\n\n## Writing Style\n\n- No em dashes (—) in comments, commit messages, or docs. Use periods, commas, or colons\n- Project name is `evcc`, always lowercase\n- In user-facing strings, only mention `evcc` when needed to understand the context. Inside evcc's own UI the self-reference is usually redundant\n- Acronyms uppercase in prose: OCPP, MQTT, HEMS, SoC\n- Terminology: agreed terms per language with explanations live in `i18n/glossary/` (`en.json` + `<lang>.json`, rules in `README.md`). Check it before wording user-facing strings (including API descriptions in `assets/js/types/evcc.ts` and `server/openapi.yaml`), and extend it when a new concept gets a name\n- Commit subjects: `Component: short description`, no trailing period. Sub-scope in parens: `Meter (Home Assistant): ...`. Use `chore:`/`fix:`/`docs:` only for non-feature changes\n\n## Comment Style\n\n- Prefer self-documenting code over comments; comment the _why_, not the _what_\n- Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary\n- Skip refs to the current task, PR, issue, or caller (\"added for X flow\", \"see #1234\"). Git history covers that\n- Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name\n\n## Go Coding Standards\n\n### Core Principles\n\n- Follow Go idioms and conventions (Effective Go)\n- Use `gofmt` for formatting, self-documenting names, early returns\n- Handle all errors explicitly with meaningful messages\n- Use interfaces for behavior contracts (small, focused, single responsibility)\n- Use `context.Context` for I/O, long-running, or cancelable operations\n- Organize code into logical packages with clear responsibilities\n- Prefer composition over inheritance, minimize external dependencies\n- Navigate Go symbols with go-to-definition and find-references rather than text search; reserve text search for comments and string literals\n\n### File Patterns\n\n- `_blueprint.go` - templates for new device implementations\n- `_enumer.go` - generated enum code\n- `*_decorators.go` - generated decorator pattern implementations\n- Validate interface implementations: `var _ Interface = (*Type)(nil)`\n- Capabilities: register via `implement.Has`/`May` only when a capability is _conditional_ (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()`\n\n### Error Handling\n\n- Wrap errors with context: `fmt.Errorf(\"context: %w\", err)`\n- Use `errors.As` and `errors.Is` for type checking\n- Use `errors.Join` for combining errors (prefer custom `joinErrors` helper)\n- Create domain-specific error types (ClassError, DeviceError)\n- Use `backoff.Permanent(err)` for non-retryable errors\n- Implement panic recovery with `defer` and `recover()` in script contexts\n\n### Testing & Code Generation\n\n- Use `testing` package with `testify/assert` and `testify/require`\n- Table-driven tests with struct definitions for multiple cases\n- Use `gomock` for interface mocking, `go:generate mockgen` for generation\n- Test both success and failure scenarios, use `require` for setup, `assert` for tests\n- Use `go:generate` for code generation, regenerate after interface/enum changes\n- Never manually edit generated files\n\n### Context & Concurrency\n\n- Use `context.Context` as first parameter for I/O operations\n- Use `context.WithTimeout`, `context.WithCancel` appropriately\n- Check `ctx.Done()` in long-running loops\n- Propagate context through goroutines for proper cancellation\n- Handle concurrent operations safely with Go's concurrency primitives\n\n### Data Validation\n\n- Filter `NaN` and `Infinity` values using `math.IsNaN()` and `math.IsInf()`\n- Validate numeric inputs from external sources\n- Use helper functions like `parseFloat()` that reject invalid values\n\n## Vue.js/TypeScript Frontend Standards\n\n### Core Architecture\n\n- Use Vue 3 Options API (preferred over Composition API)\n- Use reactive stores without Vuex/Pinia for cross-component state\n- Use global app instance (`window.app`) only for: notifications (`raise()`), offline status (`setOffline()`/`setOnline()`), clearing notifications (`clear()`)\n- Organize components by feature/domain in `assets/js/components/` subdirectories\n\n### Component Development\n\n- Use TypeScript for all new frontend code\n- Use `const` instead of `function` for component methods (e.g., `const updateType = () =>`)\n- Define TypeScript interfaces for component props, data, and API responses\n- Implement accessibility features (tabindex, aria-label, keyboard handlers)\n- Use descriptive names for variables, functions, and event handlers\n- Use early returns for readability\n- Prefer named computed properties over inline template expressions, even for single use. Readability beats saving lines\n- Use configured Axios instance for HTTP communication\n\n### State Management\n\n- Never access the store from sub-components; keep them stateless and pass the values they need as props (emit events back to the parent). Only top-level views read from the store. This keeps components reusable and testable (e.g. Storybook should never mock the store).\n- Use `reactive()` from Vue for simple global state\n- Implement property setters for nested object updates using helper functions\n- Use localStorage with reactive wrappers for persistent settings\n- Use Vue `watch()` for automatic persistence of settings changes\n- Separate concerns with dedicated stores (settings, application state)\n\n### TypeScript Patterns\n\n- Define comprehensive interfaces for API responses and application state\n- Use enums for constants (e.g., `THEME`, `CURRENCY`)\n- Extend global interfaces for window object augmentation\n- Use union types for flexible but type-safe configurations\n- Use generic types for reusable utility functions\n- Handle type assertions carefully with proper error handling\n- Create focused utility functions with proper TypeScript typing\n\n### Styling & Internationalization\n\n- Use CSS Custom Properties for theming (semantic names: `--evcc-green`, `--evcc-battery`)\n- No inline `style` attributes; use Bootstrap utility classes or scoped CSS (inline only for truly dynamic values)\n- Use the custom media queries from `assets/css/breakpoints.css` (`@media (--sm-and-up)`) for responsive breakpoints, never raw px `min-width`/`max-width`. Write mobile-first and override upwards instead of adding max-width queries\n- Use `$t()` function for all user-facing strings\n- Update both `i18n/en.json` and `i18n/de.json` for new strings\n- Use hierarchical namespace: `{section}.{component}.{purpose}`\n- Examples: `config.vehicle.titleAdd`, `main.vehicleStatus.charging`\n- Action patterns: `titleAdd`, `titleEdit`, `save`, `cancel`, `delete`, `validateSave`\n- Use placeholders for dynamic content: `{soc}`, `{duration}`, `{value}`\n- Prefer context-specific keys over generic ones\n- Test with German translations (20-40% longer text)\n- Keep separators and trailing punctuation (`: `, `…`, `—`) in the template, not in the translation value.\n\n### Testing\n\n- Write integration tests using Playwright for user workflows\n- Use Storybook for component development and visual testing\n- Use semantic selectors (roles, labels, button text); `data-testid` only when necessary\n- Test error states and loading states\n\n## Playwright Integration Testing\n\n### Test Organization\n\n- **Location**: `tests/` directory with `.spec.ts` files\n- **Configuration**: `.evcc.yaml` files for different test scenarios\n- **Utilities**: `tests/utils.ts` for common helpers, `tests/evcc.ts` for binary management\n- **Categories**: `config-*.spec.ts` (UI config), `sessions.spec.ts`/`plan.spec.ts` (workflows), `smart-cost.spec.ts`/`limits.spec.ts` (features), `backup-restore.spec.ts`/`auth.spec.ts` (integration)\n\n### Test Configuration\n\n- Base URL: `http://127.0.0.1:7070`\n- Parallel execution with different ports per worker for isolation\n- Uses `./evcc` binary with test-specific configuration files\n- Each worker uses isolated temporary database files\n- Always runs with English UI language\n\n### Essential Commands\n\n- Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev).\n- Run tests: `vp run playwright` or `vpx playwright test`\n- Debug: `vpx playwright test --debug`\n- Specific test: `vpx playwright test tests/config-loadpoint.spec.ts`\n\n### Selector Strategy\n\n- **Preferred**: Semantic selectors using `getByRole()`, `getByLabel()`, `getByText()`\n- **Fallback**: `data-testid` only when semantic selectors aren't available\n- **Examples**:\n  - `page.getByRole(\"button\", { name: \"Add charger\" })`\n  - `page.getByLabel(\"Manufacturer\").selectOption(\"Demo charger\")`\n  - `page.getByRole(\"listitem\", { name: \"Draggable: First Loadpoint\" })` (using aria-label)\n  - `page.getByTestId(\"loadpoint\")` (fallback only)\n- never use `.locator()` or `class` and `id`-based selectors\n\n### Test Patterns\n\n- Use test-specific `.evcc.yaml` configurations\n- Import utilities from `tests/utils.ts` for common operations\n- Focus on complete user journeys rather than isolated interactions\n- Use `expectModalVisible()` and `expectModalHidden()` helpers\n- Test configuration persistence across application restarts\n- Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)`\n- Never use fixed timeouts. Wait on element state (visibility, count, value) instead.\n- Never use `page.waitForLoadState(\"networkidle\")`. SPAs keep emitting requests (websockets, polling), so it either races or hangs. Wait for the specific element / value you need instead.\n- Keep test names and describe titles short and concrete. They should complement each other, not repeat. Prefer `describe(\"aux meter\") test(\"create\")` over `describe(\"aux meter\") test(\"create aux meter and verify it appears\")`. Drop scenario filler like \"and lands in section\", \"appears correctly\", \"ensure\".\n\n## Device Integration & Configuration\n\n### Plugin System\n\n- Device types: chargers, meters, vehicles, tariffs\n- Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go\n- Define device capabilities and configuration in templates at `templates/definition/[type]/`\n- Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a _different_ value. Restating the same value is redundant duplication: reference the param by `name` alone.\n- Test templates: `evcc --template-type [type] --template [file]`\n- Update docs after template changes: `make docs`\n- When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details\n\n### Configuration\n\n- Use YAML format for all configuration files (default: `evcc.yaml`, or specify with `--config`)\n- Provide clear validation and error messages for invalid configurations\n- Support template-based device configurations with meaningful defaults\n- Use SQLite as default database (default: `evcc.db`, or specify with `--database`) with proper migrations and data integrity\n\n## Security & Performance Guidelines\n\n### Security\n\n- Validate all user inputs and sanitize data before database storage\n- Use secure protocols (TLS) for external integrations\n- Implement proper authentication and authorization\n- Never log sensitive information (passwords, tokens, personal data)\n\n### Performance\n\n- Optimize database queries with appropriate indexes\n- Handle concurrent operations safely with Go's concurrency primitives\n- Implement proper caching strategies and connection pooling\n- Avoid blocking operations in main application loop\n\n## Pull Request Descriptions\n\nStructure PR descriptions in this order. No headlines. Be concise.\n\n1. **References first line**: link related issues or PRs (`fixes #1123`, `replaces #222`, `pairs with org/repo#345`). PRs should almost always reference an issue or related PR — only skip in rare exceptions (e.g. trivial typo fixes).\n2. **Intro**: one or a few concise sentences framing what the PR does and why it was created this way. The full problem description belongs in the linked issue, not here.\n3. **Bullet list**: most significant changes or user-facing implications. Lead with the most significant.\n4. **TODO section** (only if open points remain):\n\n   ```\n   **TODO**\n   - [ ] item a\n   - [ ] item b\n   ```\n\nAvoid file paths, line numbers, or code listings reproduced from the diff. Include a code snippet only when it conveys the contract (event shape, API signature) more clearly than prose. No testing checklists, no co-author footers.\n\nNever state that `go build`, `go vet`, `go test -race`, or `gofmt` pass (or any \"all checks/tests green\" phrasing). These are non-negotiable givens that must already be fulfilled, not noteworthy results.\n\n## Pull Request CI and Reviews\n\n- After opening or updating a pull request, watch CI until every check has finished. Work is not done while checks are still running. Fix failures on the same branch and keep watching until the run is green.\n- Do not argue with automated review bots such as Sourcery. Either implement the suggestion or resolve the thread. No rebuttal comments.\n\n## AI Attribution\n\nWork produced by an AI agent must be attributable as such on GitHub. Append the tool's attribution footer to every PR description, issue body, review, and comment written by an agent, for example:\n\n```\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n```\n\nCommit messages are the exception: no footer, and no `Co-Authored-By` trailer. Never dress agent work up as human review (e.g. \"PR by an agent but looks good to me\") in place of the footer.\n","isInternal":false,"tokens":4470,"sizeBytes":20226},{"name":"template.agent.md","path":".github/agents/template.agent.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/.github/agents/template.agent.md","title":"Subagent: template.agent","category":"subagent-persona","format":"markdown","content":"---\ndescription: 'Tidy templates'\ntools: [\"read\", \"edit\", \"search\",\"shell\"]\n---\n\n## Cleanse parameter defaults\n\nValidate templates files inside `templates/definition/**` against default parameters in `util/templates/defaults.yaml`.\nMake sure that templates don't repeat parts of the default parameter definition.\n\n## Reorder parameters after presets\n\nValidate templates files inside `templates/definition/**`. If the template contains parameters and presets, ensure that any parameter contained in a `preset` appears in the template after the `preset`. Reorder such parameters after the presets.\n","frontmatter":{"description":"Tidy templates","tools":["read","edit","search","shell"]},"isInternal":false,"tokens":115,"sizeBytes":596},{"name":"api-security.md","path":"docs/agents/api-security.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/api-security.md","title":"Subagent: api-security","category":"subagent-persona","format":"markdown","content":"# API Security & Authentication\n\nHow evcc authenticates HTTP requests and how endpoints are classified by\nsensitivity.\n\n## Threat Model\n\nevcc is designed for use within a trusted home network. The auth layer\nprotects credential management, configuration changes, and system operations\n(logs, backup/restore/reset, shutdown). Read-only state and basic charging\ncontrols are intentionally unauthenticated.\n\n## Auth Modes\n\n| Mode       | Trigger               | Behavior                                          |\n|------------|-----------------------|---------------------------------------------------|\n| `Enabled`  | default               | password required; JWT or API key accepted        |\n| `Disabled` | `--disable-auth` flag | all auth checks skipped                           |\n| `Locked`   | demo mode             | mutating endpoints return 403; reads still work   |\n\nMode is fixed at startup. The frontend mirrors the mode so admin actions can\nbe greyed out and a banner shown.\n\n## Endpoint Sensitivity Tiers\n\nThree tiers, by what the caller has to prove:\n\n**Public.** No auth. State, loadpoint controls, login. Anyone on the\nnetwork can read and operate.\n\n**Secure.** Requires a valid session: either the auth cookie (browser, JWT)\nor an API key in the `Authorization: Bearer …` header (automation). Used\nfor configuration and system administration.\n\n**Critical.** Secure plus an additional admin-password check inside the\nhandler. Used for destructive or credential-scoped operations.\n\nFor some Critical endpoints (backup, restore, reset) the password check is\n**skipped when the caller is authenticated via API key**, so unattended automation\ndoesn't need to embed the admin password. For credential-management\nendpoints (rotate API key, change admin password) the password check is\n**strict**: a leaked API key must not be able to rotate itself or change\nthe admin password.\n\nDisabling auth short-circuits all checks.\n\n## Sessions\n\nTwo transports, no overlap:\n\n- Browsers use a session cookie (JWT, 90-day TTL, issued on login).\n- Automation uses an API key in the `Authorization: Bearer …` header.\n\nAPI keys are random alphanumeric strings prefixed `evcc_`. The prefix makes\nleaked keys recognizable to secret-scanning tools.\n\nA single API key per installation; regenerating replaces the previous one.\nPlaintext is shown to the user **once** at generation time and cannot be\nretrieved afterwards.\n\n## Credential Storage\n\nAdmin password and API key are stored as bcrypt hashes. The JWT signing\nsecret is a per-installation random value. Plaintext credentials are never\npersisted.\n\nRemoving the admin password (CLI recovery) also clears the JWT secret and the\nAPI key, which invalidates all outstanding sessions and any previously-issued\nAPI key. Regenerating the API key replaces the stored hash; the previous key\nstops working immediately.\n\n## API Key Lifecycle\n\nTwo operations:\n\n- **Status.** Whether a key is configured. Secure tier; never returns\n  plaintext.\n- **Regenerate.** Critical tier, strict password check. Returns the new\n  plaintext key exactly once.\n\nThere is no delete operation: regenerating and discarding the new key\nachieves the same effect (the previous key stops working immediately).\n\n## Endpoint Matrix\n\n| Endpoint category                            | Tier      | Additional Requirements            |\n|----------------------------------------------|-----------|------------------------------------|\n| State / read-only / basic charging control   | Public    |                                    |\n| Set or update admin password                 | Public    | admin password                     |\n| Configuration                                | Secure    |                                    |\n| Configuration embedding a script plugin       | Critical  | api key or admin password          |\n| System: logs, cache, shutdown                | Secure    |                                    |\n| API key status                               | Secure    |                                    |\n| System: backup / restore / reset             | Critical  | api key or admin password          |\n| API key regenerate                           | Critical  | admin password                     |\n\nDevice test, create, and update (`/api/config/test/{class}` and `/api/config/devices/{class}`)\ninstantiate a config immediately, so a `script` plugin in the payload runs a shell command on the\nserver. Because that command could read credentials a session is not otherwise allowed to see (for\nexample the contents of the database), these requests are treated as Critical when the config embeds\na script plugin, at any nesting depth. A session caller must supply the admin password in the\n`X-Admin-Password` header; an API-key caller passes without it. The `go` (yaegi) and `js` (otto)\nplugins are excluded: their interpreters are sandboxed to pure computation and cannot read files,\nspawn processes, or open network connections.\n\n**Public** endpoints accept any caller. **Secure** endpoints require a\nvalid session (cookie or API key). **Critical** endpoints require extra\nauthentication in the form of an admin password (or, for some, an API\nkey).\n\n## Frontend\n\nAuth management lives under **General Config → Security**, which links to\ntwo sub-flows: change admin password, and manage the API key. The API key\nflow has a reveal view that shows the plaintext exactly once with a\ncopy-to-clipboard link.\n\nWhen auth is disabled, the security modals show a warning banner and\ndisable all action buttons. This is UI-only; the backend still accepts the\nunderlying calls so legitimate automation against a disabled-auth instance\nkeeps working.\n\n## OpenAPI\n\nThe OpenAPI spec declares two security schemes (cookie and bearer);\nprotected operations accept either.\n","isInternal":false,"tokens":1143,"sizeBytes":5784},{"name":"core-domain.md","path":"docs/agents/core-domain.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/core-domain.md","title":"Subagent: core-domain","category":"subagent-persona","format":"markdown","content":"# Core Domain: Site, Loadpoint, and the Control Loop\n\n## Object Hierarchy\n\n```\nSite (orchestrator — core/site.go)\n├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]\n├── Tariffs: Grid, FeedIn, CO2, Solar\n├── Coordinator (vehicle <-> loadpoint assignment)\n├── Prioritizer (power allocation fairness)\n└── Loadpoints[] (core/loadpoint.go)\n    ├── Charger      (api.Charger — hardware controller)\n    ├── Vehicle      (api.Vehicle — EV battery state via cloud API)\n    ├── ChargeMeter  (api.Meter — AC power at charger)\n    └── Circuit      (optional — electrical domain limits)\n```\n\n## Key Interfaces (api/api.go)\n\n### Meter\n- `Meter` — `CurrentPower() (float64, error)` — watts\n- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh\n- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings\n\n### Battery\n- `Battery` — `Soc() (float64, error)` — 0-100%\n- `BatteryCapacity` — kWh\n- `BatteryController` — set charge/discharge/hold mode\n\n### Charger\n- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)`\n- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)`\n- `PhaseSwitcher` — `Phases1p3p(int) error`\n- `ChargeRater` — `ChargedEnergy() (float64, error)`\n- `ChargeTimer` — `ChargeDuration() (time.Duration, error)`\n\n### Vehicle\n- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()`\n- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`\n- `ChargeController` — remote start/stop on vehicle\n- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits\n- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side\n\n## Charge Modes\n\n| Mode | Behavior |\n|------|----------|\n| `OFF` | Disabled (unless welcome charge) |\n| `SMART` | Ramp current with solar surplus; fast if cheap tariff or plan requires |\n| `NOW` | Max current immediately |\n\nAlways charge (`alwaysCharge`: off/on/once) is a per-loadpoint option for smart mode: never pause, keep at least min current. `once` resets on disconnect. `pv` and `minpv` are deprecated write-only aliases for smart without/with always charge.\n\n## Charge States (IEC 61851)\n\n- `A` — not connected\n- `B` — connected, not charging\n- `C` — connected, charging\n\n## The Control Loop (Site.update — runs every N seconds)\n\n```\n1. Update all meters (grid, PV, battery, aux)\n2. For each loadpoint: UpdateChargePowerAndCurrents()\n3. Calculate site power balance:\n   sitePower = gridPower + batteryPower + excessDCPower\n             + residualPower - auxPower - flexiblePower\n4. Apply battery priority rules (prioritySoc, bufferSoc)\n5. Get tariff rates\n6. For EACH loadpoint: Update(sitePower, ...)\n   ├── Read charger status\n   ├── Detect/identify vehicle\n   ├── Check plan requirements (minSOC, target time)\n   ├── Check limits (limitSOC, limitEnergy)\n   ├── MODE switch -> calculate target current\n   ├── Cap at maxCurrent, respect circuit limits\n   ├── Send MaxCurrent() to charger\n   └── Record metrics\n7. Push updates to WebSocket + metrics\n```\n\nThe loop is stateless per cycle: always re-reads actual state, calculates\noptimal current, sends single command. Resilient to restarts and missed updates.\n\n## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)\n\n```\n1. Read effective min/max current limits\n2. Reduce sitePower by battery boost power\n3. Consider phase switching (1p <-> 3p) if supported\n4. deltaCurrent = powerToCurrent(-sitePower, activePhases)\n   targetCurrent = effectiveCurrent + deltaCurrent\n5. Below minCurrent -> start disable timer (default 3 min)\n6. Surplus returns -> start enable timer (default 1 min)\n7. Cap at maxCurrent\n```\n\n## Battery Priority Rules\n\n| Setting | Effect |\n|---------|--------|\n| `prioritySoc` | Below this: battery charges first, EV gets 0 |\n| `bufferSoc` | Above this: EV can draw from battery reserves |\n| `bufferStartSoc` | Above this: EV charging can begin even if importing |\n\nNone of these apply to a loadpoint the optimizer controls: the optimizer plans the home battery, so a stop gives the battery priority and a charge takes the surplus (`loadpointSitePower`, `#34606`). A charge the home battery covers is pinned to its setpoint instead, the pv loop would count the discharge as deficit (`surplusRegime`, `#34809`).\n\n## Effective Price Calculation\n\n```\ngreenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower\neffectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare\n```\n\n## Concurrency Model\n\n- **Site** owns `RWMutex` for its state (meters, battery, tariffs)\n- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current)\n- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking\n- No global locks — ordering prevents deadlocks\n\n### Channels\n\n| Channel | Scope | Buffer | Purpose |\n|---------|-------|--------|---------|\n| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) |\n| `lpUpdateChan` | Site | 1 | Early loadpoint update requests |\n| `pushChan` | Loadpoint | 16 | User notifications, queued via `valueChan` so the message renders the state at event time |\n\n## Tariff Integration\n\nTypes: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar`\n\n### Smart Features\n- **Cheap-tariff override** — rate below threshold -> fast charge\n- **Smart feed-in** — feed-in rate above threshold -> prioritize export\n- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline\n  - `optimalPlan()` — cheapest non-contiguous slots\n  - `continuousPlan()` — cheapest continuous window (fallback)\n\n## Optimizer Suggestions (core/site_optimizer.go)\n\nThe optimizer (`optimizer.evcc.io`, sponsor-gated) solves a linear program over tariff slots. Every solved \"battery\" (home battery, vehicle/loadpoint) gets a `types.Suggestion` for the slot covering now. In `Automatic` mode these suggestions drive control; otherwise they are advisory (UI + notifications).\n\n### Request side: what becomes a battery\n\n- **Home battery meter**: `batteryRequest`. `controllable` = `api.HasCap[api.BatteryController]`. Only controllable batteries get `charge_from_grid` / `discharge_to_grid`.\n- **Loadpoint with vehicle**: `loadpointRequest`, `detail.Type = vehicle`, `detail.loadpoint = &id`, always `controllable`. Needs vehicle capacity (or a session energy limit) to model as storage. `ModeOff` sets `CMax = 0`; `ModeNow` and plans produce a `PDemand` / `SGoal`.\n- **Unmodellable loadpoints** (no vehicle, no capacity and no energy limit, or heaters) are added to the home load (`addUnmodelledLoad`) and get no suggestion.\n- Disconnected loadpoints are skipped entirely.\n\nDevice key (`batteryDetail.key()`): `battery:<name>` or `loadpoint:<id>`; empty key = cannot act.\n\n### Result side: `applyOptimizerResult`\n\nRuns after each solve, and again every cycle via `reapplySuggestions` when the active slot changes (re-derived from the cached solve, no new request). Expiry is only evaluated on that slot change: if the horizon is passed or no solve completed for `2 * tariff.SlotDuration`, `clearSuggestions()` drops suggestions, battery forecast and notification state. Within an unchanged slot the cached advice stays active regardless of age. A failed solve keeps the previous advice so controlled devices are not released for one cycle.\n\n1. `slot = schedule.activeSlot(now)`, `slotHours` = its duration. Grid import/export and all home-battery discharge for that slot are summed.\n2. `currentSlotSuggestion(detail, req, res, slot, gridImport, gridExport, slotHours)` per battery. Because the LP is linear, the slot result sits at an operating-range extreme and maps cleanly onto discrete modes. All thresholds use `suggestionThreshold = 50 W` to ignore solver noise.\n\n**Home battery** (`Action` is an `api.BatteryMode` string):\n\n| Condition (slot values, W)                         | Action       | Meaning                             |\n| -------------------------------------------------- | ------------ | ----------------------------------- |\n| charge > thr and grid importing                    | `charge`     | grid charging                       |\n| idle and grid importing and soc > `SMin`           | `hold`       | discharge deliberately withheld     |\n| idle and grid exporting and soc < `SMax`           | `holdcharge` | surplus exported instead of charged |\n| discharge > thr and grid exporting                 | `discharge`  | battery-to-grid                     |\n| otherwise (incl. idle at a soc bound, PV charging) | `normal`     | inverter self-consumption logic     |\n\n`idle` = charge and discharge both <= thr. At a soc bound nothing is withheld (the bound does it), hence `normal`.\n\n**Loadpoint/vehicle**: `charge` if charge > thr, else `stop`. `Suggestion.Battery` is set to the planned home-battery discharge so the loadpoint can tell surplus from battery-fed charging.\n\n3. Suggestions are stored only for controllable devices with a key (`site.suggestions`). Loadpoints with a plan goal (`SGoal`) also receive `optimizerPlan` (the remaining charging slots priced at `PN`) via `setOptimizerPlan`, used by `OptimizerPlan()` for the planner UI.\n4. `publishBattery` / `publishSuggestions` publish to UI and hand loadpoint suggestions to `lp.setSuggestion`. `Actionable` is evaluated on every read (`site.suggestion`) as `Action != currentAction`, where the current action is `site.batteryAction()` for batteries (the battery mode, `unknown` reads as `normal`) and `loadpointCurrentAction` (`charge` when enabled and goal not reached, else `stop`).\n5. `diffSuggestions` emits one `suggestion` messenger event per device whose actionable action changed since the last run; non-actionable or vanished devices are pruned so a later change re-notifies.\n\n### Control side: where suggestions take effect\n\nGated by `OptimizerAutomatic()`: `off` (advisory only), battery-only, or `full` (also loadpoints). `site.Automatic()` additionally requires optimizer enabled + sponsor.\n\n**Home battery** (`requiredBatteryMode`, `core/site_battery.go`), in priority order:\n\n- external mode (API) wins.\n- `Automatic()` and `unmodelledCharging(rate)`: a loadpoint the optimizer does not control is fast charging or on the smart cost limit. The suggestion did not account for that load, so force `hold`.\n- `Automatic()`: `batterySuggestionMode()` takes the first controllable battery's suggestion (`api.BatteryModeString(s.Action)`); an unknown action releases to `normal`; no suggestion releases to `normal` only if evcc had modified the mode (`batteryModeModified`), otherwise the mode is left untouched. This replaces the grid charge limit and discharge control. TODO: per-battery modes.\n- not automatic: legacy grid charge / discharge control / feed-in discharge rules.\n\n**Loadpoint** (`activeSuggestion`, `core/loadpoint_optimizer.go`): non-nil only if `optimizerControlled()` (full automatic, not a heater or integrated device, vehicle with capacity > 0) and the suggestion is younger than `suggestionMaxAge = 2 * SlotDuration` (a stalled optimizer must not keep the loadpoint gated). With an active suggestion:\n\n- `ModeSmart` strategy switch calls `optimizerCharging(s)` and skips the smart cost / feed-in price limits.\n  - `surplusRegime` (charge below max power, no grid flow, no battery discharge): not handled, the PV loop tracks measured surplus instead of the forecast.\n  - `stop` without always-charge: not handled, PV loop may still enable on measured surplus.\n  - `charge` at full power or above what the active phases deliver: `fastCharging()`.\n  - `charge` at a limited setpoint: `setLimit(PowerToCurrent(s.Charge))`, or `minCharging()` below min current.\n  - `stop` with always-charge: `minCharging()`.\n- Planner is reduced to a backstop: `plannerActive = planDeadlineCritical()`.\n- Session `LimitEnergy` / `LimitSoc` do not stop charging while the optimizer is driving a plan goal above them.\n- `loadpointSitePower`: priority soc adjustment is applied, and the battery's discharge is subtracted from the surplus unless the action is `charge` (the optimizer plans the home battery, so `bufferSoc` / `bufferStartSoc` are also disabled).\n\nEvery cycle ends with `site.publishSuggestions()` so `Actionable` reflects the updated loadpoint state.\n\n## Key File Locations\n\n- `api/api.go` — all core interfaces\n- `core/site.go` — Site orchestrator + control loop\n- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch)\n- `core/site_battery.go` — battery priority logic, battery mode from suggestion\n- `core/site_optimizer.go` — optimizer request, suggestion mapping, reapply/expiry\n- `core/loadpoint_optimizer.go` — loadpoint suggestion gate and charging strategy\n- `core/site_tariffs.go` — tariff integration\n- `core/planner/planner.go` — charge time optimization\n- `core/prioritizer/prioritizer.go` — power allocation across loadpoints\n- `core/circuit/circuit.go` — electrical domain limits\n","isInternal":false,"tokens":3143,"sizeBytes":13134},{"name":"easee-architecture.md","path":"docs/agents/easee-architecture.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/easee-architecture.md","title":"Subagent: easee-architecture","category":"subagent-persona","format":"markdown","content":"# Easee Charger Architecture\n\nThe Easee integration communicates with the Easee cloud over two distinct channels:\n\n1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration.\n2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations.\n\nCommands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.\n\n## Authentication\n\n### Flow\n\nAuthentication uses username/password credentials against:\n```\nPOST https://api.easee.com/api/accounts/login\n```\n\nReturns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`.\n\n### Token Lifecycle\n\nWrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls:\n```\nPOST https://api.easee.com/api/accounts/refresh_token\n```\nFalls back to full re-login if refresh fails.\n\n### Token Caching\n\n`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.\n\n## Initialization Sequence\n\n`NewEasee` performs these steps in order:\n\n1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger.\n2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control.\n3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates).\n4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change.\n5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`).\n6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds.\n\n## SignalR Back-Channel\n\n### Why SignalR is Required\n\n1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`.\n2. **State is event-driven, not pollable.** No REST endpoint streams charger state.\n3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out.\n\n### Server -> Client Methods\n\n#### `ProductUpdate(json.RawMessage)`\nPrimary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`.\n\n- **Timestamp deduplication**: older timestamps for the same ID are silently dropped.\n- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select.\n\n#### `CommandResponse(json.RawMessage)`\nAsync acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID).\n\nRoutes through three maps in order:\n1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands\n2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch\n3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse\n\nUnmatched responses are logged as WARN (rogue response from external system).\n\n#### `ChargerUpdate` / `SubscribeToMyProduct`\nLogged at TRACE, not processed further.\n\n## Command Flow and Async Correlation\n\n### REST Command Endpoints\n\n```\nPOST /api/chargers/{chargerID}/commands/{action}     (start/stop/pause/resume)\nPOST /api/chargers/{chargerID}/settings              (enable, DCC, PhaseMode, SmartCharging)\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings  (dynamic circuit currents)\n```\n\n### Response Handling\n\n| HTTP Status | Meaning | Behavior |\n|-------------|---------|----------|\n| `200` | Synchronous / already applied | Returns immediately |\n| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse |\n| other | Error | Returns error |\n\n### Ticks Correlation\n\nOn 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op.\n\nEach in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`.\n\n### The Sync/Async Mismatch\n\nSome 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`).\n\nHandled via the **expected-orphan counter**:\n```go\nexpectedOrphans map[easee.ObservationID]int  // protected by cmdMu\n```\n\nBefore 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.\n\n## State Management\n\n### Internal State Fields (all protected by `sync.RWMutex`)\n\n| Field | Observation | Notes |\n|-------|------------|-------|\n| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine |\n| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state |\n| `smartCharging` | `SMART_CHARGING` (102) | LED color mode |\n| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) |\n| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling |\n| `totalEnergy` | `LIFETIME_ENERGY` (124), `CHARGE_SESSION_START` (223), `CHARGING_SESSION` (129) | kWh, monotonic max, see Logical Energy Meter |\n| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A |\n| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase |\n| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit |\n| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) |\n| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit |\n| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum |\n| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F |\n| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token |\n\n### Session Energy Zero-value Protection\n\n`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. The first op mode after startup is replayed state and does not reset, so replayed session energy survives a restart. `CHARGE_SESSION_START` and a zero `SESSION_ENERGY` arrive before the mode change, so the reset is skipped when the cached session started after the previous mode observation.\n\n### Logical Energy Meter\n\n`LIFETIME_ENERGY` is pushed only ~hourly. `TotalEnergy()` therefore keeps a monotonic max of the lifetime reading, the session meter values from `CHARGE_SESSION_START` and `CHARGING_SESSION`, and `CHARGE_SESSION_START.MeterValue + sessionEnergy`. The session term is only applied while the last nonzero `SESSION_ENERGY` (or session completion) is not older than the session start, so a stale previous-session value cannot inflate the total. Ignored zero observations do not count as fresh readings.\n\n## Charger Operation Modes\n\n```\n0 = Offline               — no cloud connection\n1 = Disconnected          — no car plugged in\n2 = AwaitingStart         — car plugged, waiting for authorization/start\n3 = Charging              — actively charging\n4 = Completed             — car full or finished, cable still plugged\n5 = Error                 — fault condition\n6 = ReadyToCharge         — ready, current available\n7 = AwaitingAuthentication — RFID auth required\n8 = Deauthenticating      — finishing authentication teardown\n```\n\n### Mapping to evcc Status\n\n| opMode | evcc Status |\n|--------|------------|\n| 1 (Disconnected) | A |\n| 2, 4, 6, 7, 8 | B |\n| 3 (Charging) | C |\n| 0, 5 and others | error |\n\n## Enable/Disable Flow\n\n### Enable = true\n\n1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait.\n2. If `opMode == Disconnected`: return (no cable).\n3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`.\n4. Otherwise: action = `resume_charging`.\n5. POST `/commands/{action}` and wait.\n6. Wait for `opMode` to reach enabled state.\n7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume).\n8. Call `MaxCurrent(c.current)` to restore previous setpoint.\n\n### Enable = false\n\n1. If disconnected or (awaiting auth && !authorize): return.\n2. POST `/commands/pause_charging` and wait.\n3. Wait for `opMode` to reach disabled state.\n4. Wait for `dynamicChargerCurrent` to reach `0`.\n\n### State Waiting Pattern\n\nBoth `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use:\n1. Short-circuit check: if already in target state, return immediately.\n2. Open a timer.\n3. Loop on `obsC` channel.\n4. On timer expiry: **one final check** before returning `api.ErrTimeout`.\n\nThe final check handles the race where the state update arrived between the last channel read and the timer fire.\n\n## Phase Control\n\n### Circuit-Level (preferred, when circuit is known)\n\nPhase switching by zeroing dynamic circuit current on unused phases:\n```\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings\n```\n\nFor 1-phase: set P2=0, P3=0. For 3-phase: restore all three.\n\nThis POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan).\n\n### Charger-Level (fallback)\n\nUses `PhaseMode` setting: `1` locked single-phase, `3` locked 3-phase. `2` (auto, set by earlier versions) is reported as unknown phases so the loadpoint locks it.\nAfter changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.\n\n## Authorization Mode (`authorize`)\n\nWhen `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.\n\nWhen `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).\n\nSetting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.\n\n## Concurrency Model\n\n### Mutexes\n\n| Mutex | Type | Protects |\n|-------|------|----------|\n| `c.mux` | `sync.RWMutex` | All charger state fields |\n| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) |\n\nCommand 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.\n\n### Observation Channel\n\n`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.\n\n**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`.\n\n## Known Design Concerns\n\n1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified.\n2. **LIFETIME_ENERGY** — API pushes updates ~hourly; bridged by the logical energy meter, so accuracy between readings depends on `SESSION_ENERGY` and the session start meter value.\n3. **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.\n4. **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.\n5. **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.\n\n## API Endpoints Summary\n\n| Method | Endpoint | Used For |\n|--------|----------|----------|\n| `POST` | `/accounts/login` | Initial authentication |\n| `POST` | `/accounts/refresh_token` | Token refresh |\n| `GET` | `/chargers` | Auto-discover charger ID |\n| `GET` | `/chargers/{id}/site` | Discover site and circuit |\n| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging |\n| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging |\n| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents |\n| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) |\n\n### SignalR Hub\n\n| Endpoint | `https://streams.easee.com/hubs/chargers` |\n|----------|------------------------------------------|\n| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` |\n| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` |\n\n## Configuration\n\n| Parameter | Required | Default | Notes |\n|-----------|----------|---------|-------|\n| `user` | yes | | Easee account email |\n| `password` | yes | | Easee account password |\n| `charger` | no | | Charger serial; auto-detected if exactly one on account |\n| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits |\n| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions |\n\nSupported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.\nDeclared capabilities: `1p3p` (phase switching), `rfid` (RFID identification).\nRequires evcc sponsorship.\n","isInternal":false,"tokens":3396,"sizeBytes":13923},{"name":"hardware-integrations.md","path":"docs/agents/hardware-integrations.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/hardware-integrations.md","title":"Subagent: hardware-integrations","category":"subagent-persona","format":"markdown","content":"# Hardware Integrations: Chargers, Meters, Vehicles\n\n## Integration Pattern\n\nAll device types use a registry-based factory pattern:\n\n```go\n// Self-registration in init()\nfunc init() {\n    registry.AddCtx(\"typename\", NewFromConfig)\n}\n\nfunc NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {\n    // Parse config, create client, return implementation\n}\n```\n\nOptional interfaces are added via the decorator pattern:\n```go\n//go:generate decorate -f decorateXxx -b *Xxx -t \"api.PhaseSwitcher,Phases1p3p,func(int) error\"\n```\n\n## Charger Implementations\n\n### By Protocol\n\n| Protocol | Examples |\n|----------|---------|\n| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |\n| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |\n| OCPP 1.6 | Generic charge point server |\n| EEBus/ISO 15118 | EEBus SPINE protocol |\n| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |\n| MQTT | OpenWB, Tasmota, Shelly |\n| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |\n\n### Required Charger Interface\n\n```go\ntype Charger interface {\n    ChargeState\n    Enabled() (bool, error)\n    Enable(enable bool) error\n    CurrentController\n}\n```\n\nWhere `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and\n`CurrentController` provides `MaxCurrent(current int64) error`.\n\n### Optional Charger Interfaces\n\n- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision\n- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p\n- `Meter` / `MeterEnergy` — built-in power/energy measurement\n- `PhaseCurrents` / `PhaseVoltages` — per-phase readings\n- `ChargeRater` — `ChargedEnergy()` for session energy\n- `ChargeTimer` — `ChargeDuration()` for session time\n- `Identifier` — `Identify()` for RFID/vehicle identification\n\n### Key Implementations\n\n- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail\n- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management\n- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2\n- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC)\n- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template\n\n### Adding a New Charger\n\n1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`)\n2. Implement `NewXxxFromConfig()` returning `api.Charger`\n3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()`\n4. Register: `registry.AddCtx(\"xxx\", factory)` in `init()`\n5. Optional: add decorator for PhaseSwitcher, Meter, etc.\n6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata\n\n## Meter Implementations\n\n### By Category\n\n| Category | Examples |\n|----------|---------|\n| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |\n| HTTP/REST | Homewizard, Shelly Gen3, E3DC |\n| Smart Home | HomeAssistant entities, Homematic |\n| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |\n\n### Required Meter Interface\n\n```go\ntype Meter interface {\n    CurrentPower() (float64, error)  // watts\n}\n```\n\n### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity`\n\n### Key Implementations\n- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection\n- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries\n- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources)\n\n## Vehicle Integrations\n\n| Manufacturer | API Type |\n|-------------|----------|\n| Tesla | Fleet API + vehicle-command proxy |\n| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |\n| Hyundai/Kia | BlueLink (regional variants) |\n| BMW/Mini | ConnectedDrive v2 |\n| Mercedes | Official API |\n| Renault/Nissan | Renault API + Carwings |\n| Ford | FordConnect (US/EU) |\n| Porsche | Porsche Connect |\n| PSA Group | Peugeot, Citroen, DS, Opel |\n| Generic | OVMS, Tronity |\n\n### Required Vehicle Interface\n\n```go\ntype Vehicle interface {\n    Battery          // Soc() (float64, error)\n    BatteryCapacity  // Capacity() float64\n    IconDescriber    // Icon() string\n    FeatureDescriber // Features() []Feature\n    PhaseDescriber   // Phases() int\n    TitleDescriber   // GetTitle() string\n    SetTitle(string)\n    Identifiers() []string\n    OnIdentified() ActionConfig\n}\n```\n\n### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector`\n\n### Polling Strategy\n\nConfigurable: always / while charging / while connected. Interval-based caching\nto avoid excessive cloud API calls. OAuth2 token handling built into each provider.\n\n## Auto-Detection (`cmd/detect/`)\n\nTask-based parallel IP scanning:\n`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes\n\nDetects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,\nTasmota, Shelly, Phoenix, and many more.\n","isInternal":false,"tokens":1327,"sizeBytes":4857},{"name":"ocpp-forwarder.md","path":"docs/agents/ocpp-forwarder.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/ocpp-forwarder.md","title":"Subagent: ocpp-forwarder","category":"subagent-persona","format":"markdown","content":"# OCPP Forwarder Architecture\n\nThe OCPP forwarder (`charger/ocpp/forwarder.go`) is a hybrid proxy that lets a charger talk to evcc and an upstream OCPP server at the same time. Chargers connect directly to evcc's central system on the normal port. For each charger with a matching `ForwarderRule`, a \"sidecar\" WebSocket connection to the upstream server is opened and kept in parallel for the lifetime of the charger connection.\n\nThe forwarder is opt-in: hooks (`chargerConnectHook`, `chargerDisconnectHook`, `chargerMessageHook` in `instance.go`) are nil unless a rule matches, so a charger without a rule behaves exactly as before.\n\n## Forwarding modes\n\nTwo modes apply at the same time, selected per message by its action.\n\n### Transparent relay (billing-critical)\n\nFor the actions in `actionsRelayedToUpstream` (`Authorize`, `StartTransaction`, `StopTransaction`, `DataTransfer`), upstream is the authoritative Central System:\n\n1. Charger sends the Call to evcc.\n2. The message hook forwards it to the upstream sidecar and bypasses evcc's OCPP handler.\n3. Upstream's `CallResult`/`CallError` is relayed back to the charger.\n\nevcc's handler is never invoked for these. This lets the pay backend control authorization, issue its own transaction IDs, and see consistent Start/Stop pairs.\n\n### Sidecar observation (informational)\n\nFor all other messages (`BootNotification`, `StatusNotification`, `MeterValues`, `Heartbeat`, etc.):\n\n1. Charger sends the Call to evcc, which processes it normally.\n2. The same frame is also mirrored to the upstream sidecar.\n\nUpstream observes the session while evcc manages the charger as usual.\n\n## Upstream to charger (commands)\n\nCalls (type 2) initiated by upstream are injected into the charger via `CS.Write`. The charger's `CallResult`/`CallError` is routed back to upstream. Examples: `RemoteStartTransaction`, `RemoteStopTransaction`, `GetConfiguration`, `ChangeConfiguration`, `TriggerMessage`, `SetChargingProfile`.\n\n`ChangeConfiguration` for `MeterValueSampleInterval` is intercepted: the forwarder absorbs it as a local throttle on `MeterValues` forwarded to upstream and replies `Accepted` without touching the charger's own config. evcc still processes every `MeterValues` frame for energy management.\n\n## Read-only mode\n\nWhen a rule sets `ReadOnly`, upstream may observe but cannot control the charger. Any incoming Call from upstream is answered with a `SecurityError` and not forwarded. `ReadOnly` is applied live per message, so toggling it does not require reconnecting the sidecar.\n\n## Connection lifecycle\n\nFrames that arrive from a charger before its sidecar finishes dialling are buffered (`pendingMsgs`) and flushed in order once the sidecar connects, so early messages such as `BootNotification` still reach upstream. If the dial fails or upstream drops mid-session, any buffered or in-flight relay Calls are answered to the charger with a `CallError` so it is not left hanging, and the failure is surfaced to the UI via `forwarderErrors`.\n\nWhen the upstream connection fails while the charger stays connected, the sidecar is re-dialled automatically with exponential backoff (`runUpstreamSidecar`, 5s up to 5min; a successful session resets the backoff). Between attempts the relay actions fall back to evcc's local handler, so charging continues but upstream misses those transactions. The reconnect loop ends when the charger disconnects, the rule is removed, or its connection parameters change (`ApplyForwarderRules` dials its own sidecar in that case).\n\nThe last `BootNotification` of each connected charger is cached (`lastBoot`). When a sidecar connects mid-session (after an upstream reconnect or a rule added at runtime) and the pending buffer does not already carry a boot frame, the cached frame is replayed to upstream with a fresh message id, since many backends expect a boot before accepting transactions. Upstream's reply to the replay is discarded; evcc answered the charger's original boot long ago.\n\nRules can be changed at runtime through `ApplyForwarderRules`. Sidecars for removed rules are closed, rules with changed connection parameters are re-dialled, and rules for chargers that are not connected are test-dialled to surface unreachable hosts immediately.\n","isInternal":false,"tokens":905,"sizeBytes":4242},{"name":"plugin-system.md","path":"docs/agents/plugin-system.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/plugin-system.md","title":"Subagent: plugin-system","category":"subagent-persona","format":"markdown","content":"# Plugin System\n\nThe plugin system (`plugin/`) provides protocol-level abstraction for device\ncommunication. Plugins implement typed getter/setter interfaces and are composed\ninto charger, meter, or vehicle implementations via configuration.\n\n## Plugin Types\n\n| Plugin | Protocol | Key Config |\n|--------|----------|------------|\n| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` |\n| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` |\n| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` |\n| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree |\n| `js` | JavaScript/WASM | Inline script evaluation |\n| `go` | Go runtime | Dynamic Go code |\n| `gpio` | Linux GPIO | Digital I/O for relays |\n\n## Getter/Setter Interfaces\n\n```go\ntype StringGetter func() (string, error)\ntype FloatGetter  func() (float64, error)\ntype IntGetter    func() (int64, error)\ntype BoolGetter   func() (bool, error)\n// + corresponding Setter types\n```\n\n## Pipeline Transforms\n\nPlugins support chained transforms: `scale`, `offset`, `lookup`, `regex`.\n\n## Template-Based Device Configuration\n\nDevices can be defined entirely via YAML templates using plugins:\n\n```yaml\n# templates/definition/charger/example.yaml\nstatus:\n  source: http\n  uri: http://{{ .host }}/status\nenable:\n  source: http\n  uri: http://{{ .host }}/enable\n  method: POST\nmaxcurrent:\n  source: http\n  uri: http://{{ .host }}/current/{{ .maxcurrent }}\n```\n\nThe generic configurable charger (`charger/charger.go`) wires these plugin\nconfigs into the `api.Charger` interface at runtime.\n\nTemplates render once at config time. HTTP `uri` and `body` are templates\nthemselves, evaluated per request (`util.ReplaceFormatted`: sprig, `addDate`,\n`timeRound`). Defer `now` to request time with a raw string:\n`` {{ `{{ now | date \"2006-01-02\" }}` }} ``.\n\n## Param Suggestions and Discovery\n\nTemplate params get suggestions from services (`service:`), see the `service`\nand `discovery` sections in `templates/README.md` for the contract. Handlers\nregister in `init()` via `service.Register` (`server/service/registry.go`),\nobject responses use `service.Option` (`util/service/helper.go`) and render as\n`Helper/Combobox.vue`.\n\nLocal network discovery lives in `util/discovery` (mDNS, SSDP, neighbor table,\nreverse DNS; no raw sockets, no port scans) and feeds `network/hosts`,\n`network/report` and `network/scan` in `util/service/network*.go`. The vendor\nlist is generated from the IEEE registry (`make oui-update`), see\n[Discovery Hints](discovery-hints.md) for the update routine.\n\n- Scan: 3 s first pass (first request waits), 5 s second pass, repeated when\n  older than a minute, results combined, hosts dropped after 30 minutes.\n- mDNS: browses the types of the template hints plus all types announced in\n  the network (DNS-SD enumeration), so reports show vendor types without a hint.\n- A host keeps all its names (router name first, then mDNS), hostname hints\n  match any of them.\n- `EVCC_DISCOVERY_HOSTS` (JSON list of `discovery.Host`) replaces the scan,\n  used in tests and for environments without network access. On macOS the neighbor table is empty for binaries started with\n  `go run`, test with a built binary.\n\n## Key Files\n\n- `plugin/config.go` — plugin registry and config types\n- `plugin/http.go` — HTTP plugin\n- `plugin/mqtt.go` — MQTT plugin\n- `plugin/modbus.go` — Modbus plugin\n- `plugin/sunspec.go` — SunSpec plugin\n- `charger/charger.go` — generic configurable charger using plugins\n","isInternal":false,"tokens":886,"sizeBytes":3531},{"name":"web-ui-api.md","path":"docs/agents/web-ui-api.md","rawUrl":"https://raw.githubusercontent.com/evcc-io/evcc/HEAD/docs/agents/web-ui-api.md","title":"Subagent: web-ui-api","category":"subagent-persona","format":"markdown","content":"# Web UI & REST API\n\n## Server Architecture\n\n- **Router:** gorilla/mux, strict slash\n- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth\n- **Timeouts:** Read 5s, Write 10s, Idle 120s\n- **Static assets:** embedded in binary (`fs.FS`)\n- **Default port:** 7070\n\n## REST API (base `/api/`)\n\n### Site-level\n- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc.\n- `GET /tariff/{tariff}` — tariff rates\n- `GET /sessions` — charging history\n- `GET /state` — complete system state (supports jq filtering)\n\n### Per-loadpoint (`/loadpoints/{id}/...`)\n- `POST mode/{value}` — off/smart/now (deprecated aliases pv/minpv)\n- `POST alwayscharge/{value}` — off/on/once\n- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits\n- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits\n- `POST phases/{value}` — phase config\n- `POST priority/{value}`, `batteryboost/{value}`\n- `POST plan/energy/{value}/{time}` — schedule plan\n- `POST vehicle/{name}` — select vehicle\n- `POST smartcostlimit/{value}` — smart cost threshold\n- `POST solarshare/{value}` — required solar share of min power (default 1)\n\n### Configuration (`/config/...`, auth required)\n- CRUD for devices (chargers, meters, vehicles, tariffs)\n- Template browsing and testing\n- Site, loadpoint, circuit, HEMS, messaging config\n- `GET /config/evcc.yaml` — YAML export\n\n### System (`/system/...`, auth required)\n- Log viewing (`/log`, `/log/areas`), cache clear, shutdown\n\n### Database (`/db/...`, auth + second factor required)\n- Backup download, restore from file, selective reset\n- Second factor: admin password in request body, or API key via Bearer token (bypasses password check)\n\n### Handler Pattern\nGeneric `handler[T]` with type conversion, setter, getter.\nSpecialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`.\n\n## WebSocket (`/ws`)\n\n- `coder/websocket` (RFC 6455)\n- Pub/sub via `SocketHub`\n- Buffered channels (1024 per subscriber)\n- Welcome message with full state snapshot\n- Incremental updates as JSON key-value pairs with dot-notation keys:\n  ```json\n  {\"loadpoints.1.mode\": \"solar\", \"site.gridPower\": 1234}\n  ```\n- Write timeout: 10s, compression (disabled for Safari)\n\n## State Flow\n\n1. WS connects -> receives welcome with full state\n2. App emits `util.Param` on changes\n3. Hub broadcasts to subscribers\n4. Frontend `store.update(msg)` merges via dot-notation\n5. Components reactively re-render\n\n## Authentication\n\n- JWT, 90-day lifetime\n- HttpOnly cookie (`auth`) with `SameSite=Strict`\n- Also accepts `Authorization: Bearer <token>` header\n- Modes: Disabled, Locked (demo), Configured (password)\n- Protects `/api/config`, `/api/system`, and `/api/db`\n\n## MQTT Integration\n\n- Publishes state changes to configurable broker\n- Subscribes to control topics\n- Retained messages for state persistence\n\n## Key Files\n\n- `server/http.go` — router setup\n- `server/http_auth.go` — authentication\n- `server/http_site_handler.go` — state + request handlers\n- `server/http_config_*.go` — config endpoints\n- `server/http_loadpoint_handler.go` — per-loadpoint endpoints\n- `server/socket.go` — WebSocket pub/sub\n- `assets/js/app.ts` — Vue app entry\n- `assets/js/store.ts` — reactive state store\n- `assets/js/api.ts` — Axios clients\n- `assets/js/router.ts` — route definitions\n","isInternal":false,"tokens":884,"sizeBytes":3372}],"systemPromptSnippet":"<yakaai_skills repo=\"evcc-io/evcc\">\n<!-- File: AGENTS.md (Tokens: ~4470 | Category: root-instruction) -->\n# Agent Rules for evcc Project\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\n- evcc is an extensible EV Charge Controller and home energy management system written in Go with a Vue.js frontend\n- The system manages electric vehicle charging, integrates with solar systems, and provides local energy management without cloud dependencies\n- Architecture follows a plugin-based approach for device integrations\n\n## Essential Commands\n\n- `make` - build full application (UI + Go binary)\n- `make build` - build Go binary only\n- `make ui` - build UI assets only\n- `make install` - install Go tools and dependencies\n- `make install-ui` - install Node.js dependencies (`vp install`)\n- `make test` - run Go tests\n- `make test-ui` - run frontend tests\n- `make lint` - run Go linting (golangci-lint)\n- `make lint-ui` - run frontend linting\n- `vp run dev` - start Vue dev server (http://127.0.0.1:7071)\n- `vp run playwright` - run integration tests\n- `build`, `openapi` and `test` are cached tasks in `vite.config.ts`, run through `vp run`\n- `evcc --config [file] --disable-auth` - run a throw-away instance for UI checks without password setup\n- `evcc --template-type [type] --template [file]` - test device templates\n- `make docs` - generate template documentation\n\n## Domain Knowledge\n\nDeep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:\n\n| File                                                          | When to load                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| [Core Domain](docs/agents/core-domain.md)                     | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, optimizer  |\n| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices                    |\n| [Easee Architecture](docs/agents/easee-architecture.md)       | Easee charger (REST+SignalR, async correlation, concurrency)                 |\n| [OCPP Forwarder](docs/agents/ocpp-forwarder.md)               | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) |\n| [Plugin System](docs/agents/plugin-system.md)                 | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS)                               |\n| [Web UI & API](docs/agents/web-ui-api.md)                     | REST API, WebSocket, Vue frontend, authentication                            |\n| [API Security](docs/agents/api-security.md)                   | Auth modes, JWT/API key/session, two-tier checks, credential storage         |\n| [Provider Auth](docs/agents/provider-auth.md)                 | Third-party logins (OAuth, device code, tokens), template `auth` block, UI   |\n| [Chart Conventions](docs/agents/charts.md)                    | ECharts charts: axis units and scale rules, tooltips, layout                 |\n| [Energy Analytics](docs/agents/energy-analytics.md)           | Energy page metrics: attribution, cost/CO2, forecast accuracy, missing data  |\n| [Discovery Hints](docs/agents/discovery-hints.md)             | Vendor registry update, adding and reviewing template `discovery` hints      |\n\n### Loading guide by task type\n\n- **Charger implementation** — hardware-integrations + core-domain\n- **Easee charger work** — easee-architecture + core-domain\n- **Meter implementation** — hardware-integrations + plugin-system\n- **Vehicle implementation** — hardware-integrations\n- **UI/frontend work** — web-ui-api\n- **Chart work (ECharts, axes, tooltips)** — charts + web-ui-api\n- **API endpoint work** — web-ui-api + core-domain\n- **Auth / login / API key / permissions** — api-security + web-ui-api\n- **Vehicle or device cloud login (OAuth, tokens, captcha)** — provider-auth + hardware-integrations\n- **Config/template work** — plugin-system\n- **Discovery hints / vendor registry update** — discovery-hints + plugin-system\n- **Control loop / charging logic** — core-domain\n- **Energy page metrics (attribution, savings, accuracy)** — energy-analytics + charts\n- **Bug in any area** — core-domain + relevant topic file(s)\n\n## Architecture Guidelines\n\n### Core Components\n\n- **main.go** serves as entry point and embeds web assets and i18n files\n- **cmd/** contains CLI commands, application setup, and various utility commands (configure, detect, migrate, etc.)\n- **core/** contains core business logic with main files (loadpoint.go, site.go) and subdirectories:\n  - **loadpoint/** - EV charging point management modules\n  - **planner/** - Smart charging planning algorithms\n  - **coordinator/** - Multi-loadpoint coordination logic\n  - **session/** - Charging session management\n  - **vehicle/** - Vehicle-specific core logic\n  - **soc/** - State of charge handling\n- **api/** contains API definitions and types\n- **server/** handles HTTP server, WebSocket, MQTT, database operations, and various handlers\n- **charger/**, **meter/**, **vehicle/** contain device integrations\n- **tariff/** contains tariff integrations\n- **plugin/** implements plugin system for device and tariff communication\n- **assets/** contains Vue.js frontend application\n\n### Frontend Structure\n\n- **assets/js/** contains the main TypeScript/Vue.js application with:\n  - **views/** - Vue page components (App.vue, Config.vue, Sessions.vue, etc.)\n  - **components/** - Reusable Vue components\n  - **composables/** - Vue utility functions\n  - **types/** - TypeScript type definitions\n  - **utils/** - Utility functions\n  - **mixins/** - Vue mixins\n- **assets/css/** contains application stylesheets\n- **assets/public/** contains static assets and metadata\n- **i18n/** contains internationalization files\n- **tests/** contains Playwright integration tests and test configuration files\n- **dist/** contains built frontend assets (generated)\n\n## Writing Style\n\n- No em dashes (—) in comments, commit messages, or docs. Use periods, commas, or colons\n- Project name is `evcc`, always lowercase\n- In user-facing strings, only mention `evcc` when needed to understand the context. Inside evcc's own UI the self-reference is usually redundant\n- Acronyms uppercase in prose: OCPP, MQTT, HEMS, SoC\n- Terminology: agreed terms per language with explanations live in `i18n/glossary/` (`en.json` + `<lang>.json`, rules in `README.md`). Check it before wording user-facing strings (including API descriptions in `assets/js/types/evcc.ts` and `server/openapi.yaml`), and extend it when a new concept gets a name\n- Commit subjects: `Component: short description`, no trailing period. Sub-scope in parens: `Meter (Home Assistant): ...`. Use `chore:`/`fix:`/`docs:` only for non-feature changes\n\n## Comment Style\n\n- Prefer self-documenting code over comments; comment the _why_, not the _what_\n- Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary\n- Skip refs to the current task, PR, issue, or caller (\"added for X flow\", \"see #1234\"). Git history covers that\n- Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name\n\n## Go Coding Standards\n\n### Core Principles\n\n- Follow Go idioms and conventions (Effective Go)\n- Use `gofmt` for formatting, self-documenting names, early returns\n- Handle all errors explicitly with meaningful messages\n- Use interfaces for behavior contracts (small, focused, single responsibility)\n- Use `context.Context` for I/O, long-running, or cancelable operations\n- Organize code into logical packages with clear responsibilities\n- Prefer composition over inheritance, minimize external dependencies\n- Navigate Go symbols with go-to-definition and find-references rather than text search; reserve text search for comments and string literals\n\n### File Patterns\n\n- `_blueprint.go` - templates for new device implementations\n- `_enumer.go` - generated enum code\n- `*_decorators.go` - generated decorator pattern implementations\n- Validate interface implementations: `var _ Interface = (*Type)(nil)`\n- Capabilities: register via `implement.Has`/`May` only when a capability is _conditional_ (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()`\n\n### Error Handling\n\n- Wrap errors with context: `fmt.Errorf(\"context: %w\", err)`\n- Use `errors.As` and `errors.Is` for type checking\n- Use `errors.Join` for combining errors (prefer custom `joinErrors` helper)\n- Create domain-specific error types (ClassError, DeviceError)\n- Use `backoff.Permanent(err)` for non-retryable errors\n- Implement panic recovery with `defer` and `recover()` in script contexts\n\n### Testing & Code Generation\n\n- Use `testing` package with `testify/assert` and `testify/require`\n- Table-driven tests with struct definitions for multiple cases\n- Use `gomock` for interface mocking, `go:generate mockgen` for generation\n- Test both success and failure scenarios, use `require` for setup, `assert` for tests\n- Use `go:generate` for code generation, regenerate after interface/enum changes\n- Never manually edit generated files\n\n### Context & Concurrency\n\n- Use `context.Context` as first parameter for I/O operations\n- Use `context.WithTimeout`, `context.WithCancel` appropriately\n- Check `ctx.Done()` in long-running loops\n- Propagate context through goroutines for proper cancellation\n- Handle concurrent operations safely with Go's concurrency primitives\n\n### Data Validation\n\n- Filter `NaN` and `Infinity` values using `math.IsNaN()` and `math.IsInf()`\n- Validate numeric inputs from external sources\n- Use helper functions like `parseFloat()` that reject invalid values\n\n## Vue.js/TypeScript Frontend Standards\n\n### Core Architecture\n\n- Use Vue 3 Options API (preferred over Composition API)\n- Use reactive stores without Vuex/Pinia for cross-component state\n- Use global app instance (`window.app`) only for: notifications (`raise()`), offline status (`setOffline()`/`setOnline()`), clearing notifications (`clear()`)\n- Organize components by feature/domain in `assets/js/components/` subdirectories\n\n### Component Development\n\n- Use TypeScript for all new frontend code\n- Use `const` instead of `function` for component methods (e.g., `const updateType = () =>`)\n- Define TypeScript interfaces for component props, data, and API responses\n- Implement accessibility features (tabindex, aria-label, keyboard handlers)\n- Use descriptive names for variables, functions, and event handlers\n- Use early returns for readability\n- Prefer named computed properties over inline template expressions, even for single use. Readability beats saving lines\n- Use configured Axios instance for HTTP communication\n\n### State Management\n\n- Never access the store from sub-components; keep them stateless and pass the values they need as props (emit events back to the parent). Only top-level views read from the store. This keeps components reusable and testable (e.g. Storybook should never mock the store).\n- Use `reactive()` from Vue for simple global state\n- Implement property setters for nested object updates using helper functions\n- Use localStorage with reactive wrappers for persistent settings\n- Use Vue `watch()` for automatic persistence of settings changes\n- Separate concerns with dedicated stores (settings, application state)\n\n### TypeScript Patterns\n\n- Define comprehensive interfaces for API responses and application state\n- Use enums for constants (e.g., `THEME`, `CURRENCY`)\n- Extend global interfaces for window object augmentation\n- Use union types for flexible but type-safe configurations\n- Use generic types for reusable utility functions\n- Handle type assertions carefully with proper error handling\n- Create focused utility functions with proper TypeScript typing\n\n### Styling & Internationalization\n\n- Use CSS Custom Properties for theming (semantic names: `--evcc-green`, `--evcc-battery`)\n- No inline `style` attributes; use Bootstrap utility classes or scoped CSS (inline only for truly dynamic values)\n- Use the custom media queries from `assets/css/breakpoints.css` (`@media (--sm-and-up)`) for responsive breakpoints, never raw px `min-width`/`max-width`. Write mobile-first and override upwards instead of adding max-width queries\n- Use `$t()` function for all user-facing strings\n- Update both `i18n/en.json` and `i18n/de.json` for new strings\n- Use hierarchical namespace: `{section}.{component}.{purpose}`\n- Examples: `config.vehicle.titleAdd`, `main.vehicleStatus.charging`\n- Action patterns: `titleAdd`, `titleEdit`, `save`, `cancel`, `delete`, `validateSave`\n- Use placeholders for dynamic content: `{soc}`, `{duration}`, `{value}`\n- Prefer context-specific keys over generic ones\n- Test with German translations (20-40% longer text)\n- Keep separators and trailing punctuation (`: `, `…`, `—`) in the template, not in the translation value.\n\n### Testing\n\n- Write integration tests using Playwright for user workflows\n- Use Storybook for component development and visual testing\n- Use semantic selectors (roles, labels, button text); `data-testid` only when necessary\n- Test error states and loading states\n\n## Playwright Integration Testing\n\n### Test Organization\n\n- **Location**: `tests/` directory with `.spec.ts` files\n- **Configuration**: `.evcc.yaml` files for different test scenarios\n- **Utilities**: `tests/utils.ts` for common helpers, `tests/evcc.ts` for binary management\n- **Categories**: `config-*.spec.ts` (UI config), `sessions.spec.ts`/`plan.spec.ts` (workflows), `smart-cost.spec.ts`/`limits.spec.ts` (features), `backup-restore.spec.ts`/`auth.spec.ts` (integration)\n\n### Test Configuration\n\n- Base URL: `http://127.0.0.1:7070`\n- Parallel execution with different ports per worker for isolation\n- Uses `./evcc` binary with test-specific configuration files\n- Each worker uses isolated temporary database files\n- Always runs with English UI language\n\n### Essential Commands\n\n- Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev).\n- Run tests: `vp run playwright` or `vpx playwright test`\n- Debug: `vpx playwright test --debug`\n- Specific test: `vpx playwright test tests/config-loadpoint.spec.ts`\n\n### Selector Strategy\n\n- **Preferred**: Semantic selectors using `getByRole()`, `getByLabel()`, `getByText()`\n- **Fallback**: `data-testid` only when semantic selectors aren't available\n- **Examples**:\n  - `page.getByRole(\"button\", { name: \"Add charger\" })`\n  - `page.getByLabel(\"Manufacturer\").selectOption(\"Demo charger\")`\n  - `page.getByRole(\"listitem\", { name: \"Draggable: First Loadpoint\" })` (using aria-label)\n  - `page.getByTestId(\"loadpoint\")` (fallback only)\n- never use `.locator()` or `class` and `id`-based selectors\n\n### Test Patterns\n\n- Use test-specific `.evcc.yaml` configurations\n- Import utilities from `tests/utils.ts` for common operations\n- Focus on complete user journeys rather than isolated interactions\n- Use `expectModalVisible()` and `expectModalHidden()` helpers\n- Test configuration persistence across application restarts\n- Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)`\n- Never use fixed timeouts. Wait on element state (visibility, count, value) instead.\n- Never use `page.waitForLoadState(\"networkidle\")`. SPAs keep emitting requests (websockets, polling), so it either races or hangs. Wait for the specific element / value you need instead.\n- Keep test names and describe titles short and concrete. They should complement each other, not repeat. Prefer `describe(\"aux meter\") test(\"create\")` over `describe(\"aux meter\") test(\"create aux meter and verify it appears\")`. Drop scenario filler like \"and lands in section\", \"appears correctly\", \"ensure\".\n\n## Device Integration & Configuration\n\n### Plugin System\n\n- Device types: chargers, meters, vehicles, tariffs\n- Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go\n- Define device capabilities and configuration in templates at `templates/definition/[type]/`\n- Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a _different_ value. Restating the same value is redundant duplication: reference the param by `name` alone.\n- Test templates: `evcc --template-type [type] --template [file]`\n- Update docs after template changes: `make docs`\n- When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details\n\n### Configuration\n\n- Use YAML format for all configuration files (default: `evcc.yaml`, or specify with `--config`)\n- Provide clear validation and error messages for invalid configurations\n- Support template-based device configurations with meaningful defaults\n- Use SQLite as default database (default: `evcc.db`, or specify with `--database`) with proper migrations and data integrity\n\n## Security & Performance Guidelines\n\n### Security\n\n- Validate all user inputs and sanitize data before database storage\n- Use secure protocols (TLS) for external integrations\n- Implement proper authentication and authorization\n- Never log sensitive information (passwords, tokens, personal data)\n\n### Performance\n\n- Optimize database queries with appropriate indexes\n- Handle concurrent operations safely with Go's concurrency primitives\n- Implement proper caching strategies and connection pooling\n- Avoid blocking operations in main application loop\n\n## Pull Request Descriptions\n\nStructure PR descriptions in this order. No headlines. Be concise.\n\n1. **References first line**: link related issues or PRs (`fixes #1123`, `replaces #222`, `pairs with org/repo#345`). PRs should almost always reference an issue or related PR — only skip in rare exceptions (e.g. trivial typo fixes).\n2. **Intro**: one or a few concise sentences framing what the PR does and why it was created this way. The full problem description belongs in the linked issue, not here.\n3. **Bullet list**: most significant changes or user-facing implications. Lead with the most significant.\n4. **TODO section** (only if open points remain):\n\n   ```\n   **TODO**\n   - [ ] item a\n   - [ ] item b\n   ```\n\nAvoid file paths, line numbers, or code listings reproduced from the diff. Include a code snippet only when it conveys the contract (event shape, API signature) more clearly than prose. No testing checklists, no co-author footers.\n\nNever state that `go build`, `go vet`, `go test -race`, or `gofmt` pass (or any \"all checks/tests green\" phrasing). These are non-negotiable givens that must already be fulfilled, not noteworthy results.\n\n## Pull Request CI and Reviews\n\n- After opening or updating a pull request, watch CI until every check has finished. Work is not done while checks are still running. Fix failures on the same branch and keep watching until the run is green.\n- Do not argue with automated review bots such as Sourcery. Either implement the suggestion or resolve the thread. No rebuttal comments.\n\n## AI Attribution\n\nWork produced by an AI agent must be attributable as such on GitHub. Append the tool's attribution footer to every PR description, issue body, review, and comment written by an agent, for example:\n\n```\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n```\n\nCommit messages are the exception: no footer, and no `Co-Authored-By` trailer. Never dress agent work up as human review (e.g. \"PR by an agent but looks good to me\") in place of the footer.\n\n<!-- File: .github/agents/template.agent.md (Tokens: ~115 | Category: subagent-persona) -->\n---\ndescription: 'Tidy templates'\ntools: [\"read\", \"edit\", \"search\",\"shell\"]\n---\n\n## Cleanse parameter defaults\n\nValidate templates files inside `templates/definition/**` against default parameters in `util/templates/defaults.yaml`.\nMake sure that templates don't repeat parts of the default parameter definition.\n\n## Reorder parameters after presets\n\nValidate templates files inside `templates/definition/**`. If the template contains parameters and presets, ensure that any parameter contained in a `preset` appears in the template after the `preset`. Reorder such parameters after the presets.\n\n<!-- File: docs/agents/api-security.md (Tokens: ~1143 | Category: subagent-persona) -->\n# API Security & Authentication\n\nHow evcc authenticates HTTP requests and how endpoints are classified by\nsensitivity.\n\n## Threat Model\n\nevcc is designed for use within a trusted home network. The auth layer\nprotects credential management, configuration changes, and system operations\n(logs, backup/restore/reset, shutdown). Read-only state and basic charging\ncontrols are intentionally unauthenticated.\n\n## Auth Modes\n\n| Mode       | Trigger               | Behavior                                          |\n|------------|-----------------------|---------------------------------------------------|\n| `Enabled`  | default               | password required; JWT or API key accepted        |\n| `Disabled` | `--disable-auth` flag | all auth checks skipped                           |\n| `Locked`   | demo mode             | mutating endpoints return 403; reads still work   |\n\nMode is fixed at startup. The frontend mirrors the mode so admin actions can\nbe greyed out and a banner shown.\n\n## Endpoint Sensitivity Tiers\n\nThree tiers, by what the caller has to prove:\n\n**Public.** No auth. State, loadpoint controls, login. Anyone on the\nnetwork can read and operate.\n\n**Secure.** Requires a valid session: either the auth cookie (browser, JWT)\nor an API key in the `Authorization: Bearer …` header (automation). Used\nfor configuration and system administration.\n\n**Critical.** Secure plus an additional admin-password check inside the\nhandler. Used for destructive or credential-scoped operations.\n\nFor some Critical endpoints (backup, restore, reset) the password check is\n**skipped when the caller is authenticated via API key**, so unattended automation\ndoesn't need to embed the admin password. For credential-management\nendpoints (rotate API key, change admin password) the password check is\n**strict**: a leaked API key must not be able to rotate itself or change\nthe admin password.\n\nDisabling auth short-circuits all checks.\n\n## Sessions\n\nTwo transports, no overlap:\n\n- Browsers use a session cookie (JWT, 90-day TTL, issued on login).\n- Automation uses an API key in the `Authorization: Bearer …` header.\n\nAPI keys are random alphanumeric strings prefixed `evcc_`. The prefix makes\nleaked keys recognizable to secret-scanning tools.\n\nA single API key per installation; regenerating replaces the previous one.\nPlaintext is shown to the user **once** at generation time and cannot be\nretrieved afterwards.\n\n## Credential Storage\n\nAdmin password and API key are stored as bcrypt hashes. The JWT signing\nsecret is a per-installation random value. Plaintext credentials are never\npersisted.\n\nRemoving the admin password (CLI recovery) also clears the JWT secret and the\nAPI key, which invalidates all outstanding sessions and any previously-issued\nAPI key. Regenerating the API key replaces the stored hash; the previous key\nstops working immediately.\n\n## API Key Lifecycle\n\nTwo operations:\n\n- **Status.** Whether a key is configured. Secure tier; never returns\n  plaintext.\n- **Regenerate.** Critical tier, strict password check. Returns the new\n  plaintext key exactly once.\n\nThere is no delete operation: regenerating and discarding the new key\nachieves the same effect (the previous key stops working immediately).\n\n## Endpoint Matrix\n\n| Endpoint category                            | Tier      | Additional Requirements            |\n|----------------------------------------------|-----------|------------------------------------|\n| State / read-only / basic charging control   | Public    |                                    |\n| Set or update admin password                 | Public    | admin password                     |\n| Configuration                                | Secure    |                                    |\n| Configuration embedding a script plugin       | Critical  | api key or admin password          |\n| System: logs, cache, shutdown                | Secure    |                                    |\n| API key status                               | Secure    |                                    |\n| System: backup / restore / reset             | Critical  | api key or admin password          |\n| API key regenerate                           | Critical  | admin password                     |\n\nDevice test, create, and update (`/api/config/test/{class}` and `/api/config/devices/{class}`)\ninstantiate a config immediately, so a `script` plugin in the payload runs a shell command on the\nserver. Because that command could read credentials a session is not otherwise allowed to see (for\nexample the contents of the database), these requests are treated as Critical when the config embeds\na script plugin, at any nesting depth. A session caller must supply the admin password in the\n`X-Admin-Password` header; an API-key caller passes without it. The `go` (yaegi) and `js` (otto)\nplugins are excluded: their interpreters are sandboxed to pure computation and cannot read files,\nspawn processes, or open network connections.\n\n**Public** endpoints accept any caller. **Secure** endpoints require a\nvalid session (cookie or API key). **Critical** endpoints require extra\nauthentication in the form of an admin password (or, for some, an API\nkey).\n\n## Frontend\n\nAuth management lives under **General Config → Security**, which links to\ntwo sub-flows: change admin password, and manage the API key. The API key\nflow has a reveal view that shows the plaintext exactly once with a\ncopy-to-clipboard link.\n\nWhen auth is disabled, the security modals show a warning banner and\ndisable all action buttons. This is UI-only; the backend still accepts the\nunderlying calls so legitimate automation against a disabled-auth instance\nkeeps working.\n\n## OpenAPI\n\nThe OpenAPI spec declares two security schemes (cookie and bearer);\nprotected operations accept either.\n\n<!-- File: docs/agents/core-domain.md (Tokens: ~3143 | Category: subagent-persona) -->\n# Core Domain: Site, Loadpoint, and the Control Loop\n\n## Object Hierarchy\n\n```\nSite (orchestrator — core/site.go)\n├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]\n├── Tariffs: Grid, FeedIn, CO2, Solar\n├── Coordinator (vehicle <-> loadpoint assignment)\n├── Prioritizer (power allocation fairness)\n└── Loadpoints[] (core/loadpoint.go)\n    ├── Charger      (api.Charger — hardware controller)\n    ├── Vehicle      (api.Vehicle — EV battery state via cloud API)\n    ├── ChargeMeter  (api.Meter — AC power at charger)\n    └── Circuit      (optional — electrical domain limits)\n```\n\n## Key Interfaces (api/api.go)\n\n### Meter\n- `Meter` — `CurrentPower() (float64, error)` — watts\n- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh\n- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings\n\n### Battery\n- `Battery` — `Soc() (float64, error)` — 0-100%\n- `BatteryCapacity` — kWh\n- `BatteryController` — set charge/discharge/hold mode\n\n### Charger\n- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)`\n- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)`\n- `PhaseSwitcher` — `Phases1p3p(int) error`\n- `ChargeRater` — `ChargedEnergy() (float64, error)`\n- `ChargeTimer` — `ChargeDuration() (time.Duration, error)`\n\n### Vehicle\n- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()`\n- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`\n- `ChargeController` — remote start/stop on vehicle\n- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits\n- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side\n\n## Charge Modes\n\n| Mode | Behavior |\n|------|----------|\n| `OFF` | Disabled (unless welcome charge) |\n| `SMART` | Ramp current with solar surplus; fast if cheap tariff or plan requires |\n| `NOW` | Max current immediately |\n\nAlways charge (`alwaysCharge`: off/on/once) is a per-loadpoint option for smart mode: never pause, keep at least min current. `once` resets on disconnect. `pv` and `minpv` are deprecated write-only aliases for smart without/with always charge.\n\n## Charge States (IEC 61851)\n\n- `A` — not connected\n- `B` — connected, not charging\n- `C` — connected, charging\n\n## The Control Loop (Site.update — runs every N seconds)\n\n```\n1. Update all meters (grid, PV, battery, aux)\n2. For each loadpoint: UpdateChargePowerAndCurrents()\n3. Calculate site power balance:\n   sitePower = gridPower + batteryPower + excessDCPower\n             + residualPower - auxPower - flexiblePower\n4. Apply battery priority rules (prioritySoc, bufferSoc)\n5. Get tariff rates\n6. For EACH loadpoint: Update(sitePower, ...)\n   ├── Read charger status\n   ├── Detect/identify vehicle\n   ├── Check plan requirements (minSOC, target time)\n   ├── Check limits (limitSOC, limitEnergy)\n   ├── MODE switch -> calculate target current\n   ├── Cap at maxCurrent, respect circuit limits\n   ├── Send MaxCurrent() to charger\n   └── Record metrics\n7. Push updates to WebSocket + metrics\n```\n\nThe loop is stateless per cycle: always re-reads actual state, calculates\noptimal current, sends single command. Resilient to restarts and missed updates.\n\n## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)\n\n```\n1. Read effective min/max current limits\n2. Reduce sitePower by battery boost power\n3. Consider phase switching (1p <-> 3p) if supported\n4. deltaCurrent = powerToCurrent(-sitePower, activePhases)\n   targetCurrent = effectiveCurrent + deltaCurrent\n5. Below minCurrent -> start disable timer (default 3 min)\n6. Surplus returns -> start enable timer (default 1 min)\n7. Cap at maxCurrent\n```\n\n## Battery Priority Rules\n\n| Setting | Effect |\n|---------|--------|\n| `prioritySoc` | Below this: battery charges first, EV gets 0 |\n| `bufferSoc` | Above this: EV can draw from battery reserves |\n| `bufferStartSoc` | Above this: EV charging can begin even if importing |\n\nNone of these apply to a loadpoint the optimizer controls: the optimizer plans the home battery, so a stop gives the battery priority and a charge takes the surplus (`loadpointSitePower`, `#34606`). A charge the home battery covers is pinned to its setpoint instead, the pv loop would count the discharge as deficit (`surplusRegime`, `#34809`).\n\n## Effective Price Calculation\n\n```\ngreenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower\neffectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare\n```\n\n## Concurrency Model\n\n- **Site** owns `RWMutex` for its state (meters, battery, tariffs)\n- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current)\n- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking\n- No global locks — ordering prevents deadlocks\n\n### Channels\n\n| Channel | Scope | Buffer | Purpose |\n|---------|-------|--------|---------|\n| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) |\n| `lpUpdateChan` | Site | 1 | Early loadpoint update requests |\n| `pushChan` | Loadpoint | 16 | User notifications, queued via `valueChan` so the message renders the state at event time |\n\n## Tariff Integration\n\nTypes: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar`\n\n### Smart Features\n- **Cheap-tariff override** — rate below threshold -> fast charge\n- **Smart feed-in** — feed-in rate above threshold -> prioritize export\n- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline\n  - `optimalPlan()` — cheapest non-contiguous slots\n  - `continuousPlan()` — cheapest continuous window (fallback)\n\n## Optimizer Suggestions (core/site_optimizer.go)\n\nThe optimizer (`optimizer.evcc.io`, sponsor-gated) solves a linear program over tariff slots. Every solved \"battery\" (home battery, vehicle/loadpoint) gets a `types.Suggestion` for the slot covering now. In `Automatic` mode these suggestions drive control; otherwise they are advisory (UI + notifications).\n\n### Request side: what becomes a battery\n\n- **Home battery meter**: `batteryRequest`. `controllable` = `api.HasCap[api.BatteryController]`. Only controllable batteries get `charge_from_grid` / `discharge_to_grid`.\n- **Loadpoint with vehicle**: `loadpointRequest`, `detail.Type = vehicle`, `detail.loadpoint = &id`, always `controllable`. Needs vehicle capacity (or a session energy limit) to model as storage. `ModeOff` sets `CMax = 0`; `ModeNow` and plans produce a `PDemand` / `SGoal`.\n- **Unmodellable loadpoints** (no vehicle, no capacity and no energy limit, or heaters) are added to the home load (`addUnmodelledLoad`) and get no suggestion.\n- Disconnected loadpoints are skipped entirely.\n\nDevice key (`batteryDetail.key()`): `battery:<name>` or `loadpoint:<id>`; empty key = cannot act.\n\n### Result side: `applyOptimizerResult`\n\nRuns after each solve, and again every cycle via `reapplySuggestions` when the active slot changes (re-derived from the cached solve, no new request). Expiry is only evaluated on that slot change: if the horizon is passed or no solve completed for `2 * tariff.SlotDuration`, `clearSuggestions()` drops suggestions, battery forecast and notification state. Within an unchanged slot the cached advice stays active regardless of age. A failed solve keeps the previous advice so controlled devices are not released for one cycle.\n\n1. `slot = schedule.activeSlot(now)`, `slotHours` = its duration. Grid import/export and all home-battery discharge for that slot are summed.\n2. `currentSlotSuggestion(detail, req, res, slot, gridImport, gridExport, slotHours)` per battery. Because the LP is linear, the slot result sits at an operating-range extreme and maps cleanly onto discrete modes. All thresholds use `suggestionThreshold = 50 W` to ignore solver noise.\n\n**Home battery** (`Action` is an `api.BatteryMode` string):\n\n| Condition (slot values, W)                         | Action       | Meaning                             |\n| -------------------------------------------------- | ------------ | ----------------------------------- |\n| charge > thr and grid importing                    | `charge`     | grid charging                       |\n| idle and grid importing and soc > `SMin`           | `hold`       | discharge deliberately withheld     |\n| idle and grid exporting and soc < `SMax`           | `holdcharge` | surplus exported instead of charged |\n| discharge > thr and grid exporting                 | `discharge`  | battery-to-grid                     |\n| otherwise (incl. idle at a soc bound, PV charging) | `normal`     | inverter self-consumption logic     |\n\n`idle` = charge and discharge both <= thr. At a soc bound nothing is withheld (the bound does it), hence `normal`.\n\n**Loadpoint/vehicle**: `charge` if charge > thr, else `stop`. `Suggestion.Battery` is set to the planned home-battery discharge so the loadpoint can tell surplus from battery-fed charging.\n\n3. Suggestions are stored only for controllable devices with a key (`site.suggestions`). Loadpoints with a plan goal (`SGoal`) also receive `optimizerPlan` (the remaining charging slots priced at `PN`) via `setOptimizerPlan`, used by `OptimizerPlan()` for the planner UI.\n4. `publishBattery` / `publishSuggestions` publish to UI and hand loadpoint suggestions to `lp.setSuggestion`. `Actionable` is evaluated on every read (`site.suggestion`) as `Action != currentAction`, where the current action is `site.batteryAction()` for batteries (the battery mode, `unknown` reads as `normal`) and `loadpointCurrentAction` (`charge` when enabled and goal not reached, else `stop`).\n5. `diffSuggestions` emits one `suggestion` messenger event per device whose actionable action changed since the last run; non-actionable or vanished devices are pruned so a later change re-notifies.\n\n### Control side: where suggestions take effect\n\nGated by `OptimizerAutomatic()`: `off` (advisory only), battery-only, or `full` (also loadpoints). `site.Automatic()` additionally requires optimizer enabled + sponsor.\n\n**Home battery** (`requiredBatteryMode`, `core/site_battery.go`), in priority order:\n\n- external mode (API) wins.\n- `Automatic()` and `unmodelledCharging(rate)`: a loadpoint the optimizer does not control is fast charging or on the smart cost limit. The suggestion did not account for that load, so force `hold`.\n- `Automatic()`: `batterySuggestionMode()` takes the first controllable battery's suggestion (`api.BatteryModeString(s.Action)`); an unknown action releases to `normal`; no suggestion releases to `normal` only if evcc had modified the mode (`batteryModeModified`), otherwise the mode is left untouched. This replaces the grid charge limit and discharge control. TODO: per-battery modes.\n- not automatic: legacy grid charge / discharge control / feed-in discharge rules.\n\n**Loadpoint** (`activeSuggestion`, `core/loadpoint_optimizer.go`): non-nil only if `optimizerControlled()` (full automatic, not a heater or integrated device, vehicle with capacity > 0) and the suggestion is younger than `suggestionMaxAge = 2 * SlotDuration` (a stalled optimizer must not keep the loadpoint gated). With an active suggestion:\n\n- `ModeSmart` strategy switch calls `optimizerCharging(s)` and skips the smart cost / feed-in price limits.\n  - `surplusRegime` (charge below max power, no grid flow, no battery discharge): not handled, the PV loop tracks measured surplus instead of the forecast.\n  - `stop` without always-charge: not handled, PV loop may still enable on measured surplus.\n  - `charge` at full power or above what the active phases deliver: `fastCharging()`.\n  - `charge` at a limited setpoint: `setLimit(PowerToCurrent(s.Charge))`, or `minCharging()` below min current.\n  - `stop` with always-charge: `minCharging()`.\n- Planner is reduced to a backstop: `plannerActive = planDeadlineCritical()`.\n- Session `LimitEnergy` / `LimitSoc` do not stop charging while the optimizer is driving a plan goal above them.\n- `loadpointSitePower`: priority soc adjustment is applied, and the battery's discharge is subtracted from the surplus unless the action is `charge` (the optimizer plans the home battery, so `bufferSoc` / `bufferStartSoc` are also disabled).\n\nEvery cycle ends with `site.publishSuggestions()` so `Actionable` reflects the updated loadpoint state.\n\n## Key File Locations\n\n- `api/api.go` — all core interfaces\n- `core/site.go` — Site orchestrator + control loop\n- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch)\n- `core/site_battery.go` — battery priority logic, battery mode from suggestion\n- `core/site_optimizer.go` — optimizer request, suggestion mapping, reapply/expiry\n- `core/loadpoint_optimizer.go` — loadpoint suggestion gate and charging strategy\n- `core/site_tariffs.go` — tariff integration\n- `core/planner/planner.go` — charge time optimization\n- `core/prioritizer/prioritizer.go` — power allocation across loadpoints\n- `core/circuit/circuit.go` — electrical domain limits\n\n<!-- File: docs/agents/easee-architecture.md (Tokens: ~3396 | Category: subagent-persona) -->\n# Easee Charger Architecture\n\nThe Easee integration communicates with the Easee cloud over two distinct channels:\n\n1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration.\n2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations.\n\nCommands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.\n\n## Authentication\n\n### Flow\n\nAuthentication uses username/password credentials against:\n```\nPOST https://api.easee.com/api/accounts/login\n```\n\nReturns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`.\n\n### Token Lifecycle\n\nWrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls:\n```\nPOST https://api.easee.com/api/accounts/refresh_token\n```\nFalls back to full re-login if refresh fails.\n\n### Token Caching\n\n`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.\n\n## Initialization Sequence\n\n`NewEasee` performs these steps in order:\n\n1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger.\n2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control.\n3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates).\n4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change.\n5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`).\n6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds.\n\n## SignalR Back-Channel\n\n### Why SignalR is Required\n\n1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`.\n2. **State is event-driven, not pollable.** No REST endpoint streams charger state.\n3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out.\n\n### Server -> Client Methods\n\n#### `ProductUpdate(json.RawMessage)`\nPrimary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`.\n\n- **Timestamp deduplication**: older timestamps for the same ID are silently dropped.\n- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select.\n\n#### `CommandResponse(json.RawMessage)`\nAsync acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID).\n\nRoutes through three maps in order:\n1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands\n2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch\n3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse\n\nUnmatched responses are logged as WARN (rogue response from external system).\n\n#### `ChargerUpdate` / `SubscribeToMyProduct`\nLogged at TRACE, not processed further.\n\n## Command Flow and Async Correlation\n\n### REST Command Endpoints\n\n```\nPOST /api/chargers/{chargerID}/commands/{action}     (start/stop/pause/resume)\nPOST /api/chargers/{chargerID}/settings              (enable, DCC, PhaseMode, SmartCharging)\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings  (dynamic circuit currents)\n```\n\n### Response Handling\n\n| HTTP Status | Meaning | Behavior |\n|-------------|---------|----------|\n| `200` | Synchronous / already applied | Returns immediately |\n| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse |\n| other | Error | Returns error |\n\n### Ticks Correlation\n\nOn 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op.\n\nEach in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`.\n\n### The Sync/Async Mismatch\n\nSome 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`).\n\nHandled via the **expected-orphan counter**:\n```go\nexpectedOrphans map[easee.ObservationID]int  // protected by cmdMu\n```\n\nBefore 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.\n\n## State Management\n\n### Internal State Fields (all protected by `sync.RWMutex`)\n\n| Field | Observation | Notes |\n|-------|------------|-------|\n| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine |\n| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state |\n| `smartCharging` | `SMART_CHARGING` (102) | LED color mode |\n| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) |\n| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling |\n| `totalEnergy` | `LIFETIME_ENERGY` (124), `CHARGE_SESSION_START` (223), `CHARGING_SESSION` (129) | kWh, monotonic max, see Logical Energy Meter |\n| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A |\n| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase |\n| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit |\n| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) |\n| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit |\n| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum |\n| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F |\n| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token |\n\n### Session Energy Zero-value Protection\n\n`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. The first op mode after startup is replayed state and does not reset, so replayed session energy survives a restart. `CHARGE_SESSION_START` and a zero `SESSION_ENERGY` arrive before the mode change, so the reset is skipped when the cached session started after the previous mode observation.\n\n### Logical Energy Meter\n\n`LIFETIME_ENERGY` is pushed only ~hourly. `TotalEnergy()` therefore keeps a monotonic max of the lifetime reading, the session meter values from `CHARGE_SESSION_START` and `CHARGING_SESSION`, and `CHARGE_SESSION_START.MeterValue + sessionEnergy`. The session term is only applied while the last nonzero `SESSION_ENERGY` (or session completion) is not older than the session start, so a stale previous-session value cannot inflate the total. Ignored zero observations do not count as fresh readings.\n\n## Charger Operation Modes\n\n```\n0 = Offline               — no cloud connection\n1 = Disconnected          — no car plugged in\n2 = AwaitingStart         — car plugged, waiting for authorization/start\n3 = Charging              — actively charging\n4 = Completed             — car full or finished, cable still plugged\n5 = Error                 — fault condition\n6 = ReadyToCharge         — ready, current available\n7 = AwaitingAuthentication — RFID auth required\n8 = Deauthenticating      — finishing authentication teardown\n```\n\n### Mapping to evcc Status\n\n| opMode | evcc Status |\n|--------|------------|\n| 1 (Disconnected) | A |\n| 2, 4, 6, 7, 8 | B |\n| 3 (Charging) | C |\n| 0, 5 and others | error |\n\n## Enable/Disable Flow\n\n### Enable = true\n\n1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait.\n2. If `opMode == Disconnected`: return (no cable).\n3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`.\n4. Otherwise: action = `resume_charging`.\n5. POST `/commands/{action}` and wait.\n6. Wait for `opMode` to reach enabled state.\n7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume).\n8. Call `MaxCurrent(c.current)` to restore previous setpoint.\n\n### Enable = false\n\n1. If disconnected or (awaiting auth && !authorize): return.\n2. POST `/commands/pause_charging` and wait.\n3. Wait for `opMode` to reach disabled state.\n4. Wait for `dynamicChargerCurrent` to reach `0`.\n\n### State Waiting Pattern\n\nBoth `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use:\n1. Short-circuit check: if already in target state, return immediately.\n2. Open a timer.\n3. Loop on `obsC` channel.\n4. On timer expiry: **one final check** before returning `api.ErrTimeout`.\n\nThe final check handles the race where the state update arrived between the last channel read and the timer fire.\n\n## Phase Control\n\n### Circuit-Level (preferred, when circuit is known)\n\nPhase switching by zeroing dynamic circuit current on unused phases:\n```\nPOST /api/sites/{siteID}/circuits/{circuitID}/settings\n```\n\nFor 1-phase: set P2=0, P3=0. For 3-phase: restore all three.\n\nThis POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan).\n\n### Charger-Level (fallback)\n\nUses `PhaseMode` setting: `1` locked single-phase, `3` locked 3-phase. `2` (auto, set by earlier versions) is reported as unknown phases so the loadpoint locks it.\nAfter changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.\n\n## Authorization Mode (`authorize`)\n\nWhen `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.\n\nWhen `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).\n\nSetting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.\n\n## Concurrency Model\n\n### Mutexes\n\n| Mutex | Type | Protects |\n|-------|------|----------|\n| `c.mux` | `sync.RWMutex` | All charger state fields |\n| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) |\n\nCommand 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.\n\n### Observation Channel\n\n`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.\n\n**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`.\n\n## Known Design Concerns\n\n1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified.\n2. **LIFETIME_ENERGY** — API pushes updates ~hourly; bridged by the logical energy meter, so accuracy between readings depends on `SESSION_ENERGY` and the session start meter value.\n3. **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.\n4. **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.\n5. **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.\n\n## API Endpoints Summary\n\n| Method | Endpoint | Used For |\n|--------|----------|----------|\n| `POST` | `/accounts/login` | Initial authentication |\n| `POST` | `/accounts/refresh_token` | Token refresh |\n| `GET` | `/chargers` | Auto-discover charger ID |\n| `GET` | `/chargers/{id}/site` | Discover site and circuit |\n| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging |\n| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging |\n| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents |\n| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) |\n\n### SignalR Hub\n\n| Endpoint | `https://streams.easee.com/hubs/chargers` |\n|----------|------------------------------------------|\n| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` |\n| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` |\n\n## Configuration\n\n| Parameter | Required | Default | Notes |\n|-----------|----------|---------|-------|\n| `user` | yes | | Easee account email |\n| `password` | yes | | Easee account password |\n| `charger` | no | | Charger serial; auto-detected if exactly one on account |\n| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits |\n| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions |\n\nSupported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.\nDeclared capabilities: `1p3p` (phase switching), `rfid` (RFID identification).\nRequires evcc sponsorship.\n\n<!-- File: docs/agents/hardware-integrations.md (Tokens: ~1327 | Category: subagent-persona) -->\n# Hardware Integrations: Chargers, Meters, Vehicles\n\n## Integration Pattern\n\nAll device types use a registry-based factory pattern:\n\n```go\n// Self-registration in init()\nfunc init() {\n    registry.AddCtx(\"typename\", NewFromConfig)\n}\n\nfunc NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {\n    // Parse config, create client, return implementation\n}\n```\n\nOptional interfaces are added via the decorator pattern:\n```go\n//go:generate decorate -f decorateXxx -b *Xxx -t \"api.PhaseSwitcher,Phases1p3p,func(int) error\"\n```\n\n## Charger Implementations\n\n### By Protocol\n\n| Protocol | Examples |\n|----------|---------|\n| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |\n| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |\n| OCPP 1.6 | Generic charge point server |\n| EEBus/ISO 15118 | EEBus SPINE protocol |\n| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |\n| MQTT | OpenWB, Tasmota, Shelly |\n| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |\n\n### Required Charger Interface\n\n```go\ntype Charger interface {\n    ChargeState\n    Enabled() (bool, error)\n    Enable(enable bool) error\n    CurrentController\n}\n```\n\nWhere `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and\n`CurrentController` provides `MaxCurrent(current int64) error`.\n\n### Optional Charger Interfaces\n\n- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision\n- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p\n- `Meter` / `MeterEnergy` — built-in power/energy measurement\n- `PhaseCurrents` / `PhaseVoltages` — per-phase readings\n- `ChargeRater` — `ChargedEnergy()` for session energy\n- `ChargeTimer` — `ChargeDuration()` for session time\n- `Identifier` — `Identify()` for RFID/vehicle identification\n\n### Key Implementations\n\n- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail\n- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management\n- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2\n- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC)\n- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template\n\n### Adding a New Charger\n\n1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`)\n2. Implement `NewXxxFromConfig()` returning `api.Charger`\n3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()`\n4. Register: `registry.AddCtx(\"xxx\", factory)` in `init()`\n5. Optional: add decorator for PhaseSwitcher, Meter, etc.\n6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata\n\n## Meter Implementations\n\n### By Category\n\n| Category | Examples |\n|----------|---------|\n| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |\n| HTTP/REST | Homewizard, Shelly Gen3, E3DC |\n| Smart Home | HomeAssistant entities, Homematic |\n| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |\n\n### Required Meter Interface\n\n```go\ntype Meter interface {\n    CurrentPower() (float64, error)  // watts\n}\n```\n\n### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity`\n\n### Key Implementations\n- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection\n- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries\n- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources)\n\n## Vehicle Integrations\n\n| Manufacturer | API Type |\n|-------------|----------|\n| Tesla | Fleet API + vehicle-command proxy |\n| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |\n| Hyundai/Kia | BlueLink (regional variants) |\n| BMW/Mini | ConnectedDrive v2 |\n| Mercedes | Official API |\n| Renault/Nissan | Renault API + Carwings |\n| Ford | FordConnect (US/EU) |\n| Porsche | Porsche Connect |\n| PSA Group | Peugeot, Citroen, DS, Opel |\n| Generic | OVMS, Tronity |\n\n### Required Vehicle Interface\n\n```go\ntype Vehicle interface {\n    Battery          // Soc() (float64, error)\n    BatteryCapacity  // Capacity() float64\n    IconDescriber    // Icon() string\n    FeatureDescriber // Features() []Feature\n    PhaseDescriber   // Phases() int\n    TitleDescriber   // GetTitle() string\n    SetTitle(string)\n    Identifiers() []string\n    OnIdentified() ActionConfig\n}\n```\n\n### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector`\n\n### Polling Strategy\n\nConfigurable: always / while charging / while connected. Interval-based caching\nto avoid excessive cloud API calls. OAuth2 token handling built into each provider.\n\n## Auto-Detection (`cmd/detect/`)\n\nTask-based parallel IP scanning:\n`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes\n\nDetects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,\nTasmota, Shelly, Phoenix, and many more.\n\n<!-- File: docs/agents/ocpp-forwarder.md (Tokens: ~905 | Category: subagent-persona) -->\n# OCPP Forwarder Architecture\n\nThe OCPP forwarder (`charger/ocpp/forwarder.go`) is a hybrid proxy that lets a charger talk to evcc and an upstream OCPP server at the same time. Chargers connect directly to evcc's central system on the normal port. For each charger with a matching `ForwarderRule`, a \"sidecar\" WebSocket connection to the upstream server is opened and kept in parallel for the lifetime of the charger connection.\n\nThe forwarder is opt-in: hooks (`chargerConnectHook`, `chargerDisconnectHook`, `chargerMessageHook` in `instance.go`) are nil unless a rule matches, so a charger without a rule behaves exactly as before.\n\n## Forwarding modes\n\nTwo modes apply at the same time, selected per message by its action.\n\n### Transparent relay (billing-critical)\n\nFor the actions in `actionsRelayedToUpstream` (`Authorize`, `StartTransaction`, `StopTransaction`, `DataTransfer`), upstream is the authoritative Central System:\n\n1. Charger sends the Call to evcc.\n2. The message hook forwards it to the upstream sidecar and bypasses evcc's OCPP handler.\n3. Upstream's `CallResult`/`CallError` is relayed back to the charger.\n\nevcc's handler is never invoked for these. This lets the pay backend control authorization, issue its own transaction IDs, and see consistent Start/Stop pairs.\n\n### Sidecar observation (informational)\n\nFor all other messages (`BootNotification`, `StatusNotification`, `MeterValues`, `Heartbeat`, etc.):\n\n1. Charger sends the Call to evcc, which processes it normally.\n2. The same frame is also mirrored to the upstream sidecar.\n\nUpstream observes the session while evcc manages the charger as usual.\n\n## Upstream to charger (commands)\n\nCalls (type 2) initiated by upstream are injected into the charger via `CS.Write`. The charger's `CallResult`/`CallError` is routed back to upstream. Examples: `RemoteStartTransaction`, `RemoteStopTransaction`, `GetConfiguration`, `ChangeConfiguration`, `TriggerMessage`, `SetChargingProfile`.\n\n`ChangeConfiguration` for `MeterValueSampleInterval` is intercepted: the forwarder absorbs it as a local throttle on `MeterValues` forwarded to upstream and replies `Accepted` without touching the charger's own config. evcc still processes every `MeterValues` frame for energy management.\n\n## Read-only mode\n\nWhen a rule sets `ReadOnly`, upstream may observe but cannot control the charger. Any incoming Call from upstream is answered with a `SecurityError` and not forwarded. `ReadOnly` is applied live per message, so toggling it does not require reconnecting the sidecar.\n\n## Connection lifecycle\n\nFrames that arrive from a charger before its sidecar finishes dialling are buffered (`pendingMsgs`) and flushed in order once the sidecar connects, so early messages such as `BootNotification` still reach upstream. If the dial fails or upstream drops mid-session, any buffered or in-flight relay Calls are answered to the charger with a `CallError` so it is not left hanging, and the failure is surfaced to the UI via `forwarderErrors`.\n\nWhen the upstream connection fails while the charger stays connected, the sidecar is re-dialled automatically with exponential backoff (`runUpstreamSidecar`, 5s up to 5min; a successful session resets the backoff). Between attempts the relay actions fall back to evcc's local handler, so charging continues but upstream misses those transactions. The reconnect loop ends when the charger disconnects, the rule is removed, or its connection parameters change (`ApplyForwarderRules` dials its own sidecar in that case).\n\nThe last `BootNotification` of each connected charger is cached (`lastBoot`). When a sidecar connects mid-session (after an upstream reconnect or a rule added at runtime) and the pending buffer does not already carry a boot frame, the cached frame is replayed to upstream with a fresh message id, since many backends expect a boot before accepting transactions. Upstream's reply to the replay is discarded; evcc answered the charger's original boot long ago.\n\nRules can be changed at runtime through `ApplyForwarderRules`. Sidecars for removed rules are closed, rules with changed connection parameters are re-dialled, and rules for chargers that are not connected are test-dialled to surface unreachable hosts immediately.\n\n<!-- File: docs/agents/plugin-system.md (Tokens: ~886 | Category: subagent-persona) -->\n# Plugin System\n\nThe plugin system (`plugin/`) provides protocol-level abstraction for device\ncommunication. Plugins implement typed getter/setter interfaces and are composed\ninto charger, meter, or vehicle implementations via configuration.\n\n## Plugin Types\n\n| Plugin | Protocol | Key Config |\n|--------|----------|------------|\n| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` |\n| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` |\n| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` |\n| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree |\n| `js` | JavaScript/WASM | Inline script evaluation |\n| `go` | Go runtime | Dynamic Go code |\n| `gpio` | Linux GPIO | Digital I/O for relays |\n\n## Getter/Setter Interfaces\n\n```go\ntype StringGetter func() (string, error)\ntype FloatGetter  func() (float64, error)\ntype IntGetter    func() (int64, error)\ntype BoolGetter   func() (bool, error)\n// + corresponding Setter types\n```\n\n## Pipeline Transforms\n\nPlugins support chained transforms: `scale`, `offset`, `lookup`, `regex`.\n\n## Template-Based Device Configuration\n\nDevices can be defined entirely via YAML templates using plugins:\n\n```yaml\n# templates/definition/charger/example.yaml\nstatus:\n  source: http\n  uri: http://{{ .host }}/status\nenable:\n  source: http\n  uri: http://{{ .host }}/enable\n  method: POST\nmaxcurrent:\n  source: http\n  uri: http://{{ .host }}/current/{{ .maxcurrent }}\n```\n\nThe generic configurable charger (`charger/charger.go`) wires these plugin\nconfigs into the `api.Charger` interface at runtime.\n\nTemplates render once at config time. HTTP `uri` and `body` are templates\nthemselves, evaluated per request (`util.ReplaceFormatted`: sprig, `addDate`,\n`timeRound`). Defer `now` to request time with a raw string:\n`` {{ `{{ now | date \"2006-01-02\" }}` }} ``.\n\n## Param Suggestions and Discovery\n\nTemplate params get suggestions from services (`service:`), see the `service`\nand `discovery` sections in `templates/README.md` for the contract. Handlers\nregister in `init()` via `service.Register` (`server/service/registry.go`),\nobject responses use `service.Option` (`util/service/helper.go`) and render as\n`Helper/Combobox.vue`.\n\nLocal network discovery lives in `util/discovery` (mDNS, SSDP, neighbor table,\nreverse DNS; no raw sockets, no port scans) and feeds `network/hosts`,\n`network/report` and `network/scan` in `util/service/network*.go`. The vendor\nlist is generated from the IEEE registry (`make oui-update`), see\n[Discovery Hints](discovery-hints.md) for the update routine.\n\n- Scan: 3 s first pass (first request waits), 5 s second pass, repeated when\n  older than a minute, results combined, hosts dropped after 30 minutes.\n- mDNS: browses the types of the template hints plus all types announced in\n  the network (DNS-SD enumeration), so reports show vendor types without a hint.\n- A host keeps all its names (router name first, then mDNS), hostname hints\n  match any of them.\n- `EVCC_DISCOVERY_HOSTS` (JSON list of `discovery.Host`) replaces the scan,\n  used in tests and for environments without network access. On macOS the neighbor table is empty for binaries started with\n  `go run`, test with a built binary.\n\n## Key Files\n\n- `plugin/config.go` — plugin registry and config types\n- `plugin/http.go` — HTTP plugin\n- `plugin/mqtt.go` — MQTT plugin\n- `plugin/modbus.go` — Modbus plugin\n- `plugin/sunspec.go` — SunSpec plugin\n- `charger/charger.go` — generic configurable charger using plugins\n\n<!-- File: docs/agents/web-ui-api.md (Tokens: ~884 | Category: subagent-persona) -->\n# Web UI & REST API\n\n## Server Architecture\n\n- **Router:** gorilla/mux, strict slash\n- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth\n- **Timeouts:** Read 5s, Write 10s, Idle 120s\n- **Static assets:** embedded in binary (`fs.FS`)\n- **Default port:** 7070\n\n## REST API (base `/api/`)\n\n### Site-level\n- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc.\n- `GET /tariff/{tariff}` — tariff rates\n- `GET /sessions` — charging history\n- `GET /state` — complete system state (supports jq filtering)\n\n### Per-loadpoint (`/loadpoints/{id}/...`)\n- `POST mode/{value}` — off/smart/now (deprecated aliases pv/minpv)\n- `POST alwayscharge/{value}` — off/on/once\n- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits\n- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits\n- `POST phases/{value}` — phase config\n- `POST priority/{value}`, `batteryboost/{value}`\n- `POST plan/energy/{value}/{time}` — schedule plan\n- `POST vehicle/{name}` — select vehicle\n- `POST smartcostlimit/{value}` — smart cost threshold\n- `POST solarshare/{value}` — required solar share of min power (default 1)\n\n### Configuration (`/config/...`, auth required)\n- CRUD for devices (chargers, meters, vehicles, tariffs)\n- Template browsing and testing\n- Site, loadpoint, circuit, HEMS, messaging config\n- `GET /config/evcc.yaml` — YAML export\n\n### System (`/system/...`, auth required)\n- Log viewing (`/log`, `/log/areas`), cache clear, shutdown\n\n### Database (`/db/...`, auth + second factor required)\n- Backup download, restore from file, selective reset\n- Second factor: admin password in request body, or API key via Bearer token (bypasses password check)\n\n### Handler Pattern\nGeneric `handler[T]` with type conversion, setter, getter.\nSpecialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`.\n\n## WebSocket (`/ws`)\n\n- `coder/websocket` (RFC 6455)\n- Pub/sub via `SocketHub`\n- Buffered channels (1024 per subscriber)\n- Welcome message with full state snapshot\n- Incremental updates as JSON key-value pairs with dot-notation keys:\n  ```json\n  {\"loadpoints.1.mode\": \"solar\", \"site.gridPower\": 1234}\n  ```\n- Write timeout: 10s, compression (disabled for Safari)\n\n## State Flow\n\n1. WS connects -> receives welcome with full state\n2. App emits `util.Param` on changes\n3. Hub broadcasts to subscribers\n4. Frontend `store.update(msg)` merges via dot-notation\n5. Components reactively re-render\n\n## Authentication\n\n- JWT, 90-day lifetime\n- HttpOnly cookie (`auth`) with `SameSite=Strict`\n- Also accepts `Authorization: Bearer <token>` header\n- Modes: Disabled, Locked (demo), Configured (password)\n- Protects `/api/config`, `/api/system`, and `/api/db`\n\n## MQTT Integration\n\n- Publishes state changes to configurable broker\n- Subscribes to control topics\n- Retained messages for state persistence\n\n## Key Files\n\n- `server/http.go` — router setup\n- `server/http_auth.go` — authentication\n- `server/http_site_handler.go` — state + request handlers\n- `server/http_config_*.go` — config endpoints\n- `server/http_loadpoint_handler.go` — per-loadpoint endpoints\n- `server/socket.go` — WebSocket pub/sub\n- `assets/js/app.ts` — Vue app entry\n- `assets/js/store.ts` — reactive state store\n- `assets/js/api.ts` — Axios clients\n- `assets/js/router.ts` — route definitions\n</yakaai_skills>"}