solar charging ☀️🚘

RAW Doc

Agents/Api Security

API Security & Authentication

How evcc authenticates HTTP requests and how endpoints are classified by
sensitivity.

Threat Model

evcc is designed for use within a trusted home network. The auth layer
protects credential management, configuration changes, and system operations
(logs, backup/restore/reset, shutdown). Read-only state and basic charging
controls are intentionally unauthenticated.

Auth Modes

| Mode | Trigger | Behavior |
|------------|-----------------------|---------------------------------------------------|
| Enabled | default | password required; JWT or API key accepted |
| Disabled | --disable-auth flag | all auth checks skipped |
| Locked | demo mode | mutating endpoints return 403; reads still work |

Mode is fixed at startup. The frontend mirrors the mode so admin actions can
be greyed out and a banner shown.

Endpoint Sensitivity Tiers

Three tiers, by what the caller has to prove:

Public. No auth. State, loadpoint controls, login. Anyone on the
network can read and operate.

Secure. Requires a valid session: either the auth cookie (browser, JWT)
or an API key in the Authorization: Bearer … header (automation). Used
for configuration and system administration.

Critical. Secure plus an additional admin-password check inside the
handler. Used for destructive or credential-scoped operations.

For some Critical endpoints (backup, restore, reset) the password check is
skipped when the caller is authenticated via API key, so unattended automation
doesn't need to embed the admin password. For credential-management
endpoints (rotate API key, change admin password) the password check is
strict: a leaked API key must not be able to rotate itself or change
the admin password.

Disabling auth short-circuits all checks.

Sessions

Two transports, no overlap:

- Browsers use a session cookie (JWT, 90-day TTL, issued on login).
- Automation uses an API key in the Authorization: Bearer … header.

API keys are random alphanumeric strings prefixed evcc_. The prefix makes
leaked keys recognizable to secret-scanning tools.

A single API key per installation; regenerating replaces the previous one.
Plaintext is shown to the user once at generation time and cannot be
retrieved afterwards.

Credential Storage

Admin password and API key are stored as bcrypt hashes. The JWT signing
secret is a per-installation random value. Plaintext credentials are never
persisted.

Removing the admin password (CLI recovery) also clears the JWT secret and the
API key, which invalidates all outstanding sessions and any previously-issued
API key. Regenerating the API key replaces the stored hash; the previous key
stops working immediately.

API Key Lifecycle

Two operations:

- Status. Whether a key is configured. Secure tier; never returns
plaintext.
- Regenerate. Critical tier, strict password check. Returns the new
plaintext key exactly once.

There is no delete operation: regenerating and discarding the new key
achieves the same effect (the previous key stops working immediately).

Endpoint Matrix

| Endpoint category | Tier | Additional Requirements |
|----------------------------------------------|-----------|------------------------------------|
| State / read-only / basic charging control | Public | |
| Set or update admin password | Public | admin password |
| Configuration | Secure | |
| Configuration embedding a script plugin | Critical | api key or admin password |
| System: logs, cache, shutdown | Secure | |
| API key status | Secure | |
| System: backup / restore / reset | Critical | api key or admin password |
| API key regenerate | Critical | admin password |

Device test, create, and update (/api/config/test/{class} and /api/config/devices/{class})
instantiate a config immediately, so a script plugin in the payload runs a shell command on the
server. Because that command could read credentials a session is not otherwise allowed to see (for
example the contents of the database), these requests are treated as Critical when the config embeds
a script plugin, at any nesting depth. A session caller must supply the admin password in the
X-Admin-Password header; an API-key caller passes without it. The go (yaegi) and js (otto)
plugins are excluded: their interpreters are sandboxed to pure computation and cannot read files,
spawn processes, or open network connections.

Public endpoints accept any caller. Secure endpoints require a
valid session (cookie or API key). Critical endpoints require extra
authentication in the form of an admin password (or, for some, an API
key).

Frontend

Auth management lives under General Config → Security, which links to
two sub-flows: change admin password, and manage the API key. The API key
flow has a reveal view that shows the plaintext exactly once with a
copy-to-clipboard link.

When auth is disabled, the security modals show a warning banner and
disable all action buttons. This is UI-only; the backend still accepts the
underlying calls so legitimate automation against a disabled-auth instance
keeps working.

OpenAPI

The OpenAPI spec declares two security schemes (cookie and bearer);
protected operations accept either.

---

Agents/Core Domain

Core Domain: Site, Loadpoint, and the Control Loop

Object Hierarchy

text
Site (orchestrator — core/site.go)
├── Meters: Grid, PV[], Battery[], Auxiliary[], External[]
├── Tariffs: Grid, FeedIn, CO2, Solar
├── Coordinator (vehicle <-> loadpoint assignment)
├── Prioritizer (power allocation fairness)
└── Loadpoints[] (core/loadpoint.go)
├── Charger (api.Charger — hardware controller)
├── Vehicle (api.Vehicle — EV battery state via cloud API)
├── ChargeMeter (api.Meter — AC power at charger)
└── Circuit (optional — electrical domain limits)

Key Interfaces (api/api.go)

Meter


- MeterCurrentPower() (float64, error) — watts
- MeterEnergyTotalEnergy() (float64, error) — kWh
- PhaseCurrents / PhaseVoltages / PhasePowers — per-phase readings

Battery


- BatterySoc() (float64, error) — 0-100%
- BatteryCapacity — kWh
- BatteryController — set charge/discharge/hold mode

Charger


- ChargerStatus(), Enabled(), Enable(bool), MaxCurrent(int64)
- ChargerEx — milliamp-precision current via MaxCurrentMillis(float64)
- PhaseSwitcherPhases1p3p(int) error
- ChargeRaterChargedEnergy() (float64, error)
- ChargeTimerChargeDuration() (time.Duration, error)

Vehicle


