Removed a superfluous "Swedish: " from the description of the manual control option.
Co-authored-by: Bas Nijholt <basnijholt@users.noreply.github.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* Enhance Danish translation description for adaptive lighting
Updated the description in the Danish translation for adaptive lighting settings to include documentation and webapp URLs.
* Update custom_components/adaptive_lighting/translations/da.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
Spread recurring updates with a deterministic per-switch offset while keeping turn-on adaptation immediate. Register the delayed listener on Home Assistant's event loop and cover cancellation, reconfiguration, and cadence with real timer tests.
Closes#939.
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
* Exclude 'service' lights (entity_category) from area intercept turn-on
Fixes#1510
When Adaptive Lighting intercepts an area/label-targeted light.turn_on, the
intercept rewrites the call to target only the managed lights (see
modify_service_data), so Home Assistant's own handler turns on only those.
AL then re-issues light.turn_on for the remaining 'skipped' (unmanaged)
entities so they still come on.
The problem: HA excludes entities with an entity_category (config/diagnostic,
e.g. the Home Assistant Voice LED ring) from area/label expansion, but AL's
re-issue did not, so AL was the sole thing turning these service lights on.
Changes:
- Keep re-issuing skipped lights so unmanaged normal lights still come on,
but filter out 'service' lights (entity_category set) from that re-issue.
- Add _is_service_light helper (registry-based) and divert a *managed*
service light to 'skipped' in _separate_entity_ids, so it is excluded from
the intercept turn-on while still being adapted when on.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add toggle regression test for service light exclusion from area intercept
Mirrors test_service_light_excluded_from_area_intercept_turn_on but uses
light.toggle: a service light (entity_category set) in an area must remain
off when AL intercepts an area toggle. The managed lights still toggle on.
Refs #1510, PR #1511.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Only exclude service lights from indirect (area/device/label) expansion
Previously _is_service_light filtered service lights unconditionally, which
also excluded a service light explicitly named in `entity_id`. Home
Assistant only excludes such lights from indirect area/device/label expansion
and turns them on when directly targeted, so AL must mirror that: service
lights are now excluded from the intercept/re-issue only when not directly
targeted (`direct_entity_ids`).
Adds a regression test for the direct-target case.
Refs #1510, PR #1511.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Use if/elif/else for indirect service-light exclusion; inline check
Address review: collapse the two separate `if is_service`/"if not
is_service" into an `if/elif/else` chain (ruff PLR5501) and inline the
service check as `self._is_service_light(...) and entity_id not in
direct_entity_ids`, which conceptually is `is_indirect_service`.
Matches HA's indirect-only exclusion and keeps the flow simple.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Filter service lights directly in _get_entity_list expansion
Move service-light filtering to the single expansion site
AdaptiveLightingManager._get_entity_list. Area/device expansion is the
only place where HA excludes entity_category lights, so filtering there
mirrors HA and makes the later direct_entity_ids / skipped_normal guards
unnecessary. Explicit entity_id targets bypass expansion and therefore
still turn on service lights, matching HA.
Fully reverts the direct_entity_ids / skipped_normal addition per review.
---------
Co-authored-by: mueslo <mueslo@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
Adds two new read-only state attributes on the AdaptiveSwitch entity:
- manual_control_brightness: list of light entity_ids whose brightness
axis is currently in manual override (i.e. AL is paused for brightness
on those lights).
- manual_control_color: same, for the color axis.
These mirror the existing 'manual_control' attribute (which is a
union of both axes) but expose the LightControlAttributes bitfield
that AL already tracks internally per-light.
Why: the existing 'manual_control' state attribute and the
adaptive_lighting.manual_control event are useful, but neither lets
a template or dashboard see which axis is paused without subscribing
to events. This is especially important with take_over_control_mode:
pause_changed, where one axis can be manual while the other still
adapts. Now a Lovelace card or template sensor can display
brightness/color manual state directly via state_attr().
Test: extends test_manual_control to assert the new attributes
reflect the bitfield correctly.
No new platform, no breaking changes.
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
Handle missing polar sunrise and sunset with a shared fallback. Bound offsets between actual solar anchors while preserving the daily lighting cycle and configured time behavior.
Co-authored-by: Oscar Pacheco <oscar@gigadefense.com.mx>
* Adopt has_entity_name to fix duplicated entity ids on HA 2026.4+
Since HA core 2026.4 (PR 166246) composes entity names as device name +
entity name, and only strips the device prefix when the entity name
starts with it. Adaptive Lighting's names ('Adaptive Lighting Sleep
Mode: stairs' on device 'Adaptive Lighting: stairs') never match, so
new installs get ids like
switch.adaptive_lighting_stairs_adaptive_lighting_sleep_mode_stairs.
Adopt has_entity_name: the main switch takes the device name
('Adaptive Lighting: <name>'), the simple switches use their role
('Sleep Mode', 'Adapt Brightness', 'Adapt Color'). Unique ids are
unchanged, so existing installs keep their entity ids via the registry.
Fixes#1459
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test the new entity ids and that existing ones survive
The renamed constants were defined but never asserted, so neither the
fresh-install ids nor the registry-preservation claim were covered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: keep the apply-service test light on
Avoid generating brightness zero in the attribute-change helper, which turns the light off and makes the test depend on the current adaptive brightness.
---------
Co-authored-by: proscar87 <proscar87@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
* fix: clamp() collapsed to minimum when min_brightness > max_brightness
A user can intentionally set min_brightness > max_brightness (or the
equivalent for color temperature) for an inverted timescale -- e.g. a
porch light that should be brighter at night than during the day.
clamp()'s max(minimum, min(value, maximum)) assumed minimum <= maximum;
when inverted, min(value, maximum) is always <= maximum < minimum, so
max(minimum, ...) always returns minimum. linear and tanh brightness
modes -- both of which end in a clamp(brightness, min_brightness,
max_brightness) call -- got stuck returning one fixed value regardless
of the time of day.
Fixes#1421
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* test: accept current Home Assistant brightness validation errors
Keep asserting the maximum brightness limit without depending on the validation library's dictionary-path wording.
---------
Co-authored-by: Oscar Pacheco <proscar@MacBook-Pro-de-Oscar.local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
When the auto-reset timer fired, its callback called manager.reset(),
which pops the timer and calls timer.cancel() - cancelling the very task
that was running the callback. The manual_control flag was cleared, but
the re-adaptation that should follow was silently cancelled, so the light
only changed on the next interval pass (with the normal transition).
_AsyncSingleShotTimer.cancel() now never cancels the task that is
currently running its own callback. The trailing assert in the callback
is removed because it is reachable now and could trip when a new manual
change comes in while the re-adaptation is still running. The existing
auto-reset test also checks that a light.turn_on with the 'autoreset'
context is sent.
Fixes#1233
Co-authored-by: Dennis-Dekker <>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* Fix options flow changes being silently discarded for UI-configured entries
validate() merges config_entry.options then config_entry.data, on the
assumption that data only ever holds YAML-imported settings (which should
win) or, for UI-created entries, just the entry name (harmless to apply
last).
That assumption doesn't hold for entries created before data/options were
split: their data still carries the full settings snapshot from initial
setup. Applying it after options means any change made through the options
flow for a key that already exists in data (e.g. adding a light) is
silently ignored, even though the options flow reports success and the
entry reloads without error.
Reproduced on a real entry: added a light via the options flow, entry
reloaded cleanly, but the light was never picked up by the switch's
service-call interceptor ("No switch found for entity_id=...") because
data still held the old light list and clobbered the updated options.
Fix: only let data win over options for genuinely YAML-imported entries
(config_entry.source == SOURCE_IMPORT), matching the existing use of that
check elsewhere in this file. For UI-configured entries, apply options
last so changes made through the options flow actually take effect.
* Add focused tests for the data/options merge order in validate()
Covers both source-specific contracts the merge logic relies on, per
review feedback on this PR:
- SOURCE_USER: options must win over data (this PR's actual fix - proven
to fail against the pre-fix code, verified locally by reverting
switch.py and re-running).
- SOURCE_IMPORT: data must keep winning over options (the existing,
intentional YAML-precedence behavior - unchanged by this PR, verified
to already pass against the pre-fix code too).
Verified against a real Home Assistant instance's test harness
(pytest-homeassistant-custom-component + the actual installed
homeassistant package), not just reasoned about statically.
---------
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* fix: use quantization-aware comparison in skip_redundant_commands filter
_remove_redundant_attributes() compared target values against light state
with exact equality, but many targets can never round-trip exactly through
a device with coarser resolution:
- brightness: HA's 0-255 scale vs the 0-99 Z-Wave Multilevel Switch scale
leaves 156 of 255 targets that never converge (e.g. 230 -> 89 -> 229),
- color_temp_kelvin: the kelvin -> mired -> kelvin round trip leaves most
kelvin targets off by up to ~21 K at 6500 K (e.g. 5500 -> 182 -> 5495).
Such attributes survived the filter and were re-sent every interval
forever, which on larger Z-Wave meshes is enough to jam the controller.
Compare brightness with a tolerance of 2 (the exact worst case of the
0-99 scale) and color temperature in mired space, where devices actually
quantize and where the comparison is exact at every kelvin value. Both
are far below the manual-control detection thresholds
(BRIGHTNESS_CHANGE = 25, COLOR_TEMP_CHANGE = 100), so they cannot mask a
genuine user change.
Fixes#1512
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017P2wLQYGQH6npY5op5CKVD
* fix: tolerate one mired to cover both floor- and round-based conversions
The previous exact mired equality assumed round-based kelvin<->mired
conversion, but HA core's color_temperature_kelvin_to_mired() and
color_temperature_mired_to_kelvin() both use math.floor, under which a
target like 5500 K comes back as 5524 K in a different rounded mired
bucket and would never be filtered. Flooring in the comparison instead
would merely flip the failure onto integrations that round.
Comparing with a tolerance of one mired converges for both conversion
schemes (verified by brute force over 1000-10000 K: zero stuck targets
under either pipeline) and can hide at most ~2 mireds, far below the
~5.5 mired just-noticeable difference for color temperature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017P2wLQYGQH6npY5op5CKVD
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* fix: don't cancel adaptation when a light group turns on via a member with a reused context (#1378)
When a member of a light group is turned on (e.g., by a motion sensor
automation) while the group is off, the group turns on as a side effect,
but Home Assistant may reuse the context of the earlier turn_off call for
the group's state change. just_turned_off() saw matching context IDs and
treated the state change as a polling artifact, cancelling adaptation.
- Check whether the off->on state change comes from a light.turn_on call
before the matching-context polling-artifact check, so automations that
turn a light off and back on with a single (automation) context adapt
correctly.
- For light groups, allow adaptation when a member's turn_on event falls
between the group's on->off and off->on state changes, bounded on both
sides so stale member events are never treated as explanatory.
- Document that integration-level groups (e.g., Zigbee2MQTT groups) should
not be nested inside HA Light Groups managed by Adaptive Lighting.
* fix: time-bound the same-context turn_on check instead of reordering
Address review findings:
- Reordering the turn_on-service check above the matching-context check
reintroduced stale-event false negatives: turn_on_event entries are never
cleaned up, so a 'turn_on -> delay -> turn_off(transition)' automation
(one shared context) would defeat the polling-artifact guard and AL could
turn a light back on right after it was turned off. Restore main's check
order and instead add a time-bounded own-turn_on check inside the
matching-context branch, symmetric with the group-member check. This
also avoids emitting the 'should not happen' warning for self-context
polling artifacts.
- Add a regression test for the stale same-context turn_on case.
- Add an end-to-end test driving the event-bus listeners for the #1378
scenario (group kept in manager.lights, as in the reported setups).
- Docs: drop the inaccurate 'expands only one level deep' claim; explain
that integration-level groups cannot be expanded and nested groups make
tracking unpredictable.
* fix: replace deprecated get_astral_location with get_astral_observer (#1481)
HA 2026.7 deprecates homeassistant.helpers.sun.get_astral_location
(removal planned for 2027.7) in favor of get_astral_observer, causing a
deprecation warning in the HA logs.
- Switch SunEvents/SunLightSettings from astral.location.Location to
astral.Observer, using the astral.sun module functions (which return
UTC times by default, matching the previous local=False calls).
- Use get_astral_observer in switch.py, with a fallback for HA < 2026.7
that constructs the Observer directly from the HA config.
- Update tests and the webapp simulator accordingly.
* ci: handle removal of requirements_test_all.txt in HA 2026.8 dev
HA core removed requirements_test_all.txt (home-assistant/core#171530),
which made test_dependencies.py crash with FileNotFoundError and broke
the dev pytest job and the Docker builds. Fall back to
requirements_all.txt, which carries the same per-integration
'# homeassistant.components.x' annotations. Also extend the
aiohasupervisor pin lookup in scripts/setup-dependencies accordingly.
* test: support modern template light config for HA 2026.6+
HA 2026.6 removed the legacy `light: platform: template` YAML format
(home-assistant/core#169615), so setup_lights found no template platform
on HA dev and every test using it failed with IndexError. Detect legacy
support at runtime (PLATFORM_SCHEMA presence) and fall back to the
modern `template:` config format. The group platform is set up before
the template integration in the modern path, because setting up
`template` also sets up the `light` domain, which would make a later
async_setup_component(hass, LIGHT_DOMAIN, ...) a no-op.
* fix: reduce log verbosity for self-triggered off-to-on warning (#1433)
Move full event object dump from warning to debug level in
_off_to_on_state_event_is_from_turn_on() (switch.py:2717).
For lights with large effect_list attributes (e.g. Govee lights
with 130+ effects), the warning dumped the entire Event object
including both old_state and new_state, creating log entries
thousands of characters long.
The warning now logs only entity_id and context.id, while the
full event remains available at debug level for troubleshooting.
Fixes#1433
* fix: correct indentation for _LOGGER.debug block
* fix: correct indentation for _LOGGER.warning and _LOGGER.debug
* fix: remove unintended encoding corruption, keep only log level change
Reset switch.py to main and re-apply only the intended change:
move the full event object from warning to debug level in
_off_to_on_state_event_is_from_turn_on().
Addresses reviewer feedback about unintended Unicode corruption
(→ and ≈ characters were corrupted to mojibake).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Florian Horner <florianhorner@Mac.fritz.box>
Co-authored-by: Bas Nijholt <basnijholt@users.noreply.github.com>
## Summary
- fix hassfest validation by replacing raw options-description URLs with Home Assistant translation placeholders
- extend the pytest CI matrix to cover the latest patch release for each Home Assistant month, plus dev
- align CI/dev container Python versions and tests with newer Home Assistant behavior
## Validation
- GitHub Actions pytest matrix passed for 2024.12.5 through 2026.4.3 plus dev
- Docker passed for linux/amd64 and linux/arm64
- hassfest, HACS validation, pre-commit, pre-commit.ci, markdown-code-runner, docs build, and Release Drafter passed
* test: regression test — AL must not override manual brightness with separate_turn_on_commands
End-to-end scenario: user adjusts brightness via a directly-bound Zigbee
switch (e.g. IKEA RODRET). No HA service call is made; ZHA reports the new
brightness via async_update_entity. On the next adaptation interval AL must
detect the change and stop overriding the user's brightness.
The test verifies the user-visible symptom: after two adaptation cycles
following a simulated direct-Zigbee brightness change, the light's brightness
must still be the manually set value — not AL's own target.
NOTE: this test FAILS on the current code. It is committed here to document
the bug before the fix is applied in the next commit.
* fix: merge last_service_data across split calls to fix detect_non_ha_changes with separate_turn_on_commands
When separate_turn_on_commands=True, each adaptation cycle makes two
light.turn_on calls (brightness, then color_temp). Previously each call
overwrote last_service_data[light], so after the cycle only the color_temp
key remained. _attributes_have_changed() then saw old_brightness=None and
silently skipped the brightness comparison, so a manually-set brightness was
never detected and AL kept overriding it.
Fix: merge instead of overwrite so all split-call attributes accumulate:
self.manager.last_service_data[light] = {
**self.manager.last_service_data.get(light, {}),
**service_data,
}
* test: add intermediate assertions to regression test
Two assertions were promised in the PR description but missing:
1. After the force-adapt, assert that last_service_data contains BOTH
brightness AND color — directly proving the merge fix works.
2. After the first non-forced update, assert that BRIGHTNESS is in
manual_control — proving detection fired, not just that the final
state is right.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* refactor: remove spurious comments, trim test docstring and assertions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: strip verbose comments from test, trim assert messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* test: add message to bare assert
* Add documentation site with Zensical framework
Create comprehensive documentation site for adaptive-lighting.nijho.lt:
- Add zensical.toml configuration with Material theme (amber/orange)
- Create docs_gen.py module for extracting README sections via markers
- Add section markers to README.md for content reuse
- Create documentation pages:
- index.md: Home with features overview
- getting-started.md: Installation and quick setup
- configuration.md: Auto-generated config options table
- services.md: Auto-generated service documentation
- automation-examples.md: Real-world automation recipes
- troubleshooting.md: Common issues and solutions
- see-also.md: External resources and links
- advanced/brightness-modes.md: Brightness mode deep dive
- advanced/manual-control.md: Manual control system docs
- advanced/sleep-mode.md: Sleep mode configuration
- Add GitHub Actions workflow for building and deploying to Pages
- Add custom CSS with sun-themed styling
- Add CNAME for custom domain
Uses markdown-code-runner to auto-generate content from code schemas
and extract README sections for single-source documentation.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Remove duplicated content from docs, make pages thin wrappers
- troubleshooting.md: Remove manually written "Additional Tips" section
- automation-examples.md: Remove duplicate "Additional Examples" section
- configuration.md: Remove duplicate "Option Categories" tables
- sleep-mode.md: Simplify to reference main config, remove duplicate examples
- docs_gen.py: Remove unused get_troubleshooting() and get_sleep_mode_intro()
This reduces duplication risk by keeping README as single source of truth.
Docs pages now primarily pull content via markdown-code-runner.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Integrate webapp (simulator) into docs workflow
- Merge deploy-webapp.yml into docs.yml workflow
- Build simulator and place at /simulator/ subdirectory
- Update docs links to use relative paths to simulator
- Remove separate deploy-webapp.yml to avoid conflicts
The combined workflow now:
1. Builds docs with zensical
2. Builds webapp with shinylive
3. Copies webapp to site/simulator/
4. Deploys everything to GitHub Pages
Simulator will be at adaptive-lighting.nijho.lt/simulator/
* Fix pre-commit and CI issues
- Add site_name to zensical.toml (required by MkDocs)
- Fix RET504 in docs_gen.py (unnecessary assignment before return)
- Remove docs/run_markdown_code_runner.py (lint issues, not needed for CI)
* Fix _docs_helpers.py import error in CI
- Add try/except for relative vs absolute imports in _docs_helpers.py
- Remove silent error handling in docs workflow (fail on error)
The relative import fails when markdown-code-runner executes the code
directly via sys.path.insert. The fallback to absolute import fixes this.
* Temporarily enable deployment from feature branch
* Add tabulate dependency for pandas to_markdown()
* Fix theme configuration for proper light/dark mode
- Restructure zensical.toml to match working agent-cli config
- Add three-way palette toggle (system/light/dark)
- Use proper [project.theme] structure
- Simplify extra.css to not override theme colors
- Add Inter font for text, JetBrains Mono for code
* Add Plausible analytics and fix homepage navigation
- Add custom analytics override for plausible.nijho.lt tracking
- Remove hide:navigation from index.md to show menu on homepage
* Remove temporary feature branch deployment settings
Revert to main-only deployment for docs workflow before merging.
* Revert "Remove temporary feature branch deployment settings"
This reverts commit c568694837.
* Add markdown-gfm-admonition for GitHub-style admonitions
The zensical build was failing silently because gfm_admonition extension
was not installed. Add the dependency to pyproject.toml and docs workflow.
* Use uv sync for documentation dependencies
Switch from manual pip installs to uv sync with pyproject.toml for cleaner
dependency management and reproducible builds.
* Fix markdown rendering inside details blocks
Enable md_in_html extension and add markdown="1" attribute to <details>
tags so markdown content inside them is properly rendered.
* Remove emojis from manually written documentation
Keep emojis in auto-generated content from README, but remove from
manually maintained docs in favor of clean text and Material icons.
* Enable attr_list extension for button styling
* Improve pyproject.toml and use GitHub-style admonitions
- Add accurate project metadata (version, authors, classifiers, URLs)
- Organize dependency groups: docs, dev, test
- Add tool configs for ruff, mypy, pytest
- Convert MkDocs-style admonitions (!!! tip) to GitHub-style (> [!TIP])
- Use docs group in CI workflow
* Add homeassistant and ulid-transform as runtime dependencies
Remove speculative test dependencies since tests run inside HA core.
* Simplify docs_gen.py - remove wrapper functions
Use readme_section() directly in docs instead of 10 one-liner wrappers.
Changed default strip_heading to True since that's the common case.
* Populate empty OUTPUT sections with markdown-code-runner
* Add markdown-code-runner workflow for auto-updating docs
- Add docs/run_markdown_code_runner.py script to process all docs
- Add GitHub workflow to run on push/PR and auto-commit changes
* Exclude README.md from markdown-code-runner workflow
README.md contains code blocks that import from
homeassistant.components.adaptive_lighting, which only exists
when running inside Home Assistant core, not in a regular venv.
* Update auto-generated docs
* Use editable install for markdown-code-runner workflow
- Add setuptools.packages.find config pointing to custom_components
- Remove sys.path.insert manipulation from all docs files
- Update imports to use adaptive_lighting.* package paths
- Install package with `uv pip install -e .` in workflow
- Remove deprecated license classifier (PEP 639)
* Consolidate markdown-code-runner into single workflow
- Remove separate markdown-code-runner.yml workflow
- Update update-readme.yml to handle all markdown files (docs + README)
- Rename workflow to "Update auto-generated content"
- Update README imports to use adaptive_lighting package path
* Rename workflow to markdown-code-runner
* Remove accidentally committed files
* Remove redundant markdown-code-runner from docs workflow
* Fix: use uv pip install instead of uv add in CI
* Add webapp deps (astral, shinylive) to docs group
* Remove unused install_dependencies action
* Update to latest action versions (uv@v5, upload-pages-artifact@v4)
* Remove try/except import fallback in _docs_helpers.py
* Restore install_dependencies action (used by pytest)
* Simplify docs workflow: run on all pushes/PRs
* Simplify mcr workflow paths; revert install_dependencies to main
* Remove redundant cp+sed for webapp (file already in repo)
* Fix mcr push: pull --rebase before push
* Fix mcr: checkout PR branch instead of detached HEAD
* Update auto-generated content
* Switch from setuptools to hatch build system
Replace [tool.setuptools.packages.find] with [tool.hatch.build.targets.wheel]
for hatchling compatibility.
* Update auto-generated content
* Move homeassistant deps to docs group
This is a HA custom component, not a pip package. The homeassistant
dependency is only needed for docs building, not as a project dependency.
* Update auto-generated content
* Remove PyPI-only metadata from pyproject.toml
* Remove arbitrary version constraints from dependency groups
* Update auto-generated content
* Remove unused troubleshooting section markers from README
* Remove temporary feature branch settings from docs workflow
* Use GitHub admonition syntax for warning in change_switch_settings section
* Update auto-generated content
## Summary
- Fixes regression in v1.30.0 where lights turned on by automations were incorrectly marked as "manually controlled"
- Makes `adapt_only_on_bare_turn_on` respect individual attribute tracking from #1356
## Root Cause
PR #1356 added a call to `update_manually_controlled_from_event()` in the `turn_on_off_event_listener.on()` handler for ALL `light.turn_on` events, including when turning a light on from OFF state.
When an automation turns on a light with brightness/color attributes, this incorrectly marked the light as "manually controlled", preventing Adaptive Lighting from adapting it.
## Fix
1. Only call `update_manually_controlled_from_event()` when the light was **already ON** before the turn_on event. Turning on from OFF is handled by `_respond_to_off_to_on_event()`.
2. Make `adapt_only_on_bare_turn_on` respect `take_over_control_mode`:
- With `PAUSE_CHANGED`: Only pause adaptation of specified attributes, continue adapting unspecified ones
- With `PAUSE_ALL`: Pause all adaptation (existing behavior)
## Expected Behavior After Fix
| Scenario | `adapt_only_on_bare_turn_on` | `take_over_control_mode` | Result |
|----------|------------------------------|--------------------------|--------|
| Turn on from OFF with brightness | `false` | Either | NOT manually controlled |
| Turn on from OFF with brightness | `true` | `PAUSE_ALL` | All adaptation paused |
| Turn on from OFF with brightness | `true` | `PAUSE_CHANGED` | Only brightness paused, color adapts |
| Turn on from OFF without attributes | Either | Either | NOT manually controlled |
| Change brightness while ON | Either | Either | Brightness manually controlled |
## Test plan
- [x] Turn on light via automation with brightness/color (`adapt_only_on_bare_turn_on=false`) - should adapt
- [x] Turn on light via scene (`adapt_only_on_bare_turn_on=true`, `PAUSE_ALL`) - should pause all adaptation
- [x] Turn on light with brightness only (`adapt_only_on_bare_turn_on=true`, `PAUSE_CHANGED`) - should adapt color
- [x] Both intercept=True and intercept=False paths tested for consistency
- [x] CI tests pass
Fixes#1378
Co-authored-by: Mario Guggenberger <mg@protyposis.net>
* refactor: introduce light control parameter enum
* refactor: replace manual control flag with parameter enum
* test: update deprecated color temp attribute
* build: set execution bits on task scripts
* feat: individual manual control of brightness and color
* test: add tests for individual manual control evaluation
* fix: sequential manual changes not always detected
If multiple attributes of a light were changed within an interval, only the last change was detected because the check in the interval only used the latest event. For example, if there was a brightness change and a following color change, only the color attribute was detected as manually controlled. To fix this, the manual control attribute flags are now set directly from the event handler so that all events are processed.
* fix: invalid service description
* docs: fix missing space in config description
* refactor: pluralize multivalued bitmask enum name
Replace cv.multi_select with HA's EntitySelector for the lights field
in the options flow. This provides:
- Built-in search/typing to find entities faster
- Better UX with HA's native entity picker
- Improved handling of renamed entities
Closes#1208
* feat: add option to duplicate existing lighting instance
A menu step in the config flow was added that allows a user to create a
new instance or duplicate the options of an existing one.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add type annotation, tests, and translations for duplicate feature
- Add type annotation for source_options class attribute
- Add menu step translations to en.json
- Add tests for menu display, new instance creation, and duplication
* Simplify source_options access with class-level default
---------
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* Translated using Weblate (Galician)
Currently translated at 50.3% (77 of 153 strings)
Translated using Weblate (Galician)
Currently translated at 49.0% (75 of 153 strings)
Translated using Weblate (Galician)
Currently translated at 46.4% (71 of 153 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Yago Raña Gayoso <yago.rana.gayoso@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/gl/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Turkish)
Currently translated at 100.0% (153 of 153 strings)
Added translation using Weblate (Turkish)
Co-authored-by: Furkan Kaya <fkaya.personal@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/tr/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 56.8% (87 of 153 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rafael do Amaral Porciuncula <rafael.do.amaralp@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt_BR/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Danish)
Currently translated at 90.1% (138 of 153 strings)
Translated using Weblate (Danish)
Currently translated at 88.8% (136 of 153 strings)
Translated using Weblate (Danish)
Currently translated at 88.8% (136 of 153 strings)
Translated using Weblate (Danish)
Currently translated at 88.8% (136 of 153 strings)
Co-authored-by: Emil Friis Osmann <Emilfriisosmann@gmail.com>
Co-authored-by: Hans Henrik Juhl <hans@kopula.dk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/da/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Russian)
Currently translated at 99.3% (152 of 153 strings)
Co-authored-by: Athish Athish <athisha660@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ru/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Ukrainian)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/uk/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Japanese)
Currently translated at 48.3% (74 of 153 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: M.Sugahara <equaaqua@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ja/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Hungarian)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hu/
Translation: Adaptive Lighting/Adaptive Lighting
* Translated using Weblate (Slovenian)
Currently translated at 71.2% (109 of 153 strings)
Added translation using Weblate (Slovenian)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tim Music <tim.music98@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sl/
Translation: Adaptive Lighting/Adaptive Lighting
---------
Co-authored-by: Yago Raña Gayoso <yago.rana.gayoso@gmail.com>
Co-authored-by: Furkan Kaya <fkaya.personal@gmail.com>
Co-authored-by: Rafael do Amaral Porciuncula <rafael.do.amaralp@gmail.com>
Co-authored-by: Emil Friis Osmann <Emilfriisosmann@gmail.com>
Co-authored-by: Hans Henrik Juhl <hans@kopula.dk>
Co-authored-by: Athish Athish <athisha660@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: M.Sugahara <equaaqua@hotmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Tim Music <tim.music98@gmail.com>
* Add regression tests for SimpleSwitch initial state bug
Adds tests that verify SimpleSwitch._state is set immediately in __init__
rather than waiting for async_added_to_hass(). These tests currently FAIL
because _state is None after __init__, which causes an infinite loop in
_setup_listeners when the entity is disabled (since async_added_to_hass
is never called for disabled entities).
Regression tests for: https://github.com/basnijholt/adaptive-lighting/issues/1264
* Fix infinite loop when disabling SimpleSwitch entities
The issue was that SimpleSwitch._state was initialized to None in __init__,
but only set to a boolean value in async_added_to_hass(). When an entity
is disabled, async_added_to_hass() is never called, so _state stayed None.
The _setup_listeners() method has a while loop that waits for
_state is not None for all SimpleSwitch children (sleep_mode_switch,
adapt_brightness_switch, adapt_color_switch). With _state stuck at None,
this created an infinite loop.
The fix sets _state to initial_state directly in __init__ instead of
waiting for async_added_to_hass() to set it. The async_added_to_hass()
will still properly restore state from the last session or set based
on initial_state as before.
Fixes: https://github.com/basnijholt/adaptive-lighting/issues/1264
When start() creates a task with asyncio.create_task(), the task is scheduled
but not immediately executed. This means is_running() returns True (task exists
and not done), but start_time is still None because _run() hasn't executed yet.
This causes a TypeError when comparing event.time_fired > timer.start_time.
Fix by setting start_time in start() before creating the task.
Fixes#1272
* remove @bind_hass decorator
* attempt to pass hass
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
* suggested fix for HA2025 deprecation warnings
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>