Diagnostic sensors#1414
Conversation
florianhorner
left a comment
There was a problem hiding this comment.
Review Summary
Useful diagnostic feature — knowing why a light isn't adapting is a common user question. The architecture (opt-in sensors per profile) is well-chosen. Some issues to address before merge.
Critical Issues
1. Duplicate _expand_light_groups in sensor.py
This function already exists in switch.py (line 588). Import it from there instead of reimplementing — duplicated logic will drift over time.
2. Redundant double set_light_status(STATUS_ACTIVE, "apply")
Called in both _adapt_light AND execute_cancellable_adaptation_calls. The first call is immediately overwritten by the second. Remove the first — it's dead code.
3. Test/framework deps in runtime dependencies
pytest-asyncio==1.3.0 and homeassistant>=2024.12.5 are in [project] runtime deps. Both belong in [dependency-groups]. HA custom components cannot declare HA itself as a pip dependency.
Quality Issues
4. ensure_status_sensors_enabled overrides user-disabled entities
If a user intentionally disables a diagnostic sensor via the UI, this re-enables it. Check disabled_by != EntityRegistryEntryDisabler.USER before re-enabling.
5. STATUS constants should be StrEnum
The codebase already uses StrEnum for TakeOverControlMode. Plain strings miss typo detection. Define LightStatus(StrEnum).
6. Unused return type change
execute_cancellable_adaptation_calls return type changed to bool but neither call site checks the return value.
7. _status_priority allocates a new dict on every call
This is constant data — make it a module-level STATUS_PRIORITY constant.
Efficiency Issues
8. get_combined_status called twice per sensor write
Both native_value and extra_state_attributes call it independently. Cache the result.
9. get_manual_control_attributes called twice
Same pattern in extra_state_attributes. Cache it.
10. Unbounded light_status dict
Never pruned when lights are removed. Add self.light_status.pop(light, None) in reset().
11. Bare async_dispatcher_send bypasses dedup
In state_changed_event_listener, the dispatcher fires even when no status changed.
Main blockers are #1 (duplicated code) and #3 (runtime deps). Looking forward to the next iteration! 👍
4751047 to
cc64fbf
Compare
|
Based on the feedback found on the pull request PR #1414, here are the 11 key points and their current status after the recent refactoring:
|
florianhorner
left a comment
There was a problem hiding this comment.
Review: Diagnostic Sensors PR
Thanks for putting this together @lehneres — the concept is solid and the opt-in design via enable_diagnostic_sensors is the right call. Per-light status visibility is genuinely useful for debugging AL behavior. That said, I found a few issues that need addressing before this can merge.
P0 — Must fix
1. reset() wipes light_status immediately after writing it (switch.py, reset() method)
The code first calls set_light_status() for each switch (setting INACTIVE or ACTIVE + firing the dispatcher signal), then a few lines later does:
self.light_status.pop(light, None)This deletes everything that was just written. The sensor will briefly flash the correct state from the dispatcher, then revert to the empty-dict fallback (LightStatus.INACTIVE). Every call to reset() — switch turn_off, autoreset, manual reset service — produces a corrupted sensor state cycle.
The same issue compounds in async_turn_off: it sets INACTIVE for each light, then calls reset(*self.lights) which sets status again and then pops it all.
Fix: Remove the self.light_status.pop(light, None) line. The preceding set_light_status calls already establish the correct state. If you want a clean slate, pop before setting, not after.
2. pytest-asyncio==1.3.0 does not exist (pyproject.toml)
Current releases are 0.x (e.g., 0.25.x). This pin will cause uv sync / pip install to fail on a fresh checkout. CI will break.
Fix: Use a valid version range, e.g., pytest-asyncio>=0.23,<1.
P1 — Should fix in this PR
3. status_sensors store never cleaned on unload/reload (sensor.py:71)
hass.data[DOMAIN].setdefault("status_sensors", {}) persists across config entry reloads. After unload, the if light in store: continue check in async_setup_entry skips all lights — sensors are never recreated. The extra top-level key also prevents the manager cleanup logic in async_unload_entry (len(data) == 1 check) from ever being True.
Fix: Pop "status_sensors" in async_unload_entry, or scope the store by entry_id.
4. ensure_status_sensors_enabled overrides user-disabled sensors (sensor.py:56-58)
The check entry.disabled_by is not None also matches RegistryEntryDisabler.USER. A user who intentionally disables a noisy sensor via the HA UI will have it silently re-enabled on every restart/reload. This breaks HA's entity registry contract.
Fix: Guard with entry.disabled_by not in (None, RegistryEntryDisabler.USER), or only re-enable entities disabled by the integration itself.
5. pref_disable_new_entities silently cleared (__init__.py:78-82)
When diagnostic sensors are enabled and pref_disable_new_entities is True, the code sets it to False. This affects the entire config entry, not just diagnostic sensors — all future new entities will auto-enable, which is unexpected.
Fix: Don't clear the preference globally. Instead, explicitly enable the specific sensor entities after creation (which ensure_status_sensors_enabled already attempts to do).
6. sensor.py imports validate from switch.py (sensor.py:42)
validate() is switch-internal config processing logic. sensor.py only needs one boolean from the config. The same value is available directly via config_entry.options.get(CONF_ENABLE_DIAGNOSTIC_SENSORS, DEFAULT_ENABLE_DIAGNOSTIC_SENSORS) or config_entry.data.get(...). This creates unnecessary coupling.
P2 — Worth addressing
7. STATUS_PRIORITY uses raw string keys, not LightStatus enum (const.py:35-41)
You introduced StrEnum for LightStatus (good!) but then keyed the priority dict with raw strings. A typo like "manual_overide" would silently give priority 0. Use LightStatus.ERROR: 5, LightStatus.MANUAL_OVERRIDE: 4, ... to get the type safety you're paying for.
8. Cross-profile sensor deduplication loses second profile (sensor.py:78-80)
The store is keyed by light_entity_id only, not (entry_id, light). When two profiles control the same light (both with diagnostics enabled), the second profile's sensors are silently skipped. The PR description mentions "aggregate status across multiple profiles" but the implementation doesn't aggregate — it just skips.
9. Double status dispatch per adaptation cycle (switch.py, _adapt_light)
set_light_status(ACTIVE, reason="apply") fires before the adaptation call, then set_light_status(ACTIVE, reason="applied") fires after. Both are ACTIVE but different reasons bypass the dedup check. With 20 lights at 90s intervals, that's 40 unnecessary dispatcher fires per cycle. Remove the pre-call status; only set after the result.
10. docs/status_sensors.md referenced but doesn't exist (README.md:39)
The README links to docs/status_sensors.md which isn't in the PR. Users clicking through will get a 404.
11. Missing type annotations on async_setup_entry (sensor.py:62-64)
async def async_setup_entry(
hass: HomeAssistant,
config_entry, # missing: ConfigEntry
async_add_entities, # missing: AddEntitiesCallback
) -> None:The rest of the codebase uses full annotations. This will fail mypy strict and hassfest platform validation.
Scope concerns
12. Python 3.14 bump (.devcontainer.json, Dockerfile, scripts/setup-devcontainer)
Python 3.14 is pre-release (alpha/beta as of April 2026). HA doesn't require it. This is unrelated to diagnostic sensors and will break dev environments for anyone without a 3.14 build. Should be a separate PR.
13. validate() merge order fix (switch.py, validate())
The swap of data.update(config_entry.options) and data.update(config_entry.data) is a real bug fix (options should override data, not the other way around), but it's unrelated to diagnostic sensors and could have subtle behavior changes. Worth calling out separately or splitting into its own PR.
14. type → TypeAlias change (switch.py:1772-1773)
The type statement syntax requires Python 3.12+ and works fine. Changing to TypeAlias is a downgrade in expressiveness. If this is for 3.11 compat, the pyproject.toml already requires >=3.12. Either way, unrelated to this feature.
Test coverage
The single test (test_diagnostic_status_sensors_created) only verifies entity creation + initial INACTIVE state. Missing coverage for:
- Status transitions (ACTIVE, BLOCKED, MANUAL_OVERRIDE, ERROR)
- Combined status priority logic
- Multi-profile behavior
- Sensor cleanup on unload
enable_diagnostic_sensors=Falsepath (no sensors created)expand_light_groupsin sensor setup
Verdict
Request changes. Items 1-2 are functional bugs. Items 3-6 are architectural issues that will accumulate debt if not fixed now. Items 12-14 should be split into separate PRs to keep this one focused.
The feature itself is well-conceived and I'd like to see it land. Happy to re-review once these are addressed.
|
thank you @florianhorner for your review, see changes below:
|
Extends PR basnijholt#1414's test_sensor.py with: - test_status_since_timestamp_updates: verifies status_since attribute is a valid ISO timestamp and updates on each status transition - test_sensor_survives_light_entity_missing: verifies sensor creation succeeds when the light entity doesn't exist in the state machine - test_dispatcher_signal_updates_sensor: verifies SIGNAL_STATUS_UPDATED dispatcher signal triggers sensor state write and filters by light_entity_id Note: INP001 (missing tests/__init__.py) is a pre-existing repo issue affecting all test files, not introduced by this change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Additional test coverageI wrote 3 additional tests for the diagnostic sensor feature. The branch is at New tests (14 total, up from 11):
Also applied ruff/black auto-formatting fixes to the existing test file (trailing commas, import ordering). |
…ated into 5-step UI)
|
Hey @lehneres — nice work on the fixes, the April 3 push addresses the major issues cleanly. One thing still open: on your question about if entry.disabled_by is not None and entry.disabled_by != RegistryEntryDisabler.USER:
ent_reg.async_update_entity(entry.entity_id, disabled_by=None)Also — the 3 tests I wrote on Once those two things land I'm happy to re-review and approve. 👍 |
|
Sorry it took me so long to get to this. I've been a bit overwhelmed by the number of AI-assisted PRs opened here recently, and I've also been spending nearly every spare hour on my biggest project so far, MindRoom. I'm very supportive of using AI for coding, but many of these PRs still need careful human review because even plausible-looking changes can introduce subtle breakage. That backlog made me postpone reviewing them for a while. I've now done a batch review with Codex / GPT-5.5 (xhigh). This comment is AI-assisted, but I've reviewed the concern before posting it. Thanks for the substantial work here. I like the idea of per-light diagnostic status, but I found a couple lifecycle issues that look like they can leave stale state behind. First, Second, the manager cleanup in if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data:After this PR, Could you rework the sensor ownership/unload path so it is tied to config entries, removes stale sensors when options/lights change, and keeps the final-entry manager cleanup working? |
- Implemented per-light status tracking to provide detailed states like "active," "inactive," "blocked," and "manual override." - Added new `sensor` platform for enhanced diagnostics. - Updated README to include diagnostic status sensors. - Introduced various status-related attributes and methods for better monitoring and troubleshooting.
Added a new configuration option `enable_diagnostic_sensors` to control the creation of per-light status sensors in the Adaptive Lighting integration. Sensors are now disabled by default and can be enabled via the options flow. Updated related files, including initialization logic, constants, translations, and documentation.
4d99904 to
dde4e79
Compare
- Centralized light group expansion logic in `helpers.py` with `expand_light_groups`. - Simplified light status management using a new `LightStatus` enum with priority mapping. - Replaced scattered status strings with `LightStatus` constants for consistency. - Updated dev dependencies in `pyproject.toml` and reorganized requirements. - Enhanced `LightStatusInfo` for better status tracking and debugging.
…RITY to use LightStatus enum keys, fixed sensor store logic for multi-profile aggregation and cleanup, added status_sensors cleanup on domain unload, reverted TypeAlias and Python version changes, removed broken README link, and fixed pytest-asyncio version.
67cd7a8 to
529de19
Compare
for more information, see https://pre-commit.ci
TL;DRI rechecked the latest head ( Still Blocking
Timing / Behavior RiskThis PR also touches live adaptation paths around cancellation, transition blocking, recent-off blocking, and manual-override status updates. Please keep focused regression coverage around those paths, since this feature is diagnostic but the implementation sits directly on live lighting behavior. Once those land and pre-commit is green, I’m happy to re-review. |
florianhorner
left a comment
There was a problem hiding this comment.
TL;DR
Submitting this as my formal requested review: request changes until the remaining lifecycle and CI issues are fixed.
The concrete blockers are in my latest comment: #1414 (comment)
Main points:
- Sensor lifecycle ownership/unload still needs to be tied to config entries and stale sensors must be cleaned up.
- Final-entry manager cleanup still needs to work when
status_sensorsexists inhass.data[DOMAIN]. - pre-commit.ci is currently failing and needs to be green.
uv.lockappears stale around thepytest-asynciopin.
Happy to re-review once those are addressed.
Extends PR basnijholt#1414's test_sensor.py with: - test_status_since_timestamp_updates: verifies status_since attribute is a valid ISO timestamp and updates on each status transition - test_sensor_survives_light_entity_missing: verifies sensor creation succeeds when the light entity doesn't exist in the state machine - test_dispatcher_signal_updates_sensor: verifies SIGNAL_STATUS_UPDATED dispatcher signal triggers sensor state write and filters by light_entity_id Note: INP001 (missing tests/__init__.py) is a pre-existing repo issue affecting all test files, not introduced by this change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Adjusted `pytest` and `pytest-asyncio` version constraints for compatibility. - Optimized `LightStatusInfo` computation with caching. - Improved status sensor store cleanup and unloading on domain removal.
for more information, see https://pre-commit.ci
- Replaced `os.path` with `pathlib` for cleaner path handling in `conftest.py`. - Improved exception handling and return logic in `switch.py`. - Reduced unnecessary imports and added `TYPE_CHECKING` for typing optimizations in `sensor.py`. - Enhanced dictionary formatting consistency in `sensor.py`.
|
thanks both - see below a overview of review points and fixes. Thanks for your patience - my first project directly involving AI. Clearly game changing but its also like sailing into a new harbour. At a night. And its foggy... Review Audit — All 26 points addressedPoint-by-point check of every comment across all three reviews.
|
Summary
This PR adds optional diagnostic status sensors per light, giving visibility into whether Adaptive Lighting is actively controlling a light, blocked, manually overridden, or idle. It also aggregates status across multiple profiles controlling the same light.
Key Changes
sensor.py) with one diagnostic sensor per light.switch.pywith per-profile and combined state.const.py.enable_diagnostic_sensors(defaultfalse) to toggle sensors per profile.docs/status_sensors.mdand README note.Behavior
inactive,adopting,active,manual_override,blocked,error.error > manual_override > adopting > active > blocked > inactive.status_profilesattribute exposes per-profile detail.inactive.enable_diagnostic_sensorsis true.Testing
python3 -m py_compileon updated files.Notes