Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion app/utils/color_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand Down Expand Up @@ -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"""
Expand Down
7 changes: 6 additions & 1 deletion app/utils/wled_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [{
Expand Down
5 changes: 5 additions & 0 deletions repository.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "SpotifyToWLED Add-on Repository",
"url": "https://github.com/raphaelbleier/SpotifyToWled",
"maintainer": "Raphael Bleier <raphaelbleier@users.noreply.github.com>"
}
38 changes: 38 additions & 0 deletions tests/test_color_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down
22 changes: 22 additions & 0 deletions tests/test_wled_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down