mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
92 lines
3.9 KiB
Python
92 lines
3.9 KiB
Python
"""Integration test verifying light effects can dim to 0% brightness while staying on.
|
|
|
|
Regression test for https://github.com/esphome/esphome/issues/17639, where PR #17103's
|
|
"make turn-on visible" logic in LightCall::validate_() also clobbered brightness set by a
|
|
running effect (e.g. pulse, strobe), forcing it back to 100% and breaking the dark phase
|
|
of those effects.
|
|
|
|
Effect ticks are published with `publish: false` (so Home Assistant isn't spammed with
|
|
every frame), so the effect's actual output can't be observed via API state broadcasts.
|
|
Instead, this test reads the output component's log lines, which are written on every
|
|
update regardless of the publish flag.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import re
|
|
from typing import Any
|
|
|
|
from aioesphomeapi import EntityState, LightState
|
|
import pytest
|
|
|
|
from .state_utils import InitialStateHelper
|
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_light_effect_zero_brightness(
|
|
yaml_config: str,
|
|
run_compiled: RunCompiledFunction,
|
|
api_client_connected: APIClientConnectedFactory,
|
|
) -> None:
|
|
"""Pulse and strobe effects must be able to reach 0% brightness while the light stays on."""
|
|
output_pattern = re.compile(r"PULSE_OUTPUT:([\d.]+)")
|
|
observed: list[float] = []
|
|
|
|
def on_log_line(line: str) -> None:
|
|
match = output_pattern.search(line)
|
|
if match:
|
|
observed.append(float(match.group(1)))
|
|
|
|
async with (
|
|
run_compiled(yaml_config, line_callback=on_log_line),
|
|
api_client_connected() as client,
|
|
):
|
|
entities, _ = await client.list_entities_services()
|
|
light = next(e for e in entities if e.object_id == "test_pulse_light")
|
|
|
|
state_futures: dict[int, asyncio.Future[LightState]] = {}
|
|
|
|
def on_state(state: EntityState) -> None:
|
|
if isinstance(state, LightState) and state.key in state_futures:
|
|
future = state_futures[state.key]
|
|
if not future.done():
|
|
future.set_result(state)
|
|
|
|
# ESPHome sends the current state of every entity right after connecting; drain
|
|
# that initial burst so it can't be mistaken for the response to a command below.
|
|
initial_state_helper = InitialStateHelper(entities)
|
|
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
|
await initial_state_helper.wait_for_initial_states()
|
|
|
|
async def send_and_wait(timeout: float = 5.0, **kwargs: Any) -> LightState:
|
|
"""Send a light command and wait for the matching state response."""
|
|
state_futures[light.key] = asyncio.get_running_loop().create_future()
|
|
client.light_command(key=light.key, **kwargs)
|
|
return await asyncio.wait_for(state_futures[light.key], timeout=timeout)
|
|
|
|
# Turn the light on first so the effect starts from a known, visible state.
|
|
state = await send_and_wait(state=True, brightness=1.0)
|
|
assert state.state is True
|
|
assert state.brightness == pytest.approx(1.0)
|
|
|
|
for effect_name in ("Fast Pulse", "Fast Strobe"):
|
|
observed.clear()
|
|
state = await send_and_wait(effect=effect_name)
|
|
assert state.effect == effect_name
|
|
# Let several effect cycles run (update_interval/duration is 50ms in the fixture).
|
|
await asyncio.sleep(1.0)
|
|
|
|
assert observed, f"No output observed while running effect {effect_name!r}"
|
|
assert min(observed) == pytest.approx(0.0, abs=0.01), (
|
|
f"Effect {effect_name!r} never dimmed to 0% brightness while the light "
|
|
f"stayed on -- got min={min(observed):.4f} (values: {observed})"
|
|
)
|
|
assert max(observed) > 0.5, (
|
|
f"Effect {effect_name!r} never reached full brightness -- "
|
|
f"got max={max(observed):.4f}"
|
|
)
|
|
|
|
client.light_command(key=light.key, effect="None")
|