From 07a25aefce4eda76920825fd30cd2e21f762e7bd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 09:43:11 -0700 Subject: [PATCH] Clarify accumulated diagnostics values --- README.md | 11 +- .../adaptive_lighting/diagnostics.py | 13 +- docs/troubleshooting.md | 11 +- tests/test_diagnostics.py | 118 +++++++++++++++++- 4 files changed, 142 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 17fadaee..6310cc83 100644 --- a/README.md +++ b/README.md @@ -585,9 +585,14 @@ logger: After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). For support, use Home Assistant's **Download diagnostics** action on the -Adaptive Lighting config entry. The download is an on-demand snapshot of that -profile's current switch and effective-light facts. It does not create live -sensors; existing switch attributes remain the interface for automations. +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. diff --git a/custom_components/adaptive_lighting/diagnostics.py b/custom_components/adaptive_lighting/diagnostics.py index 22c71840..80ae56d0 100644 --- a/custom_components/adaptive_lighting/diagnostics.py +++ b/custom_components/adaptive_lighting/diagnostics.py @@ -37,11 +37,15 @@ _TARGET_ATTRIBUTES = ( ) -def _last_sent_target( +def _last_adaptation_values( manager: AdaptiveLightingManager, light: str, ) -> dict[str, Any] | None: - """Return allowlisted target attributes from the last adaptation command.""" + """Return latest retained value for each allowlisted adaptation attribute. + + Values may come from different commands because the manager merges partial + service data per attribute. + """ service_data = manager.last_service_data.get(light) if service_data is None: return None @@ -108,7 +112,10 @@ async def async_get_config_entry_diagnostics( "color": bool(manual_control & LightControlAttributes.COLOR), }, "global_manager_autoreset_seconds": _autoreset_seconds(manager, light), - "global_manager_last_sent_target": _last_sent_target(manager, light), + "global_manager_last_adaptation_values": _last_adaptation_values( + manager, + light, + ), } return { diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 92339dd1..c5f59353 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -26,9 +26,14 @@ logger: After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). For support, use Home Assistant's **Download diagnostics** action on the -Adaptive Lighting config entry. The download is an on-demand snapshot of that -profile's current switch and effective-light facts. It does not create live -sensors; existing switch attributes remain the interface for automations. +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 37c64ec2..9c96b295 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -2,10 +2,13 @@ import json from copy import deepcopy +from unittest.mock import patch import pytest from homeassistant.components.adaptive_lighting.adaptation_utils import ( + AdaptationData, LightControlAttributes, + _create_service_call_data_iterator, ) from homeassistant.components.adaptive_lighting.const import ( ATTR_ADAPTIVE_LIGHTING_MANAGER, @@ -23,11 +26,14 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + SERVICE_TURN_ON, ) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, CONF_LIGHTS, CONF_NAME, EVENT_CALL_SERVICE, @@ -35,6 +41,7 @@ from homeassistant.const import ( STATE_OFF, STATE_UNAVAILABLE, ) +from homeassistant.core import State from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -162,7 +169,7 @@ async def test_config_entry_diagnostics_reports_allowlisted_current_facts( assert result["lights"]["light_1"][ "global_manager_autoreset_seconds" ] == pytest.approx(60, abs=2) - assert result["lights"]["light_1"]["global_manager_last_sent_target"] == { + assert result["lights"]["light_1"]["global_manager_last_adaptation_values"] == { ATTR_BRIGHTNESS: 123, ATTR_COLOR_TEMP_KELVIN: 3456, ATTR_RGB_COLOR: [12, 34, 56], @@ -175,7 +182,7 @@ async def test_config_entry_diagnostics_reports_allowlisted_current_facts( "color": True, }, "global_manager_autoreset_seconds": pytest.approx(60, abs=2), - "global_manager_last_sent_target": None, + "global_manager_last_adaptation_values": None, } serialized = json.dumps(result, sort_keys=True) @@ -194,6 +201,113 @@ async def test_config_entry_diagnostics_reports_allowlisted_current_facts( assert sensitive_value not in serialized +async def test_diagnostics_labels_accumulated_partial_adaptation_values( + hass, + cleanup_diagnostics, +): + """Diagnostics do not describe merged per-attribute history as one command.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Profile", + [ENTITY_LIGHT_1], + ) + commands = [ + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (12, 34, 56), + ATTR_TRANSITION: 2, + }, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 180}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_COLOR_TEMP_KELVIN: 3500}, + ] + call_events = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, call_events.append) + + await switch._execute_adaptation_calls( + AdaptationData( + entity_id=ENTITY_LIGHT_1, + context=switch.create_context("diagnostics_test"), + sleep_time=0, + service_call_datas=_create_service_call_data_iterator( + hass, + commands, + filter_by_state=False, + ), + force=True, + max_length=len(commands), + attributes=LightControlAttributes.ALL, + ), + ) + await hass.async_block_till_done() + remove_listener() + + actual_commands = [ + event.data[ATTR_SERVICE_DATA] + for event in call_events + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + ] + assert actual_commands == commands + + result = await async_get_config_entry_diagnostics(hass, entry) + light = result["lights"]["light_1"] + assert "global_manager_last_sent_target" not in light + assert light["global_manager_last_adaptation_values"] == { + ATTR_BRIGHTNESS: 180, + ATTR_COLOR_TEMP_KELVIN: 3500, + ATTR_RGB_COLOR: [12, 34, 56], + ATTR_TRANSITION: 2, + } + + +async def test_diagnostics_preserves_restored_off_profile_tracked_group(hass): + """Diagnostics report tracked targets without refreshing late groups.""" + await setup_lights(hass) + group = "light.private_late_group" + members = [ENTITY_LIGHT_1, ENTITY_LIGHT_2] + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State("switch.restored", STATE_OFF), + ): + entry, switch = await _setup_entry( + hass, + "Private Restored Profile", + [group], + ) + assert not switch.is_on + assert switch.lights == [group] + + hass.states.async_set( + group, + STATE_UNAVAILABLE, + {ATTR_ENTITY_ID: members, "friendly_name": "Private Late Group"}, + ) + await hass.async_block_till_done() + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + manager_lights_before = set(manager.lights) + reset_times_before = dict(manager.auto_reset_manual_control_times) + + result = await async_get_config_entry_diagnostics(hass, entry) + + assert result["lights"] == { + "light_1": { + "state": STATE_UNAVAILABLE, + "global_manager_manual_control": { + "brightness": False, + "color": False, + }, + "global_manager_autoreset_seconds": None, + "global_manager_last_adaptation_values": None, + }, + } + assert switch.lights == [group] + assert manager.lights == manager_lights_before + assert manager.auto_reset_manual_control_times == reset_times_before + assert group not in json.dumps(result) + + async def test_diagnostics_handles_missing_states_and_unload_without_side_effects( hass, ):