Skip to content

Latest commit

 

History

124 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Presence Based Lighting

GitHub Release GitHub Activity License

pre-commit Black

hacs Project Maintenance BuyMeCoffee

Discord Community Forum

Intelligent, metadata-driven presence automation with manual override support for Home Assistant.

Drive lights, fans, or any switchable entity directly from HA service metadata. Presence-based actions stay in sync with manual control, and each controlled entity gets its own "Presence Allowed" toggle.

Features

  • Automatic entity control driven by presence sensors
  • ⚙️ Per-entity actions – choose exactly which services/states to call when presence appears or clears (with No Action option)
  • 🎯 Smart manual override – external control pauses automation until you re-enable it
  • 🏠 Multi-room + multi-entity – configure multiple rooms, each with any number of controlled entities
  • ⏱️ Global or per-entity delays – override turn-off timers per device when needed
  • 🌅 Single-stage light activation – PBL and intercepted plain light turn-ons target a configurable brightness and transition before dispatch; explicit brightness requests keep their requested level and gain only the missing transition
  • 🌙 Smooth light clearing – presence-driven light turn-offs use a configurable transition
  • 🔧 Completely UI-based – no YAML, selectors are built-in to the config flow
  • Presence Allowed switches – each entity gets its own switch entity for dashboards or automations

How It Works

Automatic Mode (when enabled):

  • Lights turn ON when presence is detected
  • Lights turn OFF after a configurable delay when room is unoccupied

Manual Override:

  • Turn lights OFF manually → Automation disables itself
  • Turn lights ON manually → Automation re-enables itself

Each controlled entity gets its own switch (switch.<room>_presence_<entity>_presence_allowed) so you can pause automation per device while keeping others running.

Platforms

Platform Description
switch Enable/disable presence automation with state attributes

Installation

HACS (Recommended)

  1. Open HACS in your Home Assistant instance
  2. Go to "Integrations"
  3. Click the three dots in the top right and select "Custom repositories"
  4. Add https://github.com/sfenton/presence_based_lighting as an Integration
  5. Click "Install"
  6. Restart Home Assistant

Manual Installation

  1. Using the tool of choice open the directory (folder) for your HA configuration (where you find configuration.yaml)
  2. If you do not have a custom_components directory there, you need to create it
  3. In the custom_components directory create a new folder called presence_based_lighting
  4. Download all the files from the custom_components/presence_based_lighting/ directory in this repository
  5. Place the files you downloaded in the new directory you created
  6. Restart Home Assistant

Optional pre-dispatch normalization

Install Hass Interceptor and add hass_interceptor: to configuration.yaml to normalize external plain light.turn_on calls before they reach the device. Single-target absolute brightness requests also receive the configured transition without changing their requested brightness. PBL continues to operate without Hass Interceptor, but manual HomeKit/HA turn-ons then retain their original service data.

Configuration

Configuration is done entirely in the UI:

  1. Go to SettingsDevices & Services
  2. Click "+ Add Integration"
  3. Search for "Presence Based Lighting"
  4. Configure your room:
  • Room Name: e.g., "Living Room"
  • Trigger Sensors: Fast motion or presence sensors that should turn entities on
  • Clearing Sensors: Sensors that must all be clear before entities turn off. When configured, these are the clearing authority; trigger-only sensors can turn entities on but do not veto a clear. PBL auto-fills an exact room-level Area Occupancy Detection / Real Last Changed occupancy status when one exists, so raw trigger sensors can turn lights on quickly without being trusted to clear the room. Leave empty to use the trigger sensors for both activation and clearing.
  • Global Turn-Off Delay: Seconds to wait when presence clears
  1. Add entities to control. For each entity:
  • Select the target entity
  • Pick services/states for presence detected/cleared (or No Action)
  • Decide whether the entity respects the toggle switch
  • Decide if external control should pause automation (manual turn-offs always pause actions until the entity is turned back on, even if the Presence Allowed switch is hidden)
  • For lights, optionally adjust the turn-on brightness (100% by default) and transition (1 second by default)
  • For lights, optionally adjust the turn-off transition (1 second by default)
  • Optionally set a per-entity off delay

Activation-gated entries can also choose how existing occupancy is handled when their gate opens:

  • Any occupied trigger preserves the original catch-up behavior.
  • Clearing authority occupied only catches up when the room's authoritative clearing sensor is occupied; trigger-only prelighting still works on a fresh rising edge after the gate is open.
  • Fresh trigger only never catches up from already-active sensors.

You can add multiple room configurations - each operates independently.

Usage Example

Living Room Setup

Room Name: Living Room
Presence Sensors: binary_sensor.living_room_motion
Entities:
  - light.living_room_ceiling → `turn_on` / `turn_off`, 30s delay
  - fan.living_room_ceiling → `turn_on` / `turn_off`, 120s delay

