Screen: switch off by itself when nothing is going on
With `general.screen.off_command` set, the monitor switches off once nothing has played and nobody has done anything for `auto_off_minutes` (15 by default, 0 for never, editable in parent mode). "Doing anything" is any intent a person could have caused: a remote key, a web command, or a key press or tap in the kiosk, which the page reports at most every 30s via POST /api/screen/activity. The first such intent after an auto-off runs `on_command`, so the remote wakes the screen too. The auto-off itself is silent; switches a person asks for still blubb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,18 @@ import pytest
|
||||
|
||||
from musicmouse.blubb import RATE, blubb_wav
|
||||
from musicmouse.bus import EventBus
|
||||
from musicmouse.clock import FakeClock
|
||||
from musicmouse.color import ColorRGBW
|
||||
from musicmouse.config import ScreenConfig
|
||||
from musicmouse.events import ToggleScreenRequested
|
||||
from musicmouse.effects import EffectStaticConfig
|
||||
from musicmouse.events import (
|
||||
LedEffectRequested,
|
||||
PlaybackChanged,
|
||||
PlayRemoteSlotRequested,
|
||||
ToggleScreenRequested,
|
||||
UserActivity,
|
||||
)
|
||||
from musicmouse.hardware import LedZone
|
||||
from musicmouse.services.screen import ScreenService
|
||||
|
||||
|
||||
@@ -184,3 +194,157 @@ async def test_no_blubb(
|
||||
task.cancel()
|
||||
|
||||
assert not Path(f"{fake_aplay}.wav").exists()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- idle auto-off
|
||||
|
||||
|
||||
class _Idle:
|
||||
"""A started service with a fake clock and an ``off`` and ``on`` command that log."""
|
||||
|
||||
def __init__(self, bus: EventBus, tmp_path: Path, **config: object) -> None:
|
||||
self.bus = bus
|
||||
self.off_log = tmp_path / "off"
|
||||
self.on_log = tmp_path / "on"
|
||||
self.playing = False
|
||||
self.clock = FakeClock(idle=bus.drain)
|
||||
settings: dict[str, object] = {
|
||||
"toggle_command": _python("print('toggled')"),
|
||||
"off_command": _appending(self.off_log, then="print('off')"),
|
||||
"on_command": _appending(self.on_log, then="print('on')"),
|
||||
"auto_off_minutes": 15,
|
||||
}
|
||||
settings.update(config)
|
||||
self.service = ScreenService(
|
||||
bus,
|
||||
ScreenConfig.model_validate(settings),
|
||||
alsa_device=None,
|
||||
volume=lambda: 0.5,
|
||||
is_playing=lambda: self.playing,
|
||||
clock=self.clock,
|
||||
)
|
||||
self.task: asyncio.Task[None] | None = None
|
||||
|
||||
async def __aenter__(self) -> _Idle:
|
||||
self.task = asyncio.create_task(self.service.run())
|
||||
await asyncio.sleep(0)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_: object) -> None:
|
||||
assert self.task is not None
|
||||
self.task.cancel()
|
||||
|
||||
async def wait(self, minutes: float) -> None:
|
||||
await self.clock.advance(minutes * 60)
|
||||
await _settle(self.service)
|
||||
|
||||
def offs(self) -> int:
|
||||
return len(self.off_log.read_text().splitlines()) if self.off_log.exists() else 0
|
||||
|
||||
def ons(self) -> int:
|
||||
return len(self.on_log.read_text().splitlines()) if self.on_log.exists() else 0
|
||||
|
||||
|
||||
async def test_the_screen_goes_off_after_the_idle_time(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
await idle.wait(14)
|
||||
assert idle.offs() == 0
|
||||
await idle.wait(1)
|
||||
assert idle.offs() == 1
|
||||
await idle.wait(60)
|
||||
assert idle.offs() == 1 # once, not every half minute after
|
||||
|
||||
|
||||
async def test_not_while_music_plays(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
idle.playing = True
|
||||
await idle.wait(60)
|
||||
assert idle.offs() == 0
|
||||
|
||||
# The countdown starts when the music stops.
|
||||
idle.playing = False
|
||||
await bus.emit_and_wait(PlaybackChanged(playing=False, source="player"))
|
||||
await idle.wait(14)
|
||||
assert idle.offs() == 0
|
||||
await idle.wait(1)
|
||||
assert idle.offs() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event",
|
||||
[UserActivity(source="web"), PlayRemoteSlotRequested(digit="3", source="lirc")],
|
||||
)
|
||||
async def test_activity_restarts_the_countdown(
|
||||
bus: EventBus, tmp_path: Path, event: UserActivity | PlayRemoteSlotRequested
|
||||
) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
await idle.wait(10)
|
||||
await bus.emit_and_wait(event)
|
||||
await idle.wait(10)
|
||||
assert idle.offs() == 0
|
||||
await idle.wait(5)
|
||||
assert idle.offs() == 1
|
||||
|
||||
|
||||
async def test_automations_do_not_count_as_activity(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
await idle.wait(10)
|
||||
effect = EffectStaticConfig(ColorRGBW(0, 0, 0, 0))
|
||||
automation = LedEffectRequested(zone=LedZone.MOUSE, effect=effect, source="mqtt")
|
||||
await bus.emit_and_wait(automation)
|
||||
await idle.wait(5)
|
||||
assert idle.offs() == 1
|
||||
|
||||
|
||||
async def test_someone_coming_back_switches_it_on(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
await idle.wait(15)
|
||||
assert idle.offs() == 1
|
||||
|
||||
await bus.emit_and_wait(UserActivity(source="web"))
|
||||
await _settle(idle.service)
|
||||
await bus.emit_and_wait(UserActivity(source="web"))
|
||||
await _settle(idle.service)
|
||||
assert idle.ons() == 1 # only the first wakes it
|
||||
|
||||
await idle.wait(15)
|
||||
assert idle.offs() == 2 # and the countdown runs again
|
||||
|
||||
|
||||
async def test_the_power_key_after_an_auto_off_is_left_to_the_toggle(
|
||||
bus: EventBus, tmp_path: Path
|
||||
) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
await idle.wait(15)
|
||||
await bus.emit_and_wait(ToggleScreenRequested(source="lirc"))
|
||||
await _settle(idle.service)
|
||||
await bus.emit_and_wait(UserActivity(source="web"))
|
||||
await _settle(idle.service)
|
||||
assert idle.ons() == 0
|
||||
|
||||
|
||||
async def test_zero_minutes_means_never(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path, auto_off_minutes=0) as idle:
|
||||
await idle.wait(600)
|
||||
assert idle.offs() == 0
|
||||
|
||||
|
||||
async def test_no_off_command_means_never(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path, off_command=None) as idle:
|
||||
await idle.wait(600)
|
||||
assert idle.offs() == 0
|
||||
|
||||
|
||||
async def test_a_new_idle_time_applies_at_once(bus: EventBus, tmp_path: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
idle.service.config.auto_off_minutes = 5 # what parent mode does
|
||||
await idle.wait(5)
|
||||
assert idle.offs() == 1
|
||||
|
||||
|
||||
async def test_the_auto_off_is_silent(bus: EventBus, tmp_path: Path, fake_aplay: Path) -> None:
|
||||
async with _Idle(bus, tmp_path) as idle:
|
||||
idle.service.alsa_device = "softvol_effects"
|
||||
await idle.wait(15)
|
||||
assert idle.offs() == 1
|
||||
assert not Path(f"{fake_aplay}.wav").exists()
|
||||
|
||||
Reference in New Issue
Block a user