A HACS custom integration that provides composable virtual building blocks for lighting automation: wrap your real sensors and lights in virtual entities, wire them together from the UI, and get occupancy-, daylight-, and schedule-aware lighting without writing a single automation.
Writing these automations by hand is tedious, and the complexity grows fast once you account for real-world behavior: sensors with different hold timeouts, sensors that are good at triggering occupancy but not maintaining it (or vice versa), schedules that mix fixed times with sun events, Home Assistant restarts, and sources that drop to unavailable at the worst moment. MoLight handles all of that in a small set of reusable 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 |
| 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 |
Highlights — everything below is covered by the automated test suite:
- Lights turn off a configurable time after the person actually left — countdowns anchor to each sensor's own hold time, not the moment it happens to clear.
- Occupancy takes over manually turned-on lights, so they still turn off after the room empties — but false detections (a fly, a heat blip) are classified and never cut short lights the user turned on.
- Turn-ons can be gated on darkness and/or a schedule window; getting bright can force lights off (or not, for lux sensors that can see the lights they control).
- Follow-mode schedules give porch-light behavior — on at window start, off at window end — while respecting manual overrides mid-window.
- Optional effect/warn warning: blink or dim before an automatic turn-off, then a grace period to re-trigger, instead of sudden darkness.
- Every virtual light gets a companion Auto-off switch, and any on/off entity can act as a keep-on hold (guest mode, movie night) that suspends automatic turn-offs.
- Restarts and
unavailablesources 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 lights on forever.
Requires Home Assistant 2026.1.0 or newer.
- Add this repository as a custom repository in HACS.
- Install MoLight.
- Restart Home Assistant.
- Go to Settings → Devices & Services → Add Integration and search for MoLight.
Copy custom_components/molight/ into your HA config custom_components/ directory and restart.
Everything is configured from the UI — no YAML. Adding an entry (the first via Add Integration → MoLight, later ones via Add Entry on the MoLight card) opens a menu with three ways to proceed:
- Create a single entity — pick a type and fill in its form.
- Discover… — scan existing entities and bulk-create virtual ones (see Bulk discovery).
- Assign a sensor to several lights — wire one sensor into many lights at once (see Bulk assignment).
The usual order:
- Create the virtual sensors you want lights to react to (all optional): an occupancy sensor per real motion/presence sensor, a combined sensor to merge several, an illuminance sensor, a schedule sensor. When you create an occupancy sensor, take care to set its occupancy timeout to match the real sensor's own hold time — it's the anchor for everything downstream, and MoLight can't read it for you (see the note in the reference).
- Create a Virtual Light per room or light group, pointing it at the real
lightentities and referencing any of the sensors from step 1. A virtual light with no sensors is still useful — it turns its lights off on a timer. - Use the virtual light in dashboards and voice assistants instead of the real lights.
Order matters only in that a virtual entity must exist before another can reference it. Sensors are reusable — one occupancy or illuminance sensor can serve many lights. Every entry can be edited later via its Configure button, or removed independently.
Every create form ends with an optional Entity ID field — handy when you name virtual entities after the real ones they wrap and don't want HA's _2 suffix behavior:
- Leave it blank to derive the ID from the name. If that ID is already taken, the flow warns you and offers to proceed (HA appends
_2) or go back, prefilled, and set one yourself. - Type one to pin it. A domain prefix is tolerated and stripped (
light.kitchen→kitchen), the rest is slugified. A conflicting ID re-shows the form with an error.
The field only appears when creating; to rename later, use HA's own entity settings.
The three Discover… actions scan your existing entities and create a virtual wrapper for each pick:
- Discover occupancy sensors — every
binary_sensorwith device classoccupancy,motion, orpresence. - Discover illuminance sensors — every
sensorwith device classilluminance. - Discover lights — every
lightentity.
Each starts with an optional filter form: pick areas and/or labels to narrow the scan (an entity matches through its own assignment or its device's), and choose whether the checklist starts with everything pre-selected (bulk-add, the default) or empty (handy when you only want a few). Leave the filters blank to see everything.
The next form shows the checklist of matching entities. Only useful candidates appear: MoLight's own entities, disabled entities, and anything already wrapped are hidden — re-running discovery later only offers what's new.
An optional prefix/suffix distinguishes the virtual entities from the real ones, applied verbatim (you control the spacing) to a target of your choice:
- Entity ID (default) — only the entity ID gets the affix (
v_→binary_sensor.v_hallway); the friendly name stays identical to the source. - Name — the friendly name gets the affix, and the entity ID derives from the composed name.
A final form then lets you adjust the default settings applied to every pick — for discovered lights that includes the occupancy/illuminance/schedule references. Each created entity can still be edited individually afterwards via Configure.
Assign a sensor to several lights wires one shared sensor into many virtual lights in a single pass. Pick the kind of sensor, the sensor, and how the lights should use it:
- Occupancy — with a role: regular (turns lights on and off) or maintain (only holds an already-on light on).
- Illuminance — with its mode (
controlorgate). - Schedule — with its mode (
followorgate).
The final step lists your virtual lights with current users of that sensor pre-selected, so the checklist doubles as an audit of the wiring. The submitted set is authoritative: ticked lights get the reference and mode, unticked pre-selected lights have it removed, and the summary reports both counts.
Because occupancy feeds the turn-off countdown, the light_timeout >= occupancy_timeout guard applies here too: lights whose turn-off timeout is shorter than the sensor's effective timeout are skipped and named in the summary, so you can raise their timeouts and re-run.
For worked examples with the exact field values to enter — a plain turn-off timer, a single-sensor room, the full occupancy/illuminance/schedule setup, and a porch light — see EXAMPLES.md.
Wraps a single real binary sensor (motion, presence, occupancy…). on mirrors the source directly; the value the rest of the system runs on is the latest_occupied_time attribute — the best estimate of when the person actually left.
| Config | Description |
|---|---|
| Source sensor | The real binary_sensor to wrap (device class occupancy, motion, or presence). MoLight's own occupancy entities are excluded — wrap the real sensor, or combine virtual ones with a combined sensor |
| Occupancy timeout (s) | The source's own hold time — set this to match the real sensor (see the note below). When it clears, latest_occupied_time is back-dated to clear time − timeout |
| False-detection grace (s) | 0 disables. A cycle whose on-duration exceeds the timeout by no more than the grace contained exactly one instantaneous detection — almost certainly a fly/heat blip. Such cycles don't advance latest_occupied_time, are counted in false_detection_count, and flag the clear via last_clear_false_detection so lights can turn off quickly |
| Clear after unavailable (s) | 0 disables, default 60. If the source goes unavailable/unknown while occupancy is active, latest_occupied_time advances to the dropout moment immediately, and if the source hasn't recovered after this many seconds the occupancy clears, flagged via last_clear_unavailable. Never classified as a false detection — the room may still be occupied, so dependent lights run their normal gentle countdown. A recovery cancels the pending clear |
Important
Set the occupancy timeout to match the real sensor's actual hold time. MoLight can't read this from the source — it's a value you supply, and everything downstream is anchored to it: when MoLight decides the person actually left, the turn-off countdown, and false-detection classification. Set it too high and genuine occupancy can be misread as a false detection (on_duration ≤ timeout + grace), sending the lights off early via the quick-off path; set it wrong in either direction and turn-off timing drifts from reality.
This value has no effect on the source sensor — it doesn't change the real sensor's hold time, it only tells MoLight what that hold time is. The two are not linked, so if you ever change the source sensor's own timeout, update this to match by hand.
Attributes: latest_occupied_time, occupancy_timeout, last_on_time, last_clear_false_detection, false_detection_count, last_clear_unavailable.
Combines multiple MoLight occupancy sensors into one. Constituents are usually simple Virtual Occupancy Sensors, but other combined sensors can be nested too — the flows reject self-references and cycles, and a sensor can't hold both roles at once.
| Config | Description |
|---|---|
| Trigger sensors | Any one going on starts occupancy |
| Maintain sensors (optional) | Keep occupancy alive once started, but cannot start it alone |
latest_occupied_time is the max across all constituents, so each sub-sensor's individual timeout is respected. A combined cycle during which no constituent advanced latest_occupied_time was made up entirely of false cycles and is flagged/counted the same way as on the simple sensor.
After a restart, a maintain sensor that has already been on for more than 5 seconds is assumed to reflect occupancy triggered before HA went down, and seeds the sensor on (the one exception to "maintain sensors never start occupancy").
Attributes: latest_occupied_time (max across all constituents), last_clear_false_detection, false_detection_count.
on = at or above the threshold (bright enough, no artificial lighting needed)
off = below the threshold (dark enough to warrant lighting)
| Config | Description |
|---|---|
| Source sensor | Any real sensor with device_class: illuminance |
| Threshold (lx) | The lux level at which the sensor reports on |
| Hysteresis (lx) | 0 disables. Becomes bright at threshold + hysteresis, dark below threshold − hysteresis; readings inside the band hold the current state, suppressing flapping when the light level hovers around the threshold |
An unavailable or unparsable source holds the last known value — a lux sensor dropping out must not read as "it got dark". The state also survives restarts.
Attributes: none beyond the standard bright/dark (on/off) state.
on = current time is within an active window. Transitions are event-scheduled (no polling) and fire within a second of the boundary. Overnight windows (e.g. 22:00 → 06:00) are supported.
Each window edge is a fixed time, a sun event, or both:
start:
time: "21:00" # fixed local time
sun: sunset # or sunrise
offset: -15 # minutes relative to the sun event
combine: latest # latest | earliest — which of time/sun wins
end: "07:00" # plain string = fixed timee.g. start at the later of sunset − 15 min and 21:00. On polar days where the sun event doesn't occur, the fixed time stands alone.
Attributes: current_window_start (identifies the active window; used by follow-mode lights for restart catch-up), next_transition.
Controls N real lights with an occupancy-aware state machine.
| Config | Description |
|---|---|
| Lights | Real light entities to control |
| Turn-off timeout (s) | Must be >= the occupancy timeout of any referenced occupancy entity |
| False-detection off delay (s) | When occupancy clears flagged as a false detection, lights that were lit by that cycle turn off after this short delay instead of the normal countdown. Lights turned on manually are never affected |
| Auto-on brightness (%) (optional) | Brightness applied when the light turns on automatically — by occupancy, a door opening, illuminance going dark, or a schedule window. Manual and physical turn-ons keep their own brightness. Blank = automatic turn-ons use the real lights' own last/default brightness |
| Auto-on color temperature (K) (optional) | White color temperature applied on automatic turn-ons, for members that support it (a warm hallway at night). Manual and physical turn-ons keep their own color. Mutually exclusive with the auto-on color |
| Auto-on color (optional) | RGB color applied on automatic turn-ons, for members that can show it. Mutually exclusive with the auto-on color temperature |
| Auto-on fade (s) (optional) | Fade time for automatic turn-ons. Blank or 0 sends no transition. Manual and physical turn-ons never get one |
| Auto-off fade (s) (optional) | Fade time for automatic turn-offs — timer expiry, bright forcing off, a window ending. A manual off is always immediate |
| Effect warning duration (s) | 0 disables. When the turn-off timer expires, first show a brief effect cue for this long instead of going dark (see Effect / warn warning) |
| Effect brightness (%) | Brightness during the effect stage. 0 blinks the real lights fully off — a distinct "about to turn off" flash |
| Effect color (optional) | RGB color during the effect stage, for members that can show it. Requires an effect brightness above 0 (a blink fully off has no color to show) |
| Effect fade (s) (optional) | Fade into the effect brightness. Must fit within the effect duration (a fade on a disabled stage is rejected too) |
| Warning grace period (s) | 0 disables. After the effect, the light stays on this long before finally turning off, giving you time to re-trigger |
| Warning brightness (%) (optional) | Brightness during the grace period. Blank keeps whatever brightness the light had before the warning began (full brightness if it never reported one) |
| Warning color (optional) | RGB color during the grace period — e.g. red as an unmissable "about to turn off" cue. Blank keeps the color the lights already had |
| Warning fade (s) (optional) | Fade into the warning brightness. Must fit within the grace period |
| Occupancy sensor (optional) | A MoLight occupancy sensor (simple or combined) |
| Maintain occupancy sensor (optional) | Keeps an already-on light on while occupied but never turns it on (see Maintain occupancy sensor) |
| Illuminance sensor (optional) | A MoLight Virtual Illuminance Binary Sensor |
| Illuminance mode | control — dark gates turn-ons AND turning bright forces the lights off. gate — dark gates turn-ons only; bright never turns lights off. Use gate when the lux sensor can see the controlled lights, which would otherwise oscillate |
| Schedule sensor (optional) | A MoLight Virtual Schedule Binary Sensor |
| Schedule mode | follow — lights turn on at window start and off at window end (porch lights). gate — occupancy may only activate lights inside the window; window end forces lights off |
| Door sensor (optional) | A real door/contact binary sensor (on = open). Opening it turns the lights on, gated by darkness and a gate-mode window exactly like occupancy (see Door sensor) |
| Door mode | open — opening turns the lights on with the normal timeout; the door is otherwise ignored. open_close — the lights stay on while the door is open and start the countdown when it closes |
| Keep-on entities (optional) | Any entities with an on/off state. While any is on, auto-off is held (see Holding auto-off) |
| Attribute | Description |
|---|---|
molight_state |
Current state-machine state, as a lowercase value: idle, active, occupied, countdown, scheduled, effect, warn |
auto_off_held |
Whether auto-off is currently held (Auto-off switch off or a keep-on entity on) |
last_on_physical / last_on_virtual |
Timestamp of the last turn-on at the wall vs. via the virtual light |
last_on_occupancy / last_on_illuminance |
Timestamp of the last turn-on caused by occupancy vs. going dark |
last_on_door |
Timestamp of the last turn-on caused by the door opening |
last_brightness_change_physical / last_brightness_change_virtual |
Timestamp of the last brightness change from each source |
last_color_change_physical / last_color_change_virtual |
Timestamp of the last color change from each source |
pre_warn_brightness / pre_warn_color |
Brightness and color saved before an effect/warn stage, so a restart mid-warning can restore them; null except mid-sequence |
schedule_window_start |
Follow-mode window marker used for restart catch-up |
IDLE lights off, no timer
ACTIVE lights on, timer running (manual/external turn-on, no occupancy)
OCCUPIED lights on, occupancy active — timer suspended
COUNTDOWN occupancy cleared, timer ticking toward lights-off
SCHEDULED lights on inside a follow-mode window — no timer
EFFECT auto-off imminent — showing the brief effect/blink warning stage
WARN auto-off imminent — grace period before the lights go off
IDLE+ manual/external turn-on →ACTIVE(timer starts)IDLE/ACTIVE/COUNTDOWN+ occupancy becomes active (and it's dark / in-window) →OCCUPIED- Already-active occupancy is adopted the same way: turning the light on (manually or at the wall) while the occupancy sensor is on goes straight to
OCCUPIED, as does illuminance turning dark or a gate-mode window opening while the light is on — a timer never expires despite presence OCCUPIED+ occupancy clears →COUNTDOWN; the timer is the turn-off timeout anchored to the sensor'slatest_occupied_time, so each sensor's hold time is respectedACTIVE/COUNTDOWN+ timer expires →EFFECT→WARN→IDLE(with both stages disabled this collapses to going straight toIDLE)- any state + all real lights turned off externally →
IDLE - follow-mode window start →
SCHEDULED; occupancy and illuminance are ignored until the window ends. Boundaries are edge-triggered, so manual changes mid-window stand — including turning the light back on, which rejoins the window instead of starting a timer
Manual control is never gated: the user can always turn the virtual light on, even when it's bright or outside a schedule window. The current state is exposed as the molight_state attribute.
Precedence when sources conflict. With several sources configured on one light, control resolves top-down:
- Manual / physical control — always wins and is never gated; a manual off turns the light off from any state. A manual off mid follow-window drops to
IDLEand hands control back to the sensors until the next window boundary. - Follow-mode schedule window — while
SCHEDULED, the window owns the light: occupancy, maintain, illuminance, and door changes are ignored entirely (window start forces on, window end forces off). - Forced offs — bright in illuminance
controlmode, and a gate-mode window ending, both turn the light off even while occupancy or a held-open door is active. - Occupancy and door opening — turn the light on only when it's dark (illuminance off) and inside a gate-mode window; otherwise lowest priority. An
open_closedoor then holds the light like occupancy until it closes.
By default the light turns off the instant its timer expires. Setting an effect and/or warn duration flags the impending turn-off first, so a room isn't dropped into darkness without notice:
- Effect — a brief cue for effect warning duration seconds: the real lights are driven to the effect brightness (
0blinks them fully off). Skipped when its duration is0. - Warn — a grace period of warning grace period seconds at the warning brightness (or the brightness the light already had, if blank), then the lights turn off. Skipped when its duration is
0.
Each stage can also show an optional color — a red warn stage is a much clearer "about to turn off" cue than a dim. Color-capable members show it; brightness-only members just show the stage brightness. A warn stage without a color of its own undoes an effect-stage recolor. One caveat: most lights restore their last color on the next turn-on, so after an auto-off that ended at the warning color, the real lights' next manual turn-on may come back in that color (the same already applies to the warning brightness).
Each stage can fade into its brightness over its optional fade time; a fade must fit inside its stage (a fade on a disabled stage is rejected rather than silently ignored).
Throughout both stages the virtual light stays on. Any re-trigger during the sequence behaves exactly as if the pre-off timer were still running — occupancy or maintain becoming active, a manual or physical turn-on, or an external dim cancels the warning. A re-trigger that carries no brightness or color of its own (occupancy, a turn-on without an explicit brightness) restores the pre-warning brightness and color, so the interruption leaves no trace; a physical turn-on or an external dim/recolor brings its own values, which are honored instead. The restore is deliberately immediate (no fade). Holding auto-off mid-sequence aborts it the same way, and the forced-off rules (bright in control mode, a gate/follow window ending) still turn the lights off during the sequence, just as they would mid-countdown.
The maintain occupancy sensor holds an already-on light on while it shows presence — it never turns the light on. Unlike a combined sensor's maintain sensors (which only extend occupancy started by a trigger sensor), it holds the light regardless of how it was lit: manual, wall switch, or occupancy. Typical use: an over-sensitive presence sensor (mmWave) that would false-trigger as an occupancy source but is perfect for keeping a room lit while someone sits still.
- The light being on with the maintain sensor on means
OCCUPIED— whether the sensor turns on later, was already on at turn-on time, or both were already on at startup. Illuminance/schedule gating doesn't apply, since this is not a turn-on. - The countdown starts only when the regular occupancy sensor and the maintain sensor are both clear, anchored to the latest
latest_occupied_timeof the two. - Forced offs still win, exactly as they do over regular occupancy: bright in
controlmode, a gate-mode window ending, and a manual off all turn the light off immediately; a follow-mode window owns the light entirely. - The false-detection quick off fires on a maintain clear only when both sensors flagged their clears false — genuine presence on either side earns the normal countdown.
A door sensor drives the light straight from a real door/contact binary_sensor (on = open) — a pantry, closet, wardrobe, or garage light. Opening the door is a turn-on trigger, gated by illuminance and a gate-mode schedule exactly like occupancy: it only lights the room when it's dark (if an illuminance sensor is set) and inside a gate window. What happens next depends on the door mode:
open— opening turns the lights on with the normal turn-off timeout (ACTIVE), then the door is ignored: closing does nothing and the lights time out even if the door stays open. Re-opening re-triggers the timer. Use it as a momentary "someone came through here" trigger.open_close— the open door holds the lights on with no timer (OCCUPIED, just like occupancy) for as long as it stays open, and closing starts the auto-off countdown. The close defers to presence: if a regular occupancy or maintain sensor is still active, or a keep-on entity is holding auto-off, the lights stay on — a closed door never cuts the lights over someone the room still sees. An already-on light with the door open is adopted asOCCUPIEDat startup, and forced offs (bright incontrolmode, a gate window ending, a manual off) still win over a held-open door, just as they do over occupancy. A standing-open door is also re-evaluated when a gate lifts — the room going dark or a gate-mode window starting lights the room and holds it while the door stays open — and the door's last known state is cached, so a sensor that blipsunavailablekeeps holding until it reports closed.
The door sensor is a plain real sensor, so its picker is narrowed to door-ish device classes (door, garage door, opening, window) rather than to MoLight virtual sensors. The last door-driven turn-on is exposed as the last_on_door attribute.
Each virtual light also creates a companion <name> Auto-off switch. Auto-off is held while that switch is off or any configured keep-on entity is on:
- Every automatic turn-off is suspended — the timer, the false-detection quick off, bright-forces-off, and schedule window ends. The state machine keeps transitioning; it just never arms a timer.
- Turn-ons are unaffected (occupancy, going dark, and window starts still light the room), and a manual off always works.
- When the last hold releases, the light re-evaluates its rules: a follow window that ended while held turns it off now, as does being outside a gate window or bright in
controlmode; active occupancy keeps it on (when it's dark / in-window, like any adoption); an active follow window keeps itSCHEDULED; otherwise a fresh full timer starts. - A keep-on entity dropping to
unavailable/unknownholds its last known value (a dead toggle never reads as "hold released" — or engaged). The switch state survives restarts, and a held light adopted at startup won't start a timer.
Share one keep-on entity (e.g. input_boolean.guest_mode) across all your virtual lights for a global "don't touch the lights" toggle, or give a single room its own. The current hold status is exposed as the auto_off_held attribute.
The virtual light supports brightness. External brightness changes on the real lights count as human activity and restart a running timer; brightness 0 is treated as off (and 0 → non-zero as a turn-on). Physical and virtual changes are tracked separately (last_brightness_change_physical / _virtual), as are the reasons the light last activated (last_on_physical / _virtual / _occupancy / _illuminance / _door).
Color works the same way, and its capabilities come from the real lights: the virtual light offers a color wheel when any member can show a color and a color-temperature slider when any member supports one, and stays brightness-only otherwise. Mixed setups need no configuration — a color command goes to all members in one call and Home Assistant filters/converts it per light, so the bulbs that can go red go red and the rest just dim. The virtual light mirrors the first lit member's color, and an external recolor restarts a running timer exactly like an external dim (last_color_change_physical / _virtual).
An optional auto-on brightness forces a level whenever the light comes on automatically; manual and physical turn-ons are left alone, so you can dim the room by hand without it snapping back. An auto-on color temperature or auto-on color does the same for color — think warm white for the night-time hallway. Likewise, the auto-on and auto-off fades apply only to automatic actions — flipping the switch always responds immediately. A blank fade sends no transition attribute at all, so lights keep their integration's default behavior.
- Real lights already on at startup are adopted (
ACTIVEwith a fresh timer); active occupancy (when dark / in-window) is claimed asOCCUPIED. - Follow-mode windows use
schedule_window_startas a marker: a boundary missed while HA was down is applied exactly once at startup, while a manual off mid-window is respected. A restart landing mid effect/warn restores the pre-warning brightness and color. - Entities dropping to
unavailable/unknownare never read as state changes, at any layer; recovery transitions are processed as real events. A source sensor that stays unavailable is handled by the occupancy sensor's clear after unavailable timeout, so a dead motion sensor can't hold lights on forever.
Local development uses two independent containers, each with a distinct job. They are unrelated (no shared network or startup dependency), but both bind port 8123, so only one can run at a time.
| Container | Image | What it's for |
|---|---|---|
Dev container (dev:*) |
generic Debian + Python 3.14 | Your toolchain — editing, pytest, ruff. VS Code attaches here. HA is pip-installed into a venv (/opt/molight-venv) as a library. |
HA runtime (hass:*) |
official home-assistant:stable |
The real Home Assistant app, for manual/UI testing. Your integration is mounted read-only. |
npm scripts follow a <target>:<action> naming scheme so the prefix tells you which container you're touching.
npm install # install tooling
npm run hass:down # free port 8123 (devcontainer and HA runtime can't coexist)
npm run dev:up # start the devcontainer (fast after first build)
npm testEvery command below runs inside the dev container via devcontainer exec:
npm run dev:up # create/start the dev container (fast after first build)
npm test # pytest
npm run lint # ruff check
npm run format # ruff format
npm run dev:shell # open a bash shell inside the container
npm run dev:stop # stop the container, keeping it for a fast dev:up next time
npm run dev:down # stop and remove the container (dev:up recreates it)
npm run dev:rebuild # tear down and rebuild from scratch (e.g. after changing devcontainer.json)There are two ways to get a live HA instance, depending on what you're testing:
npm run hass:up # run stock HA (stable image) with the integration mounted read-only
npm run hass:down # stop it and free port 8123
npm run dev:hass # run HA from source inside the dev container — code is live-editable,
# and debugpy is available for breakpointsUse hass:up to confirm behavior against a real, released HA build; use dev:hass for active development, where HA runs against your working tree and can be restarted and debugged in place. Remember to hass:down before starting the dev container (or vice versa) so port 8123 is free.
- Each virtual entity is its own config entry, so they can be created, edited, and removed independently.
- A virtual entity must exist before another can reference it (sensors before the lights that use them).
- Removing an entry strips references to its entities from the entries that survive it — a light whose schedule sensor is deleted loses the reference instead of keeping a gate that can never open.
- The
light_timeout >= occupancy_timeoutconstraint is validated in both directions: creating/editing a light checks its referenced occupancy entity (including through a combined sensor), and raising an occupancy sensor's timeout checks every light that depends on it. - Reference graphs stay sane by construction: an occupancy sensor can't wrap another MoLight occupancy entity, a combined sensor can't reference itself or form a cycle through other combined sensors, and a constituent can't be both a trigger and a maintain sensor.
- Once an entry's options have been edited, the options fully replace the original data (so cleared optional fields stay cleared).