Glowrium — local Bluetooth control for the INLEDCO Glowrium G7 grow light (no cloud, no app)

Hi all,

I bought an INLEDCO Glowrium G7 BLE grow light, discovered the only way to run it
was a phone app talking to the cloud, and decided that wouldn’t do. So I sniffed
the app’s Bluetooth traffic, reverse-engineered the protocol, and turned an
afternoon of staring at hex dumps into Glowrium — a fully local Home Assistant
integration. No cloud, no vendor app, verified on real hardware.

What you get

  • light — on/off + brightness
  • select — operating mode (Manual / Circadian / Schedule) and lighting mode (8 circadian presets)
  • number — ramp time, schedule gradual + brightness
  • time — schedule start / end
  • switch — indicator LED, DST · button — Sync location
  • Diagnostic latitude/longitude sensors + an Activated binary sensor

A few things it does better than the vendor app

  • Location that’s actually your location. The app builds the circadian curve
    from a city you pick off a list — so your “sunrise” is really the nearest
    listed town’s, which may be a few hundred km away. This writes your exact Home
    Assistant coordinates and lets the lamp do the astronomy, so the sun curve
    matches where your plants actually live. (Fixing this was the moment I stopped
    feeling guilty about reverse-engineering the thing.)
  • App-free setup. It can bring up a factory-reset lamp entirely locally,
    replaying the pairing handshake over BLE — so a freshly wiped device works
    without ever opening the app.
  • Plus a couple of smaller rough edges smoothed over along the way, and no cloud
    round-trip, so it just responds.

Install (HACS): Custom repositories → add
https://github.com/kugaevsky/glowrium-ha (category Integration) → install →
restart. You’ll need a Bluetooth adapter on the HA host or an ESPHome Bluetooth
proxy in range (the lamp only allows one BLE connection at a time, so keep the
app off it — it doesn’t like sharing).

:seedling: Where I’d really love a hand: testing other Glowrium models

The whole Glowrium family speaks the same BLE dialect, so these almost certainly
already work — I just can’t stamp a :white_check_mark: on them without someone running one on real
hardware. Currently not yet tested:

G2 / G2 Pro · G3 · G4 · G5 · G6 · G8 · G9 · G10

If you own any of them, please take it for a spin and tell me how it went
genuinely the most useful thing anyone can do here. The README has a “tested an
unverified model”
checklist (which entities work, what’s missing, your
device-info pkey, any preset differences). A short write-up or a PR is plenty,
and it promotes your model from “should work” to “confirmed.” Honestly worth more
to me than a :star: or the Sponsor button. :beer_mug:

Repo: GitHub - kugaevsky/glowrium-ha: Local Bluetooth control of the INLEDCO Glowrium G7 BLE grow light in Home Assistant — no cloud, no vendor app. · GitHub · MIT, unofficial (not affiliated
with INLEDCO / Glowrium — they didn’t ask for this and probably won’t thank me).
Bug reports and feedback very welcome!

Nice work on this, and thanks for keeping it local. I’ve got two G8s and yours is the only integration out there for them.

It installs and controls a G8 fine, but every state entity read unknown. The cause is that the G8’s state notification declares more CBOR pairs than the frame actually carries. Mine is 55 bytes with a header of 0xac, so it promises 12 pairs and contains 11. cbor.decode() raises IndexError on the twelfth, _on_notify catches it, and the whole frame gets dropped. Eleven perfectly good values binned every time, including power and brightness.

Nothing about that looks G8 specific. My guess is the G7 just never splits a frame, so the path has never been hit.

Fixing that turned up three more correctness problems in the same area, so there are four changes in the end. The two that matter for the symptom:

  • keep the pairs that did arrive instead of discarding a short map

  • prime state by reading facebd02 rather than writing the property request. One read returns the whole 20 key map. The request write is also unreliable on the G8, I get ATT 0x08, a not-connected error or a timeout depending on timing, and it drops the link each time. Since _connect_locked also runs from the command path, that was tearing down the connection on every command.

The other two stop schedule and mode writes inventing fields they never read, and make the light report unknown rather than a confident off when its state has not been read yet.

All tested: 14 new tests built on the real captured frame, suite green at 61, each commit green on its own. Checked on both lamps against the power meters in the smart plugs they sit on, rather than trusting the integration’s own numbers.

Everything else I found is filed so none of it depends on the PR being taken:

I’ve left models.py alone. Without a btsnoop capture I’d only be guessing at the circadian preset indices, and CONTRIBUTING is explicit that they need confirming against a capture.

Happy to rework the PR however if needed. Thanks again for the hard protocol work!