Go port of the read path of pybyd 0.0.75 (the client behind jkaberg/hass-byd-vehicle): the bangcle white-box AES envelope, the signing/checkcode layer, login, vehicle list, and realtime readings. Scraping runs on a cron schedule rather than per Prometheus request because each reading wakes the car's T-Box and drains the traction battery; /metrics serves the last result. Every numeric field of the realtime payload becomes a gauge, so the exporter does not need a hand-maintained field list that goes stale when BYD changes the payload. Tests pin the wire format against pybyd byte-for-byte and drive the client end to end against a fake cloud that speaks the real protocol. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
commit
52e1aaa0c0
30 files changed
+3501
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package byd
|
||||
|
||||
// Android device fingerprint sent with every request, ported from the HA
|
||||
// integration's device_fingerprint.py.
|
||||
//
|
||||
// The profile must stay STABLE for an account: BYD treats a new IMEI as a new
|
||||
// device, and a fingerprint that changes on every restart looks like account
|
||||
// sharing. Rather than persisting state, it is derived deterministically from a
|
||||
// seed (the username by default) — same account, same phone, forever.
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed device_pool.json
|
||||
var devicePoolJSON []byte
|
||||
|
||||
// Device is the phone identity presented to the API.
|
||||
type Device struct {
|
||||
OSType string // outer "ostype", always "and"
|
||||
IMEI string
|
||||
MAC string
|
||||
Model string
|
||||
SDK string
|
||||
Mod string
|
||||
IMEIMD5 string
|
||||
MobileBrand string
|
||||
MobileModel string
|
||||
DeviceType string // "0"
|
||||
NetworkType string // "wifi"
|
||||
AndroidVer string // Android release, sent as the inner payload's "osType"
|
||||
OSVersion string // SDK level, sent as the inner payload's "osVersion"
|
||||
}
|
||||
|
||||
type poolEntry struct {
|
||||
MobileBrand string `json:"mobile_brand"`
|
||||
MobileModel string `json:"mobile_model"`
|
||||
Model string `json:"model"`
|
||||
Mod string `json:"mod"`
|
||||
TACPrefix string `json:"tac_prefix"`
|
||||
SDKOptions []string `json:"sdk_options"`
|
||||
OSOptions []string `json:"os_options"`
|
||||
}
|
||||
|
||||
// NewDevice derives a stable, realistic device profile from seed.
|
||||
func NewDevice(seed string) (Device, error) {
|
||||
var pool []poolEntry
|
||||
if err := json.Unmarshal(devicePoolJSON, &pool); err != nil {
|
||||
return Device{}, fmt.Errorf("device pool: %w", err)
|
||||
}
|
||||
if len(pool) == 0 {
|
||||
return Device{}, fmt.Errorf("device pool is empty")
|
||||
}
|
||||
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(seed))
|
||||
rng := rand.New(rand.NewSource(int64(h.Sum64())))
|
||||
|
||||
e := pool[rng.Intn(len(pool))]
|
||||
i := rng.Intn(len(e.SDKOptions))
|
||||
|
||||
imei := generateIMEI(rng, e.TACPrefix)
|
||||
sum := md5.Sum([]byte(imei))
|
||||
|
||||
return Device{
|
||||
OSType: "and",
|
||||
IMEI: imei,
|
||||
MAC: generateMAC(rng),
|
||||
Model: e.Model,
|
||||
SDK: e.SDKOptions[i],
|
||||
Mod: e.Mod,
|
||||
IMEIMD5: hex.EncodeToString(sum[:]),
|
||||
MobileBrand: e.MobileBrand,
|
||||
MobileModel: e.MobileModel,
|
||||
DeviceType: "0",
|
||||
NetworkType: "wifi",
|
||||
AndroidVer: e.OSOptions[i],
|
||||
OSVersion: e.SDKOptions[i],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateIMEI builds a valid 15-digit IMEI: a real 8-digit TAC identifying the
|
||||
// handset model, a 6-digit serial, and a Luhn check digit.
|
||||
func generateIMEI(rng *rand.Rand, tacPrefix string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(tacPrefix)
|
||||
for n := 0; n < 6; n++ {
|
||||
fmt.Fprint(&b, rng.Intn(10))
|
||||
}
|
||||
return b.String() + luhn(b.String())
|
||||
}
|
||||
|
||||
// luhn returns the check digit for the first 14 IMEI digits.
|
||||
func luhn(partial string) string {
|
||||
total := 0
|
||||
for i, c := range partial {
|
||||
d := int(c - '0')
|
||||
if i%2 == 1 {
|
||||
if d *= 2; d > 9 {
|
||||
d -= 9
|
||||
}
|
||||
}
|
||||
total += d
|
||||
}
|
||||
return fmt.Sprint((10 - total%10) % 10)
|
||||
}
|
||||
|
||||
// generateMAC returns a locally administered unicast address.
|
||||
func generateMAC(rng *rand.Rand) string {
|
||||
octets := make([]string, 6)
|
||||
for n := range octets {
|
||||
b := rng.Intn(256)
|
||||
if n == 0 {
|
||||
b = (b | 0x02) &^ 0x01
|
||||
}
|
||||
octets[n] = fmt.Sprintf("%02x", b)
|
||||
}
|
||||
return strings.Join(octets, ":")
|
||||
}
|
||||
Reference in new issue
Block a user