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.
- ✨ Automatic entity control driven by presence sensors
- ⚙️ Per-entity actions – choose exactly which services/states to call when presence appears or clears (with
No Actionoption) - 🎯 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
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.
| Platform | Description |
|---|---|
switch |
Enable/disable presence automation with state attributes |
- Open HACS in your Home Assistant instance
- Go to "Integrations"
- Click the three dots in the top right and select "Custom repositories"
- Add
https://github.com/sfenton/presence_based_lightingas an Integration - Click "Install"
- Restart Home Assistant
- Using the tool of choice open the directory (folder) for your HA configuration (where you find
configuration.yaml) - If you do not have a
custom_componentsdirectory there, you need to create it - In the
custom_componentsdirectory create a new folder calledpresence_based_lighting - Download all the files from the
custom_components/presence_based_lighting/directory in this repository - Place the files you downloaded in the new directory you created
- Restart Home Assistant
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 is done entirely in the UI:
- Go to Settings → Devices & Services
- Click "+ Add Integration"
- Search for "Presence Based Lighting"
- 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
- 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.
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.
Each Presence Allowed switch includes:
controlled_entity: The HA entity ID being automatedrespect_presence_allowed: Whether the entity honors the switchdisable_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 commandautomation_suppressed/suppression_kind: Unified PAUSED-or-QUIETED status for downstream consumersbulk_command_policy: Whether confirmed whole-home commands pause or quiet this entityexternal_override_policy/external_override_source/external_override_reason: Why automation is currently suppressedexternal_override_batch_id/external_override_batch_size: The detected bulk command, if anyrearm_latched/rearm_latched_at/rearm_armed_by: Whether and why re-entry became eligibleexternal_override_at/external_override_expires_at: When the hold started and when it becomes stalequieted_max_age_action/quieted_max_age_reached_at: Configured and applied stale-hold handlingunknown_source_count: How many external commands could not be attributed to a known sourcehomekit_batch_mode: The active bulk-detection kill-switch modemanual_on/manual_on_owner_entry_id/manual_on_boundary_phase: Durable opt-in Presence Lock manual-On state and its owner/boundary diagnosticscontrol_lease_mode/control_lease_state: Default-off rollout mode and current lease lifecycle statecontrol_lease_id/control_lease_controller_id: Stable local short hashes for the lease and room controllercontrol_lease_occurrence_ids/control_lease_request_id: Bounded local short correlation hashescontrol_lease_target_entity_ids/control_lease_released_entity_ids: Explicit current and removed leaf targetscontrol_lease_acquired_at/control_lease_expires_at: Server-stamped lease lifetimecontrol_lease_generation: Generation used to reject stale owner contextscontrol_lease_last_transition/control_lease_last_outcome/control_lease_last_context: Latest bounded diagnostic decisioncontrol_lease_baseline_admitted/control_lease_baseline_outcome: Qualified asleep-baseline admission and compare-clear/preservation resultcontrol_lease_watchdog_state/control_lease_watchdog_due_at: Active expiry or restart-recovery watchdogcontrol_lease_denial_count/control_lease_token_mismatch_count/control_lease_blocked_command_count/control_lease_target_unavailable_count: Bounded runtime counters
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.
Each controlled entity chooses what a confirmed whole-home command means:
rearm_after_clearentersquieted.pausestays 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_ageis diagnostic-only by default. Legacy latch arming and fail-dark conversion topausedremain explicit options.
Single-accessory HomeKit offs, wall switches and any command that cannot be attributed
to a bulk burst still pause exactly as before.
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 |
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.
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.
presence_based_lighting.set_automation_state accepts a PBL switch target and one of:
on: enable Presence Allowed, clearing admin-created suppressionoff: disable Presence Allowedpaused: apply an entity-scoped indefinite pausequieted: apply an entity-scoped rearm-after-clear holdactive: force-clear local and entity-scoped suppression, then reconcile
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.
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.observereports would-grant and would-break transitions without suppressing PBL or authorizing light commands.enforcegrants 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.
# Clear every pause and every quieted hold across all rooms
- service: presence_based_lighting.resume_all_automationEvery 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.
# 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_allowedContributions are welcome! Please read the Contribution guidelines
This project was generated from @oncleben31's Home Assistant Custom Component Cookiecutter template.
Code template was mainly taken from @Ludeeus's integration_blueprint template