- VehicleSoc(), Capacity(), Identifiers(), Phases(), OnIdentified()
- VehicleRange, VehicleOdometer, VehicleClimater, VehicleFinishTimer, VehiclePosition
- ChargeController — remote start/stop on vehicle
- CurrentLimiterGetMinMaxCurrent() for vehicle-side current limits
- CurrentController — some vehicles (Tesla, Fiat) also implement MaxCurrent() to set charge current from the vehicle side

Charge Modes

| Mode | Behavior |
|------|----------|
| OFF | Disabled (unless welcome charge) |
| NOW | Max current immediately |
| MINPV | Min current when PV surplus; fast if cheap tariff |
| PV | Ramp current proportional to available solar |

Charge States (IEC 61851)

- A — not connected
- B — connected, not charging
- C — connected, charging

The Control Loop (Site.update — runs every N seconds)

text
1. Update all meters (grid, PV, battery, aux)
2. For each loadpoint: UpdateChargePowerAndCurrents()
3. Calculate site power balance:
sitePower = gridPower + batteryPower + excessDCPower
+ residualPower - auxPower - flexiblePower
4. Apply battery priority rules (prioritySoc, bufferSoc)
5. Get tariff rates
6. For EACH loadpoint: Update(sitePower, ...)
├── Read charger status
├── Detect/identify vehicle
├── Check plan requirements (minSOC, target time)
├── Check limits (limitSOC, limitEnergy)
├── MODE switch -> calculate target current
├── Cap at maxCurrent, respect circuit limits
├── Send MaxCurrent() to charger
└── Record metrics
7. Push updates to WebSocket + metrics

The loop is stateless per cycle: always re-reads actual state, calculates
optimal current, sends single command. Resilient to restarts and missed updates.

PV Surplus Charging (pvMaxCurrent in core/loadpoint.go)

text
1. Read effective min/max current limits
2. Reduce sitePower by battery boost power
3. Consider phase switching (1p <-> 3p) if supported
4. deltaCurrent = powerToCurrent(-sitePower, activePhases)
targetCurrent = effectiveCurrent + deltaCurrent
5. Below minCurrent -> start disable timer (default 3 min)
6. Surplus returns -> start enable timer (default 1 min)
7. Cap at maxCurrent

Battery Priority Rules

| Setting | Effect |
|---------|--------|
| prioritySoc | Below this: battery charges first, EV gets 0 |
| bufferSoc | Above this: EV can draw from battery reserves |
| bufferStartSoc | Above this: EV charging can begin even if importing |

Effective Price Calculation

text
greenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower
effectivePrice = gridPrice (1 - greenShare) + feedInPrice greenShare

Concurrency Model

- Site owns RWMutex for its state (meters, battery, tariffs)
- Loadpoint owns RWMutex for its state (charger, vehicle, current)
- Coordinator owns RWMutex for vehicle <-> loadpoint tracking
- No global locks — ordering prevents deadlocks

Channels

| Channel | Scope | Buffer | Purpose |
|---------|-------|--------|---------|
| valueChan | Site | Unbounded (chanx.NewUnboundedChan) | State changes -> DB + UI (ordering) |
| lpUpdateChan | Site | 1 | Early loadpoint update requests |
| pushChan | Loadpoint | 16 | User notifications, queued via valueChan so the message renders the state at event time |

Tariff Integration

Types: TariffUsageGrid, TariffUsageFeedIn, TariffUsageCo2, TariffUsagePlanner, TariffUsageSolar

Smart Features


- Cheap-tariff override — rate below threshold -> fast charge
- Smart feed-in — feed-in rate above threshold -> prioritize export
- Planner (core/planner/planner.go) — finds cheapest time slots for target SOC/energy by deadline
- optimalPlan() — cheapest non-contiguous slots
- continuousPlan() — cheapest continuous window (fallback)

Key File Locations

- api/api.go — all core interfaces
- core/site.go — Site orchestrator + control loop
- core/loadpoint.go — Loadpoint state machine (pvMaxCurrent, mode switch)
- core/site_battery.go — battery priority logic
- core/site_tariffs.go — tariff integration
- core/planner/planner.go — charge time optimization
- core/prioritizer/prioritizer.go — power allocation across loadpoints
- core/circuit/circuit.go — electrical domain limits

---

Agents/Easee Architecture

Easee Charger Architecture

The Easee integration communicates with the Easee cloud over two distinct channels:

