mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 15:30:21 +00:00
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
"""Integration test for the light `restore_state:` key.
|
|
|
|
Tests that:
|
|
1. On first boot (nothing saved yet), lights come up at their hardware default (off) --
|
|
restore_state overrides never apply before anything is ever saved.
|
|
2. After a state is saved and the device restarts, a `restore_state: {}` light comes
|
|
back exactly as saved, while a light with explicit overrides applies those overrides
|
|
on top of the loaded state regardless of what was saved.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import socket
|
|
from typing import Any
|
|
|
|
from aioesphomeapi import LightInfo, LightState
|
|
import pytest
|
|
|
|
from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client
|
|
from .host_prefs import clear_host_prefs
|
|
from .state_utils import InitialStateHelper, require_entity
|
|
from .types import CompileFunction, ConfigWriter
|
|
|
|
DEVICE_NAME = "host-light-restore-state-test"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_light_restore_state(
|
|
yaml_config: str,
|
|
write_yaml_config: ConfigWriter,
|
|
compile_esphome: CompileFunction,
|
|
reserved_tcp_port: tuple[int, socket.socket],
|
|
) -> None:
|
|
"""Test restore_state: {} vs restore_state: with overrides, across a restart."""
|
|
port, port_socket = reserved_tcp_port
|
|
|
|
clear_host_prefs(DEVICE_NAME)
|
|
|
|
config_path = await write_yaml_config(yaml_config)
|
|
binary_path = await compile_esphome(config_path)
|
|
|
|
port_socket.close()
|
|
|
|
# --- First run: nothing saved yet, both lights must come up off ---
|
|
async with (
|
|
run_binary_and_wait_for_port(binary_path, "127.0.0.1", port),
|
|
wait_and_connect_api_client(port=port) as client,
|
|
):
|
|
entities, _ = await client.list_entities_services()
|
|
keep_light = require_entity(entities, "test_light_keep", LightInfo)
|
|
override_light = require_entity(entities, "test_light_override", LightInfo)
|
|
|
|
# A single subscription serves both initial-state capture and later
|
|
# state-change waits -- a second subscribe_states call would restart the
|
|
# device's initial-state iterator and could resolve a wait on replayed data.
|
|
loop = asyncio.get_running_loop()
|
|
state_futures: dict[int, asyncio.Future[Any]] = {}
|
|
|
|
def on_state(state: Any) -> None:
|
|
if state.key in state_futures and not state_futures[state.key].done():
|
|
state_futures[state.key].set_result(state)
|
|
|
|
helper = InitialStateHelper(entities)
|
|
client.subscribe_states(helper.on_state_wrapper(on_state))
|
|
await helper.wait_for_initial_states()
|
|
|
|
keep_initial = helper.initial_states[keep_light.key]
|
|
assert isinstance(keep_initial, LightState)
|
|
assert keep_initial.state is False
|
|
|
|
override_initial = helper.initial_states[override_light.key]
|
|
assert isinstance(override_initial, LightState)
|
|
assert override_initial.state is False
|
|
|
|
# Set both lights to a known, saved state
|
|
async def wait_for_state(key: int, timeout: float = 2.0) -> Any:
|
|
state_futures[key] = loop.create_future()
|
|
try:
|
|
return await asyncio.wait_for(state_futures[key], timeout)
|
|
finally:
|
|
state_futures.pop(key, None)
|
|
|
|
client.light_command(key=keep_light.key, state=True, brightness=0.64)
|
|
await wait_for_state(keep_light.key)
|
|
|
|
client.light_command(key=override_light.key, state=True, brightness=0.3)
|
|
await wait_for_state(override_light.key)
|
|
|
|
# --- Second run: same binary, same prefs file ---
|
|
async with (
|
|
run_binary_and_wait_for_port(binary_path, "127.0.0.1", port),
|
|
wait_and_connect_api_client(port=port) as client,
|
|
):
|
|
entities, _ = await client.list_entities_services()
|
|
keep_light = require_entity(entities, "test_light_keep", LightInfo)
|
|
override_light = require_entity(entities, "test_light_override", LightInfo)
|
|
|
|
helper = InitialStateHelper(entities)
|
|
client.subscribe_states(helper.on_state_wrapper(lambda s: None))
|
|
await helper.wait_for_initial_states()
|
|
|
|
# restore_state: {} -- comes back exactly as saved
|
|
keep_state = helper.initial_states[keep_light.key]
|
|
assert isinstance(keep_state, LightState)
|
|
assert keep_state.state is True
|
|
assert keep_state.brightness == pytest.approx(0.64, abs=0.01)
|
|
|
|
# restore_state: with overrides -- state inverted, brightness forced to 100%
|
|
# regardless of what was actually saved (0.3)
|
|
override_state = helper.initial_states[override_light.key]
|
|
assert isinstance(override_state, LightState)
|
|
assert override_state.state is False
|
|
assert override_state.brightness == pytest.approx(1.0, abs=0.01)
|
|
|
|
clear_host_prefs(DEVICE_NAME)
|