Jelajahi Sumber

feat(art_frame): add random_image method (#568)

* feat(art_frame): add random_image method

Selects a random image other than the currently displayed one and sends
the set-image command over BLE, mirroring next_image/prev_image.

Moves random-selection logic out of the Home Assistant switchbot
integration (home-assistant/core#182123) into the library so consumers
call one method instead of reimplementing selection.

* fix(art_frame): use secrets.choice to satisfy ruff S311

* fix(art_frame): raise RuntimeError when no image differs from current

Guard on the filtered choices list instead of the raw all_images_index
length, so random_image raises RuntimeError (not IndexError) when every
image equals the current index or the index list is missing.

* chore(pre-commit.ci): auto fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com>
Italo Lombardi 1 Minggu lalu
induk
melakukan
b8b871e5a7
2 mengubah file dengan 77 tambahan dan 0 penghapusan
  1. 16 0
      switchbot/devices/art_frame.py
  2. 61 0
      tests/test_art_frame.py

+ 16 - 0
switchbot/devices/art_frame.py

@@ -1,6 +1,7 @@
 """Device handler for the Art Frame."""
 
 import logging
+import secrets
 from typing import Any
 
 from ..const import SwitchbotModel
@@ -87,6 +88,21 @@ class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
         result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
         return self._check_command_result(result, 0, {1})
 
+    @update_after_operation
+    async def random_image(self) -> bool:
+        """Display a random image other than the current one."""
+        await self._get_current_image_index()
+        current_index = self.get_current_image_index()
+        all_images_index = self.get_all_images_index()
+
+        choices = [idx for idx in all_images_index or [] if idx != current_index]
+        if not choices:
+            raise RuntimeError("No images available to select from.")
+
+        idx = secrets.choice(choices)
+        result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
+        return self._check_command_result(result, 0, {1})
+
     @update_after_operation
     async def set_image(self, index: int) -> bool:
         """Set the image by index."""

+ 61 - 0
tests/test_art_frame.py

@@ -168,6 +168,67 @@ async def test_prev_image(
         )
 
 
+@pytest.mark.asyncio
+async def test_random_image_with_single_image() -> None:
+    device = create_device_for_command_testing(ART_FRAME_INFO)
+
+    with (
+        patch.object(device, "get_current_image_index", return_value=1),
+        patch.object(device, "get_all_images_index", return_value=[1]),
+        pytest.raises(RuntimeError, match=r"No images available to select from."),
+    ):
+        await device.random_image()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    ("current_index", "all_images_index", "expected_choices"),
+    [
+        (100, [1, 100, 150], {1, 150}),
+        (1, [1, 100, 150], {100, 150}),
+    ],
+)
+async def test_random_image(
+    current_index: int, all_images_index: list[int], expected_choices: set[int]
+) -> None:
+    device = create_device_for_command_testing(ART_FRAME_INFO)
+
+    with (
+        patch.object(device, "get_current_image_index", return_value=current_index),
+        patch.object(device, "get_all_images_index", return_value=all_images_index),
+    ):
+        await device.random_image()
+        sent = device._send_command.await_args.args[0]
+        assert sent in {
+            COMMAND_SET_IMAGE.format(f"{idx:02X}") for idx in expected_choices
+        }
+
+
+@pytest.mark.asyncio
+async def test_random_image_all_equal_current() -> None:
+    device = create_device_for_command_testing(ART_FRAME_INFO)
+
+    with (
+        patch.object(device, "get_current_image_index", return_value=5),
+        patch.object(device, "get_all_images_index", return_value=[5, 5]),
+        pytest.raises(RuntimeError, match=r"No images available to select from."),
+    ):
+        await device.random_image()
+
+
+@pytest.mark.asyncio
+async def test_random_image_with_none_current() -> None:
+    device = create_device_for_command_testing(ART_FRAME_INFO)
+
+    with (
+        patch.object(device, "get_current_image_index", return_value=None),
+        patch.object(device, "get_all_images_index", return_value=[1, 100]),
+    ):
+        await device.random_image()
+        sent = device._send_command.await_args.args[0]
+        assert sent in {COMMAND_SET_IMAGE.format(f"{idx:02X}") for idx in (1, 100)}
+
+
 @pytest.mark.asyncio
 async def test_set_image_with_invalid_index() -> None:
     device = create_device_for_command_testing(ART_FRAME_INFO)