Skip to content

feat(vehicle): add support for LeapMotor API#29666

Draft
syphernl wants to merge 5 commits into
evcc-io:masterfrom
syphernl:feat/leapmotor_api_integration
Draft

feat(vehicle): add support for LeapMotor API#29666
syphernl wants to merge 5 commits into
evcc-io:masterfrom
syphernl:feat/leapmotor_api_integration

Conversation

@syphernl
Copy link
Copy Markdown
Contributor

@syphernl syphernl commented May 5, 2026

Summary

Adds Leapmotor cloud API support (same API the official app uses). The API is unofficial but community-documented:

App certs download automatically from markoceri/leapmotor-certs on startup — no files to provide.

Supported data

SoC, charge state, range, odometer, finish time, climate state, GPS position, SoC limit.

Caveats

  • Unofficial API — may break without warning
  • Startup fails if GitHub is unreachable (no local cert cache yet)

Status

Not tested on a real car yet. Draft until someone validates it on a T03, B10, or C10.

Docs

evcc-io/docs#1049

References

syphernl added 2 commits May 5, 2026 15:30
- Remove EnsureAuth (dead code, had TOCTOU race)
- Make addAuthHeaders void (return value was always ignored)
- Drop Python-style inline comments in deriveP12Password
- Collapse SM4 section divider to single comment line
Copy link
Copy Markdown
Contributor

@ngehrsitz ngehrsitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it briefly and it works so far 👍
Image

Comment on lines +235 to +238
// newMTLSClient creates an http.Client with optional client cert and TLS verification disabled.
// Leapmotor's API servers use self-signed certificates.
func newMTLSClient(cert *tls.Certificate) *http.Client {
tlsCfg := &tls.Config{InsecureSkipVerify: true} //nolint:gosec
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better would be to pin their Root CA ebd8bbb

}
return float64(*res.TotalMileage), nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at https://github.com/markoceri/leapmotor-api we can also implement VehicleClimater,VehiclePosition,SocLimiter and even PhaseDescriber for the 1P only models like T03

Comment thread vehicle/leapmotor.go Outdated
Comment thread vehicle/leapmotor/identity.go Outdated
Comment thread vehicle/leapmotor.go
}

func init() {
registry.Add("leapmotor", NewLeapmotorFromConfig)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need templates/definition/vehicle/leapmotor.yaml otherwise it won´t be shown in the UI

Comment thread vehicle/leapmotor/identity.go Outdated
Comment thread vehicle/leapmotor/identity.go Outdated
Comment thread vehicle/leapmotor/identity.go Outdated
Comment thread vehicle/leapmotor/api.go
Comment on lines +59 to +73
func apiPost(client *http.Client, fullURL string, headers map[string]string, body string) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, fullURL, strings.NewReader(body))
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be substituted with request.DoBody()

Comment thread vehicle/leapmotor/api.go
Comment on lines +75 to +86
// parseEnvelope decodes the API envelope, returning Data or an error for non-zero codes.
func parseEnvelope[T any](body []byte) (T, error) {
var res apiEnvelope[T]
var zero T
if err := json.Unmarshal(body, &res); err != nil {
return zero, err
}
if res.Code != 0 {
return zero, fmt.Errorf("api %d: %s", res.Code, res.Message)
}
return res.Data, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always used after apiPost() or do() which is mostly the same thing. I would either combine them, or even better reuse the shared functionality for decoding JSON.

- Rename AppCert/AppKey → auto-download app certs from markoceri/leapmotor-certs
  (removes file path requirement; certs fetched in parallel via errgroup)
- Replace custom SM4 impl with github.com/emmansun/gmsm (key recovered from
  APK round key schedule); cipher block initialised once at package init
- Replace manual JWT parsing with golang-jwt/jwt in deriveSessionDeviceID
- Stable deviceID: SHA256(email)[:16] replaces per-restart random bytes
- NewIdentity accepts PEM bytes instead of file paths
- apiPost uses request.Helper.DoBody (adds HTTP status code checking)
- Add postAndParse[T] helper combining POST + envelope decode
- Add VehicleClimater, VehiclePosition, SocLimiter interfaces
- Add templates/definition/vehicle/leapmotor.yaml for UI discovery
- Use hex.EncodeToString instead of fmt.Sprintf("%x")
@syphernl syphernl requested a review from ngehrsitz May 12, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants