Repository navigation
Tesla BLE driver + vehicle SoC + joint fuse-budget allocator + EV bubble - #211
Merged
Merged
Conversation
Read-only vehicle-side telemetry channel, distinct from DerEV (the
charger). Groundwork for showing the vehicle's own SoC + charge
limit in the EV bubble (target: "24 / 50 %" inline) and for letting
the loadpoint manager prefer vehicle-reported SoC over the
delivered-Wh inference.
- telemetry.Store: new DerVehicle type value, String() and
ParseDerType("vehicle"), plus inclusion in WatchdogScan's iterate
list so staleness bubbles up like every other DER. No Kalman — a
vehicle SoC reading is a point in time, not a power signal.
- drivers/lua.go emit switch: "vehicle" routes through the existing
emitTelemetry path (ParseDerType already maps the string); the
host-API comment documents the allowed payload fields (soc
required; charge_limit_pct, charging_state, time_to_full_min,
stale optional).
- drivers/tesla_vehicle.lua: new Lua driver against the Tesla Owner
API shape. Transparent to transport — the typical deployment
points base_url at a TeslaBLEProxy on the LAN, which speaks
HTTP/JSON upstream and BLE to the car internally. Poll
/api/1/vehicles/{id}/vehicle_data, extract charge_state.{battery_
level, charge_limit_soc, charging_state, time_to_full_charge},
emit. Falls back to last-known when the car is asleep (typical
Tesla 408 response) until stale_after_s.
Consumption side — loadpoint manager reading vehicle SoC, API state
exposure, EV-bubble UI label — is deliberately not in this commit.
Driver is inert without an entry in drivers: in config.yaml.
Future work captured on the parked task #14:
- Auto-discover which vehicle is plugged in when Easee flips
connected false → true (iterate /vehicles, match by is_charging
or nearest-SoC).
- Persist chosen vehicle_id so subsequent sessions stick.
- Support multi-vehicle configs (array instead of single id).
- Use endpoints=charge_state&wakeup=true on the vehicle_data URL
to reduce latency and auto-wake.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end driver-only slice: a Tesla vehicle reports its real BMS
SoC + charge limit + charging state into forty-two-watts via a
TeslaBLEProxy on the LAN. Read-only — this PR introduces no
loadpoint/MPC/planning integration; that comes later when we use the
actual SoC to compare against planned charging.
Driver
- drivers/tesla_vehicle.lua against the Tesla Owner API shape served
by TeslaBLEProxy. Two YAML fields: ip + vin. Polls
/api/1/vehicles/{vin}/vehicle_data?endpoints=charge_state every
60 s, falls back to last-known when the car is asleep, marks
stale after 15 min. Surfaces charge_amps + charger_actual_current
so the driver card can show "Tesla throttled to 5 of 16 A".
Driver host
- DerVehicle telemetry type added alongside DerMeter/PV/Battery/EV.
Carries soc/charge_limit_pct/charging_state/time_to_full_min/stale
in Data; RawW is always 0 (vehicle readings don't contribute to
dispatch math, only to UI + future planning compare).
- host.emit("vehicle", {...}) routes through the existing emit path.
- HTTPAllowedHosts added to HostEnv: drivers granted the http
capability can be locked to a specific allowlist via
capabilities.http.allowed_hosts. Empty list keeps the legacy
any-host behaviour. Implemented as URL-host check, port-agnostic.
- Registry wires the allowlist from config when present.
API
- POST /api/drivers/verify_tesla — operator-facing "Verify connection"
probe. Hardened against SSRF: ip is constrained to RFC1918 v4 only,
port restricted to 80/443/8080, VIN regex-validated, redirects
refused, response body never reflected, body cap 1 MB.
- GET /api/status driver entries surface vehicle_soc /
vehicle_charge_limit_pct / vehicle_charging_state /
vehicle_time_to_full_min / vehicle_charge_amps /
vehicle_charger_actual_current / vehicle_stale.
UI
- Settings → Devices renders a "Vehicle" fieldset for drivers whose
catalog entry advertises capability "vehicle" (or whose config
shape declares ip/vin). Two inputs (Proxy IP + VIN) plus a Verify
Connection button that calls the new endpoint and shows the result
inline. Catalog-add seeds {ip:"", vin:""} for vehicle drivers.
IP changes auto-sync into capabilities.http.allowed_hosts so a
fresh tesla driver doesn't bounce off the http allowlist.
- Per-driver dashboard card grows a vehicle branch: SoC "X / Y %",
charging state with semantic color, Amps (actual/limit) row that
warns when the car throttles below the offered current, time-to-
full, stale indicator, and a SoC progress bar.
What's intentionally NOT in this PR
- Loadpoint VehicleDriver binding / auto-discovery on plug-in
- MPC/planner integration (compare actual SoC to planned)
- Hero EV bubble showing vehicle SoC
- Plan-chart EV slot rendering
- Auto-charge schedule + AutoChargeTargetTimeLocal
- Driver alias UI
Those live in PR #184 and will land in follow-up PRs once we want
to plan against the actual reading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The steady-state poll deliberately omits wakeup=true so the proxy serves cached data — an early iteration that did wakeup=true on every poll caused HTTP 503 storms when polling overlapped with operator pokes. But the cached-only mode lets SoC drift forever while the car sleeps. Compromise: every WAKEUP_INTERVAL_MS (30 min) a single poll attaches wakeup=true, forcing one BLE wake. Logged at info so operators can see the cadence in the journal. Other 29 polls/hour stay cached, preserving the 503-avoidance guarantee. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fuse + DerVehicle SoC Three coupled fixes for the operator-reported oscillation when EV charging and the plan wants to charge battery simultaneously: 1) Plan-time: feed the site fuse into every MPC slot's Limits.MaxImportW so the DP joint-plans battery + EV under the fuse from the start. Slot.Limits.allowsImport was already wired in the optimizer (mpc.go:450); it was just never fed. 2) Dispatch-time: joint fuse-budget allocator runs after totalCorrection is computed and before slew/fuseGuard. When EV draw + planned battery charge would bust the fuse, both are scaled proportionally so they share the budget instead of competing. The battery side is mutated locally; the EV side is published as state.FuseEVMaxW + FuseSaturated for the loadpoint controller to read on its next Tick. Battery discharge never trips the allocator (helps the fuse rather than competing). 3) Loadpoint controller: SetFuseEVMax wires a closure into the controller; computeCommand consults it before SnapChargeW so the EV command lands under the cap. Also: BatteryCoversEV=false safety net on the energy-allocation path. The toggle was honored on the legacy reactive path (gridW -= EVChargingW) but the energy path executed plan's BatteryEnergyWh directive blindly, letting the home battery silently feed the EV. Cap commanded discharge to the level the reactive path would target (zero "house" side), leaving charge untouched. Plus a fuse-saturation rising-edge replan trigger from main.go (60s cooldown) so the MPC redraws with current overage knowledge instead of fighting itself. Plus: prefer live DerVehicle SoC over the loadpoint manager's inferred plug-in-anchor estimate when a vehicle driver is online. Multi-vehicle ranking by Tesla charging_state semantics (Charging/Starting > NoPower > Stopped/Complete > Disconnected/ unknown), tiebreaks by freshness — so a household with 2 Teslas sharing one wallbox picks the car physically connected right now. Tests: 5 new regression tests in control_test.go covering joint allocator (scaling, discharge no-op, no-EV no-op) and BatteryCoversEV energy-path (off enforces cap, on lets plan run). All green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on DerVehicle telemetry: dashboard's EV planet now
renders the connected vehicle's actual BMS SoC plus its
vehicle-configured charge limit, with honest provenance markers.
Backend (api):
- loadpoint.State gains 6 omitempty fields (VehicleSoCPct,
VehicleChargeLimitPct, VehicleChargingState, VehicleDriver,
VehicleStale, SoCSource) — the manager's internal data path is
unchanged; these are filled at API serialisation time.
- handleLoadpoints overlays the best-matching DerVehicle reading on
every plugged-in loadpoint, picking by charging_state ranking
(Charging/Starting > NoPower > Stopped/Complete > Disconnected/
unknown) with freshness as tiebreaker. Multi-vehicle households
with one wallbox get the right car automatically.
- When the overlay fires, CurrentSoCPct is replaced with the
measured value and SoCSource flips from "inferred" to "vehicle"
so clients can render provenance.
Frontend:
- next-app.js fetches /api/loadpoints in parallel with /api/status,
caches by driver_name, and feeds vehicle SoC / chargeLimit /
socStale / socSource into the EV planet builder.
- ftw-energy-flow.js renderCircleNode renders the new fields:
"SoC 24 / 50 %" both current + charge limit known
"SoC 24 %" only current known
"SoC ~24 %" soc_source="inferred" (session-Wh estimate)
"SoC 24 / 50 % ★" vehicle reading is stale
No behavioural change for sites without a DerVehicle driver — the
planet falls through to its previous kW-only rendering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use last_wakeup_ms == 0 as the "never woken since process start" sentinel so the FIRST poll always attaches wakeup=true. The car may have slept for hours while forty-two-watts was down; the proxy's cached charge_state is stale from the previous run. We want ground- truth SoC on the dashboard immediately, not after a 30-min wait. After the initial wake, the 30-min cadence takes over as before. Logged as "startup" vs "30-min cadence" so operators can tell which fired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… bubble Three small but related improvements after deploying #211 to a host with two Teslas + one wallbox: 1. Startup replan: the scheduled MPC tick is up to 15 min away. After process start (or container restart), fire one immediate replan 2 s after Service.Start so /api/mpc/plan is populated and the dashboard stops showing "plan stale" right out of the gate. 2. First-vehicle-SoC replan: the startup replan ran with whatever fallback SoC was available (loadpoint manager's pluginSoC + deliveredWh estimate, or naive default). As soon as ANY DerVehicle driver comes online and reports SoC, fire one more replan with the measured truth — especially important when a target deadline is set and the inferred SoC was off by 10+ pp. 3. EV bubble label cleanup: the "%" already says "SoC" — drop the label text. Aggregated batteries keep an "Avg " prefix to flag the cross-inverter mean. EV bubble's "24 / 50 %" tightens to "24/50%" — the slash reads as "current of limit"; the spaces just diluted the glyph. Verified live: both Tesla drivers logged "(startup)" wakeup, "mpc: startup replan completed" landed within 3 s of container start. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
operator report: "MPC plans to charge battery at 5.5 kW for hours
during PV — would way overshoot my 20 kWh battery." Investigation
showed the planner was running with capacity_wh=170000:
pixii (Pixii): 20 000 Wh ← actual home battery
tesla-vehicle (Tesla): 75 000 Wh ← Model 3 BMS (DerVehicle)
tesla-vehicle-2 (Tesla): 75 000 Wh ← Model Y BMS (DerVehicle)
──────────────────────────────────
sum: 170 000 Wh ← what the DP saw
`buildBatteryCapacities` filters EV-charger drivers via
`isLikelyEVDriver`, but its allowlist only matched
{easee, ocpp, ctek, chargestorm}. `tesla_vehicle.lua` slipped through
and its `battery_capacity_wh` was summed into the stationary fleet,
causing the DP to schedule charging into 170 kWh of phantom storage
at 11 kW (default 0.5C of 85 kW per Tesla, fuse-clamped down).
Add `tesla_vehicle` and a generic `vehicle` prefix to the allowlist.
Drivers that emit DerVehicle (any vehicle, not just Tesla) should
never be counted as stationary battery capacity — the EV is already
modelled separately via the loadpoint.LoadpointSpec dimension of the
joint DP.
Live verification on fortytwo (one Pixii + two Teslas):
Before After
capacity_wh: 170 000 capacity_wh: 20 000 (actual Pixii)
max_charge: 11 040 max_charge: 8 000 (Pixii config)
max_discharge: 11 040 max_discharge: 8 000
night discharge depth: ↓ 60.7% → ↓ 10.2%
slots charging > 5 kW: 24 → 4
Operator's expected "drain at night, refill from PV" plan now emerges
naturally — no terminal-price tuning needed; the previous bug was
masking the planner's correctness.
This is the surgical patch. Follow-up tracked separately:
- filter by emitted DerType instead of Lua filename prefix
- rename battery_capacity_wh on vehicle drivers to vehicle_capacity_wh
- gate LoadpointSpec on actual schedule presence (issue: planner
speculates EV charging across full 48h horizon today)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today the MPC extends its DP with an EV-SoC dimension whenever a loadpoint reports plugged_in — even when no target SoC or deadline has been set. The DP then plans EV charging speculatively across the full 48 h horizon, drawing battery + grid budget against a target the operator never asked for. Gate the LoadpointSpec on schedule presence: skip the spec unless the operator has set BOTH target_soc_pct > 0 AND a future target_time. With no schedule active, the EV is left to the loadpoint controller's reactive surplus-only behaviour (its existing behaviour pre-MPC). Also tighten MaxPct: use the lower of TargetSoCPct and the vehicle's own charge_limit_pct (decoded from DerVehicle data) as the DP's SoC ceiling. The car won't accept current beyond charge_limit anyway, so planning past it is wasted DP grid space. Falls back gracefully when the limit is unknown or current SoC already exceeds the ceiling. Round-trip live verification on fortytwo: no schedule → 0 EV charge slots in plan ✓ target 50% by ... → 3 EV charge slots in plan ✓ clear target → 0 EV charge slots in plan ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
erikarenhill
added a commit
that referenced
this pull request
Apr 29, 2026
fix(tesla-ble): apply must/should items from multi-agent review of #211
3 tasks done
erikarenhill
added a commit
that referenced
this pull request
Apr 29, 2026
fix(tesla-ble,mpc,api): post-#211 review fixes — fuse cap republish, vehicle SoC sanity, tesla-verify hardening
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
End-to-end Tesla BLE Proxy integration plus a fuse-aware coordination layer between battery dispatch and EV charging:
Test plan
🤖 Generated with Claude Code