mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
372 lines
12 KiB
Python
372 lines
12 KiB
Python
"""Tests for Adaptive Lighting diagnostics."""
|
|
|
|
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,
|
|
CONF_AUTORESET_CONTROL,
|
|
CONF_INTERCEPT,
|
|
CONF_MANUAL_CONTROL,
|
|
DOMAIN,
|
|
SERVICE_SET_MANUAL_CONTROL,
|
|
)
|
|
from homeassistant.components.adaptive_lighting.diagnostics import (
|
|
async_get_config_entry_diagnostics,
|
|
)
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
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,
|
|
EVENT_STATE_CHANGED,
|
|
STATE_OFF,
|
|
STATE_UNAVAILABLE,
|
|
)
|
|
from homeassistant.core import State
|
|
|
|
from tests.common import MockConfigEntry
|
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
|
|
|
from .test_switch import (
|
|
ENTITY_LIGHT_1,
|
|
ENTITY_LIGHT_2,
|
|
ENTITY_LIGHT_3,
|
|
setup_lights,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def cleanup_diagnostics(hass):
|
|
"""Cancel integration tasks created by diagnostics fixtures."""
|
|
yield
|
|
manager = hass.data.get(DOMAIN, {}).get(ATTR_ADAPTIVE_LIGHTING_MANAGER)
|
|
if manager is None:
|
|
return
|
|
for timer in manager.auto_reset_manual_control_timers.values():
|
|
timer.cancel()
|
|
for timer in manager.transition_timers.values():
|
|
timer.cancel()
|
|
for task in manager.adaptation_tasks:
|
|
task.cancel()
|
|
|
|
|
|
async def _setup_entry(hass, name, lights, **data):
|
|
"""Set up a real Adaptive Lighting config entry."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_NAME: name,
|
|
CONF_LIGHTS: lights,
|
|
CONF_INTERCEPT: False,
|
|
**data,
|
|
},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert entry.state is ConfigEntryState.LOADED
|
|
return entry, hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN]
|
|
|
|
|
|
async def test_config_entry_diagnostics_reports_allowlisted_current_facts(
|
|
hass,
|
|
hass_client,
|
|
cleanup_diagnostics,
|
|
):
|
|
"""Diagnostics report current selected-profile facts without identifiers."""
|
|
await setup_lights(hass)
|
|
entry, switch = await _setup_entry(
|
|
hass,
|
|
"Private Upstairs Profile",
|
|
[ENTITY_LIGHT_1, ENTITY_LIGHT_2],
|
|
**{CONF_AUTORESET_CONTROL: 60},
|
|
)
|
|
other_entry, _ = await _setup_entry(
|
|
hass,
|
|
"Private Basement Profile",
|
|
[ENTITY_LIGHT_3],
|
|
)
|
|
|
|
await switch.adapt_color_switch.async_turn_off()
|
|
await switch.sleep_mode_switch.async_turn_on()
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_SET_MANUAL_CONTROL,
|
|
{
|
|
ATTR_ENTITY_ID: switch.entity_id,
|
|
CONF_LIGHTS: [ENTITY_LIGHT_1],
|
|
CONF_MANUAL_CONTROL: "brightness",
|
|
},
|
|
blocking=True,
|
|
)
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_SET_MANUAL_CONTROL,
|
|
{
|
|
ATTR_ENTITY_ID: switch.entity_id,
|
|
CONF_LIGHTS: [ENTITY_LIGHT_2],
|
|
CONF_MANUAL_CONTROL: "color",
|
|
},
|
|
blocking=True,
|
|
)
|
|
hass.states.async_set(
|
|
ENTITY_LIGHT_2,
|
|
STATE_UNAVAILABLE,
|
|
{"friendly_name": "Private Bedside Lamp", "room": "Private Bedroom"},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
|
|
assert manager.get_manual_control_attributes(ENTITY_LIGHT_1) == (
|
|
LightControlAttributes.BRIGHTNESS
|
|
)
|
|
manager.last_service_data[ENTITY_LIGHT_1] = {
|
|
ATTR_ENTITY_ID: ENTITY_LIGHT_1,
|
|
ATTR_BRIGHTNESS: 123,
|
|
ATTR_COLOR_TEMP_KELVIN: 3456,
|
|
ATTR_RGB_COLOR: (12, 34, 56),
|
|
ATTR_TRANSITION: 4.5,
|
|
"context_id": "private-context-id",
|
|
"friendly_name": "Private Bedside Lamp",
|
|
}
|
|
manager.last_service_data.pop(ENTITY_LIGHT_2, None)
|
|
|
|
result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
|
|
|
|
assert result["loaded"] is True
|
|
assert result["profile_switches"] == {
|
|
"profile": True,
|
|
"adapt_brightness": True,
|
|
"adapt_color": False,
|
|
"sleep_mode": True,
|
|
}
|
|
assert result["manager_fact_scope"] == "global_shared_across_profiles"
|
|
assert list(result["lights"]) == ["light_1", "light_2"]
|
|
assert result["lights"]["light_1"]["state"] == "on"
|
|
assert result["lights"]["light_1"]["global_manager_manual_control"] == {
|
|
"brightness": True,
|
|
"color": False,
|
|
}
|
|
assert result["lights"]["light_1"][
|
|
"global_manager_autoreset_seconds"
|
|
] == pytest.approx(60, abs=2)
|
|
assert result["lights"]["light_1"]["global_manager_last_adaptation_values"] == {
|
|
ATTR_BRIGHTNESS: 123,
|
|
ATTR_COLOR_TEMP_KELVIN: 3456,
|
|
ATTR_RGB_COLOR: [12, 34, 56],
|
|
ATTR_TRANSITION: 4.5,
|
|
}
|
|
assert result["lights"]["light_2"] == {
|
|
"state": STATE_UNAVAILABLE,
|
|
"global_manager_manual_control": {
|
|
"brightness": False,
|
|
"color": True,
|
|
},
|
|
"global_manager_autoreset_seconds": pytest.approx(60, abs=2),
|
|
"global_manager_last_adaptation_values": None,
|
|
}
|
|
|
|
serialized = json.dumps(result, sort_keys=True)
|
|
for sensitive_value in (
|
|
entry.entry_id,
|
|
other_entry.entry_id,
|
|
ENTITY_LIGHT_1,
|
|
ENTITY_LIGHT_2,
|
|
ENTITY_LIGHT_3,
|
|
"Private Upstairs Profile",
|
|
"Private Basement Profile",
|
|
"Private Bedside Lamp",
|
|
"Private Bedroom",
|
|
"private-context-id",
|
|
):
|
|
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,
|
|
):
|
|
"""Diagnostics normalize states and never change live integration state."""
|
|
await setup_lights(hass)
|
|
entry, switch = await _setup_entry(
|
|
hass,
|
|
"Private Profile",
|
|
[ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3, "light.private_missing"],
|
|
)
|
|
hass.states.async_set(ENTITY_LIGHT_2, STATE_OFF)
|
|
hass.states.async_set(ENTITY_LIGHT_3, STATE_UNAVAILABLE)
|
|
await hass.async_block_till_done()
|
|
|
|
manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
|
|
manual_control_before = dict(manager.manual_control)
|
|
last_service_data_before = deepcopy(manager.last_service_data)
|
|
timers_before = dict(manager.auto_reset_manual_control_timers)
|
|
switch_states_before = (
|
|
switch.is_on,
|
|
switch.adapt_brightness_switch.is_on,
|
|
switch.adapt_color_switch.is_on,
|
|
switch.sleep_mode_switch.is_on,
|
|
)
|
|
service_events = []
|
|
state_events = []
|
|
remove_service_listener = hass.bus.async_listen(
|
|
EVENT_CALL_SERVICE,
|
|
service_events.append,
|
|
)
|
|
remove_state_listener = hass.bus.async_listen(
|
|
EVENT_STATE_CHANGED,
|
|
state_events.append,
|
|
)
|
|
|
|
result = await async_get_config_entry_diagnostics(hass, entry)
|
|
await hass.async_block_till_done()
|
|
remove_service_listener()
|
|
remove_state_listener()
|
|
|
|
assert [light["state"] for light in result["lights"].values()] == [
|
|
"on",
|
|
STATE_OFF,
|
|
STATE_UNAVAILABLE,
|
|
"missing",
|
|
]
|
|
assert json.dumps(result)
|
|
assert not service_events
|
|
assert not state_events
|
|
assert manager.manual_control == manual_control_before
|
|
assert manager.last_service_data == last_service_data_before
|
|
assert manager.auto_reset_manual_control_timers == timers_before
|
|
assert (
|
|
switch.is_on,
|
|
switch.adapt_brightness_switch.is_on,
|
|
switch.adapt_color_switch.is_on,
|
|
switch.sleep_mode_switch.is_on,
|
|
) == switch_states_before
|
|
|
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert await async_get_config_entry_diagnostics(hass, entry) == {"loaded": False}
|