From 5fa8ac3f292d8df34fecbfddc24882958a3d0073 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:04:05 +0000 Subject: [PATCH 1/3] Initial plan From 876bab554a4b03ae07cc8920cca92c502d7fcc11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:06:16 +0000 Subject: [PATCH 2/3] Add repository.json to make this a valid Home Assistant add-on repository Co-authored-by: raphaelbleier <75416341+raphaelbleier@users.noreply.github.com> --- repository.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 repository.json diff --git a/repository.json b/repository.json new file mode 100644 index 0000000..cdfec9a --- /dev/null +++ b/repository.json @@ -0,0 +1,5 @@ +{ + "name": "SpotifyToWLED Add-on Repository", + "url": "https://github.com/raphaelbleier/SpotifyToWled", + "maintainer": "Raphael Bleier " +} From 51d9e3be6d4ccdda2722b4923de35940c55e9479 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:09:32 +0000 Subject: [PATCH 3/3] Add LED-compatible color validation and clamping to ensure RGB values are always 0-255 Co-authored-by: raphaelbleier <75416341+raphaelbleier@users.noreply.github.com> --- app/utils/color_extractor.py | 21 ++++++++++++++++++- app/utils/wled_controller.py | 7 ++++++- tests/test_color_extractor.py | 38 +++++++++++++++++++++++++++++++++++ tests/test_wled_controller.py | 22 ++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/app/utils/color_extractor.py b/app/utils/color_extractor.py index 2255f07..656d3c1 100644 --- a/app/utils/color_extractor.py +++ b/app/utils/color_extractor.py @@ -34,7 +34,7 @@ def get_color(self, image_url: str, method: str = 'vibrant') -> Tuple[int, int, method: Extraction method ('vibrant', 'dominant', 'average') Returns: - RGB tuple (r, g, b) + RGB tuple (r, g, b) with values guaranteed to be in LED-compatible range (0-255) """ # Check cache first if self._is_cache_valid(image_url): @@ -56,6 +56,9 @@ def get_color(self, image_url: str, method: str = 'vibrant') -> Tuple[int, int, else: color = self._get_vibrant_color(response.content) + # Validate and ensure color is in LED-compatible range (0-255) + color = self.validate_rgb(*color) + # Cache the result self._cache[image_url] = { 'color': color, @@ -135,6 +138,22 @@ def _calculate_saturation(r: int, g: int, b: int) -> float: return 0 return (max_c - min_c) / max_c + @staticmethod + def validate_rgb(r: int, g: int, b: int) -> Tuple[int, int, int]: + """ + Validate and clamp RGB values to LED-compatible range (0-255) + + Args: + r, g, b: RGB color values + + Returns: + Clamped RGB tuple with values guaranteed to be in 0-255 range + """ + r = max(0, min(255, int(r))) + g = max(0, min(255, int(g))) + b = max(0, min(255, int(b))) + return (r, g, b) + @staticmethod def rgb_to_hex(r: int, g: int, b: int) -> str: """Convert RGB to hex color string""" diff --git a/app/utils/wled_controller.py b/app/utils/wled_controller.py index cb88609..f271ea8 100644 --- a/app/utils/wled_controller.py +++ b/app/utils/wled_controller.py @@ -23,11 +23,16 @@ def set_color(self, ip: str, r: int, g: int, b: int) -> bool: Args: ip: WLED device IP address - r, g, b: RGB color values (0-255) + r, g, b: RGB color values (will be clamped to 0-255 for LED compatibility) Returns: True if successful, False otherwise """ + # Ensure RGB values are in valid LED range (0-255) + r = max(0, min(255, int(r))) + g = max(0, min(255, int(g))) + b = max(0, min(255, int(b))) + url = f"http://{ip}/json/state" payload = { "seg": [{ diff --git a/tests/test_color_extractor.py b/tests/test_color_extractor.py index 0ae50db..8f4fb2a 100644 --- a/tests/test_color_extractor.py +++ b/tests/test_color_extractor.py @@ -65,6 +65,44 @@ def test_color_validation(self): # Saturation should be between 0 and 1 self.assertGreaterEqual(sat, 0) self.assertLessEqual(sat, 1) + + def test_validate_rgb_normal_values(self): + """Test RGB validation with normal values""" + r, g, b = ColorExtractor.validate_rgb(128, 64, 200) + self.assertEqual(r, 128) + self.assertEqual(g, 64) + self.assertEqual(b, 200) + + def test_validate_rgb_clamp_high(self): + """Test RGB validation clamps values above 255""" + r, g, b = ColorExtractor.validate_rgb(300, 256, 1000) + self.assertEqual(r, 255) + self.assertEqual(g, 255) + self.assertEqual(b, 255) + + def test_validate_rgb_clamp_low(self): + """Test RGB validation clamps negative values to 0""" + r, g, b = ColorExtractor.validate_rgb(-10, -1, -100) + self.assertEqual(r, 0) + self.assertEqual(g, 0) + self.assertEqual(b, 0) + + def test_validate_rgb_mixed_clamping(self): + """Test RGB validation with mixed values needing clamping""" + r, g, b = ColorExtractor.validate_rgb(300, 128, -50) + self.assertEqual(r, 255) + self.assertEqual(g, 128) + self.assertEqual(b, 0) + + def test_validate_rgb_edge_values(self): + """Test RGB validation with edge values""" + # Min edge + r, g, b = ColorExtractor.validate_rgb(0, 0, 0) + self.assertEqual((r, g, b), (0, 0, 0)) + + # Max edge + r, g, b = ColorExtractor.validate_rgb(255, 255, 255) + self.assertEqual((r, g, b), (255, 255, 255)) if __name__ == '__main__': diff --git a/tests/test_wled_controller.py b/tests/test_wled_controller.py index df8a7a7..ca2ac14 100644 --- a/tests/test_wled_controller.py +++ b/tests/test_wled_controller.py @@ -81,6 +81,28 @@ def test_device_status_tracking(self): # Initially unknown status = self.controller.get_device_status(ip) self.assertEqual(status['status'], 'unknown') + + @patch('app.utils.wled_controller.requests.post') + def test_set_color_clamps_values(self, mock_post): + """Test that color values are clamped to valid LED range (0-255)""" + mock_response = Mock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + # Test with values that need clamping + result = self.controller.set_color('192.168.1.100', 300, -50, 128) + + self.assertTrue(result) + + # Verify the clamped values were sent in the payload + call_args = mock_post.call_args + payload = call_args[1]['json'] + sent_color = payload['seg'][0]['col'][0] + + # Should be clamped to [255, 0, 128] + self.assertEqual(sent_color[0], 255) # 300 -> 255 + self.assertEqual(sent_color[1], 0) # -50 -> 0 + self.assertEqual(sent_color[2], 128) # 128 -> 128 if __name__ == '__main__':