mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-28 22:27:06 +01:00
641 lines
22 KiB
Python
641 lines
22 KiB
Python
"""Independent native requests retain the established adaptation timing policy."""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
from homeassistant.components.adaptive_lighting.const import (
|
|
CONF_ADAPT_DELAY,
|
|
CONF_INTERCEPT,
|
|
CONF_SEND_SPLIT_DELAY,
|
|
CONF_SEPARATE_TURN_ON_COMMANDS,
|
|
)
|
|
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN
|
|
from homeassistant.components.light.const import DATA_COMPONENT
|
|
from homeassistant.const import STATE_ON
|
|
from homeassistant.core import Context
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from .support.async_helpers import DelayBarrier
|
|
from .support.native_lights import GROUP, RIGHT, RecordingGroup, RecordingMember
|
|
from .support.native_scenarios import (
|
|
prepare_reported_members,
|
|
setup_reported_group,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("planner_enabled", [False, True])
|
|
async def test_split_transition_duration_is_preserved(hass, planner_enabled):
|
|
setup = await setup_reported_group(
|
|
hass,
|
|
**{
|
|
CONF_INTERCEPT: True,
|
|
CONF_SEPARATE_TURN_ON_COMMANDS: True,
|
|
CONF_SEND_SPLIT_DELAY: 0,
|
|
},
|
|
)
|
|
await prepare_reported_members(
|
|
hass,
|
|
setup,
|
|
on=(False, False, False),
|
|
brightness_adaptation=True,
|
|
)
|
|
setup.switch.manager.native_groups.disabled = not planner_enabled
|
|
await hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": GROUP, "transition": 2.5},
|
|
blocking=True,
|
|
context=Context(),
|
|
)
|
|
await hass.async_block_till_done()
|
|
adaptation = [
|
|
call
|
|
for call in setup.commands
|
|
if "brightness" in call or "color_temp_kelvin" in call
|
|
]
|
|
assert adaptation, setup.commands
|
|
assert all(call.get("transition") == 1.25 for call in adaptation), setup.commands
|
|
|
|
|
|
@pytest.mark.parametrize("planner_enabled", [False, True])
|
|
async def test_split_delay_includes_first_axis_transition(
|
|
hass,
|
|
monkeypatch,
|
|
planner_enabled,
|
|
):
|
|
setup = await setup_reported_group(
|
|
hass,
|
|
**{
|
|
CONF_INTERCEPT: True,
|
|
CONF_SEPARATE_TURN_ON_COMMANDS: True,
|
|
CONF_SEND_SPLIT_DELAY: 1234,
|
|
},
|
|
)
|
|
await prepare_reported_members(
|
|
hass,
|
|
setup,
|
|
on=(False, False, False),
|
|
brightness_adaptation=True,
|
|
)
|
|
planner = setup.switch.manager.native_groups
|
|
planner.disabled = not planner_enabled
|
|
barrier = DelayBarrier(
|
|
monkeypatch,
|
|
planner if planner_enabled else setup.switch,
|
|
"_execute_dispatch" if planner_enabled else "_execute_adaptation_calls",
|
|
)
|
|
task = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": GROUP, "transition": 2.5},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(barrier.entered.wait(), 2)
|
|
assert any("brightness" in call for call in setup.commands), setup.commands
|
|
assert not setup.colour_commands, setup.commands
|
|
actual_delay = barrier.delay
|
|
finally:
|
|
barrier.release.set()
|
|
await task
|
|
await hass.async_block_till_done()
|
|
assert actual_delay == 2.484, (actual_delay, setup.commands)
|
|
adaptation = [
|
|
call
|
|
for call in setup.commands
|
|
if "brightness" in call or "color_temp_kelvin" in call
|
|
]
|
|
assert any("color_temp_kelvin" in call for call in adaptation), setup.commands
|
|
assert all(call.get("transition") == 1.25 for call in adaptation), setup.commands
|
|
|
|
|
|
async def _setup_independent_groups(hass):
|
|
"""Install disjoint native targets under one adaptive profile."""
|
|
setup = await setup_reported_group(
|
|
hass,
|
|
**{
|
|
CONF_SEPARATE_TURN_ON_COMMANDS: True,
|
|
CONF_SEND_SPLIT_DELAY: 1234,
|
|
},
|
|
)
|
|
await prepare_reported_members(
|
|
hass,
|
|
setup,
|
|
on=(False, False, False),
|
|
brightness_adaptation=True,
|
|
)
|
|
await setup.switch.async_turn_off()
|
|
second_members = [
|
|
RecordingMember(f"light.native_z_member_{index}", setup.commands)
|
|
for index in range(3)
|
|
]
|
|
second_group = RecordingGroup(second_members, setup.commands)
|
|
second_group.entity_id = "light.native_z_group"
|
|
second_group._attr_is_on = False
|
|
for member in second_members:
|
|
member._attr_color_temp_kelvin = 4000
|
|
await hass.data[DATA_COMPONENT].async_add_entities([*second_members, second_group])
|
|
setup.switch._configured_lights.append(second_group.entity_id)
|
|
setup.switch._expand_light_groups()
|
|
await setup.switch.async_turn_on(adapt_lights=False)
|
|
await hass.async_block_till_done()
|
|
setup.commands.clear()
|
|
return setup, second_group
|
|
|
|
|
|
@pytest.mark.parametrize("planner_enabled", [False, True])
|
|
async def test_split_group_does_not_delay_independent_group_brightness(
|
|
hass,
|
|
monkeypatch,
|
|
planner_enabled,
|
|
):
|
|
setup, second_group = await _setup_independent_groups(hass)
|
|
second_brightness = asyncio.Event()
|
|
original = second_group.async_turn_on
|
|
|
|
async def observed(**kwargs):
|
|
await original(**kwargs)
|
|
if "brightness" in kwargs:
|
|
second_brightness.set()
|
|
|
|
monkeypatch.setattr(second_group, "async_turn_on", observed)
|
|
planner = setup.switch.manager.native_groups
|
|
planner.disabled = not planner_enabled
|
|
barrier = DelayBarrier(
|
|
monkeypatch,
|
|
planner if planner_enabled else setup.switch,
|
|
"_execute_dispatch" if planner_enabled else "_execute_adaptation_calls",
|
|
)
|
|
task = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second_group.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(barrier.entered.wait(), 2)
|
|
assert barrier.delay == 1.234
|
|
try:
|
|
await asyncio.wait_for(second_brightness.wait(), 0.5)
|
|
other_group_started = True
|
|
except TimeoutError:
|
|
other_group_started = False
|
|
paused_commands = list(setup.commands)
|
|
finally:
|
|
barrier.release.set()
|
|
await task
|
|
await hass.async_block_till_done()
|
|
assert other_group_started, paused_commands
|
|
|
|
|
|
@pytest.mark.parametrize("planner_enabled", [False, True])
|
|
async def test_explicit_scene_colour_not_held_by_other_members_adapt_delay(
|
|
hass,
|
|
monkeypatch,
|
|
planner_enabled,
|
|
):
|
|
setup = await setup_reported_group(hass, **{CONF_ADAPT_DELAY: 1})
|
|
planner = setup.switch.manager.native_groups
|
|
planner.disabled = not planner_enabled
|
|
barrier = DelayBarrier(
|
|
monkeypatch,
|
|
planner if planner_enabled else setup.switch,
|
|
"_execute_dispatch" if planner_enabled else "_respond_to_off_to_on_event",
|
|
)
|
|
entities = {
|
|
m.entity_id: {"state": STATE_ON, ATTR_BRIGHTNESS: 100} for m in setup.members
|
|
}
|
|
member = setup.members[0]
|
|
entities[member.entity_id][ATTR_COLOR_TEMP_KELVIN] = 3500
|
|
explicit_colour = asyncio.Event()
|
|
original_turn_on = member.async_turn_on
|
|
|
|
async def observe_colour(**kwargs):
|
|
await original_turn_on(**kwargs)
|
|
if kwargs.get(ATTR_COLOR_TEMP_KELVIN) == 3500:
|
|
explicit_colour.set()
|
|
|
|
monkeypatch.setattr(member, "async_turn_on", observe_colour)
|
|
task = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"scene",
|
|
"apply",
|
|
{"entities": entities},
|
|
blocking=True,
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(barrier.entered.wait(), 2)
|
|
assert barrier.delay == 1
|
|
try:
|
|
await asyncio.wait_for(explicit_colour.wait(), 0.5)
|
|
explicit_completed = True
|
|
except TimeoutError:
|
|
explicit_completed = False
|
|
paused_commands = list(setup.commands)
|
|
finally:
|
|
barrier.release.set()
|
|
await task
|
|
await hass.async_block_till_done()
|
|
assert explicit_completed, paused_commands
|
|
assert member.physical_kelvin == 3500, setup.commands
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"failed_axis",
|
|
["power", ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN],
|
|
)
|
|
async def test_independent_native_group_error_keeps_other_group(
|
|
hass,
|
|
monkeypatch,
|
|
failed_axis,
|
|
):
|
|
"""One failed sequence neither cancels a disjoint group nor retries its axis."""
|
|
setup, second_group = await _setup_independent_groups(hass)
|
|
error = HomeAssistantError("first native group unavailable")
|
|
original = setup.group.async_turn_on
|
|
attempts = []
|
|
|
|
async def fail_axis(**kwargs):
|
|
attempts.append(dict(kwargs))
|
|
if (failed_axis == "power" and not kwargs) or failed_axis in kwargs:
|
|
raise error
|
|
await original(**kwargs)
|
|
|
|
monkeypatch.setattr(setup.group, "async_turn_on", fail_axis)
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="first native group unavailable",
|
|
) as exc:
|
|
await hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second_group.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert exc.value is error
|
|
assert (
|
|
sum(
|
|
(not command if failed_axis == "power" else failed_axis in command)
|
|
for command in attempts
|
|
)
|
|
== 1
|
|
)
|
|
assert all(member.physical_kelvin == 4000 for member in setup.members)
|
|
assert all(member.physical_kelvin == 3000 for member in second_group.members)
|
|
assert all(member.brightness == 128 for member in second_group.members)
|
|
assert all(
|
|
call["target"] in (GROUP, second_group.entity_id) for call in setup.commands
|
|
), setup.commands
|
|
|
|
|
|
@pytest.mark.parametrize("brightness_adaptation", [False, True])
|
|
async def test_member_fallback_preserves_single_and_split_axis_transitions(
|
|
hass,
|
|
monkeypatch,
|
|
brightness_adaptation,
|
|
):
|
|
"""Colour fallback retains split timing and independently compatible brightness."""
|
|
setup = await setup_reported_group(
|
|
hass,
|
|
**{CONF_SEPARATE_TURN_ON_COMMANDS: True, CONF_SEND_SPLIT_DELAY: 0},
|
|
)
|
|
await prepare_reported_members(
|
|
hass,
|
|
setup,
|
|
on=(False, False, False),
|
|
brightness_adaptation=brightness_adaptation,
|
|
)
|
|
original = setup.group.async_turn_on
|
|
|
|
async def restrict_group_colour(**kwargs):
|
|
await original(**kwargs)
|
|
setup.group._attr_max_color_temp_kelvin = 2500
|
|
setup.group.async_write_ha_state()
|
|
|
|
monkeypatch.setattr(setup.group, "async_turn_on", restrict_group_colour)
|
|
await hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": GROUP, "transition": 2.5},
|
|
blocking=True,
|
|
context=Context(),
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert all(member.is_on for member in setup.members)
|
|
assert all(member.physical_kelvin == 3000 for member in setup.members)
|
|
assert setup.commands[0] == {
|
|
"target": GROUP,
|
|
"service": "turn_on",
|
|
"transition": 2.5,
|
|
}
|
|
expected_duration = 1.25 if brightness_adaptation else 2.5
|
|
assert len(setup.commands) == (5 if brightness_adaptation else 4), setup.commands
|
|
brightness_commands = [
|
|
command for command in setup.commands if ATTR_BRIGHTNESS in command
|
|
]
|
|
if brightness_adaptation:
|
|
assert brightness_commands == [
|
|
{
|
|
"target": GROUP,
|
|
"service": "turn_on",
|
|
ATTR_BRIGHTNESS: 128,
|
|
"transition": expected_duration,
|
|
},
|
|
]
|
|
assert setup.commands[1] == brightness_commands[0]
|
|
assert all(member.brightness == 128 for member in setup.members)
|
|
else:
|
|
assert not brightness_commands
|
|
assert [member.brightness for member in setup.members] == [80, 110, 26]
|
|
assert len(setup.colour_commands) == 3
|
|
assert {command["target"] for command in setup.colour_commands} == {
|
|
member.entity_id for member in setup.members
|
|
}
|
|
for command in setup.colour_commands:
|
|
assert command["transition"] == expected_duration
|
|
assert ATTR_BRIGHTNESS not in command
|
|
|
|
|
|
@pytest.mark.parametrize("planner_enabled", [False, True])
|
|
async def test_independent_group_adapts_while_other_original_power_handler_waits(
|
|
hass,
|
|
monkeypatch,
|
|
planner_enabled,
|
|
):
|
|
setup, second = await _setup_independent_groups(hass)
|
|
setup.switch.manager.native_groups.disabled = not planner_enabled
|
|
setup.switch._send_split_delay = 0
|
|
entered, release, second_ready = asyncio.Event(), asyncio.Event(), asyncio.Event()
|
|
first_on, second_on = setup.group.async_turn_on, second.async_turn_on
|
|
|
|
async def first(**kwargs):
|
|
if not kwargs:
|
|
entered.set()
|
|
await asyncio.wait_for(release.wait(), 5)
|
|
await first_on(**kwargs)
|
|
|
|
async def other(**kwargs):
|
|
await second_on(**kwargs)
|
|
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
|
second_ready.set()
|
|
|
|
monkeypatch.setattr(setup.group, "async_turn_on", first)
|
|
monkeypatch.setattr(second, "async_turn_on", other)
|
|
task = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(entered.wait(), 2)
|
|
try:
|
|
await asyncio.wait_for(second_ready.wait(), 0.5)
|
|
other_completed = True
|
|
except TimeoutError:
|
|
other_completed = False
|
|
paused_commands = list(setup.commands)
|
|
paused_colour = [member.physical_kelvin for member in second.members]
|
|
finally:
|
|
release.set()
|
|
await task
|
|
await hass.async_block_till_done()
|
|
assert other_completed, (paused_commands, paused_colour)
|
|
assert all(member.physical_kelvin == 3000 for member in second.members)
|
|
|
|
|
|
@pytest.mark.parametrize("partial_parent", [False, True])
|
|
async def test_shared_ancestor_keeps_grouping_and_late_aggregate_coverage(
|
|
hass,
|
|
partial_parent,
|
|
):
|
|
"""Shared full/partial native ancestors keep one owner for aggregate reports."""
|
|
setup, second = await _setup_independent_groups(hass)
|
|
setup.switch._send_split_delay = 0
|
|
await setup.switch.async_turn_off()
|
|
selected_members = [*setup.members, *second.members]
|
|
extra = RecordingMember("light.native_unselected", setup.commands)
|
|
parent = RecordingGroup(
|
|
[*selected_members, *([extra] if partial_parent else [])],
|
|
setup.commands,
|
|
)
|
|
parent.entity_id = "light.native_parent"
|
|
parent._attr_is_on = False
|
|
await hass.data[DATA_COMPONENT].async_add_entities([extra, parent])
|
|
setup.switch._configured_lights.append(parent.entity_id)
|
|
setup.switch._expand_light_groups()
|
|
await setup.switch.async_turn_on(adapt_lights=False)
|
|
await hass.async_block_till_done()
|
|
setup.commands.clear()
|
|
await hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert all(member.physical_kelvin == 3000 for member in selected_members)
|
|
assert all(member.brightness == 128 for member in selected_members)
|
|
assert not extra.is_on
|
|
assert extra.physical_kelvin == 4000
|
|
assert {command["target"] for command in setup.colour_commands} == (
|
|
{GROUP, second.entity_id} if partial_parent else {parent.entity_id}
|
|
), setup.commands
|
|
before_report = list(setup.commands)
|
|
# A separate aggregate ON report may arrive after the complete service.
|
|
parent.async_set_context(Context())
|
|
parent._attr_is_on = True
|
|
parent.async_write_ha_state()
|
|
await hass.async_block_till_done()
|
|
assert setup.commands == before_report
|
|
assert not extra.is_on
|
|
|
|
|
|
async def test_unsupported_overlapping_native_target_keeps_shared_dispatch_boundary(
|
|
hass,
|
|
monkeypatch,
|
|
):
|
|
"""An ineligible native target still shares physical members with its peer."""
|
|
setup, second = await _setup_independent_groups(hass)
|
|
setup.switch._send_split_delay = 0
|
|
extra = RecordingMember("light.native_foreign_member", setup.commands)
|
|
overlapping = RecordingGroup([*second.members, extra], setup.commands)
|
|
overlapping.entity_id = "light.native_unowned_overlap"
|
|
overlapping._attr_is_on = False
|
|
await hass.data[DATA_COMPONENT].async_add_entities([extra, overlapping])
|
|
entered, release = asyncio.Event(), asyncio.Event()
|
|
peer_power, peer_colour = asyncio.Event(), asyncio.Event()
|
|
original_overlap, original_peer = overlapping.async_turn_on, second.async_turn_on
|
|
|
|
async def hold_overlap(**kwargs):
|
|
entered.set()
|
|
await asyncio.wait_for(release.wait(), 5)
|
|
await original_overlap(**kwargs)
|
|
|
|
async def observe_peer(**kwargs):
|
|
await original_peer(**kwargs)
|
|
if not kwargs:
|
|
peer_power.set()
|
|
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
|
peer_colour.set()
|
|
|
|
monkeypatch.setattr(overlapping, "async_turn_on", hold_overlap)
|
|
monkeypatch.setattr(second, "async_turn_on", observe_peer)
|
|
task = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [overlapping.entity_id, second.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(entered.wait(), 2)
|
|
await asyncio.wait_for(peer_power.wait(), 2)
|
|
try:
|
|
await asyncio.wait_for(peer_colour.wait(), 0.5)
|
|
correction_before_shared_power = True
|
|
except TimeoutError:
|
|
correction_before_shared_power = False
|
|
finally:
|
|
release.set()
|
|
await task
|
|
await hass.async_block_till_done()
|
|
assert not correction_before_shared_power, setup.commands
|
|
assert all(member.physical_kelvin == 3000 for member in second.members)
|
|
assert extra.is_on
|
|
assert extra.physical_kelvin == 4000
|
|
|
|
|
|
@pytest.mark.parametrize("action", ["cancel", "new_colour"])
|
|
async def test_parallel_split_dispatch_respects_parent_cancel_and_new_member_intent(
|
|
hass,
|
|
monkeypatch,
|
|
action,
|
|
):
|
|
setup, second = await _setup_independent_groups(hass)
|
|
planner = setup.switch.manager.native_groups
|
|
barrier = DelayBarrier(monkeypatch, planner, "_execute_dispatch")
|
|
other_brightness = asyncio.Event()
|
|
original = second.async_turn_on
|
|
|
|
async def observe(**kwargs):
|
|
await original(**kwargs)
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
other_brightness.set()
|
|
|
|
monkeypatch.setattr(second, "async_turn_on", observe)
|
|
parent = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(barrier.entered.wait(), 2)
|
|
assert barrier.delay == 1.234
|
|
await asyncio.wait_for(other_brightness.wait(), 2)
|
|
assert not setup.colour_commands
|
|
if action == "cancel":
|
|
parent.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await parent
|
|
else:
|
|
await hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": RIGHT, ATTR_COLOR_TEMP_KELVIN: 4500},
|
|
blocking=True,
|
|
context=Context(),
|
|
)
|
|
finally:
|
|
barrier.release.set()
|
|
await asyncio.gather(parent, return_exceptions=True)
|
|
await hass.async_block_till_done()
|
|
|
|
if action == "cancel":
|
|
assert not setup.colour_commands, setup.commands
|
|
assert all(m.physical_kelvin == 4000 for m in [*setup.members, *second.members])
|
|
else:
|
|
assert [m.physical_kelvin for m in setup.members] == [3000, 4500, 3000]
|
|
assert all(m.physical_kelvin == 3000 for m in second.members)
|
|
assert not any(c["target"] == GROUP for c in setup.colour_commands)
|
|
assert all(m.is_on for m in [*setup.members, *second.members])
|
|
|
|
|
|
async def test_completed_partition_retains_late_report_claim_after_peer_cancel(
|
|
hass,
|
|
monkeypatch,
|
|
):
|
|
setup, second = await _setup_independent_groups(hass)
|
|
setup.switch._send_split_delay = 0
|
|
held_member = second.members[0]
|
|
held_member.hold_reports = True
|
|
entered, release, partition_done = asyncio.Event(), asyncio.Event(), asyncio.Event()
|
|
first_on = setup.group.async_turn_on
|
|
planner = setup.switch.manager.native_groups
|
|
original_run = planner._run
|
|
|
|
async def observe_partition(batch, invoke):
|
|
result = await original_run(batch, invoke)
|
|
if second.entity_id in batch.plan.requests:
|
|
partition_done.set()
|
|
return result
|
|
|
|
monkeypatch.setattr(planner, "_run", observe_partition)
|
|
|
|
async def first(**kwargs):
|
|
if not kwargs:
|
|
entered.set()
|
|
await asyncio.wait_for(release.wait(), 5)
|
|
await first_on(**kwargs)
|
|
|
|
monkeypatch.setattr(setup.group, "async_turn_on", first)
|
|
parent = asyncio.create_task(
|
|
hass.services.async_call(
|
|
"light",
|
|
"turn_on",
|
|
{"entity_id": [GROUP, second.entity_id]},
|
|
blocking=True,
|
|
context=Context(),
|
|
),
|
|
)
|
|
try:
|
|
await asyncio.wait_for(entered.wait(), 2)
|
|
await asyncio.wait_for(partition_done.wait(), 2)
|
|
assert held_member.physical_kelvin == 3000
|
|
assert hass.states.is_state(held_member.entity_id, "off")
|
|
parent.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await parent
|
|
finally:
|
|
release.set()
|
|
await asyncio.gather(parent, return_exceptions=True)
|
|
await hass.async_block_till_done()
|
|
before_report = list(setup.commands)
|
|
held_member.hold_reports = False
|
|
held_member.async_set_context(Context())
|
|
held_member.async_write_ha_state()
|
|
await hass.async_block_till_done()
|
|
assert hass.states.is_state(held_member.entity_id, "on")
|
|
assert setup.commands == before_report
|
|
assert all(m.physical_kelvin == 3000 for m in second.members)
|