This creates per-entity switches such as switch.living_room_presence_light_living_room_ceiling_presence_allowed.

Switch Attributes

Each Presence Allowed switch includes:

  • controlled_entity: The HA entity ID being automated
  • respect_presence_allowed: Whether the entity honors the switch
  • disable_on_external_control: Whether external control pauses automation. Manual turn-offs always pause actions until you manually turn the entity back on, even if the Presence Allowed switch is hidden.
  • automation_quieted / quieted: Whether the entity is holding dark after a whole-home command
  • automation_suppressed / suppression_kind: Unified PAUSED-or-QUIETED status for downstream consumers
  • bulk_command_policy: Whether confirmed whole-home commands pause or quiet this entity
  • external_override_policy / external_override_source / external_override_reason: Why automation is currently suppressed
  • external_override_batch_id / external_override_batch_size: The detected bulk command, if any
  • rearm_latched / rearm_latched_at / rearm_armed_by: Whether and why re-entry became eligible
  • external_override_at / external_override_expires_at: When the hold started and when it becomes stale
  • quieted_max_age_action / quieted_max_age_reached_at: Configured and applied stale-hold handling
  • unknown_source_count: How many external commands could not be attributed to a known source
  • homekit_batch_mode: The active bulk-detection kill-switch mode
  • manual_on / manual_on_owner_entry_id / manual_on_boundary_phase: Durable opt-in Presence Lock manual-On state and its owner/boundary diagnostics
  • control_lease_mode / control_lease_state: Default-off rollout mode and current lease lifecycle state
  • control_lease_id / control_lease_controller_id: Stable local short hashes for the lease and room controller
  • control_lease_occurrence_ids / control_lease_request_id: Bounded local short correlation hashes
  • control_lease_target_entity_ids / control_lease_released_entity_ids: Explicit current and removed leaf targets
  • control_lease_acquired_at / control_lease_expires_at: Server-stamped lease lifetime
  • control_lease_generation: Generation used to reject stale owner contexts
  • control_lease_last_transition / control_lease_last_outcome / control_lease_last_context: Latest bounded diagnostic decision
  • control_lease_baseline_admitted / control_lease_baseline_outcome: Qualified asleep-baseline admission and compare-clear/preservation result
  • control_lease_watchdog_state / control_lease_watchdog_due_at: Active expiry or restart-recovery watchdog
  • control_lease_denial_count / control_lease_token_mismatch_count / control_lease_blocked_command_count / control_lease_target_unavailable_count: Bounded runtime counters

Whole-Home ("All Lights Off") Commands

A native HomeKit/Siri "turn off all the lights" is not one command. Home Assistant's HomeKit bridge creates a fresh context for every accessory, so the house sees N unrelated single-entity turn_off calls. Treating each one as manual control paused every room indefinitely.

This integration groups same-service HomeKit commands that arrive close together and treats a large burst as a whole-home command rather than N manual overrides.

Per-entity bulk policy

Each controlled entity chooses what a confirmed whole-home command means:

  • rearm_after_clear enters quieted.
  • pause stays dark until an explicit resume.

This lets sleep-sensitive rooms fail dark while hallways and other rooms can rearm. The policy applies even when the managed entity was already off when the batch arrived; redundant singleton offs remain no-ops.

For a quieted entity:

  • Entering the state emits no service call.
  • Reconciliation and Presence Lock are both suppressed, so an occupied room cannot bounce the light straight back on.
  • When the room genuinely becomes vacant, a rearm latch is armed. This only records that vacancy happened; the entity stays dark.
  • The next rising presence edge after that vacancy releases the hold and normal presence automation resumes.
  • Reaching quieted_max_age is diagnostic-only by default. Legacy latch arming and fail-dark conversion to paused remain explicit options.

Single-accessory HomeKit offs, wall switches and any command that cannot be attributed to a bulk burst still pause exactly as before.

Settings

Per config entry:

Setting Default Purpose
homekit_batch_mode enforce Kill switch: off, observe (classify and log only), enforce
batch_window_ms 250 Grouping window for same-service HomeKit commands
batch_retain_seconds 10 How long a context stays resolvable to its batch
batch_min_distinct_entities 8 Distinct target entities required to call a burst a bulk command

The observer is domain-wide but settings are per entry, so values are reduced deterministically (independently of entry setup order): mode takes the least behaviour-changing value (off beats observe beats enforce), window takes the smallest, min distinct entities takes the largest, and retention takes the longest. One entry set to off therefore disables bulk detection for the whole house; set it per entry only if that is what you want.

The shared homekit_state_change listener is reference counted: it attaches when the first entry loads and detaches only when the last entry unloads or when the effective mode becomes off.

