Test / test (push) Failing after 25m47s
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]>
127 lines
3.2 KiB
Go
127 lines
3.2 KiB
Go
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, ":")
|
|
}
|