Skip to content

Commit 7b78779

Browse files
committed
fix: move changes to new protocol
1 parent cbe4bc2 commit 7b78779

File tree

1 file changed

+121
-153
lines changed

1 file changed

+121
-153
lines changed

flux_led/protocol.py

Lines changed: 121 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,30 +1032,14 @@ def _is_start_of_power_state_response(self, data: bytes) -> bool:
10321032
"""Check if a message is the start of a state response."""
10331033
return _message_type_from_start_of_msg(data) == MSG_POWER_STATE
10341034

1035-
def is_valid_extended_state_response(self, raw_state: bytes) -> bool:
1036-
"""Check if this is an extended state response (0xEA 0x81 format)."""
1037-
return len(raw_state) >= 20 and raw_state[0] == 0xEA and raw_state[1] == 0x81
1038-
10391035
def is_valid_state_response(self, raw_state: bytes) -> bool:
10401036
"""Check if a state response is valid."""
1041-
# Check for extended state format (0xEA 0x81) used by 25BYTE protocol
1042-
if self.is_valid_extended_state_response(raw_state):
1043-
return True
1044-
# Check for standard state format (0x81)
10451037
if len(raw_state) != self.state_response_length:
10461038
return False
10471039
if raw_state[0] != 129:
10481040
return False
10491041
return self.is_checksum_correct(raw_state)
10501042

