Skip to content

feat: instant context-based manual-control marking#1470

Draft
jaredjxyz wants to merge 2 commits into
basnijholt:mainfrom
jaredjxyz:instant-context-based-manual-marking
Draft

feat: instant context-based manual-control marking#1470
jaredjxyz wants to merge 2 commits into
basnijholt:mainfrom
jaredjxyz:instant-context-based-manual-marking

Conversation

@jaredjxyz
Copy link
Copy Markdown

Summary

Adds an immediate, context-driven path for marking lights manually controlled when a non-AL service call changes a tracked light's brightness or color, complementing the existing periodic `significant_change` comparison.

Problem

AL currently detects manual control of in-HA changes via the threshold-based `significant_change` comparison inside `update_manually_controlled_from_untracked_change`, which only runs on the adapt cycle (every `interval` seconds, default 90 s).

This produces a race window: while a non-AL service call is in flight (e.g. a continuous dim, a brightness ramp from a remote dimmer blueprint, an automation that incrementally adjusts brightness), AL's adapt tick can fire and re-assert its own values before the cumulative drift crosses the brightness/kelvin/rgb thresholds. Each individual sub-threshold tick is correctly ignored, but the cumulative operation is no longer in flight by the time the periodic check sees it.

In practice this manifests as 'AL fights my dim' for users with continuous-adjustment automations, even though the eventual end-state (after the next adapt tick) is correctly flagged manual.

The threshold-based path is still necessary for true `detect_non_ha_changes` use cases (state drift caused by non-HA actors like the Hue companion app or physical bulb buttons), which have no service-call context to inspect.

Change

In `state_changed_event_listener`, when:

  • the `new_state` is on,
  • the `old_state` was on (off→on already has its own dedicated path),
  • `is_our_context(new_state.context)` is False (external service call),
  • and at least one of {`brightness`, `color_temp_kelvin`, `rgb_color`} differs from `old_state`,

the affected light is added to `manual_control` on the relevant axes via `add_manual_control_attributes`, and `adaptive_lighting.manual_control` is fired with the new flags. The marking is gated on the existing `take_over_control` config switch.

To avoid double-marking during a single service call's transition (which emits multiple `state_changed` events that share a `context.id`), a new per-light dict `last_external_marking_context` dedupes by `context.id`. It is cleared in `reset()` alongside the other per-light tracking dicts.

The threshold-based path (firing on the adapt tick) is preserved unchanged: it now serves its original niche of catching truly non-HA state drift.

Effects

  • Eliminates the 'AL re-adapts mid-operation' race for in-HA service calls.
  • Per-axis precision: if only brightness changed, only `BRIGHTNESS` is flagged. With `take_over_control_mode: pause_changed`, color keeps adapting.
  • No threshold tuning required for the deliberate-user-action case (the threshold exists to filter noise from non-HA state polling, which is a different signal).
  • `detect_non_ha_changes` remains the safety net for out-of-HA drift.

Compatibility

Marking happens earlier in time than before, but the resulting state of `manual_control` is identical to what would eventually be produced by the periodic check. Effects on consumers:

  • Automations driven by the `adaptive_lighting.manual_control` event fire sooner.
  • Templates / dashboards reading the `manual_control` state attribute see flags appear without the up-to-90-second delay.
  • No config flag added; new path is gated by the existing `take_over_control` switch (which any user already needs enabled to have manual_control behavior at all).

Test plan

  • New unit test: dim a light via a non-AL context, assert manual_control flagged before any adapt tick.
  • Existing `test_manual_control` still passes.
  • Manual: continuous dim from a remote dimmer no longer fights AL adapt cycle.

Status

Draft. Posting for design feedback before adding test coverage and polish. Specifically interested in:

  1. Whether a config flag (`instant_manual_control: true`, default false) is preferred over gating only on `take_over_control`.
  2. Whether marking should explicitly require non-empty `old_attributes` (currently it just diffs both `.get()` values, treating None as a value).
  3. Whether the rgb diff should keep using `color_difference_redmean` like the threshold path, or just plain inequality (current PR uses inequality, since context-based detection doesn't need to filter color noise).

🤖 Generated with Claude Code

jaredjxyz and others added 2 commits April 25, 2026 14:34
Adds an immediate, context-driven path for marking lights manually
controlled when a non-AL service call changes a tracked light's
brightness or color, in addition to the existing periodic
significant_change comparison.

## Background

AL currently detects manual control of in-HA changes via the
significant_change comparison in update_manually_controlled_from_untracked_change,
which only runs on the adapt cycle (every interval seconds, default 90s).
This produces a race window: while a non-AL service call is in flight
(e.g. a continuous dim, a brightness ramp, etc.), AL's adapt tick can
fire and re-assert its own values before the cumulative drift becomes
significant enough for significant_change to flag manual.

The threshold-based path is also necessary for true detect_non_ha_changes
use (state drift caused by non-HA actors like the Hue app or physical
button cycling), which has no service-call context to inspect.

## What this PR adds

In state_changed_event_listener, when the new_state's context.id does
not carry AL's :al: marker (i.e. an external service call) and at least
one of {brightness, color_temp_kelvin, rgb_color} differs from
old_state, the affected light is added to manual_control on the
relevant axes immediately.

To avoid double-marking during a single service call's transition
(which emits multiple state_changed events with the same context.id),
a per-light last_external_marking_context dict dedupes by context.id.
The dict is cleared in reset() alongside the other per-light tracking
dicts.

The threshold-based check (still firing on the adapt tick when
detect_non_ha_changes is enabled) is preserved unchanged: it now
serves its original niche of catching truly non-HA state drift.

## Effects

- Eliminates the 'AL re-adapts mid-dim' race for in-HA service calls.
- Per-axis precision: if only brightness changed, only BRIGHTNESS is
  flagged, leaving color free to keep adapting (matches pause_changed
  intent).
- No threshold tuning required for the deliberate-user-action case.
- detect_non_ha_changes remains the safety net for out-of-HA drift.

## Backward compatibility

Marking happens earlier than before, but the resulting state of
manual_control is identical to what would eventually be produced by
the periodic check. Automations driven by adaptive_lighting.manual_control
events will fire sooner; automations reading the manual_control
state attribute will see flags appear without the up-to-90-second
delay. No config flag is added; the new path is gated by the existing
take_over_control switch, which any user already needs enabled to
have manual_control behavior at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant