TeslaMate core roadmap: modular data sources, then Rust #5558
Replies: 11 comments 24 replies
|
@jlestel, @Bre77 — you operate the largest proxy-based setup we know of, so two of the open questions above are ones you have real-world data on:
To be clear on scope: the proxy path keeps working as described above, and nothing here asks you to change anything. We're after your operational experience for the event contract design. |
|
Just my two cents guys, chose Go instead of Rust for this solution. As much as prefer rust, Go is better for this case IMHO. Again, I am not telling what, just adding some advice. |
|
I think we need to rethink our testing strategy. Currently testing occurs by pushing inputs into a Vehicle and looking at the outputs. And maybe the key problem here is that Vehicle is too complicated. The tests are fragile, changes to Vehicle is likely to break the tests, including tests that are not related to the change. And if the tests break it is difficult to find out what went wrong. Even AI models can struggle and go around in circles (seen this happen with my refactoring PR). I consider these tests integration tests just because of the large scope of code being tested. We probably do need to have some integration tests. But we shouldn't rely entirely on them. We should ideally have more unit tests then integration tests. Possible things to think about:
Not sure how practical this will be. I often start new projects with good intentions and then get stuck along the way, e.g. thin API wrappers are not good candidates for unit testing IMHO. If we do encounter problems, we should probably discuss them. |
|
I have had several attempts at writing Tesla owners API clients in rust. old now, but still might have some good ideas: https://github.com/brianmay/fake_luxary_api/blob/main/fla_client/src/lib.rs still current: https://github.com/brianmay/robotica-rust/blob/main/robotica-tokio/src/services/tesla/api.rs I haven't looked at these in a while. If I did I imagine I might want to have words with my past self as to why I did various things :-) The new version has support for logging metrics using opentelemetry (see init code https://github.com/brianmay/robotica-rust/blob/main/robotica-backend/src/logging.rs). Good metrics/remote logging is probably a good idea. Although I do find keeping it up-to-date and working can be painful (all relevant crates have to be updated at the same time; and recently AI incorrectly told me the TLS configuration wasn't required which broke all logging). And I never remember to look at the logged data. |
|
Name? Teslamate V5? Perhaps the first and one of the most important decisions we will need to make... Need to be something distinct so we don't get confused when referencing old and new code. |
|
Hi @JakobLichterfeld, @brianmay — greetings from Augsburg, and from near Stuttgart as of tomorrow. One note upfront: this is written with help from Claude 5, since my English is nowhere near as fluent as my Swabian. I run a self-hosted TeslaMate whose data comes from Fleet Telemetry streaming: my own receiver, no proxy in between, in production since mid-June. Since you asked for actual field delivery, here is one day of measurements: 24 drives, one Supercharger session, ~15.5k records. 57 of 59 configured fields are delivered. Never seen: Configured intervals bound the rate, they are not a delivery promise:
Fields that rarely change simply arrive rarely. Conversely almost anything can burst down to ~1 s. A source can promise "not faster than N", not "a sample every N". Fidelity: the gap to old streaming is categorical, not gradual. Charging is absent from the old format entirely — Backfill: reconciliation has to be idempotent and keyed on something stable, not on "what happened since last time". After an offline gap the car replays buffered data, and a naive scan creates duplicate sessions. Marking reconstructed ranges explicitly was the only thing that made repeated scans safe. Policy placement: treating the feed as active based on freshness rather than on the connection being up removed a whole class of bugs — a connected stream delivering nothing is indistinguishable from a live one at socket level. Happy to post the full 57-field table with observed intervals if that helps the contract. |
|
Thanks for putting this together so clearly — the combination of modular sources + explicit event contract, sequenced properly before the Rust port, feels like the right long-term direction. The emphasis on characterization first and keeping the architecture change decoupled from the language change is especially appreciated; that kind of discipline is rare and valuable on a project that holds people’s historical driving/charging data. I’d like to offer concrete help on the database side and the official Grafana dashboards once the shape of the schema changes becomes clearer. Happy to review migration plans, help keep the shipped dashboards in sync, or assist with any performance-related adjustments that surface. A few design questions that would help me (and probably others who maintain custom panels or care about long-term reporting) understand the intended scope:
No pressure on answers; just trying to understand where the boundary sits so any help I offer stays aligned with the actual goals. Happy to stay quiet on the implementation side and only chime in on design / dashboard / migration topics as they surface. |
|
Hi @JakobLichterfeld, coming to this late. Most of the delivery side has since been measured far better by @kdjkdjkdj, so let me stick to the one seat nobody else here occupies: I run the only setup that speaks the owner-API shape end to end. I terminate Fleet Telemetry (Pub/Sub) and re-emit it as the two surfaces TeslaMate consumes, which is exactly the "detour" your Q1 was about. Here's where that format caps, concretely. Q1, the cap is 13 fields wide, and the rest isn't streamed at all. TeslaMate's streaming channel is a fixed 13-slot CSV vector: [ts, speed, odometer, soc, elevation, est_heading, est_lat, est_lng, power, shift_state, range, est_range, heading] That's the whole vocabulary the stream can carry. Everything else Fleet Telemetry gives me, the entire charging group, plus doors/windows, sentry, lock, TPMS, climate, software-update progress, has no slot, and reaches TeslaMate only by round-tripping through polled vehicle_data, which I assemble by folding the fields back into the owner-API JSON. So the poll loop you want to delete isn't optional in my topology: it's the only path for ~90% of the fields, and it's drive-shaped, I only forward a frame carrying lat/lng + gear, so charge-only and parked frames never hit the stream at all. That's the concrete form of "categorical, not gradual": on the owner-API path a streaming-only source can't express a charge, full stop. One field-level note (same as @kdjkdjkdj): no Power field, I take charging power from AC/DCChargingPower and reconstruct drive power from EnergyRemaining deltas. A derivation with my own conventions, which is your argument for it living in the source, normalized, before the core sees it. Initial-state / resync. My resync is the poll: I serve a server-side vehicle_data cache, precisely because the stream can't cold-start a consumer. Subscribing alone never produces correct state, so "how does this source bring a consumer to current state" is worth declaring as an explicit capability rather than assuming it. Mine happens to be a pollable cache atm. Q2, guarantees. I've seen a few out-of-order arrivals and normalize them back before they reach TeslaMate with a per-VIN FIFO plus a monotonic createdAt guard that drops any frame older than the last forwarded for that VIN. So it's mine to remove, exactly your "sources normalize before handing over", it shouldn't land in the contract on my account. One production number reinforcing a source obligation already raised: TeslaMate's close_drive/1 deletes any drive with <2 odometer samples or <10 m distance. At coarse odometer downsampling I measured 100% of sub-60 s drives and ~60% of the 60–120 s band silently deleted, until I moved Location/Odometer to a 25 s interval and dropped the odometer minimum_delta to 0.1 mi. So "a drive is at least two positions far enough apart" is a hard floor a source must guarantee, and Fleet Telemetry defaults don't meet it out of the box. Happy to share my full Fleet-Telemetry-field => vehicle_data mapping if it helps the fidelity table. |
|
The per-field clock table I promised on 3 Aug, from a day with real driving. It answers the question, but the answer is a negative result — and the two things I found while producing it matter more than the table itself. Basis: 10,629 The table: there is no per-field structure
Every one of the 57 fields has a median of 0 s. The only spread sits on The Two things reproduce from earlier in the thread, now on a driving day: The comparison also separates three regimes cleanly, which is worth noting as a method: normal operation sits at 0–1 s; a replay after a receiver outage shows a monotone positive ramp up to the outage length (the staged 3-minute outage from 4 Aug is visible in this data as single values stepping to +187 s); and cold session starts show something else entirely. Finding 1 — the session-start snapshot's event time postdates its own arrivalFive records out of 10,629 carry a
Across all 15 session starts in the window the split is clean: ten starts after gaps of 0.14–2.68 h sit at 0 to +9 s, five starts after gaps of 7.97–42.48 h are negative, and within those the magnitude grows with sleep length. The effect is gone immediately — the next record, 1–3 s later, is back to +1…+5 s. Our side is excluded by measurement rather than by argument. That leaves two candidates on the vehicle side which this data cannot separate: either the vehicle's clock was off at that moment, or For what it's worth the shape of the data does suggest a mechanism, offered as a guess rather than a finding: the vehicle's clock free-runs while the car is in deep sleep and resynchronises within seconds of waking, so the snapshot — emitted before that resync — carries the free-running value. That would fit three things at once: the error is gone on the very next record 1–3 s later, its magnitude grows with the length of the sleep, and the threshold sits between 2.68 h and 7.97 h, which is roughly where this car stops being merely idle and goes properly to sleep. It is also direction-agnostic, which is rather the point — it predicts drift whichever way the oscillator errs, and this measurement only ever gets to see the half that runs ahead. Finding 2 — the snapshot's content is the pre-sleep stateThis one is sharper, and I think it bears directly on the contract. Outside temperature as a probe, because it cannot possibly be unchanged across a night:
Five out of five: exactly the pre-sleep value, and 2.0 to 5.5 K away from reality. Inside temperature behaves the same (four exact, one within 0.1 K), as do
The car was plugged in and drawing current while the snapshot said it wasn't. What this means for the contractTaken together, the useful statement doesn't require knowing which clock is off. On a cold start, I'd flag that as something worth looking at critically before ordering or backfill gets keyed on it, rather than draw the conclusion myself — the measurement establishes the discrepancy, not what should follow from it. The two findings also compound in an unhelpful direction. The stamp isn't merely old, which would at least make the staleness self-evident; it postdates arrival, so a consumer trusting This is also the case where "absent is not null" doesn't help. Nothing is absent. All 57 fields are present, complete and plausible — and wrong. Concretely for the backfill framing from your 4 Aug reply: in the 42-hour case, a backfill keyed on If it's useful for the contract, the shape that would fix this is a source declaring whether its initial-state event is a fresh read or a cached last-known-value, rather than consumers having to infer it from a temperature that didn't change overnight. All of the above is measured at the receiver's own 🤖 Measured and drafted with Claude Code (Opus 5) |
|
Follow-up on the two open points from your 7 Aug comment — one with a result that changes what we told you on 4 Aug, one where we have to report a dead end. A. Staged outages, now without a clean closeYour caveat was right, and it turned out to be the whole story rather than a footnote: Setup. Vehicle kept awake with Sentry Mode (parked, at home) so it streams continuously — measured idle cadence 21 records/min over the five minutes before each run. The unclean outage is staged by silently dropping the vehicle's packets at the bridge on the hypervisor, both directions: No FIN, no RST reaches the vehicle — it just stops hearing back. One methodological note for anyone reproducing this: disabling the WAN port-forward is not enough. The established flow keeps being translated from the conntrack table and the vehicle streamed on undisturbed; the drop has to happen on the packet path. Results — four staged outages, the 4 Aug run included for comparison:
The 89 s boundary reproduced to the second across both 180 s runs. The 60 s run ended before it, which is why nothing at all came back. The 20 min clean run came back complete — earliest replayed event at T0 + 0 s, latest at T1 − 0 s, largest gap inside the replay 10 s (i.e. the normal cadence), delivered 7 s after the receiver returned. What this means. The vehicle hands data to a socket it believes is alive and considers it delivered. Nothing is buffered during that time. Only when its TCP gives up — consistently ~89 s here — does application-level buffering start, and from that point the replay is complete and in order. A clean close skips that phase entirely, which is why the 4 Aug run looked perfect. So the limit is not depth. The buffer survived 20 minutes without losing a single record; its upper bound is still unknown. What costs data is the start: a silent disconnect loses the first ~90 seconds, irrecoverably, no matter how long the outage lasts. This corrects our own statement from 4 Aug ("Fleet Telemetry buffers and replays without loss"). That holds for a clean close only. For the contract the consequence is sharper than the original point: a source's recovery capability is not one property but two — does it replay, and from when. A source that replays "from when it noticed" silently drops the beginning of every gap, and the consumer sees nothing unusual: correct For completeness, receiver-side detection lagged even further: Limits of this. One vehicle, one network path, one receiver. The ~89 s is presumably the vehicle's TCP timeout and will depend on the environment, so treat it as "about a minute and a half", not as a constant. The 60 s run only establishes that the threshold is above 60 s. Expected record counts are extrapolated from the pre-outage cadence, so the time boundaries are the solid part, not the ratios. Anything else worth staging? The setup now reproduces on demand: the vehicle can be held awake with Sentry, the connection can be cut either cleanly or silently, for any duration, and both clocks are recorded on every record. If there is a question in the contract that a single-car setup can actually settle, I would rather measure it than guess — tell me what would be useful and I will run it. B.
|
| connections in the window | 40 |
| median connection | 11 min |
| longest uninterrupted connection | 5.11 h |
| connected in total | 18.5 h of 137.2 h (13.5 %) |
A 24 h timer cannot elapse while the connection stands. The vehicle sleeps far too aggressively; even a full driving day (nine drives, 8.2 h awake) produced no connection longer than 5.11 h. So the 21,974 false are not evidence against your reading — the flag simply never had the opportunity to fire.
No field shows a send interval in the 23–25 h band either. The closest candidate is a 42.7 h gap after which 56 slow fields (Version, VehicleName, TPMS, Odometer, …) were sent again — but 87 s after the vehicle woke up, not at the 24 h mark. That is indistinguishable from the wake-up snapshot documented in the clock table from 8 Aug; both explanations fit and the data cannot separate them.
And we see no way to fix that from here. It would need a vehicle that stays connected for more than 24 h without sleeping — and, if the timer turns out to be per-connection, without a single reconnect in between. Neither is producible on a car in normal use. If this matters for the contract, the people who can answer it are the proxy operators seeing many vehicles and long-lived sessions (@jlestel, @Bre77), not a single-car setup.
If the flag is meant to carry weight in the contract, that in itself is worth stating: its semantics could not be established by a normal installation over a week of continuous capture.
🤖 Drafted with Claude Code (Opus 5) — sponsored by Claude for Open Source
Self-hosted Fleet Telemetry reportFollowing the three requests in the roadmap: 1. Source in use todayI run a self-hosted Fleet Telemetry receiver for one personal vehicle: TeslaMate uses 2. Fields delivered and intervalsAfter a receiver restart, the vehicle reconnected and Fleet Telemetry logged a 644-record connection. The bridge received MQTT messages and TeslaMate marked the car real online. I then ran a normal ~15-minute drive. TeslaMate created a complete drive record from that stream. The Fleet Telemetry fields observed on the MQTT metric path were:
No other 3. What breaks in the current detourThe compatibility bridge makes the existing setup work, but it can only emit TeslaMate historical streaming fields. That matches the roadmap description: Fleet Telemetry fields outside the old owner-API-shaped stream cannot surface in TeslaMate, and TeslaMate still uses its polling loop alongside the push stream. For this setup, the requested native Fleet Telemetry source and event contract would remove the translation layer and let TeslaMate reason directly about partial Fleet Telemetry events. I can provide another controlled charge sample if there is a preferred field list and interval-report format. |
Uh oh!
There was an error while loading. Please reload this page.
What this is
This documents the direction @brianmay and I are taking the TeslaMate core, and collects input on the open design questions. It is documentation and brainstorming — not a vote, and not a call for contributions.
Two things came up in a closed PR (#5557) and in the older go-forward thread (#3416), and both belong somewhere visible:
We're implementing both. What is genuinely open — and what this thread is for — clusters in three areas: the event contract, the fidelity gap between Fleet Telemetry and old streaming, and what happens to the web UI. The open questions are marked as such below; the rest is decided and documented here so it can be reasoned about.
Ownership — please read before replying
Review capacity is the bottleneck in this project. A half-agreed contract implemented twice consumes it faster than anything else. That is why this section exists, and why it is strict.
This work touches the heart of TeslaMate: the state machine that decides what a drive is, what a charge is, and what ends up in your database. It cannot be assembled from independent pull requests.
If you want to help concretely:
Data sources: modular sources, one core pipeline
To be precise about where we stand today: TeslaMate already works against the Fleet API and Fleet Telemetry — but only by way of a detour. It speaks exactly one wire protocol, the owner-API shape, and alternative backends are reached by pointing
TESLA_API_HOST,TOKENandTESLA_WSS_HOSTat a third party (MyTeslaMate, Teslemetry) or a self-hosted proxy that translates them back into that shape.That detour works, and it will keep working. But it has a price:
vehicle_dataeven when the source underneath is push-only.The target shape, which @brianmay sketched in #5557: a core process receives a stream of events, splits them into drives, charges and so on, and persists them. Which source runs is a matter of configuration:
To set expectations: sources are an internal structuring device, not a plugin API. Which sources exist, and what the contract between them and the core looks like, is a maintainer decision and will stay one. We're documenting the contract so the design can be reasoned about, not so it can be extended from outside.
Today acquisition and interpretation are entangled in
Vehicle, which is why "streaming only" and "additional API" are hard rather than merely new. The polling loop is not a property of the owner API — it's a property of our architecture.The deliverable of this step is the event contract: what an event is, which guarantees a source must provide, and what the core may assume. The refactor is just how we get there.
Open — input wanted
Language: Rust for the core
The core moves to Rust. This decision is made; this thread is not the place to relitigate the language choice, and replies arguing Elixir vs. Rust in the abstract will not move it. What is genuinely open is listed below — most importantly the web UI question.
The reasons, in the order that matters to us:
Elixir/OTP has served this project well, and its supervision model is a good fit for "many vehicles, many long-lived processes". This is not a complaint about Elixir. It's a decision about where the core is easiest to keep correct over the next years.
What it costs
The Phoenix LiveView web UI is the largest single item. Settings, geofences, import and sign-in have no drop-in Rust equivalent. The realistic options are: keep the UI as a separate Elixir service talking to the same database, rewrite it against an HTTP API exposed by the Rust core, or replace it with something else entirely. This is open, and it's the question we'd most like input on.
Beyond that: migration ownership once Ecto is no longer the writer, and CI build times.
What stays stable for you
For everyone running TeslaMate, these hold across all of it:
What is not a stable interface: the database schema
The PostgreSQL schema is internal, and it is worth being blunt about this. It has never been a documented interface, and this work will not make it one. A restructuring of this size is exactly when the freedom to reshape storage is needed, and we intend to use it: expect substantial schema changes, not cosmetic ones.
Concretely: if you read the tables directly — custom Grafana panels, scripts, third-party or commercial tools built on today's schema — expect your integration to break during this work. Not might: will. There is no compatibility promise, no deprecation window, no advance notice beyond the migrations themselves, and we will not slow down, stage, or route the redesign around anything that depends on the current table shapes. Issues asking us to restore a schema detail or to coordinate changes with external tools will be closed. Your data is safe — migrations carry it forward — but every assumption about how it is stored is up for renegotiation, and nobody outside this repository is part of that negotiation. The supported surfaces are the dashboards we ship and MQTT. Everything else is and always was at your own risk.
If something we haven't listed here would break your setup, say so now, not after the migration.
Sequencing
The safety net comes before the surgery, and the architecture change stays decoupled from the language change — a rewrite that also changes the architecture has no reference implementation to test against.
Phase 1 — a characterization suite at the outer boundary. Before any code moves, the current behaviour gets pinned down: a recorded sequence of Tesla API payloads goes in, and the resulting persisted data and MQTT messages are asserted, with the vehicle state machine taken to ~100% coverage — the point of the number is that the suite must reliably catch behavioural deviations, not that a metric looks good. Crucially, the cases are stored as data rather than as code, and nothing in them refers to an internal function.
That boundary already exists —
TeslaMate.VehicleCase.start_vehicle/3drives exactly such a sequence through the API mock today. What's missing is coverage, the move from Elixir literals to data fixtures, and assertions against real persisted rows and messages instead of mock call assertions.This suite is what makes the following phases safe, and it stays valid across both of them: whatever happens inside, identical API payloads must keep producing identical results.
One caveat we'll handle explicitly: pinning current behaviour also pins current bugs. Cases we believe are wrong get marked as such and fixed in their own commits — never silently, in the middle of a refactor.
Phase 2 — source abstraction in Elixir. Introduce the source/event boundary and make the event contract explicit, incrementally, with the Phase 1 suite green at every step. Delivers native streaming-only support without a rewrite. Event-level fixtures — the ones that can only exist once the contract does — are written here, at the new boundary, alongside the outer suite.
Phase 3 — Rust. Port module by module behind the same contract (strangler pattern). Both fixture sets are the gate: the same files drive both cores and the outputs are diffed. Anything the Rust core cannot reproduce does not replace the Elixir path. How the two runtimes coexist during the port — NIFs, a separate process, or running both cores side by side and diffing offline — is deliberately left open here and will be decided in the Phase 3 issues; the single-binary deployment benefit arrives at the end of this phase, not during it.
Users will not be moved onto the Rust core silently. How the switch is offered — an opt-in image, a beta tag, or both cores running against the same data and diffed before anything ships — will be settled there too, and announced before it happens.
Each phase stands on its own. Phase 1 is worth having whatever happens afterwards, Phase 2 removes the bottleneck regardless of the language, and Phase 3 is gated by both.
Where your input goes, and how this gets tracked
The questions marked Open above — the event contract, the fidelity gap between Fleet Telemetry and old streaming, and what happens to the web UI — are the ones where a good answer changes what we build. Corrections to the "what stays stable" list are equally welcome.
There are no dates attached to any of this, and regular development continues throughout: bug fixes, dashboards and features ship as usual, and none of this work blocks a release. Once the design questions have settled, we'll cut the work into issues that link back to this thread — those issues carry scope and acceptance criteria, not deadlines. Hard commitments made in this document — dashboards shipping together with schema changes, MQTT compatibility asserted by the suite — become acceptance criteria in those issues, so they can't silently slip. The issues are the tracking surface; the reasoning lives here. This thread stays open for the design discussion and will not be used as a task list.
What this thread is not: a place to claim implementation work. See "Ownership" above.
cc for visibility: @adriankumpf, @cwanja, @DrMichael, @Dulanic, @swiffer, @tobiasehlert — no action needed, this is a heads-up so you read it here first.
🤖 Drafted with Claude Code (Opus 5 high and Fable 5 high) — sponsored by Claude for Open Source
All reactions