This document contains developer-focused documentation for the mcs CLI tool. For user-facing documentation (installation, usage, configuration), see README.md.
cmd/mcs/main.go Entry point
internal/
api/
auth.go Authentication, encryption keys, login
client.go API request handling with retry logic
control.go Vehicle control endpoints (lock, start, etc.)
crypto.go API wrappers (base64, RSA, uses fixed IV)
errors.go Custom error types
keys.go Encryption key storage struct
maphelpers.go Type-safe map accessor functions
types.go Response types and data structures
vehicle.go Vehicle data retrieval endpoints
cache/
cache.go Token caching (~/.cache/mcs/token.json)
cli/
root.go Cobra root command
client.go API client creation with caching
command_factory.go Command builder helpers
status_cmd.go Status command orchestration
status_display.go Status display formatting
status_extract.go Data extraction for JSON output
status_format.go Formatting helpers
lock.go, engine.go Control commands
charge.go, climate.go EV/HVAC commands
raw.go Debug raw JSON output
config/
config.go Config loading (TOML + env vars)
crypto/
crypto.go Low-level AES-128-CBC and PKCS7 primitives
sensordata/
sensor_data.go Anti-bot fingerprinting (16-round Feistel cipher, see line 255)
service/checkVersion(baseURL) → GetencKeyandsignKey(AES keys)system/encryptionKey(usherURL) → Get RSA public keyuser/login(usherURL) → Encrypt password with RSA, getaccessToken- All subsequent requests use
accessToken+ encrypted payloads
Note: The API uses two base URLs - baseURL for vehicle operations and usherURL for authentication.
Two methods for API requests:
APIRequest()→ Returnsmap[string]interface{}for dynamic accessAPIRequestJSON()→ Returns raw bytes for direct unmarshaling to typed structs (preferred)
Every request needs:
X-acf-sensor-dataheader (anti-bot fingerprint using Feistel cipher)signheader (SHA256 of encrypted payload + timestamp + signKey)- Encrypted query params and body (AES-128-CBC with encKey)
AppVersion = "9.0.5" // Must match mobile app version
IV = "0102030405060708" // AES initialization vector
SignatureMD5 = "C383D8C4..." // For key derivationIf the manufacturer updates the app, AppVersion and related user-agent strings may need updating.
Control commands (lock, start, climate, etc.) support confirmation polling to wait for the vehicle to report the new state:
- 20s initial delay before first poll (allows vehicle time to process)
- 5s poll interval thereafter
- Configurable timeout via
--confirm-waitflag (default: 2 minutes) - Disable with
--confirm=false
Constants in command_factory.go:
ConfirmationInitialDelay = 20 * time.Second
DefaultPollInterval = 5 * time.SecondGetter methods return strongly-typed structs instead of tuples:
| Method | Returns | Fields |
|---|---|---|
GetBatteryInfo() |
BatteryInfo |
BatteryLevel, RangeKm, ChargeTimeACMin, ChargeTimeQBCMin, PluggedIn, Charging, HeaterOn, HeaterAuto |
GetFuelInfo() |
FuelInfo |
FuelLevel, RangeKm |
GetTiresInfo() |
TireInfo |
FrontLeftPsi, FrontRightPsi, RearLeftPsi, RearRightPsi |
GetLocationInfo() |
LocationInfo |
Latitude, Longitude, Timestamp |
GetDoorsInfo() |
DoorStatus |
DriverOpen, PassengerOpen, RearLeftOpen, RearRightOpen, TrunkOpen, HoodOpen, FuelLidOpen, DriverLocked, PassengerLocked, RearLeftLocked, RearRightLocked, AllLocked |
GetWindowsInfo() |
WindowStatus |
DriverPosition, PassengerPosition, RearLeftPosition, RearRightPosition |
GetHvacInfo() |
HVACInfo |
HVACOn, FrontDefroster, RearDefroster, InteriorTempC, TargetTempC |
GetOdometerInfo() |
OdometerInfo |
OdometerKm |
All getters return (T, error) for proper error handling.
Named constants for API status values (in types.go):
// Temperature units
Celsius, Fahrenheit = 1, 2
// Result codes
ResultCodeSuccess = "200S00"
// Charger status
ChargerConnected, ChargerDisconnected = 1, 0
// Charging status
ChargeStatusCharging, ChargeStatusNotCharging = 6, 0
// Battery heater
BatteryHeaterOn, BatteryHeaterOff = 1, 0
BatteryHeaterAutoEnabled, BatteryHeaterAutoDisabled = 1, 0
// HVAC
HVACStatusOn, HVACStatusOff = 1, 0
// Defrosters
DefrosterOn, DefrosterOff = 1, 0
// Doors
DoorOpen, DoorClosed = 1, 0
DoorLocked, DoorUnlocked = 0, 1 // Note: inverted!
// Hazard lights
HazardLightsOn, HazardLightsOff = 1, 0
// Windows
WindowClosed, WindowFullyOpen = 0, 100For working with map[string]interface{} responses safely (in maphelpers.go):
getString(m, "key") // (string, bool)
getInt(m, "key") // (int, bool) - handles float64 from JSON
getFloat64(m, "key") // (float64, bool)
getBool(m, "key") // (bool, bool)
getMap(m, "key") // (map[string]interface{}, bool)
getSlice(m, "key") // ([]interface{}, bool)
getMapSlice(m, "key") // ([]map[string]interface{}, bool)
getMapFromSlice(s, idx) // (map[string]interface{}, bool)These prevent runtime panics from unsafe type assertions.
Custom error types in errors.go for specific error handling:
// Error codes from API
ErrorCodeEncryption = 600001 // Server rejected encrypted request
ErrorCodeTokenExpired = 600002 // Access token expired
ErrorCodeRequestIssue = 920000 // Check ExtraCode for details
// Extra codes (used with ErrorCodeRequestIssue)
ExtraCodeRequestInProgress = "400S01" // Request already in progress
ExtraCodeEngineStartLimit = "400S11" // Engine start limit reached
// Error types (use errors.Is/errors.As for checking)
*APIError // General API error
*EncryptionError // Triggers key refresh and retry
*TokenExpiredError // Triggers re-login and retry
*RequestInProgressError // Vehicle is processing another request
*EngineStartLimitError // Remote start limit (2x) reached
*ResultCodeError // Unexpected result code from APIThe API returns location in two places:
alertInfos[].PositionInfo- correct longitude signremoteInfos[].PositionInfo- sometimes wrong sign
Always use alertInfos for location data.
The internalVin field comes as either string or float64 from JSON. This is handled automatically by the custom InternalVIN type which implements UnmarshalJSON.
Credentials are cached in ~/.cache/mcs/token.json (see cache/cache.go). The cache stores:
accessToken+ expiration timestampencKeyandsignKey
Without caching, each command takes ~4.5s (full auth). With caching: ~2.7s.
go test ./... # Run all tests
golangci-lint run # Lint check
go test -cover ./... # With coverageIntegration tests cover config→client→cache flows. See *_integration_test.go files.
Use specific testify assertions for better failure messages:
| Instead of | Use |
|---|---|
assert.True(t, strings.Contains(s, sub)) |
assert.Contains(t, s, sub) |
assert.True(t, bytes.Equal(a, b)) |
assert.Equal(t, a, b) |
assert.True(t, regexp.MatchString(p, s)) |
assert.Regexp(t, p, s) |
assert.True(t, err != nil) |
assert.Error(t, err) |
assert.True(t, x == y) |
assert.Equal(t, x, y) |
assert.False(t, x != y) |
assert.Equal(t, x, y) |
assert.False(t, s != "") |
assert.Empty(t, s) |
require.False(t, len(x) == 0) |
require.NotEmpty(t, x) |
assert.True(t, x == 0) |
assert.Zero(t, x) |
Omit assertion messages when the assertion type and variable names are self-explanatory:
// Good - variable name is descriptive
assert.True(t, callbackExecuted)
assert.NotNil(t, client)
// Good - adds context not obvious from the assertion
assert.Equal(t, 0600, perm, "cache file permissions")Uses bd (bead) for issue tracking. Database in .beads/.
bd list # List issues
bd create "title" # Create issue
bd close mcs-XX # Close issue
bd ready # Show unblocked workPorted from a reference Python implementation. The core API behavior follows the manufacturer's mobile app protocol. The anti-bot fingerprinting uses a 16-round Feistel cipher (documented in sensordata/sensor_data.go).