1. REST API (https://api.easee.com/api) — synchronous control commands and configuration.
2. SignalR WebSocket (https://streams.easee.com/hubs/chargers) — asynchronous real-time state updates and command confirmations.

Commands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR.

Authentication

Flow

Authentication uses username/password credentials against:

text
POST https://api.easee.com/api/accounts/login

Returns a Token struct with accessToken (short-lived JWT), refreshToken, expiresIn, and tokenType.

Token Lifecycle

Wrapped in oauth2.TokenSource via oauth.RefreshTokenSource. Near expiry, automatically calls:

text
POST https://api.easee.com/api/accounts/refresh_token

Falls back to full re-login if refresh fails.

Token Caching

TokenSource is shared per user via cache.New[oauth2.TokenSource](). Multiple Easee instances with the same user email share a single token source, preventing redundant re-authentication.

Initialization Sequence

NewEasee performs these steps in order:

1. Charger Discovery — If no serial provided, queries GET /api/chargers and expects exactly one charger.
2. Site and Circuit DiscoveryGET /api/chargers/{chargerID}/site. Searches for a single-charger circuit for circuit-level phase control.
3. SignalR Connection — Creates client with WithMaxElapsedTime(0) to retry forever (default 15-min cap would silently stop updates).
4. Subscription — On every ClientConnected, sends SubscribeWithCurrentState(chargerID, true) to replay full current state before switching to push-on-change.
5. Startup Gate — Blocks until CHARGER_OP_MODE is received (one-shot sync.OnceFunc).
6. Optional State Wait — Waits up to 3s for SESSION_ENERGY, LIFETIME_ENERGY, TOTAL_POWER. WARN if missing but initialization succeeds.

SignalR Back-Channel

Why SignalR is Required

1. Commands are fire-and-forget at HTTP level. HTTP response only confirms cloud received the request. Success/failure arrives via SignalR CommandResponse.
2. State is event-driven, not pollable. No REST endpoint streams charger state.
3. Ticks correlation only works with a live connection. If SignalR drops mid-command, the waiter times out.

Server -> Client Methods

#### ProductUpdate(json.RawMessage)
Primary state channel. Carries a single Observation with ID (ObservationID), Value, DataType, and Timestamp.

- Timestamp deduplication: older timestamps for the same ID are silently dropped.
- Non-blocking fan-out: observation sent on obsC via non-blocking select.

#### CommandResponse(json.RawMessage)
Async acknowledgement for REST commands. Contains Ticks (correlation key), WasAccepted, ResultCode, and ID (ObservationID).

Routes through three maps in order:
1. pendingTicks[res.Ticks] — primary correlation for async (HTTP 202) commands
2. pendingByID[ObservationID(res.ID)] — fallback when Ticks mismatch
3. expectedOrphans[ObservationID(res.ID)] — counter for sync (HTTP 200) endpoints that still produce a CommandResponse

Unmatched responses are logged as WARN (rogue response from external system).

#### ChargerUpdate / SubscribeToMyProduct
Logged at TRACE, not processed further.

Command Flow and Async Correlation

REST Command Endpoints

text
POST /api/chargers/{chargerID}/commands/{action}     (start/stop/pause/resume)
POST /api/chargers/{chargerID}/settings (enable, DCC, PhaseMode, SmartCharging)
POST /api/sites/{siteID}/circuits/{circuitID}/settings (dynamic circuit currents)

Response Handling

| HTTP Status | Meaning | Behavior |
|-------------|---------|----------|
| 200 | Synchronous / already applied | Returns immediately |
| 202 | Asynchronous, Ticks provided | Waits for matching CommandResponse |
| other | Error | Returns error |

Ticks Correlation

On 202, the body contains RestCommandResponse with a Ticks field (.NET DateTime.Ticks). If Ticks == 0, the command was a no-op.

Each in-flight command creates a buffered channel (capacity 1), registered in both pendingTicks and pendingByID, cleaned up via defer.

The Sync/Async Mismatch

Some endpoints return HTTP 200 but still fire a CommandResponse via SignalR. The observed case is circuit settings (POST /api/sites/{siteID}/circuits/{circuitID}/settings) which returns 200 but generates CommandResponse with ID=22 (CIRCUIT_MAX_CURRENT_P1).

Handled via the expected-orphan counter:

go
expectedOrphans map[easee.ObservationID]int  // protected by cmdMu

Before a POST to a known 200-returning endpoint, increment the counter. When CommandResponse arrives with no pending match, decrement and silently consume. Counter at 0 means genuinely rogue.

State Management

Internal State Fields (all protected by sync.RWMutex)

| Field | Observation | Notes |
|-------|------------|-------|
| opMode | CHARGER_OP_MODE (109) | Central state machine |
| chargerEnabled | IS_ENABLED (31) | Hardware enable state |
| smartCharging | SMART_CHARGING (102) | LED color mode |
| currentPower | TOTAL_POWER (120) | Watts (API sends kW, multiplied by 1000) |
| sessionEnergy | SESSION_ENERGY (121) | kWh, special zero-handling |
| totalEnergy | LIFETIME_ENERGY (124) | kWh, updated ~hourly |
| currentL1/L2/L3 | IN_CURRENT_T3/T4/T5 (183/184/185) | Phase currents in A |
| phaseMode | PHASE_MODE (38) | 1=single, 2=auto, 3=locked 3-phase |
| dynamicCircuitCurrent[3] | DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3 (111/112/113) | Per-phase circuit limit |
| maxChargerCurrent | MAX_CHARGER_CURRENT (47) | Hardware max (non-volatile) |
| dynamicChargerCurrent | DYNAMIC_CHARGER_CURRENT (48) | Volatile current limit |
| reasonForNoCurrent | REASON_FOR_NO_CURRENT (96) | Debug enum |
| pilotMode | PILOT_MODE (100) | CP signal state A-F |
| rfid | USER_IDTOKEN (128) | Last scanned RFID token |

Session Energy Zero-value Protection

sessionEnergy is never set to 0 from a ProductUpdate — the API sends spurious zeros erratically. Session reset is driven by op-mode transition: when CHARGER_OP_MODE transitions from disconnected to awaiting-start, sessionEnergy resets to 0 with a fresh timestamp.

Charger Operation Modes

text
0 = Offline               — no cloud connection
1 = Disconnected — no car plugged in
2 = AwaitingStart — car plugged, waiting for authorization/start
3 = Charging — actively charging
4 = Completed — car full or finished, cable still plugged
5 = Error — fault condition
6 = ReadyToCharge — ready, current available
7 = AwaitingAuthentication — RFID auth required
8 = Deauthenticating — finishing authentication teardown

Mapping to evcc Status

| opMode | evcc Status |
|--------|------------|
| 1 (Disconnected) | A |
| 2, 4, 6, 7, 8 | B |
| 3 (Charging) | C |
| 0, 5 and others | error |

Enable/Disable Flow

Enable = true

1. If chargerEnabled == false: POST settings { enabled: true } and wait.
2. If opMode == Disconnected: return (no cable).
3. If opMode == AwaitingAuthentication && authorize: action = start_charging.
4. Otherwise: action = resume_charging.
5. POST /commands/{action} and wait.
6. Wait for opMode to reach enabled state.
7. Wait for dynamicChargerCurrent to reach 32 (Easee sets this on resume).
8. Call MaxCurrent(c.current) to restore previous setpoint.

Enable = false

1. If disconnected or (awaiting auth && !authorize): return.
2. POST /commands/pause_charging and wait.
3. Wait for opMode to reach disabled state.
4. Wait for dynamicChargerCurrent to reach 0.

State Waiting Pattern

Both waitForChargerEnabledState and waitForDynamicChargerCurrent use:
1. Short-circuit check: if already in target state, return immediately.
2. Open a timer.
3. Loop on obsC channel.
4. On timer expiry: one final check before returning api.ErrTimeout.

The final check handles the race where the state update arrived between the last channel read and the timer fire.

Phase Control

Circuit-Level (preferred, when circuit is known)

Phase switching by zeroing dynamic circuit current on unused phases:

text
POST /api/sites/{siteID}/circuits/{circuitID}/settings

For 1-phase: set P2=0, P3=0. For 3-phase: restore all three.

This POST returns HTTP 200 but still fires a CommandResponse with ID=22 (expected orphan).

Charger-Level (fallback)

Uses PhaseMode setting: 1 for single-phase, 2 (auto) for 3-phase.
After changing PhaseMode, Enable(false) is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart.

Authorization Mode (authorize)

When authorize: true, evcc sends start_charging to authorize sessions when the charger enters ModeAwaitingAuthentication. This enables fully unattended operation but is incompatible with RFID-based vehicle identification.

When authorize: false, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app).

Setting authorize: true also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp.

Concurrency Model

Mutexes

| Mutex | Type | Protects |
|-------|------|----------|
| c.mux | sync.RWMutex | All charger state fields |
| dispatcher.mu | sync.Mutex | pendingTicks, pendingByID, expectedOrphans maps (inside CommandDispatcher) |

Command dispatch was extracted into charger/easee/dispatcher.go (CommandDispatcher struct). The two mutexes are intentionally separate to prevent the SignalR receive loop from blocking on command dispatch operations.

Observation Channel

obsC chan Observation is unbuffered. ProductUpdate sends via non-blocking select — if no waiter is listening, the notification is dropped. The authoritative state is always in the struct fields; the channel is only a notification mechanism.

Design constraint: any waiter on obsC must include a final state check after timer expiry before returning api.ErrTimeout.

Known Design Concerns

1. SESSION_ENERGY zero-value protection — defensive measure based on field observations; root cause unverified.
2. LIFETIME_ENERGY — inaccurate by design, API pushes updates ~hourly.
3. current vs dynamicChargerCurrent drift — evcc's desired setpoint and charger's confirmed value can drift around pause/resume cycles. Resynced via MaxCurrent(c.current) after resume.
4. Multi-charger circuits — only circuit-level phase control when charger is alone on its circuit. Multi-charger circuits fall back to less precise charger-level control.
5. Stale CommandResponses after reconnect — if SignalR drops mid-command, the response may arrive after reconnect with no pending entry, triggering a false-positive rogue WARN. Acceptable trade-off.

API Endpoints Summary

| Method | Endpoint | Used For |
|--------|----------|----------|
| POST | /accounts/login | Initial authentication |
| POST | /accounts/refresh_token | Token refresh |
| GET | /chargers | Auto-discover charger ID |
| GET | /chargers/{id}/site | Discover site and circuit |
| POST | /chargers/{id}/settings | Enable/disable, DCC, PhaseMode, SmartCharging |
| POST | /chargers/{id}/commands/{action} | start/stop/pause/resume charging |
| GET | /sites/{siteId}/circuits/{circuitId}/settings | Read max circuit currents |
| POST | /sites/{siteId}/circuits/{circuitId}/settings | Set dynamic circuit currents (phase switching) |

SignalR Hub

| Endpoint | https://streams.easee.com/hubs/chargers |
|----------|------------------------------------------|
| Client -> Server | SubscribeWithCurrentState(chargerID, true) |
| Server -> Client | ProductUpdate, ChargerUpdate, SubscribeToMyProduct, CommandResponse |

Configuration

| Parameter | Required | Default | Notes |
|-----------|----------|---------|-------|
| user | yes | | Easee account email |
| password | yes | | Easee account password |
| charger | no | | Charger serial; auto-detected if exactly one on account |
| timeout | no | 20s | HTTP timeout for all API calls and command waits |
| authorize | no | false | If true, evcc sends start_charging to authorize sessions |

Supported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core.
Declared capabilities: 1p3p (phase switching), rfid (RFID identification).
Requires evcc sponsorship.

---

Agents/Hardware Integrations

Hardware Integrations: Chargers, Meters, Vehicles

Integration Pattern

All device types use a registry-based factory pattern:

go
// Self-registration in init()
func init() {
registry.AddCtx("typename", NewFromConfig)
}

func NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
// Parse config, create client, return implementation
}

