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!

1 Like

@pentafive — a month late, and I’m sorry about that. Part of it is on GitHub and worth flagging for anyone about to send their first PR somewhere: runs from a first-time contributor sit in action_required until the maintainer clicks approve, so your CI never actually started and the failure you were looking at wasn’t yours. The rest of the delay is just on me.

Merged as-is — all four commits, no rework needed. I fed your 55-byte frame to the current decoder before reading the patch and watched the same eleven values go in the bin. The full technical reply is in the PR review, so I won’t re-run it here.

What it means for everyone else: on any lamp whose property map spans more than one notification, every state entity read unknown, and the property-request write could drop the link on each command. The G8 splits frames; the G7 seemingly never does, which is why nothing I own ever hit the path. Fixed on main and shipping in 0.2.0, which goes out this week — note that the current release, v0.1.1, does not have it yet.

G8 status: core control is now confirmed on real hardware, with brightness measured against the power meters in the smart plugs rather than against the integration’s own numbers — the right way round. The circadian presets are still unconfirmed: models.py has no G8 entry, so it falls back to the G7 indices. One btsnoop capture from the vendor app settles that, and any G8 owner can take it, not only Pentafive — it’s the last thing standing between the G8 and a real :white_check_mark: in the README.

Your two issues are queued behind the release, not forgotten. And the rest of the list — G2/G2 Pro, G3, G4, G5, G6, G9, G10 — is still wide open, if anyone’s lamp is feeling brave. :seedling:

I have quite a few different types of Glowrium lights, super thankful that you created this integration. I haven’t tested all the functions of the lights that your integration found, but I can tell you I was able to power on/off my G6, G8, G11, and it recognized my H4 light but I couldn’t test it yet since I didn’t want to wake my husband up :rofl: . I did notice that it did not recognize my G3 light. I don’t know how I could help you with getting that to work, but if you know let me know.

@cristinakirkp0815 — late, sorry. Thank you: yours is the first report of power working on a G6 or a G11, and the G11 was not even on my list. You asked how you could help. Lamp by lamp:

  • G3. A lamp is offered only if the Bluetooth name it advertises starts with Glowrium. Force-close the vendor app on every phone (a lamp takes one connection at a time), then search for glow in the advertisement monitor. Listed: Add integration → Glowrium should offer it. Not listed: nRF Connect on a phone held against the lamp shows it as the strongest signal — tell me the name it goes by and, if it does not start with Glowrium, send a screenshot of the services it lists.

  • G6, G11. The model id from each lamp’s device page (Glowrium-C051 on my G7), and whether brightness works as well as power: that puts them in the README’s table. Lighting mode and Ramp are confirmed only for the G7.

  • H4. No need to wake anyone: its model id shows on the device page without switching anything on, and a daytime test is as good. The README calls the H-series out of scope, so its turning up at all is news.

  • G8. Only if you feel like it: a Bluetooth capture of the vendor app switching presets, taken on an Android phone — the last thing between the G8 and a confirmed entry. I would walk you through it.

    A reply here is enough. And do update to 0.3.0 (below): if your lamps connect through a Bluetooth adapter on the Home Assistant host rather than an ESPHome proxy, every one of them counted toward the leak it fixes, and you have four.

    For everyone following: 0.2.0, a reliability release, went out on 25 August, the day after I said “this week”, and I never came back to say so. It carried @pentafive’s fix for state reporting on the G8 — and a defect that showed within the hour: the light answered commands while four entities never appeared. 0.2.1 fixed that the same day.

0.3.0 is out — release notes. It is about two things 0.2.1 did to its own link, both of which I had put down to a weak signal. First, it read the lamp’s state on every connect, and on my Linux host (BlueZ 5.82) a read ends the link: the read succeeds, and two seconds later the lamp is disconnected — six runs of six, against nine of nine still connected when nothing was read. So the link was made and lost every thirty seconds, a hundred times an hour. 0.3.0 asks for the state with a write instead, which the lamp answers at once. Second, every link 0.2.1 let go of left one D-Bus connection open for as long as Home Assistant ran, and the system bus allows a user 256: about two and a half hours after a start, Home Assistant could open no new Bluetooth connection — to this lamp or to anything else. If Bluetooth on your host has been failing a few hours after every restart of Home Assistant, this is a likely reason, and the update is the fix.

The same investigation went through how a link is let go of. BlueZ before 5.84 can go on reporting the lamp connected after the link is gone, and then the lamp answers nothing until the Bluetooth adapter is power-cycled; 0.3.0 cannot end that, but it no longer leaks through it, tries less often, and says what it is - in the log and as a repair on the dashboard. It notices a link that died without BlueZ saying so. A reload no longer leaks a connection, or leaves the instance it replaced competing for the lamp’s single connection, and the link is hung up as soon as Home Assistant says it is stopping. The cost: a command that needs a second attempt now waits for the old link to close first, so it takes longer; one that works first time is unaffected.

0.3.0 also keeps the lamp’s clock right instead of setting it once (issue #4) — my G7 was 39 minutes slow after five weeks — keeps the DST offset the lamp reports, and notices settings changed from the vendor app while Home Assistant was disconnected. A G8 refuses the state request, so it is still read; what that costs there I cannot measure, and a report from a G8 after updating is very welcome.

One change asks something of you, which is why it is 0.3.0 and not 0.2.2: the options of the Lighting mode select are keys now (sun_sync) where they were names (Sun SYNC). The device page looks the same; an automation, script or template that names a lighting mode needs the key, and the release notes open with the table. There is also a diagnostics download now, so that a report can come as one file instead of a list of questions from me.

1 Like

I haven’t had a chance to get to helping you out with this, but I am hoping that I will have time later on today to see what I can come up with.