|
24 | 24 | ) |
25 | 25 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
26 | 26 |
|
| 27 | +from ..adv_parser import populate_model_to_mac_cache |
27 | 28 | from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID |
28 | 29 | from ..const import ( |
29 | 30 | DEFAULT_RETRY_COUNT, |
|
37 | 38 | from ..discovery import GetSwitchbotDevices |
38 | 39 | from ..helpers import create_background_task |
39 | 40 | from ..models import SwitchBotAdvertisement |
| 41 | +from ..utils import format_mac_upper |
40 | 42 |
|
41 | 43 | _LOGGER = logging.getLogger(__name__) |
42 | 44 |
|
| 45 | + |
| 46 | +def _extract_region(userinfo: dict[str, Any]) -> str: |
| 47 | + """Extract region from user info, defaulting to 'us'.""" |
| 48 | + if "botRegion" in userinfo and userinfo["botRegion"] != "": |
| 49 | + return userinfo["botRegion"] |
| 50 | + return "us" |
| 51 | + |
| 52 | + |
| 53 | +# Mapping from API model names to SwitchbotModel enum values |
| 54 | +API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = { |
| 55 | + "WoHand": SwitchbotModel.BOT, |
| 56 | + "WoCurtain": SwitchbotModel.CURTAIN, |
| 57 | + "WoHumi": SwitchbotModel.HUMIDIFIER, |
| 58 | + "WoPlug": SwitchbotModel.PLUG_MINI, |
| 59 | + "WoPlugUS": SwitchbotModel.PLUG_MINI, |
| 60 | + "WoContact": SwitchbotModel.CONTACT_SENSOR, |
| 61 | + "WoStrip": SwitchbotModel.LIGHT_STRIP, |
| 62 | + "WoSensorTH": SwitchbotModel.METER, |
| 63 | + "WoMeter": SwitchbotModel.METER, |
| 64 | + "WoMeterPlus": SwitchbotModel.METER_PRO, |
| 65 | + "WoPresence": SwitchbotModel.MOTION_SENSOR, |
| 66 | + "WoBulb": SwitchbotModel.COLOR_BULB, |
| 67 | + "WoCeiling": SwitchbotModel.CEILING_LIGHT, |
| 68 | + "WoLock": SwitchbotModel.LOCK, |
| 69 | + "WoBlindTilt": SwitchbotModel.BLIND_TILT, |
| 70 | + "WoIOSensor": SwitchbotModel.IO_METER, # Outdoor Meter |
| 71 | + "WoButton": SwitchbotModel.REMOTE, # Remote button |
| 72 | + "WoLinkMini": SwitchbotModel.HUBMINI_MATTER, # Hub Mini |
| 73 | + "W1083002": SwitchbotModel.RELAY_SWITCH_1, # Relay Switch 1 |
| 74 | + "W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant) |
| 75 | + "W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3 |
| 76 | +} |
| 77 | + |
43 | 78 | REQ_HEADER = "570f" |
44 | 79 |
|
45 | 80 |
|
@@ -164,6 +199,113 @@ def __init__( |
164 | 199 | self._last_full_update: float = -PASSIVE_POLL_INTERVAL |
165 | 200 | self._timed_disconnect_task: asyncio.Task[None] | None = None |
166 | 201 |
|
| 202 | + @classmethod |
| 203 | + async def _async_get_user_info( |
| 204 | + cls, |
| 205 | + session: aiohttp.ClientSession, |
| 206 | + auth_headers: dict[str, str], |
| 207 | + ) -> dict[str, Any]: |
| 208 | + try: |
| 209 | + return await cls.api_request( |
| 210 | + session, "account", "account/api/v1/user/userinfo", {}, auth_headers |
| 211 | + ) |
| 212 | + except Exception as err: |
| 213 | + raise SwitchbotAccountConnectionError( |
| 214 | + f"Failed to retrieve SwitchBot Account user details: {err}" |
| 215 | + ) from err |
| 216 | + |
| 217 | + @classmethod |
| 218 | + async def _get_auth_result( |
| 219 | + cls, |
| 220 | + session: aiohttp.ClientSession, |
| 221 | + username: str, |
| 222 | + password: str, |
| 223 | + ) -> dict[str, Any]: |
| 224 | + """Authenticate with SwitchBot API.""" |
| 225 | + try: |
| 226 | + return await cls.api_request( |
| 227 | + session, |
| 228 | + "account", |
| 229 | + "account/api/v1/user/login", |
| 230 | + { |
| 231 | + "clientId": SWITCHBOT_APP_CLIENT_ID, |
| 232 | + "username": username, |
| 233 | + "password": password, |
| 234 | + "grantType": "password", |
| 235 | + "verifyCode": "", |
| 236 | + }, |
| 237 | + ) |
| 238 | + except Exception as err: |
| 239 | + raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err |
| 240 | + |
| 241 | + @classmethod |
| 242 | + async def get_devices( |
| 243 | + cls, |
| 244 | + session: aiohttp.ClientSession, |
| 245 | + username: str, |
| 246 | + password: str, |
| 247 | + ) -> dict[str, SwitchbotModel]: |
| 248 | + """Get devices from SwitchBot API and return formatted MAC to model mapping.""" |
| 249 | + try: |
| 250 | + auth_result = await cls._get_auth_result(session, username, password) |
| 251 | + auth_headers = {"authorization": auth_result["access_token"]} |
| 252 | + except Exception as err: |
| 253 | + raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err |
| 254 | + |
| 255 | + userinfo = await cls._async_get_user_info(session, auth_headers) |
| 256 | + region = _extract_region(userinfo) |
| 257 | + |
| 258 | + try: |
| 259 | + device_info = await cls.api_request( |
| 260 | + session, |
| 261 | + f"wonderlabs.{region}", |
| 262 | + "wonder/device/v3/getdevice", |
| 263 | + { |
| 264 | + "required_type": "All", |
| 265 | + }, |
| 266 | + auth_headers, |
| 267 | + ) |
| 268 | + except Exception as err: |
| 269 | + raise SwitchbotAccountConnectionError( |
| 270 | + f"Failed to retrieve devices from SwitchBot Account: {err}" |
| 271 | + ) from err |
| 272 | + |
| 273 | + items: list[dict[str, Any]] = device_info["Items"] |
| 274 | + mac_to_model: dict[str, SwitchbotModel] = {} |
| 275 | + |
| 276 | + for item in items: |
| 277 | + if "device_mac" not in item: |
| 278 | + continue |
| 279 | + |
| 280 | + if ( |
| 281 | + "device_detail" not in item |
| 282 | + or "device_type" not in item["device_detail"] |
| 283 | + ): |
| 284 | + continue |
| 285 | + |
| 286 | + mac = item["device_mac"] |
| 287 | + model_name = item["device_detail"]["device_type"] |
| 288 | + |
| 289 | + # Format MAC to uppercase with colons |
| 290 | + formatted_mac = format_mac_upper(mac) |
| 291 | + |
| 292 | + # Map API model name to SwitchbotModel enum if possible |
| 293 | + if model_name in API_MODEL_TO_ENUM: |
| 294 | + model = API_MODEL_TO_ENUM[model_name] |
| 295 | + mac_to_model[formatted_mac] = model |
| 296 | + # Populate the cache |
| 297 | + populate_model_to_mac_cache(formatted_mac, model) |
| 298 | + else: |
| 299 | + # Log the full item payload for unknown models |
| 300 | + _LOGGER.debug( |
| 301 | + "Unknown model %s for device %s, full item: %s", |
| 302 | + model_name, |
| 303 | + formatted_mac, |
| 304 | + item, |
| 305 | + ) |
| 306 | + |
| 307 | + return mac_to_model |
| 308 | + |
167 | 309 | @classmethod |
168 | 310 | async def api_request( |
169 | 311 | cls, |
@@ -809,34 +951,13 @@ async def async_retrieve_encryption_key( |
809 | 951 | device_mac = device_mac.replace(":", "").replace("-", "").upper() |
810 | 952 |
|
811 | 953 | try: |
812 | | - auth_result = await cls.api_request( |
813 | | - session, |
814 | | - "account", |
815 | | - "account/api/v1/user/login", |
816 | | - { |
817 | | - "clientId": SWITCHBOT_APP_CLIENT_ID, |
818 | | - "username": username, |
819 | | - "password": password, |
820 | | - "grantType": "password", |
821 | | - "verifyCode": "", |
822 | | - }, |
823 | | - ) |
| 954 | + auth_result = await cls._get_auth_result(session, username, password) |
824 | 955 | auth_headers = {"authorization": auth_result["access_token"]} |
825 | 956 | except Exception as err: |
826 | 957 | raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err |
827 | 958 |
|
828 | | - try: |
829 | | - userinfo = await cls.api_request( |
830 | | - session, "account", "account/api/v1/user/userinfo", {}, auth_headers |
831 | | - ) |
832 | | - if "botRegion" in userinfo and userinfo["botRegion"] != "": |
833 | | - region = userinfo["botRegion"] |
834 | | - else: |
835 | | - region = "us" |
836 | | - except Exception as err: |
837 | | - raise SwitchbotAccountConnectionError( |
838 | | - f"Failed to retrieve SwitchBot Account user details: {err}" |
839 | | - ) from err |
| 959 | + userinfo = await cls._async_get_user_info(session, auth_headers) |
| 960 | + region = _extract_region(userinfo) |
840 | 961 |
|
841 | 962 | try: |
842 | 963 | device_info = await cls.api_request( |
@@ -1023,3 +1144,13 @@ def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> No |
1023 | 1144 | ) |
1024 | 1145 | if current_state != new_state: |
1025 | 1146 | create_background_task(self.update()) |
| 1147 | + |
| 1148 | + |
| 1149 | +async def fetch_cloud_devices( |
| 1150 | + session: aiohttp.ClientSession, |
| 1151 | + username: str, |
| 1152 | + password: str, |
| 1153 | +) -> dict[str, SwitchbotModel]: |
| 1154 | + """Fetch devices from SwitchBot API and return MAC to model mapping.""" |
| 1155 | + # Get devices from the API (which also populates the cache) |
| 1156 | + return await SwitchbotBaseDevice.get_devices(session, username, password) |
0 commit comments