@@ -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
17561607class 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
19961964class ProtocolLEDENETAddressableBase (ProtocolLEDENET9Byte ):
19971965 """Base class for addressable protocols."""
0 commit comments