|
28 | 28 | LevelWriteModeData, |
29 | 29 | MultiColorEffects, |
30 | 30 | ) |
31 | | -from .timer import LedTimer |
| 31 | +from .timer import LedTimer, LedTimerExtended |
32 | 32 | from .utils import ( |
33 | 33 | scaled_color_temp_to_white_levels, |
34 | 34 | utils, |
@@ -130,6 +130,7 @@ class PowerRestoreStates: |
130 | 130 | MSG_STATE = "state" |
131 | 131 | MSG_TIME = "time" |
132 | 132 | MSG_TIMERS = "timers" |
| 133 | +MSG_TIMERS_EXTENDED = "timers_extended" |
133 | 134 | MSG_MUSIC_MODE_STATE = "music_mode_state" |
134 | 135 | MSG_ADDRESSABLE_STATE = "addressable_state" |
135 | 136 | MSG_DEVICE_CONFIG = "device_config" |
@@ -165,6 +166,7 @@ class PowerRestoreStates: |
165 | 166 | (0x63,): MSG_A1_DEVICE_CONFIG, |
166 | 167 | (0x72,): MSG_MUSIC_MODE_STATE, |
167 | 168 | (0x2B,): MSG_REMOTE_CONFIG, |
| 169 | + (0xE0, 0x06): MSG_TIMERS_EXTENDED, # 0xB6 device timer response |
168 | 170 | } |
169 | 171 |
|
170 | 172 | MSG_LENGTHS = { |
@@ -2033,6 +2035,115 @@ def construct_scribble_paint( |
2033 | 2035 | msg, inner_pre_constructed=True, version=0x02 |
2034 | 2036 | ) |
2035 | 2037 |
|
| 2038 | + # --- Timer Support for 0xB6 devices --- |
| 2039 | + # This protocol uses a different timer format than other protocols: |
| 2040 | + # - Query: e0 06 (wrapped) |
| 2041 | + # - Response: e0 06 + variable length slot data |
| 2042 | + # - Set: e0 05 SS f0 HH MM 00 RR ... (wrapped) |
| 2043 | + |
| 2044 | + def construct_get_timers(self) -> bytearray: |
| 2045 | + """Construct a get timers request. |
| 2046 | +
|
| 2047 | + 0xB6 devices use the wrapped e0 06 command for timer queries. |
| 2048 | + """ |
| 2049 | + inner = bytearray([0xE0, 0x06]) |
| 2050 | + return self.construct_wrapped_message( |
| 2051 | + inner, inner_pre_constructed=True, version=0x01 |
| 2052 | + ) |
| 2053 | + |
| 2054 | + def construct_set_timer(self, timer: LedTimerExtended) -> bytearray: |
| 2055 | + """Construct a set timer command for a single timer. |
| 2056 | +
|
| 2057 | + 0xB6 devices set timers one at a time using e0 05 SS ... |
| 2058 | +
|
| 2059 | + Args: |
| 2060 | + timer: The LedTimerExtended object to set |
| 2061 | +
|
| 2062 | + Returns: |
| 2063 | + Wrapped command bytearray |
| 2064 | + """ |
| 2065 | + inner = bytearray([0xE0, 0x05, timer.slot]) |
| 2066 | + inner.extend(timer.to_bytes()) |
| 2067 | + return self.construct_wrapped_message( |
| 2068 | + inner, inner_pre_constructed=True, version=0x01 |
| 2069 | + ) |
| 2070 | + |
| 2071 | + def construct_set_timers( # type: ignore[override] |
| 2072 | + self, timer_list: list[LedTimerExtended] |
| 2073 | + ) -> list[bytearray]: |
| 2074 | + """Construct set timer commands for multiple timers. |
| 2075 | +
|
| 2076 | + 0xB6 devices set timers one at a time, so this returns a list |
| 2077 | + of commands (one per timer). |
| 2078 | +
|
| 2079 | + Args: |
| 2080 | + timer_list: List of LedTimerExtended objects to set |
| 2081 | +
|
| 2082 | + Returns: |
| 2083 | + List of wrapped command bytearrays |
| 2084 | + """ |
| 2085 | + return [self.construct_set_timer(timer) for timer in timer_list] |
| 2086 | + |
| 2087 | + def is_valid_timers_response(self, msg: bytes) -> bool: |
| 2088 | + """Check if a message is a valid timers response. |
| 2089 | +
|
| 2090 | + 0xB6 timer responses start with e0 06. |
| 2091 | + """ |
| 2092 | + if len(msg) < 2: |
| 2093 | + return False |
| 2094 | + return msg[0] == 0xE0 and msg[1] == 0x06 |
| 2095 | + |
| 2096 | + def parse_get_timers(self, msg: bytes) -> list[LedTimerExtended]: # type: ignore[override] |
| 2097 | + """Parse timer response into list of LedTimerExtended objects. |
| 2098 | +
|
| 2099 | + Response format: |
| 2100 | + - Empty: e0 06 (2 bytes) |
| 2101 | + - With timers: e0 06 [slot1] [slot2] ... [slot6] |
| 2102 | +
|
| 2103 | + Each slot is either: |
| 2104 | + - 7 bytes if empty/inactive |
| 2105 | + - 21 bytes if simple timer (ON/OFF/color) |
| 2106 | + - Variable (48+) bytes if scene timer |
| 2107 | + """ |
| 2108 | + if len(msg) <= 2: |
| 2109 | + # Empty response or just header - no timers configured |
| 2110 | + return [] |
| 2111 | + |
| 2112 | + timers: list[LedTimerExtended] = [] |
| 2113 | + offset = 2 # Skip e0 06 header |
| 2114 | + |
| 2115 | + while offset < len(msg): |
| 2116 | + # Check if we have enough bytes for minimum slot (7 bytes) |
| 2117 | + if offset + 7 > len(msg): |
| 2118 | + break |
| 2119 | + |
| 2120 | + timer, consumed = LedTimerExtended.from_bytes(msg, offset) |
| 2121 | + timers.append(timer) |
| 2122 | + offset += consumed |
| 2123 | + |
| 2124 | + return timers |
| 2125 | + |
| 2126 | + @property |
| 2127 | + def timer_count(self) -> int: |
| 2128 | + """Number of timer slots supported.""" |
| 2129 | + return 6 |
| 2130 | + |
| 2131 | + def expected_timers_response_length(self, data: bytes) -> int: |
| 2132 | + """Calculate expected timer response length. |
| 2133 | +
|
| 2134 | + For 0xB6 devices, the timer response has variable length |
| 2135 | + based on the inner message length encoded in the wrapped message. |
| 2136 | + """ |
| 2137 | + # Timer responses are wrapped, so we extract from the wrapper |
| 2138 | + if ( |
| 2139 | + len(data) >= OUTER_MESSAGE_WRAPPER_START_LEN |
| 2140 | + and data[0] == OUTER_MESSAGE_FIRST_BYTE |
| 2141 | + ): |
| 2142 | + inner_msg_len = (data[8] << 8) + data[9] |
| 2143 | + return OUTER_MESSAGE_WRAPPER_START_LEN + inner_msg_len + CHECKSUM_LEN |
| 2144 | + # Fallback |
| 2145 | + return len(data) |
| 2146 | + |
2036 | 2147 |
|
2037 | 2148 | class ProtocolLEDENETAddressableBase(ProtocolLEDENET9Byte): |
2038 | 2149 | """Base class for addressable protocols.""" |
|
0 commit comments