Per controlled entity:

Setting Default Purpose
honor_external_override true Consult entity-scoped overrides recorded by sibling entries
unknown_source_policy pause Policy for external commands that cannot be attributed
bulk_command_policy rearm_after_clear rearm_after_clear (Quieted) or pause for confirmed whole-home commands
quieted_max_age 14400 Seconds before a quieted hold is marked stale
quieted_max_age_action diagnostic diagnostic, pause, or legacy arm

Entity-scoped overrides and paired profiles

An override caused by an external action on a controlled entity is a fact about that entity, not about one config entry. Paired profiles (for example a room and its "…(other room lights off)" counterpart) control the same light behind opposing activation gates, so an override recorded by whichever profile was active is visible to the other. Without this, flipping the activation gate hands control to a profile that never saw the override and it immediately resurrects the light.

Explicit per-entry controls — the Presence Allowed switch and pause_automation targeting a specific PBL switch — remain entry-local. Set honor_external_override to false on an entity to restore the old entry-local behaviour.

Scheduled auto re-enable

When the configured vacancy threshold is met at the end of the auto-reenable window, the reset clears shared non-admin pause overrides before resuming and reconciling the loaded room profiles. Physical wall-switch, single-accessory HomeKit, and batch-derived pauses therefore remain fail-dark until the scheduled reset, then release consistently across paired profiles.

Admin-created pauses remain in place until an explicit admin resume. Quieted rearm_after_clear holds also keep their vacancy-and-next-presence lifecycle.

Administrative state service

presence_based_lighting.set_automation_state accepts a PBL switch target and one of:

  • on: enable Presence Allowed, clearing admin-created suppression
  • off: disable Presence Allowed
  • paused: apply an entity-scoped indefinite pause
  • quieted: apply an entity-scoped rearm-after-clear hold
  • active: force-clear local and entity-scoped suppression, then reconcile

Presence Lock manual On holds

Presence Lock entities may opt in to presence_lock_manual_on_override_enabled. It is disabled by default and is not enabled by config-entry migration. An exact root turn_on from an authenticated app user can then establish an entity-wide durable manual-On hold. The hold is represented as a compatible pause override with a versioned intent=manual_on record and releases only after a later occupied boundary, settled vacancy, and the configured off-delay. Unknown or unavailable sensors never prove vacancy. Sibling profiles suppress actuation but cannot advance or release the owner-bound hold.

Wall automations should use the schema-validated presence_based_lighting.manual_control service with exactly one config_entry_id, configured root entity_id, action (turn_on or turn_off), and bounded light_data. The integration binds one-use provenance before dispatch; caller-supplied trust tokens are not accepted.

Temporary Control Leases

PBL 2.5 adds a default-off, token-guarded handoff for a Home Assistant-owned controller that temporarily needs exclusive authority over one controlled light root. The initial registered owner is wake_light.

Each controlled light chooses a control_lease_mode:

  • off (default) preserves all existing behavior.
  • observe reports would-grant and would-break transitions without suppressing PBL or authorizing light commands.
  • enforce grants a lease only when exact context enforcement, current group membership, configured external blockers, target availability, and existing PBL suppression state are all safe.

Enforce mode may also admit the exact household asleep baseline: an entity paused by an unchanged, non-admin external manual-off override. The lease stores an immutable private fingerprint of that override and each matching local pause. Only release_control with outcome: completed and cause: hold_complete compare-clears that same fingerprint. A replaced record, admin/service pause, cancelled/failed release, or stale generation remains fail-dark.

One caller-supplied lease ID represents the room coordinator. Duplicate acquire atomically replaces its bounded occurrence and target sets but never silently extends the original expiry. Release requires the latest returned generation as well as the lease/controller token, and never calls resume_automation, clears an external override, or restores a state snapshot. Manual group off, bulk off, PBL administrative changes, expiry, and target unavailability revoke the lease before existing PBL behavior runs. Transition and revoke telemetry retain the post-terminal generation for diagnostics and also publish previous_generation, which is the exact active generation the owner used before the terminal transition.

OFF uses the same canonical selector extraction as brightness commands, then includes nested HA/Z2M group members. An OFF targeting all leased leaves revokes before dispatch even when an excluded member keeps the aggregate root on. If selector extraction fails, OFF still passes and only proven explicit scope is revoked; the failure is logged rather than guessing unrelated rooms.

An owner observing all leased targets switched off without a service event can release with outcome: cancelled and cause: external_targets_off. Exact token/generation checks still precede mutation. PBL applies its normal unknown-source external policy before release persistence yields, preserving an already qualified asleep/manual baseline. With the normal pause policy this creates a qualified external override, not an administrative pause, so ordinary presence changes stay suppressed and a later independent wake remains eligible. Duplicate/stale releases cannot reapply that policy. The wake owner still owns connected-occurrence fencing and single-leaf exclusion.

