Last weekend, I unlocked my car using my own Dart package (according to pub.dev, the first one ever built for it). I walked down to the parking lot, opened the app, tapped the button, heard the clunk. "Worth it." Then I locked it again, went back upstairs, and called it a productive Sunday.
But let's get back a few months earlier, when I had bought a connected MG. I opened the official app, and like any developer who gets a new piece of technology, I spent about five minutes enjoying it as a normal person. Then I started wondering how it worked, what APIs were behind the app, and whether I could build something on top of it myself.
A quick search confirmed that no Dart package existed for the SAIC iSmart API. That's how I ended up building the first one.
My initial approach was to decompile the official app.
What the official app reveals, and what it doesn't
I extracted the APK directly from my phone via adb and decompiled it with apktool. The official iSmart app is a native Kotlin Android app.
Most of the business logic is protected by ijiami encryption, so the core algorithms aren't readable statically. From the unprotected resources (strings, drawables, layout files) I was able to confirm a handful of things: undocumented features, error codes, model naming conventions. Useful context, but not enough. The actual API protocol wasn't there. It lives in the encrypted DEX (the compiled Android bytecode), and I had hit a wall.
The community that got there first
I wasn't the first to hit that wall. A GitHub organization called SAIC-iSmart-API has been reverse-engineering the SAIC API for years via traffic capture. They have a Python client, a Java client, a Home Assistant integration, an MQTT gateway, and even a dedicated documentation repo with real captured API responses.
What wasn't covered was mobile-first development. No Dart client, no Swift client, no TypeScript package designed for mobile development. The Java client exists but is built for server-side use (Maven, MQTT gateways, Spring). A workaround exists: run the Python client on a server and use it as a proxy. But a mobile app should ideally call the API directly.
So, as a Flutter developer, I built saic_ismart: the first pure Dart package for the SAIC iSmart API. MG, Roewe, Maxus/LDV: brands that together represent millions of connected vehicles across Europe, Australia, India, and beyond.
Understanding the protocol
The SAIC API isn't officially documented. There's no Swagger file, no official SDK. Everything I know about it came from reading the Python and Java clients line by line (well, Claude did most of the reading), cross-referenced with the community documentation repo.
Here's what's actually going on under the hood.
The SAIC API isn't REST. It's encrypted REST, with AES-128-CBC on every request, HMAC-SHA-256 signatures, and an event-id polling loop for asynchronous commands. Non-trivial to implement, but the Python client had a test vector. Matching it exactly in Dart was the first real milestone.
A few other things worth knowing before you use it:
- Sessions are single-tenant: using the client disconnects the official app for ~900 seconds
- The VIN is never sent raw, always
sha256(vin) - The login uses a hardcoded credential that decodes to
sword:sword_secret
Some of these quirks are just amusing. Others are real constraints. The package surfaces them clearly so you know exactly what you're dealing with.
Building it with AI assistance
Claude Code was part of my stack on this one, and it made a real difference. Not just for code generation.
Before writing a single line of Dart, I had Claude analyze both the Python and Java repos and produce a concrete technical spec: authentication flow, encryption pipeline, endpoint behavior, known quirks. What would have taken days of reading became a structured document in a fraction of the time. That's how TECHNICAL_REFERENCE.md was born, not as an afterthought but as the foundation the implementation was built on.
Claude also helped port the AES-128-CBC crypto pipeline from Python to Dart and write the HMAC-SHA-256 implementation with the right key derivation. Technically straightforward once you know what to do, but tedious to get right.
There's a limit to what AI can do here though. No language model can plug a phone into a car and tell you what the raw values actually mean in the real world. For that, I had to test on my MG3 EU. (Though technically, an MCP integration could have automated that too. Maybe next time.)
What the car actually tells you
The Python client leaves some fields uninterpreted. So I ran the code, looked at the numbers, and reasoned about them. A few examples:
| Field | Raw value | Unit | Converted |
|---|---|---|---|
| Mileage | 243790 |
decameters | 24 379 km |
| Tyre pressure (FL) | 61 |
PSI×2 | 30.5 PSI / 2.1 bar |
| Tyre pressure (RR) | 70 |
PSI×2 | 35 PSI / 2.4 bar |
Many other fields follow similar patterns: sentinel values, unexpected scales, units that only make sense once you cross-reference with the real world. All of these field experiments enriched the TECHNICAL_REFERENCE.md, turning it from a protocol spec into something that actually reflects how the API behaves on a real vehicle.
What you can do with it
Here's what the package actually lets you do:
- Lock and unlock your vehicle remotely
- Start or stop climate control (A/C, heating, fan-only, defrost)
- Control heated seats with level support (off / low / medium / high)
- Monitor tyre pressure with per-wheel values in bar
- Get GPS location and vehicle status in real time
- Find my car by triggering horn and lights remotely
- Open the tailgate remotely
-
Detect vehicle features: does your car have a sunroof? Heated seats? TPMS? The package reads
vehicleModelConfigurationand exposes 23 typed getters
A few lines of setup, then straight to the data:
final client = SaicClient(
config: SaicConfig(
username: 'you@example.com',
password: 'yourpassword',
region: SaicRegion.europe,
),
);
await client.login();
final vehicles = await client.getVehicles();
final vin = vehicles.first.vin;
// Status
final status = await client.getVehicleStatus(vin);
print(status.basicVehicleStatus?.mileageKm); // 24447.0
print(status.basicVehicleStatus?.lockState); // LockStatus.locked
print(status.basicVehicleStatus?.frontLeftTyrePressureBar); // 2.48
// Remote commands
await client.lockVehicle(vin);
await client.startClimate(vin, temperatureIndex: 8);
await client.controlHeatedSeats(vin, driverLevel: HeatLevel.medium);
await client.findMyCar(vin);
All API calls are serialized. The SAIC server doesn't support concurrent requests on the same session.
The first time I unlocked my car from my own Flutter app, not from iSmart, not from a terminal, but from a UI I had built on top of code I had written, was a genuinely satisfying moment. And it kept happening: the first successful findMyCar() triggering the horn from across the street, the first time heated seats started before I even got to the car. Each feature that went from "POST /vehicle/control" to a real physical reaction felt like a small proof of concept becoming real.
The result
452 tests. 160/160 on pub.dev. 23 vehicle feature detection getters. Tested in production on a real MG3 EU. Pure Dart, no native code, works in any Flutter or Dart project.
The package is on pub.dev: saic_ismart
What's next
EV features like battery SoC, charging management, and climate scheduling are the natural next milestone. I don't own an EV myself, so I can't develop or validate those features without the right hardware. I would be genuinely happy to welcome contributors who own an MG4, ZS EV, or Roewe. The architecture is in place and ready to be extended. Open an issue on GitHub.
The client is pure Dart, so wherever Flutter runs, this can run too. The package is a foundation. Build what's missing.
Built with significant help from the research work of the SAIC-iSmart-API community. Their work greatly accelerated my development.
Top comments (0)