Optional interfaces are added via the decorator pattern:

go
//go:generate decorate -f decorateXxx -b *Xxx -t "api.PhaseSwitcher,Phases1p3p,func(int) error"

Charger Implementations

By Protocol

| Protocol | Examples |
|----------|---------|
| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly |
| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes |
| OCPP 1.6 | Generic charge point server |
| EEBus/ISO 15118 | EEBus SPINE protocol |
| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick |
| MQTT | OpenWB, Tasmota, Shelly |
| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT |

Required Charger Interface

go
type Charger interface {
ChargeState
Enabled() (bool, error)
Enable(enable bool) error
CurrentController
}

Where ChargeState provides Status() (ChargeStatus, error) (A/B/C) and
CurrentController provides MaxCurrent(current int64) error.

Optional Charger Interfaces

- ChargerExMaxCurrentMillis(float64) for milliamp precision
- PhaseSwitcherPhases1p3p(int) to switch 1p/3p
- Meter / MeterEnergy — built-in power/energy measurement
- PhaseCurrents / PhaseVoltages — per-phase readings
- ChargeRaterChargedEnergy() for session energy
- ChargeTimerChargeDuration() for session time
- IdentifierIdentify() for RFID/vehicle identification

Key Implementations

- Easee — REST + async SignalR; see docs/agents/easee-architecture.md for full detail
- OCPP (charger/ocpp.go) — Full 1.6 with charge point management
- go-e — Dual API (v1 local HTTP, v2 cloud); phase switching on v2
- EEBus — Complex SPINE protocol with USE cases (CEM, EV, EVCC)
- Generic configurable (charger/charger.go) — plugin-driven via YAML template