Revoke telemetry also includes context_classification and authority_policy. The user-ID/parent-ID classification is diagnostic, not proof of human intent: unknown/HomeKit/helper commands keep conservative external-control authority. Only exact registered owner contexts receive owner treatment.

A foreign light.turn_on with explicit absolute/step brightness, a light profile, or an explicit white level revokes the whole room lease with manual_brightness_control before the command continues. A command aimed at either the controlled root/group or any current leased leaf has this room-level effect; PBL does not make unsafe unilateral leaf-generation changes. Exact lease-owner and PBL automatic contexts are exempt. Color-only commands coexist with the lease, and a plain turn-on is left unmodified while the lease owns brightness rather than being normalized into an implicit brightness command. PBL uses Home Assistant's 2026.8-compatible canonical async_extract_entity_ids(ServiceCall) target expansion for entity_id, area_id, device_id, label_id, floor_id, and mixed selectors. If expansion fails, the interceptor blocks the explicit-brightness command and leaves unrelated leases unchanged rather than guessing at targets. The service-call path applies the command's configured external HomeKit/unknown-source policy exactly once, then classifies later state feedback as owned coordination so it cannot duplicate that handling. An admitted asleep/manual-off baseline is claimed but left unchanged rather than replaced or compare-cleared.

Lease commands must use the internal async_call_with_control_lease path or the response-enabled presence_based_lighting.dispatch_control backend service. Both require the exact root, owner, lease/controller, latest generation, command ID, explicit leaf targets, and bounded light.turn_on data with positive absolute brightness or brightness_pct. Bare light.turn_on is rejected because PBL and other Home Assistant normalization can otherwise jump directly to full brightness. Calls are filtered to the lease's explicit member targets; the root group is never assumed to contain only appropriate wake lights.

In-flight owner contexts remain identifiable past the ordinary context-cache TTL until dispatch returns. The pending set is bounded at 256; capacity exhaustion rejects new commands instead of evicting a pending safety token. Lease expiry is checked again at the pre-dispatch guard. This protects queued software commands, not an already accepted physical device packet.

The wake controller owns active leaf observation and replaces the lease target set when one leaf is released. PBL v2.5 does not persist leaf exclusions after the lease ends; durable post-occurrence leaf behavior remains an explicit future product decision. A full root/group off uses the existing manual-off pause handoff.

The store restores an unexpired lease as command-blocked recovering state for at most 120 seconds and never extends the original expiry. Observe-only records also expire on their supplied deadline. A bounded terminal history and schema-versioned transition events support incident reconstruction. Public attributes, events, diagnostics, terminal summaries, and lifecycle logs expose only stable per-install 12-character SHA-256 correlation hashes for caller-controlled IDs; full IDs remain private to active CAS state and direct service responses.

Acquisition also fails closed with the stable actuation_in_flight blocker while a PBL automatic ON or OFF has already been dispatched or is currently dispatching. The service call is not treated as canceled by clearing local bookkeeping. Once that actuation settles, the caller may safely retry the same unretired lease ID.

Operational gates outside PBL still apply:

  • Adaptive Lighting or another controller can be configured as an external blocker and must be off until ownership compatibility is proven.
  • A 100%/brightness-255 command may trigger unrelated Home Assistant automations; the wake backend must validate those consumers.
  • A group may include adjacent-room members, so the wake backend must submit an explicit validated target list.
  • PBL cannot recall a command already transmitted to Zigbee. It records a late exact-context ON after revoke; the optional corrective OFF is disabled by default and requires every documented safety gate.
  • Post-wake hold duration is owner policy. PBL enforces only the initial lease expiry and performs no implicit per-step renewal.

Escape hatch

# Clear every pause and every quieted hold across all rooms
- service: presence_based_lighting.resume_all_automation

Diagnostics

Every classification decision fires a presence_based_lighting_command_intent event with entry_id, room, entity_id, source, policy, reason, batch_id, batch_size and batch_mode. Run with homekit_batch_mode: observe first if you want to confirm classification against your own house before enabling enforcement.

Use in Automations

# Disable automation for a single lamp during movie time
- service: switch.turn_off
  target:
    entity_id: switch.living_room_presence_light_living_room_lamp_presence_allowed

# Re-enable after movie
- service: switch.turn_on
  target:
    entity_id: switch.living_room_presence_light_living_room_lamp_presence_allowed

Contributions

Contributions are welcome! Please read the Contribution guidelines

Credits

This project was generated from @oncleben31's Home Assistant Custom Component Cookiecutter template.

Code template was mainly taken from @Ludeeus's integration_blueprint template


About

Presence-aware lighting automation with manual override and control leases for Home Assistant.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages