Photoperiod scheduling for grow lights nobody should ever see — a Home Assistant custom integration.
Grow lights are great for plants and terrible to look at. Grow Conductor delivers a configurable number of light hours per day, in blocks as long and continuous as possible, scheduled exclusively into the moments when no one can see the light: while the household sleeps, while nobody is home, or while everyone is provably settled in a room without sight of it (working from home in the office). Someone getting up at night, walking through the room, or turning on the TV cuts the light instantly; it quietly resumes once the coast has been clear for a while.
One config entry = one grow light = one device.
The scheduler is a small deterministic engine (pure Python, no Home Assistant imports, fully unit-tested) specified rule-by-rule in docs/ENGINE_SPEC.md; the reasoning behind the design lives in docs/DECISION.md. In short:
- Plant day: the daily budget (
target_hours) resets at a fixed anchor time (default 22:00). No carry-over between days. - Visibility: the light may run only while unobserved — asleep, away, or confirmed refuge (sustained occupancy in a room that cannot see the light). Viewer-room activity and hard vetoes (e.g. TV) force it off immediately; a quiet-time hold prevents flicker on the way back.
- Fail-safe: unknown sensors resolve toward "someone could see it". A sensor outage starves the plants a little; it never shows the light.
- Truth-based accounting: hours are counted from the physical switch state, so manual/HomeKit light counts toward the day's budget too.
| Entity | Purpose |
|---|---|
number.…_target_hours |
The knob: hours of light per plant day (0–20, step 0.5) |
switch.…_enabled |
Off = standby: no commands, accounting continues |
sensor.…_state |
lit / observed / cooldown / sated / standby, with a reason attribute |
sensor.…_lit_today |
Hours delivered this plant day (0.1 h resolution) |
- HACS → Integrations → ⋮ → Custom repositories →
https://github.com/leiklier/grow-conductor(category: Integration). - Install Grow Conductor and restart Home Assistant.
- Settings → Devices & services → Add integration → Grow Conductor.
The flow asks for the light switch and the visibility signals:
- Household asleep — a binary sensor/toggle that is on while the household sleeps. This is the prime window.
- Anyone home — a zone (occupant count), person, tracker, or binary sensor. Away time is the second-best window.
- Rooms with a view of the light — occupancy sensors covering every spot the light is visible from. Any activity cuts the light and starts the quiet-time hold (night movement, morning routine — same rule).
- Out-of-sight rooms — occupancy for rooms that cannot see the light (home office). Sustained presence there allows daytime runs while you are awake at home — the work-from-home case. Refuge blocks are deliberately tolerant: a coffee run through a viewer room neither cuts the light nor resets anything (exposure grace), and short sensor dropouts don't release the block (refuge hold). Settling within sight of the light still cuts it.
- Hard vetoes — signals that force the light off regardless, e.g. the TV in the same room.
- Movement triggers — entities whose state changes mean someone is on the move near the sleep boundary, e.g. the bedroom door contact: every change cuts the light instantly and starts the quiet-time hold. Only transitions count — a door left open all day never blocks the schedule.
All signals are optional, but with none configured the fail-safe keeps the light permanently off. Timing tunables (anchor, quiet-time hold, refuge confirmation/hold, exposure grace, minimum block) live under Options → Timing.
Same policy as sonos-conductor:
new behavior ships as a vX.Y.Z-beta.N pre-release first (HACS: enable
"Show beta versions"), soaks on a live install, then is promoted
unchanged to stable vX.Y.Z.
uv sync # pinned to the production HA version
uv run pytest # pure-core scenario tests + full HA e2e tests
uv run ruff check .The core/ package must never import homeassistant — enforced by
ruff (TID251) and a poisoned-import test. Code and tests cite the spec's
rule numbers; if behavior and spec disagree, one of them is a bug.