Adding a New Charger

1. Create charger/xxx.go (or YAML template in templates/definition/charger/)
2. Implement NewXxxFromConfig() returning api.Charger
3. Required: Status(), Enabled(), Enable(), MaxCurrent()
4. Register: registry.AddCtx("xxx", factory) in init()
5. Optional: add decorator for PhaseSwitcher, Meter, etc.
6. Add template: templates/definition/charger/xxx.yaml for UI metadata

Meter Implementations

By Category

| Category | Examples |
|----------|---------|
| Modbus/SunSpec | SDM630, SMA, Fronius, Victron |
| HTTP/REST | Homewizard, Shelly Gen3, E3DC |
| Smart Home | HomeAssistant entities, Homematic |
| Battery/Storage | Tesla Powerwall, LG ESS, Zendure |

Required Meter Interface

go
type Meter interface {
CurrentPower() (float64, error) // watts
}

Optional: MeterEnergy, PhaseCurrents/Voltages/Powers, Battery, BatteryCapacity

Key Implementations


- mbmd (meter/mbmd.go) — RS485 device library with auto-detection
- SunSpec (plugin/sunspec.go) — Modbus model-based point queries
- Generic — plugin-driven (HTTP, Modbus, MQTT sources)

Vehicle Integrations

| Manufacturer | API Type |
|-------------|----------|
| Tesla | Fleet API + vehicle-command proxy |
| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) |
| Hyundai/Kia | BlueLink (regional variants) |
| BMW/Mini | ConnectedDrive v2 |
| Mercedes | Official API |
| Renault/Nissan | Renault API + Carwings |
| Ford | FordConnect (US/EU) |
| Porsche | Porsche Connect |
| PSA Group | Peugeot, Citroen, DS, Opel |
| Generic | OVMS, Tronity |

Required Vehicle Interface

go
type Vehicle interface {
Battery // Soc() (float64, error)
BatteryCapacity // Capacity() float64
IconDescriber // Icon() string
FeatureDescriber // Features() []Feature
PhaseDescriber // Phases() int
TitleDescriber // GetTitle() string
SetTitle(string)
Identifiers() []string
OnIdentified() ActionConfig
}

Optional: SocLimiter, ChargeState, VehicleRange, VehicleOdometer, VehicleClimater, VehicleFinishTimer, VehiclePosition, CurrentLimiter, CurrentController, ChargeController, Resurrector

Polling Strategy

Configurable: always / while charging / while connected. Interval-based caching
to avoid excessive cloud API calls. OAuth2 token handling built into each provider.

Auto-Detection (cmd/detect/)

Task-based parallel IP scanning:
ping -> tcp_http -> tcp_modbus -> sunspec -> device-specific probes

Detects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius,
Tasmota, Shelly, Phoenix, and many more.

---

Agents/Ocpp Forwarder

OCPP Forwarder Architecture

The 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.

The 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.

Forwarding modes

Two modes apply at the same time, selected per message by its action.

Transparent relay (billing-critical)

For the actions in actionsRelayedToUpstream (Authorize, StartTransaction, StopTransaction, DataTransfer), upstream is the authoritative Central System:

1. Charger sends the Call to evcc.
2. The message hook forwards it to the upstream sidecar and bypasses evcc's OCPP handler.
3. Upstream's CallResult/CallError is relayed back to the charger.

evcc'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.

Sidecar observation (informational)

For all other messages (BootNotification, StatusNotification, MeterValues, Heartbeat, etc.):

1. Charger sends the Call to evcc, which processes it normally.
2. The same frame is also mirrored to the upstream sidecar.

Upstream observes the session while evcc manages the charger as usual.

Upstream to charger (commands)

Calls (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.

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.

Read-only mode

When 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.

Connection lifecycle

Frames 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.

When 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).

The 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.

Rules 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.

---

Agents/Plugin System

Plugin System

The plugin system (plugin/) provides protocol-level abstraction for device
communication. Plugins implement typed getter/setter interfaces and are composed
into charger, meter, or vehicle implementations via configuration.

Plugin Types

| Plugin | Protocol | Key Config |
|--------|----------|------------|
| http | HTTP/REST | uri, method, headers, auth, cache, timeout |
| mqtt | MQTT | topic, retained, payload template, timeout |
| modbus | Modbus TCP/RTU | uri, register, scale, baudrate, rtu |
| sunspec | SunSpec/Modbus | Model-based point queries via device tree |
| js | JavaScript/WASM | Inline script evaluation |
| go | Go runtime | Dynamic Go code |
| gpio | Linux GPIO | Digital I/O for relays |

Getter/Setter Interfaces

go
type StringGetter func() (string, error)
type FloatGetter func() (float64, error)
type IntGetter func() (int64, error)
type BoolGetter func() (bool, error)
// + corresponding Setter types

Pipeline Transforms

Plugins support chained transforms: scale, offset, lookup, regex.

Template-Based Device Configuration

Devices can be defined entirely via YAML templates using plugins:

yaml

templates/definition/charger/example.yaml


status:
source: http
uri: http://{{ .host }}/status
enable:
source: http
uri: http://{{ .host }}/enable
method: POST
maxcurrent:
source: http
uri: http://{{ .host }}/current/{{ .maxcurrent }}