1051-
def extended_state_to_state(self, raw_state: bytes) -> bytes:
1052-
"""Convert an extended state response to a standard state response.
1053-
1054-
This is overridden by ProtocolLEDENET25Byte with the actual conversion logic.
1055-
"""
1056-
# Default implementation for protocols that don't use extended state
1057-
return raw_state
1058-
10591043
def construct_state_change(self, turn_on: int) -> bytearray:
10601044
"""
10611045
The bytes to send for a state change request.
@@ -1461,17 +1445,9 @@ def name(self) -> str:
14611445
"""The name of the protocol."""
14621446
return PROTOCOL_LEDENET_25BYTE
14631447

1464-
def is_valid_state_response(self, raw_state: bytes) -> bool:
1465-
"""Check if a state response is valid."""
1466-
# This protocol can respond with either standard 0x81 format or extended 0xEA 0x81 format
1467-
if self.is_valid_extended_state_response(raw_state):
1468-
return True
1469-
# Fall back to parent class check for standard 0x81 responses
1470-
return super().is_valid_state_response(raw_state)
1471-
14721448
def is_valid_extended_state_response(self, raw_state: bytes) -> bool:
14731449
"""Check if a state response is valid."""
1474-
return len(raw_state) >= 20 and raw_state[0] == 0xEA and raw_state[1] == 0x81
1450+
return raw_state[0] == 0xEA and raw_state[1] == 0x81 and len(raw_state) >= 20
14751451

14761452
def extended_state_to_state(self, raw_state: bytes) -> bytes:
14771453
"""Convert an extended state response to a state response."""
@@ -1514,16 +1490,10 @@ def extended_state_to_state(self, raw_state: bytes) -> bytes:
15141490

15151491
white_temp = raw_state[14]
15161492
white_brightness = raw_state[15]
1493+
levels = scaled_color_temp_to_white_levels(white_temp, white_brightness)
15171494

1518-
# When in custom pattern mode or RGB mode, white_temp may be 0xFF (255)
1519-
# In this case, there are no white channels active
1520-
if white_temp > 100 or white_brightness == 0:
1521-
cool_white = 0
1522-
warm_white = 0
1523-
else:
1524-
levels = scaled_color_temp_to_white_levels(white_temp, white_brightness)
1525-
cool_white = levels.cool_white
1526-
warm_white = levels.warm_white
1495+
cool_white = levels.cool_white
1496+
warm_white = levels.warm_white
15271497

15281498
# Convert HSV to RGB
15291499
h = (hue * 2) / 360
@@ -1535,8 +1505,6 @@ def extended_state_to_state(self, raw_state: bytes) -> bytes:
15351505
blue = min(int(max(0, b_f) * 255), 255)
15361506

15371507
# Fill standard state structure
1538-
# Note: For devices with extended custom effects (ProtocolLEDENETExtendedCustom),
1539-
# the child class overrides this method to handle mode extraction from position 8.
15401508
mode = 0
15411509
color_mode = 0
15421510
check_sum = 0 # Set to 0; not critical
@@ -1635,123 +1603,6 @@ def construct_levels_change(
16351603
)
16361604
]
16371605

1638-
def _rgb_to_hsv_bytes(self, r: int, g: int, b: int) -> list[int]:
1639-
"""Convert RGB (0-255) to 5-byte HSV format [H/2, S, V, 0, 0].
1640-
1641-
This format is used by extended commands (0xE1 0x21, 0xE1 0x22).
1642-
"""
1643-
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
1644-
return [int(h * 180), int(s * 100), int(v * 100), 0x00, 0x00]
1645-
1646-
def construct_extended_custom_effect(
1647-
self,
1648-
pattern_id: int,
1649-
colors: list[tuple[int, int, int]],
1650-
speed: int = 50,
1651-
density: int = 50,
1652-
direction: int = 0x01,
1653-
option: int = 0x00,
1654-
) -> bytearray:
1655-
"""Construct an extended custom effect command.
1656-
1657-
Protocol format:
1658-
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...
1659-
e1 21 00 64 PP OO DD NS SP 00 00 00 00 00 00 CC [H S V 00 00] x N
1660-
| | | | | | colors (5 bytes each)
1661-
| | | | | color count
1662-
| | | | speed (0-100)
1663-
| | | density (0-100)
1664-
| | direction (01=L->R, 02=R->L)
1665-
| option (00=default, 01=color change)
1666-
pattern_id (1-24 or 101-102)
1667-
1668-
Args:
1669-
pattern_id: Pattern ID 1-24 or 101-102
1670-
colors: List of 1-8 RGB color tuples (0-255 per channel)
1671-
speed: Animation speed 0-100 (default 50)
1672-
density: Pattern density 0-100 (default 50)
1673-
direction: 0x01=L->R, 0x02=R->L (default L->R)
1674-
option: Pattern-specific option (default 0)
1675-
1676-
Returns:
1677-
Wrapped command bytearray
1678-
"""
1679-
# Clamp values to valid range
1680-
speed = max(0, min(100, speed))
1681-
density = max(0, min(100, density))
1682-
1683-
# Convert Enum to value if needed
1684-
if hasattr(pattern_id, "value"):
1685-
pattern_id = pattern_id.value
1686-
if hasattr(option, "value"):
1687-
option = option.value
1688-
if hasattr(direction, "value"):
1689-
direction = direction.value
1690-
1691-
# Build inner message
1692-
msg = bytearray(
1693-
[
1694-
0xE1,
1695-
0x21,
1696-
0x00, # Command type
1697-
0x64, # Constant (100)
1698-
pattern_id,
1699-
option,
1700-
direction,
1701-
density,
1702-
speed,
1703-
0x00,
1704-
0x00,
1705-
0x00,
1706-
0x00,
1707-
0x00,
1708-
0x00, # Reserved (6 bytes)
1709-
len(colors), # Color count
1710-
]
1711-
)
1712-
1713-
# Add colors as HSV (5 bytes each)
1714-
for r, g, b in colors:
1715-
msg.extend(self._rgb_to_hsv_bytes(r, g, b))
1716-
1717-
return self.construct_wrapped_message(
1718-
msg, inner_pre_constructed=True, version=0x02
1719-
)
1720-
1721-
def construct_custom_segment_colors(
1722-
self,
1723-
segments: list[tuple[int, int, int] | None],
1724-
) -> bytearray:
1725-
"""Construct a custom segment colors command (0xE1 0x22).
1726-
1727-
Sets static HSV colors for each of 20 segments on the light strip.
1728-
Used by devices like AK001-ZJ21413 (model 0xB6) under the "colorful" menu.
1729-
1730-
Protocol format:
1731-
e1 22 00 00 00 00 14 [H/2 S V 00 00] x 20
1732-
1733-
Args:
1734-
segments: List of up to 20 segment colors. Each segment is either:
1735-
- None or (0, 0, 0) for off
1736-
- (R, G, B) tuple with values 0-255
1737-
1738-
Returns:
1739-
Wrapped command bytearray
1740-
"""
1741-
# Build inner message: header + 20 segments
1742-
msg = bytearray([0xE1, 0x22, 0x00, 0x00, 0x00, 0x00, 0x14])
1743-
1744-
for i in range(20):
1745-
segment = segments[i] if i < len(segments) else None
1746-
if segment and segment != (0, 0, 0):
1747-
msg.extend(self._rgb_to_hsv_bytes(*segment))
1748-
else:
1749-
msg.extend([0x00, 0x00, 0x00, 0x00, 0x00]) # Off
1750-
1751-
return self.construct_wrapped_message(
1752-
msg, inner_pre_constructed=True, version=0x02
1753-
)
1754-
17551606

17561607
class ProtocolLEDENETExtendedCustom(ProtocolLEDENET25Byte):
17571608
"""Protocol for devices with extended state format (0xEA 0x81) and custom effects.
@@ -1992,6 +1843,123 @@ def construct_levels_change(
19921843
)
19931844
]
19941845

