From 1268aecd657cff2724d58d21145c68b97fd8960d Mon Sep 17 00:00:00 2001 From: Joostlek Date: Fri, 11 Sep 2026 22:11:30 +0200 Subject: [PATCH] Reject a time based screensaver without both times Setting the screensaver time did not work. The device wants both times on every time based write, even one that only toggles the mode, and it fails in three different ways without them: start_time alone 200, and quietly ignored end_time alone 400 Bad end time params neither 400 Bad end time params The first is the bad one. Changing only the start time, which is the obvious way to use this, reported success and returned a Display still holding the old times. display() now raises ValueError for the time based mode unless both times are given, so the mismatch surfaces at the call instead of looking like it worked. The other modes take no times and are unaffected. Verified against a LaMetric TIME (sa8, OS 3.2.7). Co-Authored-By: Claude Opus 5 --- src/demetriek/device.py | 21 ++++++++++++++++- tests/test_display.py | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/demetriek/device.py b/src/demetriek/device.py index 7b802b66..937b7144 100644 --- a/src/demetriek/device.py +++ b/src/demetriek/device.py @@ -13,6 +13,7 @@ from aiohttp.helpers import BasicAuth from yarl import URL +from .const import ScreensaverMode from .exceptions import ( LaMetricAuthenticationError, LaMetricConnectionError, @@ -32,7 +33,7 @@ if TYPE_CHECKING: from datetime import time - from .const import BrightnessMode, DeviceMode, ScreensaverMode + from .const import BrightnessMode, DeviceMode @dataclass @@ -203,7 +204,25 @@ async def display( # noqa: PLR0913 # pylint: disable=too-many-arguments A Display object, with latest or updated information about the display of the LaMetric device. + Raises: + ------ + ValueError: The time based mode was given without both of + its times. + """ + # The device wants both times on every time based write, even one + # that only toggles the mode. It rejects a lone end time and + # quietly ignores a lone start time, so catch that here rather + # than let it look like it worked. + if screensaver_mode is ScreensaverMode.TIME_BASED and ( + screensaver_start_time is None or screensaver_end_time is None + ): + msg = ( + "The time based screensaver mode needs both" + " screensaver_start_time and screensaver_end_time" + ) + raise ValueError(msg) + data: dict[str, Any] = {} if brightness is not None: diff --git a/tests/test_display.py b/tests/test_display.py index 8e233eea..0e51ada7 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -4,6 +4,7 @@ from datetime import time import aiohttp +import pytest from aresponses import Response, ResponsesMockServer from demetriek import LaMetricDevice @@ -161,3 +162,52 @@ async def response_handler(request: aiohttp.ClientResponse) -> Response: async with aiohttp.ClientSession() as session: demetriek = LaMetricDevice(host="127.0.0.2", api_key="abc", session=session) await demetriek.display(screensaver_mode=ScreensaverMode.WHEN_DARK) + + +@pytest.mark.parametrize( + ("start_time", "end_time"), + [ + (time(23, 0, 0), None), + (None, time(7, 0, 0)), + (None, None), + ], +) +async def test_set_display_screensaver_time_based_needs_both_times( + start_time: time | None, + end_time: time | None, +) -> None: + """Test the time based mode is rejected without both of its times.""" + demetriek = LaMetricDevice(host="127.0.0.2", api_key="abc") + with pytest.raises(ValueError, match="needs both"): + await demetriek.display( + screensaver_mode=ScreensaverMode.TIME_BASED, + screensaver_mode_enabled=True, + screensaver_start_time=start_time, + screensaver_end_time=end_time, + ) + + +async def test_set_display_screensaver_when_dark_needs_no_times( + aresponses: ResponsesMockServer, +) -> None: + """Test the other modes are unaffected by the time based requirement.""" + + async def response_handler(request: aiohttp.ClientResponse) -> Response: + """Response handler for this test.""" + assert await request.json() == { + "screensaver": {"mode": "when_dark", "mode_params": {"enabled": True}}, + } + return aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixture("display_set_screensaver.json"), + ) + + aresponses.add("127.0.0.2:4343", "/api/v2/device/display", "PUT", response_handler) + + async with aiohttp.ClientSession() as session: + demetriek = LaMetricDevice(host="127.0.0.2", api_key="abc", session=session) + await demetriek.display( + screensaver_mode=ScreensaverMode.WHEN_DARK, + screensaver_mode_enabled=True, + )