The generic configurable charger (charger/charger.go) wires these plugin
configs into the api.Charger interface at runtime.

Key Files

- plugin/config.go — plugin registry and config types
- plugin/http.go — HTTP plugin
- plugin/mqtt.go — MQTT plugin
- plugin/modbus.go — Modbus plugin
- plugin/sunspec.go — SunSpec plugin
- charger/charger.go — generic configurable charger using plugins

---

Agents/Web Ui Api

Web UI & REST API

Server Architecture

- Router: gorilla/mux, strict slash
- Middleware: GZIP, CORS (*), ETag caching, request logging, JSON headers, JWT auth
- Timeouts: Read 5s, Write 10s, Idle 120s
- Static assets: embedded in binary (fs.FS)
- Default port: 7070

REST API (base /api/)

Site-level


- POST /buffersoc/{value}, /prioritysoc/{value}, /residualpower/{value} etc.
- GET /tariff/{tariff} — tariff rates
- GET /sessions — charging history
- GET /state — complete system state (supports jq filtering)

Per-loadpoint (/loadpoints/{id}/...)


- POST mode/{value} — off/now/minpv/pv
- POST limitsoc/{value}, limitenergy/{value} — charge limits
- POST mincurrent/{value}, maxcurrent/{value} — current limits
- POST phases/{value} — phase config
- POST priority/{value}, batteryboost/{value}
- POST plan/energy/{value}/{time} — schedule plan
- POST vehicle/{name} — select vehicle
- POST smartcostlimit/{value} — smart cost threshold

Configuration (/config/..., auth required)


- CRUD for devices (chargers, meters, vehicles, tariffs)
- Template browsing and testing
- Site, loadpoint, circuit, HEMS, messaging config
- GET /config/evcc.yaml — YAML export

System (/system/..., auth required)


- Log viewing (/log, /log/areas), cache clear, shutdown

Database (/db/..., auth + second factor required)


- Backup download, restore from file, selective reset
- Second factor: admin password in request body, or API key via Bearer token (bypasses password check)

Handler Pattern


Generic handler[T] with type conversion, setter, getter.
Specialized: floatHandler, intHandler, boolHandler, durationHandler.

WebSocket (/ws)

- coder/websocket (RFC 6455)
- Pub/sub via SocketHub
- Buffered channels (1024 per subscriber)
- Welcome message with full state snapshot
- Incremental updates as JSON key-value pairs with dot-notation keys:

json
{"loadpoints.1.mode": "solar", "site.gridPower": 1234}

- Write timeout: 10s, compression (disabled for Safari)

State Flow

1. WS connects -> receives welcome with full state
2. App emits util.Param on changes
3. Hub broadcasts to subscribers
4. Frontend store.update(msg) merges via dot-notation
5. Components reactively re-render

Authentication

- JWT, 90-day lifetime
- HttpOnly cookie (auth) with SameSite=Strict
- Also accepts Authorization: Bearer <token> header
- Modes: Disabled, Locked (demo), Configured (password)
- Protects /api/config, /api/system, and /api/db

MQTT Integration

- Publishes state changes to configurable broker
- Subscribes to control topics
- Retained messages for state persistence

Key Files

- server/http.go — router setup
- server/http_auth.go — authentication
- server/http_site_handler.go — state + request handlers
- server/http_config_*.go — config endpoints
- server/http_loadpoint_handler.go — per-loadpoint endpoints
- server/socket.go — WebSocket pub/sub
- assets/js/app.ts — Vue app entry
- assets/js/store.ts — reactive state store
- assets/js/api.ts — Axios clients
- assets/js/router.ts — route definitions

---

CONTRIBUTING

Contributing

Developing

Development environment

Developing evcc requires [Go][1] [Node][2] and [Vite+][3]. We recommend VSCode with the Go, Oxc and Vue extensions.

Alternatively, if you use VS Code and devcontainers, you can use the "Dev containers: Clone repository in container volume" action. This will create a devcontainer with the required toolchain and install the prerequisites as explained below. Wait until the startup log says "Done. Press any key to close the terminal." and check for any errors.

We use linters (golangci-lint, oxlint/oxfmt via Vite+) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. You can manually reformat your code by running:

sh
make lint
make lint-ui

Device templates

The software supports a massive amount of different devices (charger, meter, vehicle, tariff) that are defined by templates.
A template can use the plugin system (preferred) for communication with the device or reference a dedicated Go implementation.
All bundled templates are located in the /templates/definition directory.

If you want to add a new plugin we recommend looking at existing, similar implementations for reference.
When your template requires Go code you have to build the project from source (see instructions below).
Otherwise you can use the evcc binary and point it to your new template file for testing.

sh
evcc --template-type charger --template new-charger-template.yaml

Besides the actual device configuration, templates contain meta-data like product name, manufacturer, instructions how to configure the device to work with evcc.
On release, this data is extracted and pushed to the evcc-io/docs repository to keep the documentation in sync. You can verify the generated meta-data by running:

sh
make docs

This will write the documentation-relevant data to /templates/docs.

Building from source

Install prerequisites (once):

sh
make install-ui
make install

Build and run:

sh
make
./evcc

Open UI at http://127.0.0.1:7070

To run without creating the evcc binary use:

go run ./...

Cross Compiling

To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg:

sh
GOOS=linux GOARCH=arm GOARM=6 make

Publishing docker images

sh
make docker DOCKER_IMAGE=my/docker DOCKER_TAG=0815

Releases

Releases are cut by pushing a MAJOR.MINOR.PATCH tag. Any other tag is ignored.

Feature releases (0.313.0) must be tagged on master. The release workflow
rejects a feature tag that is not reachable from master.

Bugfix releases (0.313.1) may be tagged on any branch. That allows servicing
an older release line without shipping everything that has landed on master
since. Only the newest release moves the latest pointers, so a bugfix release
of an older line publishes its artifacts, but leaves the evcc/evcc:latest
docker tag, the homebrew formula, the GitHub latest release, the hassio addon
and the demo instance untouched.

