"""Tests for the LIFX LAN LED client + provider.""" from __future__ import annotations import struct from unittest.mock import MagicMock import numpy as np import pytest from ledgrab.core.devices.device_config import LIFXConfig from ledgrab.core.devices.led_client import ProviderDeps from ledgrab.core.devices.lifx_client import ( LIFX_PORT, MSG_GET_SERVICE, MSG_SET_COLOR, MSG_SET_POWER, MSG_STATE_SERVICE, LIFXClient, _average_color, _build_packet, _build_set_color_payload, _build_set_power_payload, _parse_state_service_reply, parse_lifx_url, rgb_to_hsbk, ) from ledgrab.core.devices.lifx_provider import LIFXDeviceProvider # ============================================================================ # URL parsing # ============================================================================ @pytest.mark.parametrize( "url,expected", [ ("lifx://192.168.1.50", ("192.168.1.50", LIFX_PORT)), ("lifx://192.168.1.50:56700", ("192.168.1.50", 56700)), ("192.168.1.50", ("192.168.1.50", LIFX_PORT)), ("192.168.1.50:56700", ("192.168.1.50", 56700)), ("bulb.local", ("bulb.local", LIFX_PORT)), ], ) def test_parse_lifx_url(url, expected): assert parse_lifx_url(url) == expected @pytest.mark.parametrize("url", ["", " ", "lifx://", "://192.168.1.1"]) def test_parse_lifx_url_rejects_empty(url): with pytest.raises(ValueError): parse_lifx_url(url) # ============================================================================ # RGB → HSBK conversion # ============================================================================ def test_rgb_to_hsbk_pure_red(): h, s, b, k = rgb_to_hsbk(255, 0, 0) assert h == 0 assert s == 65535 assert b == 65535 assert 2500 <= k <= 9000 def test_rgb_to_hsbk_pure_green(): h, s, b, k = rgb_to_hsbk(0, 255, 0) # Green is at 120° → hue = 120/360 * 65535 ≈ 21845 assert abs(h - 21845) < 5 assert s == 65535 assert b == 65535 def test_rgb_to_hsbk_pure_blue(): h, s, b, _k = rgb_to_hsbk(0, 0, 255) assert abs(h - 43690) < 5 assert s == 65535 assert b == 65535 def test_rgb_to_hsbk_black_is_zero_brightness(): h, s, b, _k = rgb_to_hsbk(0, 0, 0) assert b == 0 assert s == 0 def test_rgb_to_hsbk_white_has_full_brightness_zero_saturation(): _h, s, b, _k = rgb_to_hsbk(255, 255, 255) assert s == 0 assert b == 65535 def test_rgb_to_hsbk_clamps_out_of_range_input(): # Negative / >255 should still produce a valid HSBK h, s, b, _k = rgb_to_hsbk(-50, 999, 128) assert 0 <= h <= 0xFFFF assert 0 <= s <= 0xFFFF assert 0 <= b <= 0xFFFF # ============================================================================ # Packet construction # ============================================================================ def test_build_packet_size_header_matches_total(): packet = _build_packet(msg_type=MSG_SET_POWER, payload=b"\x00\x01\x02\x03\x04\x05") size = struct.unpack_from(" LIFXClient: client = LIFXClient("lifx://127.0.0.1", led_count=10, min_interval_s=min_interval_s) transport = MagicMock() transport.sendto = MagicMock() transport.close = MagicMock() client._transport = transport client._protocol = MagicMock() client._connected = True return client def _sent_packets(client: LIFXClient) -> list[bytes]: return [bytes(call.args[0]) for call in client._transport.sendto.call_args_list] @pytest.mark.asyncio async def test_send_pixels_emits_one_set_color_packet(): client = _make_connected_client() pixels = np.array([[255, 0, 0]], dtype=np.uint8) await client.send_pixels(pixels) packets = _sent_packets(client) assert len(packets) == 1 msg_type = struct.unpack_from("