Add BYD vehicle telemetry exporter
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]>
This commit is contained in:
thistineandClaude Opus 5 committed 2026-09-05 22:03:16 +07:00
commit 52e1aaa0c0
30 files changed
+3501

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
package bangcle
// White-box AES block functions.
//
// The index arithmetic below is a one-for-one transcription of decompiled code
// (via pybyd). It is deliberately not tidied or renamed: keeping it diffable
// against the reference is worth more than readable variable names, because the
// only way to debug a mistake here is to compare intermediate state with pybyd.
import "encoding/binary"
// prepareMatrix transposes a 4x4 block into the working state layout (col*8+row).
func prepareMatrix(in, state []byte) {
for col := 0; col < 4; col++ {
for row := 0; row < 4; row++ {
state[col*8+row] = in[col+row*4]
}
}
}
func decryptBlock(t *lookupTables, block []byte) []byte {
state := make([]byte, 32)
temp64 := make([]byte, 64)
tmp32 := make([]byte, 32)
out := make([]byte, 16)
prepareMatrix(block, state)
for rnd := 9; rnd >= 1; rnd-- {
l21 := rnd * 4
permPtr := 0
for i := 0; i < 4; i++ {
b := int(t.permDecrypt[permPtr])
l16 := i * 8
base := i * 16
for j := 0; j < 4; j++ {
u7 := (b + j) & 3
idx := int(state[l16+u7]) + (i+(l21+u7)*4)*256
binary.LittleEndian.PutUint32(temp64[base+j*4:], binary.LittleEndian.Uint32(t.invRound[idx*4:]))
}
permPtr += 2
}
i15 := 1
for l21x := 0; l21x < 4; l21x++ {
pb := l21x
for l9 := 0; l9 < 4; l9++ {
u6 := int(temp64[pb]) & 0xF
u26 := int(temp64[pb]) & 0xF0
f := [3]int{int(temp64[pb+0x10]), int(temp64[pb+0x20]), int(temp64[pb+0x30])}
l2 := l9*0x18 + rnd*0x60
i25 := i15
for k := 0; k < 3; k++ {
b := f[k]
u27 := u6 | ((b << 4) & 0xFF)
u26 = ((u26 >> 4) | ((b >> 4) << 4)) & 0xFF
u6 = int(t.invXor[(l2+i25-1)*0x100+u27]) & 0xF
u26 = (int(t.invXor[(l2+i25)*0x100+u26]) & 0xF) << 4
i25 += 2
}
state[l9+l21x*8] = byte((u26 | u6) & 0xFF)
pb += 4
}
i15 += 6
}
}
copy(tmp32, state)
u8, u10, u12 := 1, 3, 2
for row := 0; row < 4; row++ {
state[row] = t.invFirst[int(tmp32[row])+row*0x400]
r1 := u10 & 3
state[8+row] = t.invFirst[int(tmp32[8+r1])+r1*0x400+0x100]
r2 := u12 & 3
state[0x10+row] = t.invFirst[int(tmp32[0x10+r2])+r2*0x400+0x200]
r3 := u8 & 3
state[0x18+row] = t.invFirst[int(tmp32[0x18+r3])+r3*0x400+0x300]
u8++
u10++
u12++
}
for col := 0; col < 4; col++ {
for row := 0; row < 4; row++ {
out[col+row*4] = state[col*8+row]
}
}
return out
}
func encryptBlock(t *lookupTables, block []byte) []byte {
state := make([]byte, 32)
temp64 := make([]byte, 64)
tmp32 := make([]byte, 32)
out := make([]byte, 16)
prepareMatrix(block, state)
for rnd := 0; rnd < 9; rnd++ {
l21 := rnd * 4
permPtr := 0
for i := 0; i < 4; i++ {
b := int(t.permEncrypt[permPtr])
l16 := i * 8
base := i * 16
for j := 0; j < 4; j++ {
u8 := (b + j) & 3
idx := int(state[l16+u8]) + (i+(l21+u8)*4)*256
binary.LittleEndian.PutUint32(temp64[base+j*4:], binary.LittleEndian.Uint32(t.round[idx*4:]))
}
permPtr += 2
}
i16 := 1
for l22 := 0; l22 < 4; l22++ {
pb := l22
for l10 := 0; l10 < 4; l10++ {
u7 := int(temp64[pb]) & 0xF
u26 := int(temp64[pb]) & 0xF0
f := [3]int{int(temp64[pb+0x10]), int(temp64[pb+0x20]), int(temp64[pb+0x30])}
l2 := l10*0x18 + rnd*0x60
i25 := i16
for k := 0; k < 3; k++ {
b := f[k]
u27 := u7 | ((b << 4) & 0xFF)
u26 = ((u26 >> 4) | ((b >> 4) << 4)) & 0xFF
u7 = int(t.xor[(l2+i25-1)*0x100+u27]) & 0xF
u26 = (int(t.xor[(l2+i25)*0x100+u26]) & 0xF) << 4
i25 += 2
}
state[l10+l22*8] = byte((u26 | u7) & 0xFF)
pb += 4
}
i16 += 6
}
}
copy(tmp32, state)
for row := 0; row < 4; row++ {
r0 := row & 3
state[row] = t.final[int(tmp32[r0])+r0*0x400]
r1 := (1 + row) & 3
state[8+row] = t.final[int(tmp32[8+r1])+r1*0x400+0x100]
r2 := (2 + row) & 3
state[0x10+row] = t.final[int(tmp32[0x10+r2])+r2*0x400+0x200]
r3 := (3 + row) & 3
state[0x18+row] = t.final[int(tmp32[0x18+r3])+r3*0x400+0x300]
}
for col := 0; col < 4; col++ {
for row := 0; row < 4; row++ {
out[col+row*4] = state[col*8+row]
}
}
return out
}