To move a merged pull request onto a release branch, comment /backport on it.
The pull request has to carry the bug label and must not be marked (BC),
only non-breaking bugfixes are backported.
The commit is cherry-picked onto the branch of the next bugfix release, e.g.
release/0.313.1, and a pull request is opened against it. The branch is
created at the newest tag of that line if it does not exist yet. Pass a branch
name, /backport release/0.312.2, to service an older line.

Releasing a bugfix deletes its release branch, since the tag is the branch tip
and everything on it has shipped. A branch that received further backports after
the tag is kept.

Debugging in VS Code

evcc Core

To debug a local evcc build in VS Code, add the following entry to your launch.json.
You can adjust the referred configuration as needed to e.g. use your live configuration.

json
{
"name": "Launch evcc local build with demo config",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": ["-c", "${workspaceFolder}/cmd/demo.yaml"],
"cwd": "${workspaceFolder}",
},

UI

For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the live reloading development server. It pulls its data from port 7070 (see above).

sh
vp install
vp run dev

Start the backend with --disable-auth when checking the configuration UI of a
throw-away instance. Without it the UI asks for an administrator password first.

sh
./evcc --config tests/config-with-tariffs.evcc.yaml --disable-auth

Storybook

We're using storybook to develop and visualize UI components in different states. Running the command below will open your browser at http://127.0.0.1:6006/.

sh
vp run storybook

Integration testing

We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first.

sh
make ui build
vp run playwright

Simulating device state

Since we don't want to run tests against real devices or cloud services, we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072.

text
vp run simulator

Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to speed up testing.

text
make ui build
./evcc --config tests/simulator.evcc.yaml

Communication Language

evcc has a large German-speaking user base, but we want to be open and accessible to everyone in the global community. To balance these needs:

- Pull Requests
- 🇬🇧 English required
- Issues
- 🇬🇧 English recommended
- 🇩🇪 German acceptable to start, must switch to English after first English comment
- GitHub Discussions
- 🇬🇧 🇩🇪 Both English and German allowed

💬 _Non-German speakers: We strongly encourage you to ask participants to switch to English. For pull requests, we have a language check bot that does this automatically._

Thank you all for helping make evcc accessible! 🌍

AI-Generated Content

AI tools can be valuable aids for writing code, documentation, and creating issue reports.
We welcome their use as part of the development process.

When submitting AI-assisted contributions, keep these principles in mind:

- Understanding: Fully understand all changes and be prepared to answer questions about them.
- Human-written intent: Write issue descriptions, PR explanations, and commit messages in your own words. Keep them clear and concise, not lengthy generated text.
- Value: Ensure contributions justify the review effort required from maintainers.
- Prior consensus: Only open a PR if there is a related issue or discussion where the team has indicated a positive tendency toward the proposed change. Link to it in your PR description.

Contributors remain responsible for their work regardless of which tools were used to create it.

Contributions that appear to violate these principles will be closed with the following comment:

text
This contribution does not appear to meet our AI contribution guidelines.

Adding or modifying translations

evcc already includes many translations for the UI. We're using Weblate to maintain translations. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications to the evcc repository where they get reviewed and merged.

If you find a text that is not yet translatable in Weblate, you can help us by making it translatable. To do this, you can simply find the missing translation text in the code and apply similar changes as in these two Pull Requests:

- UI: Add missing translation for Error during startup
- Translation: kein Plan, keine Grenze

Note: To ensure the build succeeds after creating new translations, make sure to include your new translations in both the de.json and en.json files.