1846+
def _rgb_to_hsv_bytes(self, r: int, g: int, b: int) -> list[int]:
1847+
"""Convert RGB (0-255) to 5-byte HSV format [H/2, S, V, 0, 0].
1848+
1849+
This format is used by extended commands (0xE1 0x21, 0xE1 0x22).
1850+
"""
1851+
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
1852+
return [int(h * 180), int(s * 100), int(v * 100), 0x00, 0x00]
1853+
1854+
def construct_extended_custom_effect(
1855+
self,
1856+
pattern_id: int,
1857+
colors: list[tuple[int, int, int]],
1858+
speed: int = 50,
1859+
density: int = 50,
1860+
direction: int = 0x01,
1861+
option: int = 0x00,
1862+
) -> bytearray:
1863+
"""Construct an extended custom effect command.
1864+
1865+
Protocol format:
1866+
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...
1867+
e1 21 00 64 PP OO DD NS SP 00 00 00 00 00 00 CC [H S V 00 00] x N
1868+
| | | | | | colors (5 bytes each)
1869+
| | | | | color count
1870+
| | | | speed (0-100)
1871+
| | | density (0-100)
1872+
| | direction (01=L->R, 02=R->L)
1873+
| option (00=default, 01=color change)
1874+
pattern_id (1-24 or 101-102)
1875+
1876+
Args:
1877+
pattern_id: Pattern ID 1-24 or 101-102
1878+
colors: List of 1-8 RGB color tuples (0-255 per channel)
1879+
speed: Animation speed 0-100 (default 50)
1880+
density: Pattern density 0-100 (default 50)
1881+
direction: 0x01=L->R, 0x02=R->L (default L->R)
1882+
option: Pattern-specific option (default 0)
1883+
1884+
Returns:
1885+
Wrapped command bytearray
1886+
"""
1887+
# Clamp values to valid range
1888+
speed = max(0, min(100, speed))
1889+
density = max(0, min(100, density))
1890+
1891+
# Convert Enum to value if needed
1892+
if hasattr(pattern_id, "value"):
1893+
pattern_id = pattern_id.value
1894+
if hasattr(option, "value"):
1895+
option = option.value
1896+
if hasattr(direction, "value"):
1897+
direction = direction.value
1898+
1899+
# Build inner message
1900+
msg = bytearray(
1901+
[
1902+
0xE1,
1903+
0x21,
1904+
0x00, # Command type
1905+
0x64, # Constant (100)
1906+
pattern_id,
1907+
option,
1908+
direction,
1909+
density,
1910+
speed,
1911+
0x00,
1912+
0x00,
1913+
0x00,
1914+
0x00,
1915+
0x00,
1916+
0x00, # Reserved (6 bytes)
1917+
len(colors), # Color count
1918+
]
1919+
)
1920+
1921+
# Add colors as HSV (5 bytes each)
1922+
for r, g, b in colors:
1923+
msg.extend(self._rgb_to_hsv_bytes(r, g, b))
1924+
1925+
return self.construct_wrapped_message(
1926+
msg, inner_pre_constructed=True, version=0x02
1927+
)
1928+
1929+
def construct_custom_segment_colors(
1930+
self,
1931+
segments: list[tuple[int, int, int] | None],
1932+
) -> bytearray:
1933+
"""Construct a custom segment colors command (0xE1 0x22).
1934+
1935+
Sets static HSV colors for each of 20 segments on the light strip.
1936+
Used by devices like AK001-ZJ21413 (model 0xB6) under the "colorful" menu.
1937+
1938+
Protocol format:
1939+
e1 22 00 00 00 00 14 [H/2 S V 00 00] x 20
1940+
1941+
Args:
1942+
segments: List of up to 20 segment colors. Each segment is either:
1943+
- None or (0, 0, 0) for off
1944+
- (R, G, B) tuple with values 0-255
1945+
1946+
Returns:
1947+
Wrapped command bytearray
1948+
"""
1949+
# Build inner message: header + 20 segments
1950+
msg = bytearray([0xE1, 0x22, 0x00, 0x00, 0x00, 0x00, 0x14])
1951+
1952+
for i in range(20):
1953+
segment = segments[i] if i < len(segments) else None
1954+
if segment and segment != (0, 0, 0):
1955+
msg.extend(self._rgb_to_hsv_bytes(*segment))
1956+
else:
1957+
msg.extend([0x00, 0x00, 0x00, 0x00, 0x00]) # Off
1958+
1959+
return self.construct_wrapped_message(
1960+
msg, inner_pre_constructed=True, version=0x02
1961+
)
1962+
19951963

19961964
class ProtocolLEDENETAddressableBase(ProtocolLEDENET9Byte):
19971965
"""Base class for addressable protocols."""

0 commit comments

Comments
 (0)