Skip to content

jharris4/molight

Repository files navigation

MoLight — Smart Virtual Light Control for Home Assistant

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 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 lights on forever.

Installation

Requires Home Assistant 2026.1.0 or newer.

HACS (recommended)

  1. Add this repository as a custom repository in HACS.
  2. Install MoLight.
  3. Restart Home Assistant.
  4. Go to Settings → Devices & Services → Add Integration and search for MoLight.

Manual

Copy custom_components/molight/ into your HA config custom_components/ directory and restart.

Getting started

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:

  1. 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).
  2. Create a Virtual Light per room or light group, pointing it at the real light entities 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.
  3. 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.

Choosing the entity ID

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.kitchenkitchen), 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.

Bulk discovery

The three Discover… actions scan your existing entities and create a virtual wrapper for each pick:

  • Discover occupancy sensors — every binary_sensor with device class occupancy, motion, or presence.
  • Discover illuminance sensors — every sensor with device class illuminance.
  • Discover lights — every light entity.

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.

Bulk assignment

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 (control or gate).
  • Schedule — with its mode (follow or gate).

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.

Examples

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.

Entity reference

Virtual Occupancy Binary Sensor

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.

Virtual Combined Occupancy Binary Sensor

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.

Virtual Illuminance Binary Sensor

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.

Virtual Schedule Binary Sensor

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 time

e.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.

Virtual Light

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)

Attributes

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

State machine

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's latest_occupied_time, so each sensor's hold time is respected
  • ACTIVE/COUNTDOWN + timer expires → EFFECTWARNIDLE (with both stages disabled this collapses to going straight to IDLE)
  • 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:

  1. 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 IDLE and hands control back to the sensors until the next window boundary.
  2. 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).
  3. Forced offs — bright in illuminance control mode, and a gate-mode window ending, both turn the light off even while occupancy or a held-open door is active.
  4. 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_close door then holds the light like occupancy until it closes.

Effect / warn warning

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:

  1. Effect — a brief cue for effect warning duration seconds: the real lights are driven to the effect brightness (0 blinks them fully off). Skipped when its duration is 0.
  2. 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.

Maintain occupancy sensor

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_time of the two.
  • Forced offs still win, exactly as they do over regular occupancy: bright in control mode, 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.

Door sensor

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 as OCCUPIED at startup, and forced offs (bright in control mode, 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 blips unavailable keeps 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.

Holding auto-off

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 control mode; active occupancy keeps it on (when it's dark / in-window, like any adoption); an active follow window keeps it SCHEDULED; otherwise a fresh full timer starts.
  • A keep-on entity dropping to unavailable/unknown holds 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.

Brightness, color, and fades

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.

Restarts and unavailability

  • Real lights already on at startup are adopted (ACTIVE with a fresh timer); active occupancy (when dark / in-window) is claimed as OCCUPIED.
  • Follow-mode windows use schedule_window_start as 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/unknown are 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.

Development

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.

Running the tests

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 test

Working in the dev container

Every 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)

Running Home Assistant

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 breakpoints

Use 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.

Design notes

  • 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_timeout constraint 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).

About

Smart Virtual Light Control Entities for Home Assistant

Topics

Resources

License

Stars

9 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors