← Creative Lab
Personal R&D — 2025

CORONA

Solar data locked in a vendor app, recovered by replaying the app's own query

My solar panels report to a vendor app with no public API and no export. I captured the mobile app's token and replayed its own GraphQL query with the app's headers to recover the readings, and the collector labels token-expiry staleness instead of hiding it.

3
Spoofed Headers
110
Polls Made
7
Distinct Readings
The hard part

Three spoofed headers and a captured bearer token are the whole trick. Of 110 polls, only 7 returned distinct readings: the token expired and the API kept serving the last value instead of an error.

Client
Personal R&D
Role
Creator / Developer Reverse Engineering / Data Recovery
Timeline
2025
The problemyour data, someone else's walled garden

A rooftop solar system generates a stream of numbers all day — what the panels make, what the house draws, what flows to and from the grid. The hardware is on my roof. The numbers are about my house. But the only way to see them is a phone app that talks to a private server, and there is no export button, no public API, no webhook. The data is real and it is mine and I cannot get at it.

That is not a hardware problem. The numbers are already leaving the house every few minutes — they just travel in a conversation I am not invited to. So the task is not to build a sensor. It is to get invited to a conversation that is already happening.

The interceptwear the mobile client's identity, replay its own call

The app authenticates, then makes a GraphQL call for the current power. Point the phone at mitmproxy once and that whole exchange is visible: the endpoint, the query, the bearer token, and the headers the server uses to decide the caller is the real mobile app. Copy those, and a plain Python script can make the identical request — the server cannot tell the difference, because from where it stands there isn't one.

the request, impersonating the app

POST edp-api-graphql.mysunstrong.com/graphql

apollographql-client-name: SunStrongConnectMobile
apollographql-client-version: 1.1.2
originatingfrom: MOBILE
authorization: Bearer •••• (from mitmproxy)

query FetchCurrentPower($siteKey: String!) {
  currentPower(siteKey: $siteKey) {
    production consumption storage grid timestamp
  }
}
→ looks like the app

what comes back

// press “run the collector” below
The three amber headers are the whole trick. Without them the server answers a stranger; with them it answers the app it thinks it is talking to. Reverse-engineered from a single captured session — no credential is shown here, only the shape of the request.
What came homea real solar day — press play and watch the collector poll

Here is the physics the vendor did not want to hand over, recovered anyway. Production climbs from nothing at dawn to 1.8 kW at midday; watch the grid line cross below zero around noon — that is the house exporting surplus power back to the grid instead of drawing from it. Then it tapers into the afternoon. Every point is a reading the collector actually received.

production kW home consumption kW grid kW — below zero = exporting the stale reading
110 polls were made. Watch how many were the same number.
The catch, shown not hidden110 polls, 7 readings

You may have noticed the cursor sit still for a long time near dawn. That is the honest part. The collector polled 110 times and got back only 7 distinct readings — 63 of those polls returned the identical 07:20 value, because the captured token had expired and the API quietly kept serving the last thing it had rather than an error. The tool's own STALE_DATA_FIX.md names the cause exactly: the bearer token lasts about a day, and there is no automated re-capture yet, so the whole thing goes stale until the phone is put back behind the proxy.

That is why this page charts seven points and labels the poll count on each, rather than drawing a smooth 110-sample line it never actually had. The interesting engineering was getting the door open. Keeping it open — rotating the token without a human and a proxy every morning — is the honest unfinished edge, and pretending otherwise would undo the one thing the project is really about: getting at the truth of the numbers.

What genuinely works

The impersonation. Three spoofed headers plus a captured bearer token, and the private mobile endpoint answers a Python script as if it were the app. Reverse-engineered from one proxied session.

The readings are real. This ran against production and the numbers describe a real solar day, down to the grid line going negative at noon.

A type-coercion catch. The GraphQL API returns its numerics as strings; the collector coerces them before they reach SQLite, or every reading would sort and chart as text.

The honest edge

Token lifetime ends the party. The bearer token lasts roughly 24 hours and there is no automated re-capture, so collection goes stale daily until the phone is re-proxied.

The dashboard is half-wired. The React front end does not boot as committed — an import path — and some documented endpoints were aspirational.

Small sample. Seven readings is a proof of access, not a season of data. The access is the achievement; the volume was never the point.

mitmproxyGraphQLclient-header spoofingbearer replay PythonSQLitereverse engineeringpersonal data recovery
Scope

Project Elements

Traffic InterceptionClient-Header ReplayGraphQL CollectorSQLite StorageHonest Data Labelling

Have a Project in Mind?

Animation direction, VFX and creative technology. Message me on LinkedIn or get in touch.