|
9 | 9 | from bleak.backends.device import BLEDevice |
10 | 10 |
|
11 | 11 | from ..const import SwitchbotModel |
12 | | -from ..const.lock import LockStatus |
| 12 | +from ..const.lock import LockStatus, QuickKeyFunction |
13 | 13 | from .device import ( |
14 | 14 | SwitchbotEncryptedDevice, |
15 | 15 | SwitchbotOperationError, |
|
57 | 57 | SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000008", |
58 | 58 | } |
59 | 59 |
|
| 60 | +# Quick Key — a Lock Ultra setting. All three of |
| 61 | +# its settings (enabled / single-vs-double press / function) live in a single config |
| 62 | +# byte: read it with 0x4f, masked-write it with 0x4e. Lock Ultra only (untested on |
| 63 | +# other lock models). |
| 64 | +COMMAND_GET_QUICK_KEY = { |
| 65 | + SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4f0401", |
| 66 | +} |
| 67 | +# Append "<mask><value>ff" (each one hex byte) for a masked write. |
| 68 | +COMMAND_SET_QUICK_KEY_PREFIX = { |
| 69 | + SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e040100", |
| 70 | +} |
| 71 | +# Quick Key config-byte layout (the high bits 0xC0 are constant status flags). |
| 72 | +QUICK_KEY_ENABLED_BIT = 0x08 |
| 73 | +QUICK_KEY_DOUBLE_PRESS_BIT = 0x04 |
| 74 | +QUICK_KEY_FUNCTION_MASK = 0x03 |
| 75 | + |
60 | 76 | COMMAND_ENABLE_NOTIFICATIONS = { |
61 | 77 | SwitchbotModel.LOCK: f"{COMMAND_HEADER}0e01001e00008101", |
62 | 78 | SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0e01001e00008101", |
@@ -141,6 +157,71 @@ async def half_lock(self) -> bool: |
141 | 157 | {LockStatus.HALF_LOCKED, LockStatus.LOCKING}, |
142 | 158 | ) |
143 | 159 |
|
| 160 | + async def get_quick_key(self) -> dict[str, Any] | None: |
| 161 | + """Return the Quick Key settings (Lock Ultra only). |
| 162 | +
|
| 163 | + Returns |
| 164 | + ``{"enabled": bool, "double_press": bool, "function": QuickKeyFunction}``, |
| 165 | + or ``None`` if it can't be read. |
| 166 | + """ |
| 167 | + if self._model not in COMMAND_GET_QUICK_KEY: |
| 168 | + raise SwitchbotOperationError(f"Quick Key is not supported on {self._model}") |
| 169 | + result = await self._send_command( |
| 170 | + key=COMMAND_GET_QUICK_KEY[self._model], retry=self._retry_count |
| 171 | + ) |
| 172 | + if not self._check_command_result(result, 0, COMMAND_RESULT_EXPECTED_VALUES): |
| 173 | + _LOGGER.error("Failed to read Quick Key settings") |
| 174 | + return None |
| 175 | + if len(result) < 2: |
| 176 | + _LOGGER.error("Invalid Quick Key response: %s", result) |
| 177 | + return None |
| 178 | + return self._parse_quick_key(result[1]) |
| 179 | + |
| 180 | + @staticmethod |
| 181 | + def _parse_quick_key(cfg: int) -> dict[str, Any]: |
| 182 | + """Parse the Quick Key config byte.""" |
| 183 | + return { |
| 184 | + "enabled": bool(cfg & QUICK_KEY_ENABLED_BIT), |
| 185 | + "double_press": bool(cfg & QUICK_KEY_DOUBLE_PRESS_BIT), |
| 186 | + "function": QuickKeyFunction(cfg & QUICK_KEY_FUNCTION_MASK), |
| 187 | + } |
| 188 | + |
| 189 | + async def set_quick_key( |
| 190 | + self, |
| 191 | + *, |
| 192 | + enabled: bool | None = None, |
| 193 | + double_press: bool | None = None, |
| 194 | + function: QuickKeyFunction | None = None, |
| 195 | + ) -> bool: |
| 196 | + """Update one or more Quick Key settings (Lock Ultra only). |
| 197 | +
|
| 198 | + Only the fields you pass are changed (a masked write); the others keep their |
| 199 | + current value. Returns ``True`` if the lock acknowledges with the requested |
| 200 | + bits set. |
| 201 | + """ |
| 202 | + if self._model not in COMMAND_SET_QUICK_KEY_PREFIX: |
| 203 | + raise SwitchbotOperationError(f"Quick Key is not supported on {self._model}") |
| 204 | + mask = 0 |
| 205 | + value = 0 |
| 206 | + if enabled is not None: |
| 207 | + mask |= QUICK_KEY_ENABLED_BIT |
| 208 | + value |= QUICK_KEY_ENABLED_BIT if enabled else 0 |
| 209 | + if double_press is not None: |
| 210 | + mask |= QUICK_KEY_DOUBLE_PRESS_BIT |
| 211 | + value |= QUICK_KEY_DOUBLE_PRESS_BIT if double_press else 0 |
| 212 | + if function is not None: |
| 213 | + mask |= QUICK_KEY_FUNCTION_MASK |
| 214 | + value |= function.value |
| 215 | + if not mask: |
| 216 | + raise ValueError("set_quick_key requires at least one setting to change") |
| 217 | + command = f"{COMMAND_SET_QUICK_KEY_PREFIX[self._model]}{mask:02x}{value:02x}ff" |
| 218 | + result = await self._send_command(key=command, retry=self._retry_count) |
| 219 | + if not self._check_command_result(result, 0, COMMAND_RESULT_EXPECTED_VALUES): |
| 220 | + _LOGGER.error("Failed to set Quick Key settings") |
| 221 | + return False |
| 222 | + # The lock echoes the resulting config byte; confirm our bits stuck. |
| 223 | + return len(result) >= 2 and (result[1] & mask) == (value & mask) |
| 224 | + |
144 | 225 | def _parse_basic_data(self, basic_data: bytes) -> dict[str, Any]: |
145 | 226 | """Parse basic data from lock.""" |
146 | 227 | return { |
|
0 commit comments