Skip to content

Nyx000/acinfinity-ble

Repository files navigation

acinfinity-ble

A clean-room Bluetooth LE implementation of the AC Infinity UIS protocol — read sensors and write settings directly to the controller, with no vendor cloud account and no network dependency.

Zero runtime dependencies. 165 tests. Developed against a Controller 69 Pro (firmware version ≥ 6, multi-port) and a UIS power strip.

Why

The official app talks to the controller over BLE and mirrors everything through AC Infinity's cloud. If you want local automation — Home Assistant, a Raspberry Pi, your own scheduler — you need the wire protocol, and it isn't published. This repository is that protocol, written down with its provenance, plus a tested implementation of it.

It was extracted from a running grow-tent telemetry system, where it has driven live hardware continuously since June 2026.

Status

Area State
GATT service / characteristic UUIDs Confirmed
Frame envelope + CRC16 Confirmed bit-identical to the app across 20,000 random vectors and every captured reference frame
Sensor read (self-stream V6, A5 TLV queries) Confirmed, per device type
Port mode activation (0x10) Confirmed live, byte-exact
AUTO thresholds (0x13) Encoder confirmed for firmware ≥ 6 (10-byte block); the write capture is still open
VPD (0x51) Confirmed all versions
Brightness (0x21) Confirmed
Temperature unit (0x20) Confirmed — 1 = °C, 0 = °F
Calibration (0x24) Partial. Bytes 1 and 2 map directly to the UI steppers; byte 0 is unresolved

Known-unresolved items are listed as unresolved. See docs/protocol/wire-protocol.md for each open question in full, including one capture attempt that failed and why.

The settings-bundle gotcha

Worth knowing before you write anything. Controller settings are always sent as one bundled write, never per-field. A lone single-field write — changing only the temperature unit, say — sends zero bytes over BLE and reports no error. It is silently a no-op.

Any encoder for a settings field must read-modify-write the whole bundle. This is the class of bug a unit test against your own encoder will never catch, because your encoder is correct and the device simply ignores it.

Install

git clone https://github.com/Nyx000/acinfinity-ble
cd acinfinity-ble
npm install          # dev dependencies only — there are no runtime dependencies
npm test

BLE transport goes through a small Python sidecar using bleak, which is the only supported transport. src/acinfinity/ble-sidecar.py is the sidecar; point BLE_SIDECAR_PYTHON and BLE_SIDECAR_SCRIPT at your interpreter and that file.

CONTROLLER_SOURCE=ble \
BLE_CONTROLLER_MAC=XX:XX:XX:XX:XX:XX \
BLE_POWERSTRIP_MAC=XX:XX:XX:XX:XX:XX \
BLE_SIDECAR_PYTHON=/usr/bin/python3 \
BLE_SIDECAR_SCRIPT=./src/acinfinity/ble-sidecar.py \
npm run ble:check

CONTROLLER_SOURCE defaults to mock, so nothing touches the radio unless you ask it to.

Layout

src/acinfinity/     protocol core — framing, CRC, encoders, decoders, transport
src/lib/            small pure helpers (hex, MAC, units, VPD, setpoints)
src/audit/          mode-ID → human name mapping
docs/protocol/      the wire protocol and how each claim was established
test/               165 tests
.claude/skills/     the live-capture methodology, as a runnable procedure

How this was verified

Static analysis alone was not treated as sufficient, and that turned out to matter.

The protocol was first recovered by decompiling the vendor's Android app. It was then re-validated three independent ways: in-process hooks on the live app capturing real GATT writes, a second independent decompile of a freshly pulled APK, and a host-controller (HCI) snoop. That pass overturned two findings this project had already documented and believed — in both cases the shipping code was right and the written protocol was wrong.

Every claim in docs/protocol/ therefore carries its evidence, and disagreements between sources are recorded rather than smoothed over. Where a question is open, it says so.

Legal

This is interoperability work. The protocol facts here — byte layouts, selectors, checksums, GATT UUIDs — were determined by analysing a program lawfully obtained, for the sole purpose of making independently written software talk to hardware the operator owns.

  • No vendor code is reproduced or redistributed. No APK, no decompiler output, no decompiled source. Citations of the form jm1.java:1348 are pointers that let a reader re-derive a finding themselves; they are not the vendor's code.
  • No vendor assets are included — no UI strings, no artwork, no branding.
  • Protocol facts are not copyrightable subject matter.
  • Not affiliated with, endorsed by, or supported by AC Infinity. "AC Infinity" and "UIS" are the marks of their owner, used here only to say what hardware this speaks to.

Writing to hardware carries real risk. See the warranty disclaimer in LICENSE, and prefer verifying any new write against a read-back.

Contributing

Captures from other UIS devices are the most useful thing anyone can add — particularly firmware < 6 single-port controllers, which this implementation handles from the decompile rather than from live evidence. The capture procedure is in .claude/skills/ble-app-capture/SKILL.md.

If you are adding a protocol claim, cite how you established it. A finding with no provenance is the thing this repository is organised to avoid.

License

MIT — see LICENSE.

About

Clean-room Bluetooth LE protocol for AC Infinity UIS controllers — local read/write, no vendor cloud. Zero dependencies, 165 tests.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages