🎥 Prefer video? Watch the full walkthrough here:
In a previous video I shared an idea to show what’s possible with presence automations. It was more of a concept demo than a deep dive into the logic.
If you haven’t seen that yet, start there.
👇 Watch the original walkthrough
That video shows the behaviour in action.
This article explains how it actually works under the hood.
After that video went live, a few people asked for two things. Especially @Mr_nah over on YouTube:
- Can you show the helpers and the “dark enough” logic?
- Can we pause automations when someone manually changes the light?
So that’s exactly what we’re building here.
For this guide I’ll use the lounge as the example room. Everything can be adapted to any space by swapping entity names.
I’ll also be publishing a full build walkthrough video for this article. The link will live at the bottom once it’s up.
Disclaimer: This is the method I have used. It may not be the best or correct depending on who you ask :-)
Why Smart Lights Still Feel Dumb #
At first, your automations feel clever… until you actually start living with them.
You dim the lights to settle in for a movie and thirty seconds later they jump back to bright white.
The kids set the lounge to red and it quietly snaps back to whatever the automation thinks is correct.
You walk into the lounge during the day and the lights flash on even though you can see perfectly fine.
Technically, nothing is broken. Everything is working as designed.
But it does not feel intelligent. It feels reactive.
That difference matters.
Why This Actually Matters #
When lighting behaves poorly, you stop trusting it.
You start reaching for the switch. You disable automations. You tell yourself smart lighting “just isn’t worth it.”
The problem isn’t automation.
The problem is automation without context.
When lights understand darkness properly, respect time of day, and back off when you take control, they disappear into the background.
That’s the goal.
Not clever. Not flashy. Simply Invisible.
There is a big difference between lights that react… and lights that behave.
What we’re building here is lighting that understands context. Time of day. Actual darkness. And most importantly, human intent.
What We’re Actually Building #
We are not building “motion lighting.”
We are building lighting that:
- Knows what time of day it is
- Understands what “dark enough” actually means
- Does not flicker around thresholds
- Gives humans veto power
- Resumes gracefully
There are three stages to get there:
- Define what dark enough actually means
- Combine presence with that definition
- Give humans control without breaking the system
Let’s start with the foundation.
What You’ll Need #
Before we get into templates and helpers, let’s talk about the physical side of this.
You need two core signals:
- Presence – Is someone actually in the room?
- Lux – Is it genuinely dark enough to justify turning lights on?
Without both of those, this whole system falls apart.
Presence without lux means lights turn on in broad daylight. Lux without presence means lights react to clouds instead of people.
You need both.
Note: I think it goes without saying that you will also need a controllable light source 😊
Presence Sensor #
I’m using Apollo Automation mmWave sensors, specifically the MSR-2.
They’ve been rock solid for me and include mmWave presence detection, Built-in lux, Temperature, Pressure, and UV sensors.
Which opens up all sorts of additional logic options later if you want to get clever.
I’m not affiliated and I get nothing from this.They’re just genuinely excellent sensors from a friendly team.
If you’re interested, check them out here:
👉 https://apolloautomation.com/collections/sensors
That said, this logic works with any presence sensor. PIR, Zigbee, Wi-Fi, ESPHome, whatever you’re running. Mileage may vary depending on how stable your sensor is.
Your presence sensor needs to expose its occupancy.
In my case that’s: binary_sensor.lounge_presence_ld2450_presence
Yours will be different.
Lux Sensor #
You also need to expose a lux value.
In my case that’s: sensor.lounge_presence_ltr390_light
Again, yours will be different. If your sensor doesn’t have built-in lux, you can use a separate light sensor.
The key is simple: We need a reliable lux number to compare against our thresholds.
Once you’ve got presence and lux available in Home Assistant, we can start building intelligence around them.
Step 1: Define Time Context #
Before we talk about lux, we need context. Light that feels right at 7am feels harsh at 9pm.
Instead of using one static threshold, we create time “slots” that the rest of the system references.
Time of Day Slot Sensor #
This sensor decides whether it’s morning, day, evening, or night.
Template: Time of Day Slot
# ============================================================================
# Global – Time of Day Slot (for lux thresholds)
# ============================================================================
- trigger:
- platform: time_pattern
minutes: "/1"
- platform: homeassistant
event: start
sensor:
- name: Time of Day Slot
unique_id: time_of_day_slot
icon: mdi:clock-outline
state: >
{% set t = now().strftime('%H:%M:%S') %}
{% if '06:30:00' <= t < '12:00:00' %}
morning
{% elif t < '17:00:00' %}
day
{% elif t < '21:00:00' %}
evening
{% else %}
night
{% endif %}
You can create this as a Template Sensor in the UI:
Settings → Devices & Services → Helpers → Create Helper → Template → Template Sensor
Copy only the Jinja inside the state: block from the YAML below.
Or keep it in YAML
Lighting Scene Slot #
This takes it one step further. Instead of hardcoding brightness into your automation, we output scene-friendly slots such as: overnight, early_morning, morning, daytime, evening, or late_evening.
This keeps your motion automation clean. It just says “apply the current scene slot” rather than embedding brightness logic everywhere.
You can create this the same way as above in the UI:
Settings → Devices & Services → Helpers → Create Helper → Template → Template Sensor
And copy only the Jinja inside the state: block from the YAML below.
Or use YAML:
Template: Lighting Scene Slot
# ============================================================================
# Global – Lighting Scene Slot (for choosing scenes)
# ============================================================================
- trigger:
- platform: time_pattern
minutes: "/1"
- platform: state
entity_id: input_boolean.bed_time
- platform: homeassistant
event: start
sensor:
- name: Lighting Scene Slot
unique_id: lighting_scene_slot
icon: mdi:theme-light-dark
state: >
{% set t = now().strftime('%H:%M:%S') %}
{% set bed = is_state('input_boolean.bed_time', 'on') %}
{# Overnight always wins #}
{% if t >= '23:00:00' or t < '06:30:00' %}
overnight
{# Early morning and morning split #}
{% elif '06:30:00' <= t < '07:30:00' %}
early_morning
{% elif '07:30:00' <= t < '09:00:00' %}
morning
{# Daytime #}
{% elif '09:00:00' <= t < '17:00:00' %}
daytime
{# Evening and late evening with bedtime influence #}
{% elif '17:00:00' <= t < '21:00:00' %}
evening
{% elif '21:00:00' <= t < '23:00:00' %}
{{ 'late_evening' if bed else 'evening' }}
{% else %}
evening
{% endif %}
Step 2: Define What “Dark Enough” Means #
Why a Single Lux Number Does Not Work #
As mentioned earlier, if you use one lux threshold for the entire day, your lighting will always feel slightly off. Slightly too bright. Slightly too eager.
Instead of one number, we will use multiple thresholds based on time of day.
That gives us nuance.
The Lux Threshold Helpers We Need #
Create four number helpers in the UI:
Settings → Devices & Services → Helpers → Create Helper → Number
| Name | Entity | Purpose |
|---|---|---|
| Lux Lounge Morning | input_number.lux_lounge_morning |
Lux threshold for morning |
| Lux Lounge Day | input_number.lux_lounge_day |
Lux threshold for daytime |
| Lux Lounge Evening | input_number.lux_lounge_evening |
Lux threshold for evening |
| Lux Lounge Night | input_number.lux_lounge_night |
Lux threshold for night |
These allow you to tune brightness expectations throughout the day without editing YAML.
You can absolutely create these in YAML if that’s your preference. The UI works perfectly fine though.
Below are my settings. Note: Your lux numbers will vary wildly depending on the sensor. I have different lux sensors vary at the same time, same place from 1000 - 2000lx, whereas the lux sensor on my Apollo R Pro-1 is currently sitting at 71.3lx. You just need to ensure your helpers are calibrated to your sensor.
Number: Lux threshold
lux_lounge_morning:
name: "Lux: Lounge Morning"
min: 0
max: 200
step: 5
mode: slider
initial: 50
icon: mdi:weather-sunset-up
lux_lounge_day:
name: "Lux: Lounge Day"
min: 0
max: 200
step: 5
mode: slider
initial: 100
icon: mdi:white-balance-sunny
lux_lounge_evening:
name: "Lux: Lounge Evening"
min: 0
max: 200
step: 5
mode: slider
initial: 5
icon: mdi:weather-sunset
lux_lounge_night:
name: "Lux: Lounge Night"
min: 0
max: 200
step: 5
mode: slider
initial: 20
icon: mdi:weather-night
Hysteresis #
Hysteresis is what stops your house from overreacting.
Without it, if lux hovers around your threshold, lights can flick on and off repeatedly.
Hysteresis adds a margin so lights only turn on when it is clearly dark, and only turn off when it is clearly bright again.
So, let’s create one more helper:
| Name | Entity | Purpose |
|---|---|---|
| Lux Hysteresis Margin | input_number.lux_hysteresis_margin |
Buffer to prevent flicker |
Number: Hysteresis
lux_hysteresis_margin:
name: "Lux Hysteresis Margin"
min: 0
max: 100
step: 5
mode: slider
initial: 5
icon: mdi:tune-variant
Active Lux Threshold Sensor #
Now we create a template sensor that decides which threshold applies right now. This keeps the motion automation clean. It only ever checks one value.
If using the UI, paste only the Jinja from the state blocks (Make sure you set unit of measure to lx)
Or use YAML:
Lux Threshold (Active) Logic
# ============================================================================
# Lounge – Active Lux Threshold
# ============================================================================
- trigger:
- platform: time_pattern
minutes: "/1"
- platform: homeassistant
event: start
- platform: state
entity_id:
- input_number.lux_lounge_morning
- input_number.lux_lounge_day
- input_number.lux_lounge_evening
- input_number.lux_lounge_night
sensor:
- name: Lounge Lux Threshold (active)
unique_id: lounge_lux_threshold_active
unit_of_measurement: lx
icon: mdi:brightness-6
state: >
{% set t = now().strftime('%H:%M:%S') %}
{% if '06:30:00' <= t < '12:00:00' %}
{{ states('input_number.lux_lounge_morning') | float(0) }}
{% elif t < '17:00:00' %}
{{ states('input_number.lux_lounge_day') | float(0) }}
{% elif t < '21:00:00' %}
{{ states('input_number.lux_lounge_evening') | float(0) }}
{% else %}
{{ states('input_number.lux_lounge_night') | float(0) }}
{% endif %}Note: Change the time ranges and entity IDs to match your setup.
You will now have: sensor.lounge_lux_threshold_active
That sensor automatically outputs the correct lux threshold for the current time of day.
If you named your helpers differently, update the entity IDs inside the template.
If your time ranges are different, adjust the clock values.
The motion automation now only needs to check one clean sensor instead of managing time logic itself.
The “Dark Enough” Binary Sensor #
Now we create the binary sensor that actually answers the question:
Is it dark enough to justify turning the lights on?
To do this, we compare actual lux against the active threshold and apply hysteresis.
Go to: Settings → Devices & Services → Helpers → Create Helper
Choose: Template → Template Binary Sensor
| Name | Entity | Purpose |
|---|---|---|
| Lounge Dark Enough | binary_sensor.lounge_dark_enough |
Is it dark enough to turn lights on |
This gives us: binary_sensor.lounge_dark_enough
True means “worth turning the lights on.”
If creating from UI, paste only the Jinja from the state blocks.
Or use YAML:
Dark Enough Logic
# ============================================================================
# Lounge – Dark Enough (lux + hysteresis)
# ============================================================================
- trigger:
- platform: state
entity_id:
- sensor.lounge_presence_ltr390_light
- sensor.lounge_lux_threshold_active
- input_number.lux_hysteresis_margin
- platform: homeassistant
event: start
binary_sensor:
- name: Lounge Dark Enough
unique_id: lounge_dark_enough
icon: mdi:weather-night
state: >
{% set lux = states('sensor.lounge_presence_ltr390_light') | float(0) %}
{% set thr = states('sensor.lounge_lux_threshold_active') | float(0) %}
{% set m = states('input_number.lux_hysteresis_margin') | float(0) %}
{% set was_on = (this.state == 'on') %}
{{ (lux < (thr - m)) or (was_on and lux < (thr + m)) }}
Make sure you replace: sensor.lounge_presence_ltr390_light with your actual lux sensor entity.
What this does:
- It turns on when lux drops below
threshold - margin - It stays on until lux rises above
threshold + margin
That buffer zone is the hysteresis.
Without it, your lights can flap on and off every time lux hovers around the threshold.
With it, the system feels calm and confident instead of twitchy.
OK we now have a clean binary sensor:
binary_sensor.lounge_dark_enough
That becomes the gatekeeper for your motion automation.
Now we have a stable definition of darkness.
Step 3: Combine Presence with Darkness #
Presence sensors are fast. Humans are not.
If you turn lights off the moment presence drops false, the room feels twitchy.
Instead we introduce a vacancy timer inside the automation so lights only turn off after a short buffer.
Why We Use a Vacancy Timer #
If you turn lights off the instant presence goes false, your room will feel twitchy.
We all know the feeling, when someone steps slightly out of presence range and then everything goes dark.
Instead:
- Presence detected → cancel timer
- Presence cleared → start timer
- Timer finishes → turn lights off
This makes the room feel calm instead of reactive.
Let’s create a Timer helper:
Go to: Settings → Devices & Services → Helpers → Create Helper
Choose: Timer
Name: Lounge Vacancy Timer
Entity ID: timer.lounge_vacancy
Duration: 00:05:00 (adjust to taste)
Five minutes is a good starting point for a lounge.
For hallways you might use one minute.
For bedrooms, maybe longer.
How “Time of Day” Drives the Lighting #
Before we dive into the automation, it is important to understand the logic behind this.
Once you’ve got a sensor.lighting_scene_slot, you can stop hardcoding “turn on the light to X”.
Instead, the automation can ask a much more human question:
“What kind of lighting should this room have right now?”
That’s what the scene slot gives you.
It outputs a simple text value like:
early_morningmorningdaytimeeveninglate_eveningovernight
Then inside the motion automation, we use a choose: block to map each slot to a scene.
So in the morning you might prefer something warmer and softer, during the day you might not want lights at all, and late evening might be a dim “don’t wake the house” scene.
This is the part that makes it feel less like a motion sensor and more like the room has manners.
The Pattern (small example) #
Here’s the basic idea. Your full YAML below contains the complete list.
# Choose a scene based on your time-of-day slot sensor
- choose:
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: early_morning
sequence:
- service: scene.turn_on
target:
entity_id: scene.lights_lounge_early_morning
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: morning
sequence:
- service: scene.turn_on
target:
entity_id: scene.lights_lounge_morning
Step 4 – Build the Lounge Scenes #
Up to this point we’ve defined when the lights should turn on.
Now we define how they should look.
The automation does not set brightness or colour directly.
Instead, it calls a scene based on the value of: sensor.lighting_scene_slot
This keeps logic and lighting design separate.
The automation decides if.
The scene decides what it feels like.
That separation matters.
Create These Exact Scene Names #
Go to:
Settings → Automations & Scenes → Scenes → Create Scene
Create the following scenes exactly as named below:
| Time Slot State | Scene Entity Required |
|---|---|
| early_morning | scene.lights_lounge_early_morning |
| morning | scene.lights_lounge_morning |
| daytime | scene.lights_lounge_daytime |
| evening | scene.lights_lounge_evening |
| late_evening | scene.lights_lounge_late_evening |
| overnight | scene.lights_lounge_overnight |
⚠ The entity IDs must match what the automation references.
If you change the names, update the automation YAML accordingly.
What Should Each Scene Look Like? #
There is no “correct” setting.
But here is a practical guide:
- early_morning → very soft, low brightness
- morning → comfortable neutral white
- daytime → bright and practical
- evening → warm and relaxed
- late_evening → dim, warm, lower brightness
- overnight → very low or night-light only
Tune these to your home.
The automation will simply ask:
“What time-of-day slot are we in?”
Then run the matching scene. That’s it.
No brightness logic inside the automation.
No colour temperature spaghetti.
Just clean separation of intent.
Motion Automation #
Now we combine everything. This is the automation that ties presence and “dark enough” together for the lounge.
Motion Automation (Lounge)
alias: "Motion: Lounge"
description: >
Lounge lights based on motion + dark-enough (lux), lighting scene slot,
and a countdown timer.
# =============================================================================
# TRIGGERS
# =============================================================================
# We react to:
# 1. Motion turning ON
# 2. Motion turning OFF (after 15 seconds)
# 3. Our countdown timer finishing
triggers:
# Motion detected
- entity_id: binary_sensor.lounge_presence_ld2450_presence # <-- Change to YOUR presence sensor
to: "on"
id: motion-on
trigger: state
# Motion cleared (after small delay to avoid flapping)
- entity_id: binary_sensor.lounge_presence_ld2450_presence # <-- Change to YOUR presence sensor
to: "off"
for:
seconds: 15 # <-- Adjust to suit your sensor sensitivity
id: motion-off
trigger: state
# Timer finished (used to turn lights off gracefully)
- event_type: timer.finished
event_data:
entity_id: timer.countdown_motion_lounge # <-- Change if your timer name differs
id: timer-finished
trigger: event
# =============================================================================
# CONDITIONS (Global Gates)
# =============================================================================
# These conditions prevent the automation from running in certain scenarios.
# You can disable or remove ones you don’t use.
conditions:
# # Global override (optional – disabled by default)
# - condition: state
# entity_id: input_boolean.light_override # <-- Only if you use a house-wide override
# state: "off"
# enabled: false
# # Room suppression switch (optional – disabled by default)
# - condition: state
# entity_id: input_boolean.lounge_motion_suppressed # <-- Optional per-room suppression
# state: "off"
# enabled: false
# # Manual override (this is the important one)
# # If this is ON, the automation does nothing.
# - condition: state
# entity_id: input_boolean.lounge_manual_override # <-- Change per room
# state: "off"
# =============================================================================
# ACTIONS
# =============================================================================
actions:
- choose:
# -----------------------------------------------------------------------
# MOTION ON
# -----------------------------------------------------------------------
- conditions:
- condition: trigger
id: motion-on
- condition: state
entity_id: binary_sensor.lounge_dark_enough # <-- Your "dark enough" binary sensor
state: "on"
sequence:
# Cancel any running countdown timer
- target:
entity_id: timer.countdown_motion_lounge # <-- Your room timer
action: timer.cancel
# Choose lighting scene based on time-of-day slot
- choose:
# EARLY MORNING
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot # <-- Your time-of-day sensor
state: early_morning
sequence:
- target:
entity_id: scene.lights_lounge_early_morning # <-- Change per room
action: scene.turn_on
# MORNING
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: morning
sequence:
- target:
entity_id: scene.lights_lounge_morning
action: scene.turn_on
# DAYTIME
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: daytime
sequence:
- target:
entity_id: scene.lights_lounge_daytime
action: scene.turn_on
# EVENING
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: evening
sequence:
- target:
entity_id: scene.lights_lounge_evening
action: scene.turn_on
# LATE EVENING
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: late_evening
sequence:
- target:
entity_id: scene.lights_lounge_late_evening
action: scene.turn_on
# OVERNIGHT
- conditions:
- condition: state
entity_id: sensor.lighting_scene_slot
state: overnight
sequence:
- target:
entity_id: scene.lights_lounge_overnight
action: scene.turn_on
# Default fallback if slot not matched
default:
- target:
entity_id: scene.lights_lounge_daytime # <-- Safe fallback
action: scene.turn_on
# -----------------------------------------------------------------------
# MOTION OFF
# -----------------------------------------------------------------------
# Instead of turning lights off immediately,
# we start a countdown timer.
- conditions:
- condition: trigger
id: motion-off
sequence:
- target:
entity_id: timer.countdown_motion_lounge # <-- Your room timer
action: timer.start
# -----------------------------------------------------------------------
# TIMER FINISHED
# -----------------------------------------------------------------------
# If no motion occurred during the timer period,
# we gently turn off the light.
- conditions:
- condition: trigger
id: timer-finished
sequence:
- target:
entity_id: light.lounge # <-- Change to your room light
data:
transition: 60 # <-- Fade out over 60 seconds (adjust to taste)
action: light.turn_off
mode: single
If:
- Room is occupied
- It is dark enough
Then turn on the light.
You will also have the timer logic for turning lights off when the room is empty.
At this point, your lighting already feels significantly better than basic motion setups.
But we still haven’t solved the “kids set it to red” problem.
Step 5 – Give Humans Veto Power #
Up to this point, the system is behaving “intelligently”.
Now we give humans the final say.
If someone manually changes brightness or colour, the automation should pause.
Not argue. Not “correct” them. Just back off.
To do that, we create a simple gatekeeper.
Create the Manual Override Helper #
When this is on, motion automation should do nothing.
Go to: Settings → Devices & Services → Helpers → Create Helper
Choose: Toggle
Name: Lounge Manual Override
Entity ID: input_boolean.lounge_manual_override
When this helper is ON, the motion automation will do nothing.
That’s it. It becomes a simple on/off gate in front of the automation.
Manual Override Detection Automation #
Creating this automation detects meaningful “Human intent” with manual changes (colour, brightness, on/off) and sets the manual override flag.
Create a new automation:
Manual Override Detection
alias: Lounge - Manual Override Detected
description: >
Turns on lounge_manual_override when a human makes a meaningful change to the
lounge light (brightness, colour temp, colour, or manual on/off).
trigger:
- platform: state
entity_id: light.lounge_light # <-- CHANGE THIS to your light
condition:
# Ignore startup / null transitions
- condition: template
value_template: "{{ trigger.from_state is not none and trigger.to_state is not none }}"
# Only set override if currently OFF
- condition: state
entity_id: input_boolean.lounge_manual_override # <-- CHANGE IF NEEDED
state: "off"
# Treat events with no parent_id as "manual-ish"
- condition: template
value_template: "{{ trigger.to_state.context.parent_id is none }}"
# Meaningful change logic (MUST render ONLY true/false)
- condition: template
value_template: >
{% set from_s = trigger.from_state %}
{% set to_s = trigger.to_state %}
{% set from_state = from_s.state %}
{% set to_state = to_s.state %}
{# Safe reads: treat missing attributes as 0 #}
{% set from_b = from_s.attributes.brightness | default(0, true) | int(0) %}
{% set to_b = to_s.attributes.brightness | default(0, true) | int(0) %}
{% set b_delta = (to_b - from_b) | abs %}
{% set from_ct = from_s.attributes.color_temp | default(0, true) | int(0) %}
{% set to_ct = to_s.attributes.color_temp | default(0, true) | int(0) %}
{% set ct_delta = (to_ct - from_ct) | abs %}
{% set from_h = from_s.attributes.hs_color | default('', true) %}
{% set to_h = to_s.attributes.hs_color | default('', true) %}
{% set from_mode = from_s.attributes.color_mode | default('', true) %}
{% set to_mode = to_s.attributes.color_mode | default('', true) %}
{# Thresholds: increase if auto adjustments trigger override #}
{% set BRIGHTNESS_DELTA_MIN = 8 %}
{% set CT_DELTA_MIN = 15 %}
{# Only treat colour changes as meaningful in real colour modes #}
{% set COLOUR_MODES = ['hs', 'rgb', 'xy'] %}
{% set meaningful_colour_change =
(from_mode in COLOUR_MODES or to_mode in COLOUR_MODES) and (from_h != to_h)
%}
{% set meaningful_brightness_change = (b_delta >= BRIGHTNESS_DELTA_MIN) %}
{% set meaningful_ct_change = (ct_delta >= CT_DELTA_MIN) %}
{% set turned_off = (from_state == 'on' and to_state == 'off') %}
{% set turned_on = (from_state != 'on' and to_state == 'on') %}
{{
meaningful_brightness_change
or meaningful_ct_change
or meaningful_colour_change
or turned_off
or turned_on
}}
action:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.lounge_manual_override # <-- CHANGE IF NEEDED
mode: restart
Now when someone sets the light to red, the system pauses. No arguments.
To wrap it all up, you can go back into your **motion automation ** and add the below condition (It has been remarked out in the full automation above):
Add Manual Override Condition
conditions:
- condition: state
entity_id: input_boolean.lounge_manual_override
state: "off"
- Room is occupied
- It is dark enough
- Manual override is OFF
Then turn on the light.
Why I Didn’t Rely Only on Adaptive Lighting’s Built-In Takeover #
Adaptive Lighting includes a take_over_control option designed to pause adaptation when a light is manually adjusted.
In theory, when you change brightness or colour, Adaptive Lighting should mark that light as manually controlled and stop adjusting it.
In practice, during testing, I found that this behaviour is not always consistent.
Even with:
take_over_control: true
Adaptive Lighting continued to adjust the light after I manually changed brightness.
After reviewing the GitHub issues, there are recurring themes around this behaviour:
https://github.com/basnijholt/adaptive-lighting/issues
The key nuance appears to be this:
Not all “manual” changes are treated as true takeover events.
If the brightness or colour change is still issued through Home Assistant, for example:
- From the dashboard
- From a scene
- From another automation
- From a voice assistant integrated via Home Assistant
The integration may not always classify that change as a takeover.
From a user perspective, this can feel like the light is “fighting you”.
Why the Template-Based Manual Override Is More Reliable #
Instead of relying solely on the integration’s internal takeover logic, the template-based override:
- Detects meaningful state changes
- Ignores Adaptive Lighting micro-adjustments
- Explicitly sets a manual override flag
- Blocks further automation until cleared
This makes the behaviour deterministic.
You are not depending on the integration to decide whether a change is manual.
You are explicitly telling your automation stack to stop adjusting the light.
For simple setups, the built-in takeover may work perfectly.
If you want predictable, “respect humans” behaviour in a real household, explicit override logic gives you tighter control.
Smart homes should adapt to people.
Not argue with them.
Resume Script #
So we have finished with our red-themed light dance party and want to resume normal lighting and automations again.
We simply create a script that turns off the override helper:
Resume Automations Script (Lounge)
alias: Lounge - Resume Automations
description: >
Clears manual override so the motion + lux automation can take control again.
###############################################################################
# WHAT THIS SCRIPT DOES
#
# This is the “make it normal again” button.
#
# When someone has manually changed the light (for example, set it to red),
# the manual override helper is turned ON.
#
# That blocks the motion automation from touching the light.
#
# Running this script:
# - Turns OFF input_boolean.lounge_manual_override
# - Allows normal presence + lux behaviour to resume
#
# WHAT YOU MUST CUSTOMISE
# - input_boolean.lounge_manual_override (if you renamed it)
###############################################################################
mode: single
sequence:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.lounge_manual_override
That is all it does.
It does not force lights on.
It does not reset scenes.
It simply clears input_boolean.lounge_manual_override.
When that flag turns off, the motion automation is allowed to operate again.
How You Can Trigger the Resume Script #
There are multiple clean ways to call this script:
• From a dashboard button
• From another automation after a timeout
• From a physical button
• From Apple Home, Google Home, etc.
Choose whatever fits your household.
The important part is that resuming automation becomes intentional, not automatic.
Yes we can create an automation that when the input_boolean.lounge_manual_override is enabled, it starts a countdown, afterwhich it clears input_boolean.lounge_manual_override. But this is already long enough 😴
The point is that you decide when control returns.
Apple Home and Google Home #
If I’ve intentionally overridden the lighting, then I should also intentionally resume it.
In my mind, the automation should not silently take control back without me knowing.
So, I handle this inside Apple Home.
Exposing the Script to Apple Home #
The resume script is just another entity in Home Assistant.
To make it available in Apple Home:
- Go to Settings → Devices & Services → HomeKit
- Edit your HomeKit bridge configuration
- Ensure Scripts are included in the entities exposed
- Select
script.resume_lounge_lights(or whatever you named it) - Reload or restart the HomeKit integration if required
If you are using Exclude mode, you simply need to allow the script domain.
If you are using Include mode, you must:
- Allow the
scriptdomain - Explicitly add the resume script entity (for example
script.resume_lounge_lights)
Once exposed, reload the HomeKit integration if required.
The script will now appear inside Apple Home. By default, it shows up in the “Default Room.”
In my case, that room was called Home (Must have changed with one of their updates). Depending on your HomeKit setup, yours may differ.
I moved my entity to the Lounge room inside Apple Home.
I suggest you now rename it to something meaningful like Reset Lounge Lights or Resume Lounge Lights.
Now it behaves like a scene. That means you can say:
“Hey Siri, resume/reset lounge lights.”
And the override flag is cleared.
No special Voice Assistant setup is required beyond normal HomeKit exposure. If Home Assistant is already connected to Apple Home, this is simply exposing one more entity.
You overrode the system and you decide when it resumes.
Google Home #
Google Home works the same way conceptually (I am not using my Google home for automations anymore).
What This System Now Does #
- Lights only turn on when it is genuinely dark
- Threshold adapts by time of day
- Hysteresis prevents flicker
- Presence feels stable
- Manual colour or brightness changes pause automation
- Resume restores normal behaviour
No snapping back.
No fighting.
No rude behaviour.
Just lighting that behaves.
Full Build Guide Video #
I’ve published a full step-by-step build walkthrough for this exact setup.
👉
If you build this in another room, just swap “lounge” for whatever space you’re working on.
And if your lights stop arguing with you… you’ve done it right.