Module M3 - Light
Files: packages/grow_light.yaml (this module), packages/grow_actuators.yaml
(switch.grow_actuator_top_light_1..4 and
switch.grow_actuator_under_canopy_1..4 - the lights this module drives),
packages/grow_core.yaml and packages/grow_mapping_tool.yaml (the 8
mapping fields these actuators proxy).
Note on phase numbering
Per the Phase 0 plan’s own table this is Phase 6 (“Light”), not Phase 5 (“Substrate Monitoring & Crop-Steering Basis” - VWC/EC/Temp trend graphs and generative/vegetative preset templates). Built now at the user’s request, ahead of Phase 5, the same way Phase 4 (irrigation) was previously referred to as “Phase 3” in conversation - noted here for anyone following the plan document, not a blocker to building it in this order.
What’s implemented (Phase 6, v5)
Replaces the old Growzelt Zeitschaltuhr - Licht An/Aus, Growzelt - Undercanopy einschalten/ausschalten, and Blüten Trocknen automations
with:
- One start time + duration (hours) on/off window per light group (Top Lights, Under-Canopy) - e.g. Top Light: start 06:00, run 18h; Under Canopy: start 12:00, run 12h.
- Up to 4 physical lights per group, each with its own In Use toggle - a slot you don’t have wired up is fully excluded from both the on/off commands and the fault watchdog, instead of always targeting all 4 and reporting a fault for whichever ones are unmapped/unavailable.
- A simple beam timeline of when each light group was actually on, so you can visually confirm the window is doing what you expect.
- A fault watchdog: every on/off command to a group’s enabled lights is verified within 15 seconds, and a notification fires (with an automation-managed status flag on the dashboard) if any of them doesn’t confirm.
- A master switch (
grow_light_control_enabled, default off) and a separate Under-Canopy Group Enabled toggle (default off) so nothing runs until you’re ready, and the under-canopy group can be turned off as a whole independent of its own window setting.
Version history / why it looks like this
This module went through five shapes the same day, each corrected from direct user feedback:
- v1 gave every grow-tracker phase (Seedling/Vegetative/Flowering)
its own on/off
input_datetimepair per light group, plus a separate Drying profile - 12+ helper entities across 5 dashboard cards. Live testing showed most of that many new entities as “entity not found” (most likely a missed full restart, needed for genuinely newinput_*helpers), and it was simply more configuration than was wanted. - v2 descoped to one on/off cycle per light group and switched to a
schedule:helper per group (the same pattern already proven for the irrigation pump schedules - a UI-created weekly block calendar, since a YAML-definedschedule:entity has no editor at all). - v3: per further feedback (“I dont want to use the schedule. I just
want to say top light: 18 hours start time 06:00, under canopy 12
hours starting from 12:00 - a little bit similar as we do it on the P1
irrigation window definition”), switched again to a plain start time
(
input_datetime) + duration in hours (input_number) pair per light group - the exact same “window” shape already used forgrow_irrigation_p1_window_start/_endinpackages/grow_irrigation.yaml, just expressed as start+duration instead of start+end. - v4: per further feedback (“let’s also rework the control chart…
a simple beam chart with under canopy and top light underneath each
other”), replaced the 5-range
apexcharts-cardoverlay (4 series sharing one chart) with a simple nativehistory-graphtimeline - one row per light group. - v5 (current): per further feedback (“extend the controller to 4
lights (top and undercanopy each) and give me the options to disable
unused in the config. otherwise i am getting frequent errors of the not
available or not used lights”), extended each group from 2 to 4
slots, added
packages/grow_actuators.yamlswitch.grow_actuator_top_light_3/_4andswitch.grow_actuator_under_canopy_3/_4(plus the matching Dashboard 1 mapping fields), and gave every one of the 8 slots its own In Useinput_booleantoggle. The automation now builds its target entity list dynamically from only the enabled slots each cycle - a disabled slot is never commanded and never checked for a fault, which is what stops the notification spam for lights that aren’t actually wired up. See “How slots and the fault watchdog interact” below.
Per-grow-phase lighting profiles (a different window during Flowering, etc.) from v1 were not carried forward - can be added later as presets layered on top of this simpler design, if actually wanted.
How start time + duration becomes an on/off window
off time = start time + duration hours, wrapping past midnight the same
way the irrigation P1 window does. A few worked examples:
| Start | Duration | Effective window |
|---|---|---|
| 06:00 | 18h | on 06:00 - 24:00 (i.e. through end of day), off 00:00 - 06:00 |
| 12:00 | 12h | on 12:00 - 24:00, off 00:00 - 12:00 |
| 08:00 | 12h | on 08:00 - 20:00 (doesn’t reach midnight, no wrap) |
duration_hours is clamped 0-24 on the dashboard slider/field: 0 means
“never on,” 24 means “always on” - both handled explicitly rather than
left to fall out of the wrap math, since a full 24h window has no distinct
start/end left to compare against. Verified by dry-run test (including the
wrap and both edge-case durations) before shipping. This window applies
to the group as a whole - all of a group’s enabled slots share one
on/off time, there’s no per-light schedule.
How slots and the fault watchdog interact
Each group’s input_boolean.grow_light_<top|undercanopy>_<N>_enabled
(N = 1-4) controls whether switch.grow_actuator_<top_light|under_canopy>_<N>
participates at all:
- Disabled slot: never included in the group’s
switch.turn_on/turn_offtarget list, never checked by the fault watchdog, never contributes to the “Light Timeline” chart’s on/off aggregate. It’s as if that slot doesn’t exist. - Enabled slot: included in every on/off command for its group, and its state is part of the wait-and-verify check - if any enabled slot in the group fails to confirm the commanded state within 15s, that group’s fault flag turns on and the notification names exactly which entities were commanded.
- A group with zero slots enabled is skipped entirely each cycle - no command, no verification, no fault. Turning off the last enabled slot in a group (or leaving new slots 3-4 off, their default) means that group simply does nothing, silently, rather than erroring.
Slots 1-2 in both groups default enabled (the hardware every earlier version of this module assumed); slots 3-4 default disabled until you map them on Dashboard 1 and switch them on via the “Top Light Slots” / “Under Canopy Slots” cards on the Light view. Dry-run tested including the “zero slots enabled” and “mixed availability” cases.
How the automation decides on/off
On top of triggering on every start-time/duration/slot-toggle change (prompt reaction), a 5-minute heartbeat re-checks both groups’ windows and enabled-slot lists from scratch every cycle and self-corrects any drift - protects against a missed HA restart or reload leaving lights wrong for hours. Same fresh-evaluation-every-cycle rationale as the climate regulation automation (Phase 3) and the irrigation P1/P2 clamp (Phase 4); it matters most for Flowering-stage photoperiod integrity, where unexpected light during the dark period (“light leak”) is a real cultivation risk (hermaphroditism stress), not just cosmetic.
The Under-Canopy group only turns on when both its own window is
active and grow_light_undercanopy_enabled is on - the toggle lets
you disable the whole group without having to turn off every slot
individually.
Fault detection & notification
Every switch.turn_on/turn_off call (targeting only a group’s enabled
slots) is followed by a wait_template that confirms all of them actually
reached the commanded state within 15 seconds. If any doesn’t (device
offline, Zigbee drop, a mapping pointing at a stale entity, etc.):
input_boolean.grow_light_top_fault/_undercanopy_faultturns on - shown on the Light view’s status line and Master Control card.- A
persistent_notificationfires with a fixednotification_id(so repeated failures update the same notification instead of spamming a new one every 5-minute cycle), naming exactly which entities were commanded and suggesting you disable the slot if you don’t actually use it. - The automation does not give up - the next 5-minute heartbeat (or the next window transition) tries the command again automatically.
The fault clears itself and a persistent_notification.dismiss fires the
next time all enabled actuators do confirm the commanded state - no
manual “clear fault” step needed, same self-recovering shape as the
irrigation substrate failsafe (Phase 4), just without a separate
hysteresis threshold since this is a binary confirm/fail rather than a
continuous sensor reading.
Historical chart (timeline)
Dashboard 2’s Light view has a simple “beam” timeline - Home Assistant’s
native history-graph card, one horizontal row per light group, filled
while that group is on, so you can see at a glance exactly when each
group was actually on. This is a read-only visualization for
confirmation; use the Start Time / Duration fields above it to change
when lights turn on/off, and the Slots cards to change which lights
count.
Two light-weight template binary_sensors collapse each group’s
enabled slots into exactly 1 timeline row: binary_sensor. grow_light_top_active (on if any enabled switch.grow_actuator_top_light_1..4
is on) and binary_sensor.grow_light_undercanopy_active (same for the
under-canopy slots) - unavailable specifically means “zero slots enabled
for this group,” not a device-communication problem.
Bug fix (2026-08-27): these two entities were originally named
“Light: Top Light Active” / “Light: Under-Canopy Active” with no
default_entity_id pin. Home Assistant generates a template entity’s
entity_id from its name on first creation - unique_id alone does
NOT control it - so the actual entity_id ended up as
binary_sensor.light_top_light_active /
binary_sensor.light_under_canopy_active, not the
grow_light_top_active / grow_light_undercanopy_active every other
file in this project (Climate’s daytime-follows-light automation, Air’s
CO2 light-window check, this view’s own Light Timeline card) reads by
that exact hardcoded name. This is the root cause of “the Light Timeline
shows no chart at all.” Fixed by adding default_entity_id: to both
entities. If you already deployed the pre-fix version, the fix alone
won’t move an already-registered entity - see the rollout checklist
below for the one-time manual rename.
Wattage % + PPFD estimator (2026-08-27)
Two new optional per-group sensors let you see how hard each light group is currently running, and roughly what PPFD it’s putting out at that dim level, without a permanently-mounted PAR meter.
- Requires the Energy module’s per-device power sub-metering (Dashboard 1, “Energy Sub-Metering - Light Devices”) to be mapped for at least one slot in the group - this feature reuses those power readings rather than adding a second, separate power mapping.
input_number.grow_light_top_max_watts/_undercanopy_max_watts- set this to the group’s combined draw at 100% power across whichever slots you currently have enabled. This is a manually-entered reference value, not auto-detected - if you later enable/disable a slot or change a fixture, update it, or the % figure below will be off.sensor.grow_light_top_power_w/_undercanopy_power_w- live sum of the group’s mapped per-device power sensors fromgrow_core.yaml. Note this is scoped to just Top Light or just Under Canopy - a different grouping than the Energy module’s own “Light” cost bucket, which combines both groups together for billing purposes.sensor.grow_light_top_percent/_undercanopy_percent-power_w / max_watts * 100, guarded against division by zero both in availability and in the template itself.- PPFD calibration: with a PAR meter at a fixed 30cm distance
straight down at the canopy, dim the group to 20%, note the reading into
input_number.grow_light_top_ppfd_20(or_undercanopy_ppfd_20), then repeat at 50% and 100% into the matching_ppfd_50/_ppfd_100fields. Three points is enough for a useful estimate without needing a meter permanently wired in. sensor.grow_light_top_ppfd_estimate/_undercanopy_ppfd_estimate- piecewise-linear interpolation between your three calibration points (through the origin at 0%), evaluated live against the current % figure above. Deliberately kept simple per the confirmed scope: fixed 30cm reference only, no adjustment for actual canopy distance/height - if your canopy moves closer to or further from the light than your calibration distance, the estimate will be off by whatever the inverse-square difference is at your setup.
Dashboard (Light view, Dashboard 2)
A status banner (control state, fault flags, last decision), a Master Control entities card (control switch, under-canopy enable, both fault flags, last decision), a “Top Light” and an “Under Canopy” entities card (start time + duration each), “Top Light Slots” / “Under Canopy Slots” entities cards (4 In-Use toggles each), a 4x2 tile grid for the eight light actuators for manual override, and the “Light Timeline” beam chart.
Rollout checklist
- If you already deployed this module before 2026-08-27, do this
once before anything else: in Home Assistant, go to Settings ->
Devices & Services -> Entities, search for “Top Light Active” and
“Under-Canopy Active”. Click each, open its settings (gear icon), and
change the Entity ID field to
grow_light_top_active/grow_light_undercanopy_activerespectively (thebinary_sensor.prefix is fixed - you’re only editing the part after the dot). This corrects the entity_id Home Assistant already assigned them, which the newdefault_entity_id:in the YAML can’t retroactively fix on its own (it only takes effect the first time an entity with thatunique_idis ever created). Skip this step entirely on a fresh deploy - it isn’t needed there. - Deploy
packages/grow_light.yaml, the updatedpackages/grow_core.yaml(adds the 4 new mapping fields),packages/grow_mapping_tool.yaml(adds them to the search-based mapping picker), andpackages/grow_actuators.yaml(adds the 4 newswitch.grow_actuator_*proxies) - check config, restart (new helper entities need one). If you deployed an earlier version of this module (v1’s per-phase helpers, or v2’sschedule.*entities), those are now orphaned and will show as unavailable in Settings -> Helpers - safe to delete. - Re-import
dashboards/dashboard_1_configuration.yaml(adds the 4 new mapping rows),dashboards/dashboard_2_control_panel.yaml, anddashboards/dashboard_3_log.yaml. - On Dashboard 1, map Top Light 3/4 and Under Canopy 3/4 only if you actually have that hardware - leave them empty otherwise (an unmapped, disabled slot is fine and expected).
- On the Light view’s “Top Light Slots” / “Under Canopy Slots” cards, turn on the In-Use toggle for every slot you actually mapped, and make sure any slot you don’t have stays off (1-2 are on by default; 3-4 are off by default).
- Disable the old
Growzelt Zeitschaltuhr - Licht An/Aus,Growzelt - Undercanopy einschalten/ausschalten, andBlüten Trocknenautomations so they don’t fight this module over the same lights. - With
grow_light_control_enabledstill off, set each group’s Start Time and Duration. - Test manually via the Light view’s tile grid (or the Devices view) to confirm the mapped lights actually respond.
- Once you’re confident, turn
grow_light_control_enabledon (andgrow_light_undercanopy_enabledon too, if you want that group active). Watch Dashboard 3’s “Light Configuration & Automation Status” and “Device Activity” logbook cards, plus the historical chart, for the first day or two to confirm lights switch exactly when expected, and that no fault notification appears. If one does appear for a slot you don’t actually use, that’s the signal to turn its In-Use toggle off. - Optional - Wattage % / PPFD estimator: if you have power monitoring on your lights, map it under Dashboard 1’s “Energy Sub-Metering - Light Devices” section first. Then, per group, set the Max Wattage field to its 100%-power draw, and calibrate PPFD by taking 3 meter readings at 20/50/100% at a fixed 30cm distance and entering them into the PPFD calibration fields - both now live in Dashboard 1’s “Light Output Calibration” section (moved there 2026-08-29, alongside the other one-time setup fields; Dashboard 2’s Light view keeps only the live measured power, running %, and PPFD estimate readouts). Skip this step entirely if you don’t have power monitoring or a PAR meter - everything else in this module works without it.