[](https://hosted.weblate.org/engage/evcc/)

[1]: https://go.dev
[2]: https://nodejs.org/
[3]: https://viteplus.dev/guide/#install-vp

Documentation, Website and iOS/Android App

We're always thankful for contributions.
Docs, website and app have dedicated repositories.
Please open a GitHub pull request in the respective repository.

- Documentation: evcc-io/docs
- Website: evcc-io/evcc.io
- iOS/Android App: evcc-io/app

License

By contributing to evcc, you agree that your contributions will be licensed under the existing license terms that apply to the respective parts of the project.
This constitutes an implicit Contributor License Agreement (CLA), following GitHub's standard practice where contributions are made under the same terms as the project license.

---

README

evcc 🚘☀️

[](https://github.com/evcc-io/evcc/actions/workflows/nightly.yml)
[](https://status.evcc.io/)
[](https://hosted.weblate.org/engage/evcc/)

[](https://cloudsmith.io/~evcc/packages/)
[](https://github.com/evcc-io/evcc/releases)
<br/>
[](https://depot.dev/?utm_source=evcc)

evcc is an extensible EV Charge Controller and home energy management system.

Our goal is to provide local energy management, without relying on cloud services.
Featured in PV Magazine and c’t Magazin.

Features

- simple and clean user interface
- support for many EV chargers:
- ABB, ABL, Alfen, Alphatec, Amperfied, Ampure, Audi, AUTEL, Autoaid, Bender, BMW, cFos, Charge Amps, Compleo, CUBOS, Cupra, Dadapower, DaheimLaden, Delta, E.ON Drive, E3/DC, Easee, Ebee, echarge, EcoHarmony, Edgetech, Elecq, eledio, Elli, EM2GO, EN+, enercab, Ensto, EntraTek, ESL, eSystems, Etrel, EVBox, Free2Move, Free2move eSolutions, Fronius, Garo, go-e, Hardy Barth, Heidelberg, Hesotec, Homecharge, Huawei, Innogy, INRO, Juice, Kathrein, KEBA, Kontron Solar, Kostal, KSE, LadeFoxx, LRT, Mennekes, NRGkick, OBO Bettermann, OpenEVSE, openWB, Optec, Orbis, PC Electric, Peblar, Phoenix Contact, Plugchoice, Porsche, Pracht, Pulsares, Pulsatrix, Qcells, Schneider, Schrack, SENEC, Siemens, Skoda, SMA, Smartfox, SolarEdge, Solax, Sonnen, Spelsberg, Stark in Strom, Sungrow, TechniSat, Tesla, Tigo, TinkerForge, Ubitricity, V2C Trydan, Vestel, Victron, Viridian EV, Volkswagen, Volt Time, Wallbe, wallbox, Walther Werke, Webasto, Weidmüller, Zaptec, ZJ Beny. Read more.
- EEBus support (Elli, PMCC)
- OCPP support
- build-your-own: Phoenix Contact (includes ESL Walli), EVSE DIN
- smart switches: AVM, FRITZ!, Home Assistant, Homematic IP, HomeWizard, myStrom, Shelly, Tasmota, TP-Link. Read more.
- heat pumps and electric heaters: alpha innotec, Bosch, Buderus, Bösch, CTA All-In-One, Daikin, Elco, IDM, Junkers, Kermi, Lambda, my-PV, Nibe, Novelan, Roth, Stiebel Eltron, Tecalor, Vaillant, Viessmann, Wolf, Zewotherm. Read more.
- support for many energy meters:
- solar inverters and battery systems: A-Tronix, Acrel, Ads-tec, Alpha ESS, Ampere, Anker, APsystems, AVM, Axitec, BGEtech, Bosch, Bosswerk, Carlo Gavazzi, Deye, E3/DC, Eastron, Enphase, FENECON, FRITZ!, FoxESS, Fronius, Ginlong, go-e, GoodWe, Growatt, Homematic IP, HomeWizard, Hoymiles, Huawei, IAMMETER, IGEN Tech, Kostal, LG, Loxone, M-TEC, Marstek, myStrom, OpenEMS, Powerfox, Qcells, RCT, SAJ, SAX, SENEC, Senergy, Shelly, Siemens, Sigenergy, SMA, Smartfox, SofarSolar, Solaranzeige, SolarEdge, SolarMax, Solarwatt, Solax, Solinteng, Sonnen, St-ems, Steca, Sungrow, Sunsynk, Sunway, Tasmota, Tesla, TP-Link, VARTA, Victron, Wattsonic, Youless, ZCS Azzurro, Zendure. Read more.
- general energy meters: A-Tronix, ABB, Acrel, Alpha ESS, Ampere, AVM, Axitec, Bernecker Engineering, BGEtech, Bosch, Carlo Gavazzi, cFos, Deye, DSMR, DZG, E3/DC, Eastron, Enphase, ESPHome, FENECON, FoxESS, FRITZ!, Fronius, Ginlong, go-e, GoodWe, Growatt, Homematic IP, HomeWizard, Huawei, IAMMETER, inepro, IOmeter, Janitza, KEBA, Kostal, LG, Loxone, M-TEC, mhendriks, my-PV, myStrom, OpenEMS, ORNO, P1Monitor, Powerfox, Qcells, RCT, Saia-Burgess Controls (SBC), SAJ, SAX, Schneider Electric, SENEC, Shelly, Siemens, Sigenergy, SMA, Smartfox, SofarSolar, Solaranzeige, SolarEdge, SolarMax, Solarwatt, Solax, Solinteng, Sonnen, St-ems, Sungrow, Sunsynk, Sunway, Tasmota, Tesla, Tibber, TQ, VARTA, Victron, Volkszähler, Wago, Wattsonic, Weidmüller, Youless, ZCS Azzurro, Zuidwijk. Read more.
- integrated systems: SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx)
- sunspec-compatible inverter or home battery devices
- mbmd-compatible devices, see volkszaehler/mbmd for a complete list
- vehicle integrations (state of charge, remote charge, battery and preconditioning status):
- Aiways, Audi, BMW, Citroën, Dacia, DS, Fiat, Ford, Genesis, Hyundai, Jeep, Kia, Mercedes-Benz, MG, Mini, Nissan, NIU, Opel, Peugeot, Polestar, Renault, Seat, Skoda, Smart, Subaru, Tesla, Toyota, Volkswagen, Volvo, Zero Motorcycles. Read more.
- services: OVMS, Tronity, evNotify, ioBroker.bmw, mg2mqtt, mz2mqtt, TeslaLogger, TeslaMate, Tessi, volvo2mqtt
- plugins for integrating with any charger, smartswitch, heatpump, electric heater, meter, solar- / battery-inverter or vehicle:
- Modbus, HTTP, MQTT, JavaScript, WebSocket, Go and shell scripts
- status notifications using Telegram, PushOver and many more
- logging using InfluxDB and Grafana
- REST and MQTT APIs for integration with home automation systems
- Add-ons for Home Assistant and openHAB (not maintained by the evcc core team)

Getting Started

You'll find everything you need in our documentation.

Contributing

Technical details on how to contribute, how to add translations and how to build evcc from source can be found here.

[](https://hosted.weblate.org/engage/evcc/)

Sponsorship

<img src="assets/github/evcc-gopher.png" align="right" width="150" />

evcc believes in open source software. We're committed to provide best in class EV charging experience.
Maintaining evcc consumes time and effort. With the vast amount of different devices to support, we depend on community and vendor support to keep evcc alive.

While evcc is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like ours that provide additional value to otherwise closed hardware. Where this is not the case, evcc requires "sponsor token" to finance ongoing development and support of evcc.

Learn more about our sponsorship model.

License

This project is licensed under the MIT License. See the LICENSE file for details.

For additional license information regarding fonts, icons, and other assets, please see the LICENSES folder.

Note: All sponsor-required components are excluded from the MIT License.
See file license header for details.
If you want to use them in your own project, one evcc sponsorship token is required per evcc instance.
Custom licensing agreements are available - please contact us to discuss your specific requirements.

---