MoLight β€” composable virtual entities for occupancy/daylight/schedule-aware lighting (no automations)

Hi all :waving_hand:

I've been building MoLight, a HACS custom integration that lets you get occupancy-, daylight-, and schedule-aware lighting without writing a single automation. Instead of YAML, you wrap your real sensors and lights in virtual building blocks and wire them together entirely from the UI.

The problem it solves

Hand-writing "turn the lights off a while after the room empties" automations starts simple and gets ugly fast once you deal with the real world:

  • motion sensors with different hold timeouts
  • sensors that are great at triggering occupancy but bad at maintaining it (mmWave, I'm looking at you), or vice-versa
  • schedules that mix fixed times and sun events
  • Home Assistant restarts landing in the middle of a countdown
  • a sensor dropping to unavailable at the worst possible moment

MoLight folds all of that into a small set of reusable, composable entities.

The building blocks

Each is its own config entry β€” create as many as you like:

Entity What it does
Virtual Occupancy Sensor Wraps one motion/presence sensor; estimates when the person actually left
Virtual Combined Occupancy Sensor Merges several occupancy sensors with trigger/maintain roles
Virtual Illuminance Sensor Turns a lux reading into a steady bright/dark signal (with hysteresis)
Virtual Schedule Sensor On inside time windows defined by fixed times and/or sun events
Virtual Light Controls N real lights with an occupancy/illuminance/schedule-aware state machine

Sensors are reusable β€” one occupancy or illuminance sensor can feed many lights. You use the virtual light in your dashboards and voice assistants instead of the real ones.

Some things it handles that I'm happy with

  • Countdowns anchor to when the person actually left, not the moment a sensor happens to clear β€” each sensor's own hold time is respected.
  • False-detection filtering β€” a fly or a heat blip is classified and won't cut short lights you turned on by hand, but it can let auto-lit lights turn off quickly.
  • Manual control is never gated. You can always turn a light on, even when it's bright or outside a schedule window.
  • Follow-mode schedules give porch-light behavior (on at window start, off at window end) while respecting manual overrides mid-window.
  • Optional blink/dim warning before an automatic turn-off, plus a grace period to re-trigger β€” instead of a room suddenly going dark.
  • Keep-on holds β€” every virtual light gets a companion Auto-off switch, and any on/off entity (e.g. input_boolean.guest_mode) can suspend automatic turn-offs for guest mode / movie night.
  • Restarts and unavailable sources are handled everywhere β€” missed schedule boundaries are applied exactly once, sensor blips are never misread as state changes, and a dead motion sensor can't hold the lights on forever.

Everything is local_push, no cloud, no polling β€” schedule transitions fire on the exact boundary. And all of the behavior above is covered by an automated test suite.

Bulk setup

If you've got a lot of rooms, there are Discover… actions that scan your existing entities and bulk-create virtual wrappers, plus an Assign a sensor to several lights flow that wires one shared sensor into many lights in a single pass.

Install

It's a HACS custom repository:

  1. HACS β†’ add https://github.com/jharris4/molight as a custom repository (type: Integration)
  2. Install MoLight, restart HA
  3. Settings β†’ Devices & Services β†’ Add Integration β†’ MoLight

Everything after that is configured from the UI β€” no YAML.

Repo / full docs: GitHub - jharris4/molight: Smart Virtual Light Control Entities for Home Assistant Β· GitHub

This is a fairly new project (currently v1.3.0), so I'd really value feedback, bug reports, and ideas β€” especially edge cases around weird sensors and restart timing. Happy to answer questions in this thread. :folded_hands:

2 Likes

Since the configuration of MoLight is all UI-driven, here are some examples of the actual field values you'd enter for various scenarios. Every create form starts with a Name (the entity ID derives from it) and ends with an optional Entity ID override if you want to pin it. :folded_hands:


Example 1 β€” Just a turn-off timer (the simplest case)

No sensors at all β€” a virtual light that turns its lights off a set time after they come on. Great for a closet, pantry, or garage where you just want "never left on all day." One entry.

Virtual Light

Name:              Pantry                       # β†’ light.pantry
Lights:            light.pantry_real            # pin the Entity ID field if it
Turn-off timeout:  300         # off 5 min after it's turned on

Turn it on (app, voice, or wall switch) β†’ 5 minutes later it turns itself off. That's the whole thing.


Example 2 β€” Simple occupancy case

One motion sensor, lights off a bit after you leave. Two entries.

1. Virtual Occupancy Sensor β€” wraps the real motion sensor

Name:                 Hallway Occupancy        # β†’ binary_sensor.hallway_occupancy
Source sensor:        binary_sensor.hallway_motion
Occupancy timeout:    30      # my sensor holds "on" ~30s after last motion
False-detection grace: 5      # a single instantaneous blip = fly/heat, ignored
Clear after unavailable: 60   # if the sensor dies, don't hold lights forever

2. Virtual Light β€” points at the real light, references the sensor above

Name:              Hallway                     # β†’ light.hallway  (pin the Entity ID
Lights:            light.hallway_real          #    field if it clashes with the real one)
Turn-off timeout:  60         # 60s after you actually left
Occupancy sensor:  binary_sensor.hallway_occupancy

Walk in β†’ lights on. Room empties β†’ 60s countdown anchored to when you actually left (not when the sensor cleared) β†’ off. Turn it on by hand and it still turns off after the room empties, but a false blip never cuts short a manual on.

Note the guard: Turn-off timeout (60) must be β‰₯ the occupancy timeout (30). MoLight enforces this both ways.


Example 3 β€” Living room (the whole toolbox)

Motion for triggering, an mmWave sensor to hold while you sit still, only when it's dark, and a warning blink before it drops you into darkness.

1. Virtual Occupancy Sensor β€” the trigger (regular PIR)

Name:               Living Occupancy           # β†’ binary_sensor.living_occupancy
Source sensor:      binary_sensor.living_motion
Occupancy timeout:  30

2. Virtual Occupancy Sensor β€” the mmWave, used only to maintain

Name:               Living Presence            # β†’ binary_sensor.living_presence
Source sensor:      binary_sensor.living_mmwave
Occupancy timeout:  120

(mmWave is too twitchy to start occupancy, but perfect for keeping the room lit while you're still.)

3. Virtual Illuminance Sensor

Name:           Living Dark Enough             # β†’ binary_sensor.living_dark_enough
Source sensor:  sensor.living_lux
Threshold:      40
Hysteresis:     10     # bright at 50lx, dark below 30lx β€” stops flapping

4. Virtual Light

Name:                   Living Room            # β†’ light.living_room
Lights:                 light.living_lamps
Turn-off timeout:       180
Occupancy sensor:       binary_sensor.living_occupancy    # regular = turns on & off
Maintain occupancy:     binary_sensor.living_presence     # only holds an on light on
Illuminance sensor:     binary_sensor.living_dark_enough
Illuminance mode:       control   # dark gates turn-ons AND bright forces off
Auto-on brightness:     60%       # automatic turn-ons come up at 60%; manual left alone
Effect warning duration: 3        # 3s "about to turn off" cue...
Effect brightness:       0%       # ...a blink fully off
Warning grace period:    20       # then 20s at current brightness to re-trigger

Behavior: motion + it's dark β†’ lights on at 60%. Sit still β†’ mmWave keeps them on even after the PIR clears. Leave β†’ countdown starts only once both sensors are clear. Before turning off you get a quick blink, then 20s grace to wave and cancel. Get up in that window and it's as if nothing happened β€” original brightness restored, no trace.

Use Illuminance mode: gate instead if your lux sensor can see the lights it controls (otherwise they'd oscillate).


Example 4 β€” Porch light (schedule follow-mode)

On at the later of sunsetβˆ’15 and 21:00, off at 07:00. The schedule window is just a set of fields on the form β€” one row for the start edge, one for the end:

1. Virtual Schedule Sensor

Name:          Porch Schedule                  # β†’ binary_sensor.porch_schedule
Start time:    21:00
Start sun:     sunset
Start offset:  -15         # minutes relative to the sun event
Start combine: latest      # whichever of time / sun is later wins
End time:      07:00       # (no sun anchor on this edge)

2. Virtual Light

Name:            Porch                          # β†’ light.porch
Lights:          light.porch_real
Schedule sensor: binary_sensor.porch_schedule
Schedule mode:   follow    # on at window start, off at window end

follow = porch-light behavior; the window owns the light but manual changes mid-window still stand. Use gate instead if you want occupancy to control the light only inside the window.


Global "don't touch my lights" toggle: make an input_boolean.guest_mode and drop it into the Keep-on entities field of any virtual light β€” while it's on, every automatic turn-off is suspended (timers, forced-offs, window ends), but turn-ons and manual off still work. Every light also gets its own companion ... Auto-off switch for the same thing per-room.

I really like the possibilities with MoLight. It looks like it'll solve a lot of edge cases in lighting automation that I just haven't figured out how best to deal with.

I'm still thinking on how to incorporate door sensors here. But I'm sure liking all the situations you've covered for PIR/radar motion & occupancy, and illuminance, & sun position, & timing / schedules. Nice work!

1 Like

Thanks for the feedback!

I thought about door sensors. I have a couple in my house but don’t use them for lighting automation.

I could see two scenarios:

  • turn on lights when door opens then countdown to off
  • turn on lights when a door opens and leave it on until closed.

Kind of like a motion vs closet mode.

1 Like

I like the idea of handling all the occupancy and lighting logic through reusable virtual entities instead of maintaining lots of automations. The attention to edge cases like restarts, sensor hold times and manual overrides is especially impressive. I'll definitely be keeping an eye on this project.

1 Like

Yep I've got a couple places where I use door sensors for lighting control.

For closets, I should really be using door sensors for both lights-on & off. (vs PIR motion + countdown). I do like having some delay before turn-off, e.g. if someone changes their mind & re-enters the closet.

My main case is door-open event to get an earliest possible lights-on when entering rooms that have normally closed door. And then motion sensors for occupancy, and countdown after unoccupied. Person often closes the door once they're in the room, so door sensor can't tell me anything re when they left that room; just when they entered.

I also have some door-open logic that turns on lights in the area on the other side of the door, e.g., if anyone in the room goes out into an unlit hallway, or leaves an outbuilding at night, to turn on outside lights. Those have proven very useful.