diff --git a/switchbot/adv_parser.py b/switchbot/adv_parser.py index 738cb731..917612c1 100644 --- a/switchbot/adv_parser.py +++ b/switchbot/adv_parser.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from collections import defaultdict from collections.abc import Callable from functools import lru_cache from typing import Any, TypedDict @@ -78,6 +79,12 @@ class SwitchbotSupportedType(TypedDict): "func": process_wocontact, "manufacturer_id": 2409, }, + "D": { + "modelName": SwitchbotModel.CONTACT_SENSOR, + "modelFriendlyName": "Contact Sensor", + "func": process_wocontact, + "manufacturer_id": 2409, + }, "H": { "modelName": SwitchbotModel.BOT, "modelFriendlyName": "Bot", @@ -90,90 +97,180 @@ class SwitchbotSupportedType(TypedDict): "func": process_wopresence, "manufacturer_id": 2409, }, + "S": { + "modelName": SwitchbotModel.MOTION_SENSOR, + "modelFriendlyName": "Motion Sensor", + "func": process_wopresence, + "manufacturer_id": 2409, + }, "r": { "modelName": SwitchbotModel.LIGHT_STRIP, "modelFriendlyName": "Light Strip", "func": process_wostrip, "manufacturer_id": 2409, }, + "R": { + "modelName": SwitchbotModel.LIGHT_STRIP, + "modelFriendlyName": "Light Strip", + "func": process_wostrip, + "manufacturer_id": 2409, + }, "{": { "modelName": SwitchbotModel.CURTAIN, "modelFriendlyName": "Curtain 3", "func": process_wocurtain, "manufacturer_id": 2409, }, + "[": { + "modelName": SwitchbotModel.CURTAIN, + "modelFriendlyName": "Curtain 3", + "func": process_wocurtain, + "manufacturer_id": 2409, + }, "c": { "modelName": SwitchbotModel.CURTAIN, "modelFriendlyName": "Curtain", "func": process_wocurtain, "manufacturer_id": 2409, }, + "C": { + "modelName": SwitchbotModel.CURTAIN, + "modelFriendlyName": "Curtain", + "func": process_wocurtain, + "manufacturer_id": 2409, + }, "w": { "modelName": SwitchbotModel.IO_METER, "modelFriendlyName": "Indoor/Outdoor Meter", "func": process_wosensorth, "manufacturer_id": 2409, }, + "W": { + "modelName": SwitchbotModel.IO_METER, + "modelFriendlyName": "Indoor/Outdoor Meter", + "func": process_wosensorth, + "manufacturer_id": 2409, + }, "i": { "modelName": SwitchbotModel.METER, "modelFriendlyName": "Meter Plus", "func": process_wosensorth, "manufacturer_id": 2409, }, + "I": { + "modelName": SwitchbotModel.METER, + "modelFriendlyName": "Meter Plus", + "func": process_wosensorth, + "manufacturer_id": 2409, + }, "T": { "modelName": SwitchbotModel.METER, "modelFriendlyName": "Meter", "func": process_wosensorth, "manufacturer_id": 2409, }, + "t": { + "modelName": SwitchbotModel.METER, + "modelFriendlyName": "Meter", + "func": process_wosensorth, + "manufacturer_id": 2409, + }, "4": { "modelName": SwitchbotModel.METER_PRO, "modelFriendlyName": "Meter Pro", "func": process_wosensorth, "manufacturer_id": 2409, }, + b"\x14": { + "modelName": SwitchbotModel.METER_PRO, + "modelFriendlyName": "Meter Pro", + "func": process_wosensorth, + "manufacturer_id": 2409, + }, "5": { "modelName": SwitchbotModel.METER_PRO_C, "modelFriendlyName": "Meter Pro CO2", "func": process_wosensorth_c, "manufacturer_id": 2409, }, + b"\x15": { + "modelName": SwitchbotModel.METER_PRO_C, + "modelFriendlyName": "Meter Pro CO2", + "func": process_wosensorth_c, + "manufacturer_id": 2409, + }, "v": { "modelName": SwitchbotModel.HUB2, "modelFriendlyName": "Hub 2", "func": process_wohub2, "manufacturer_id": 2409, }, + "V": { + "modelName": SwitchbotModel.HUB2, + "modelFriendlyName": "Hub 2", + "func": process_wohub2, + "manufacturer_id": 2409, + }, "g": { "modelName": SwitchbotModel.PLUG_MINI, "modelFriendlyName": "Plug Mini", "func": process_woplugmini, "manufacturer_id": 2409, }, + "G": { + "modelName": SwitchbotModel.PLUG_MINI, + "modelFriendlyName": "Plug Mini", + "func": process_woplugmini, + "manufacturer_id": 2409, + }, "j": { "modelName": SwitchbotModel.PLUG_MINI, "modelFriendlyName": "Plug Mini (JP)", "func": process_woplugmini, "manufacturer_id": 2409, }, + "J": { + "modelName": SwitchbotModel.PLUG_MINI, + "modelFriendlyName": "Plug Mini (JP)", + "func": process_woplugmini, + "manufacturer_id": 2409, + }, "u": { "modelName": SwitchbotModel.COLOR_BULB, "modelFriendlyName": "Color Bulb", "func": process_color_bulb, "manufacturer_id": 2409, }, + "U": { + "modelName": SwitchbotModel.COLOR_BULB, + "modelFriendlyName": "Color Bulb", + "func": process_color_bulb, + "manufacturer_id": 2409, + }, "q": { "modelName": SwitchbotModel.CEILING_LIGHT, "modelFriendlyName": "Ceiling Light", "func": process_woceiling, "manufacturer_id": 2409, }, + "Q": { + "modelName": SwitchbotModel.CEILING_LIGHT, + "modelFriendlyName": "Ceiling Light", + "func": process_woceiling, + "manufacturer_id": 2409, + }, "n": { "modelName": SwitchbotModel.CEILING_LIGHT, "modelFriendlyName": "Ceiling Light Pro", "func": process_woceiling, "manufacturer_id": 2409, }, + "N": { + "modelName": SwitchbotModel.CEILING_LIGHT, + "modelFriendlyName": "Ceiling Light Pro", + "func": process_woceiling, + "manufacturer_id": 2409, + }, "e": { "modelName": SwitchbotModel.HUMIDIFIER, "modelFriendlyName": "Humidifier", @@ -181,228 +278,452 @@ class SwitchbotSupportedType(TypedDict): "manufacturer_id": 741, "manufacturer_data_length": 6, }, + "E": { + "modelName": SwitchbotModel.HUMIDIFIER, + "modelFriendlyName": "Humidifier", + "func": process_wohumidifier, + "manufacturer_id": 741, + "manufacturer_data_length": 6, + }, "#": { "modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER, "modelFriendlyName": "Evaporative Humidifier", "func": process_evaporative_humidifier, "manufacturer_id": 2409, }, + b"\x03": { + "modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER, + "modelFriendlyName": "Evaporative Humidifier", + "func": process_evaporative_humidifier, + "manufacturer_id": 2409, + }, "o": { "modelName": SwitchbotModel.LOCK, "modelFriendlyName": "Lock", "func": process_wolock, "manufacturer_id": 2409, }, + "O": { + "modelName": SwitchbotModel.LOCK, + "modelFriendlyName": "Lock", + "func": process_wolock, + "manufacturer_id": 2409, + }, "$": { "modelName": SwitchbotModel.LOCK_PRO, "modelFriendlyName": "Lock Pro", "func": process_wolock_pro, "manufacturer_id": 2409, }, + b"\x04": { + "modelName": SwitchbotModel.LOCK_PRO, + "modelFriendlyName": "Lock Pro", + "func": process_wolock_pro, + "manufacturer_id": 2409, + }, "x": { "modelName": SwitchbotModel.BLIND_TILT, "modelFriendlyName": "Blind Tilt", "func": process_woblindtilt, "manufacturer_id": 2409, }, + "X": { + "modelName": SwitchbotModel.BLIND_TILT, + "modelFriendlyName": "Blind Tilt", + "func": process_woblindtilt, + "manufacturer_id": 2409, + }, "&": { "modelName": SwitchbotModel.LEAK, "modelFriendlyName": "Leak Detector", "func": process_leak, "manufacturer_id": 2409, }, + b"\x06": { + "modelName": SwitchbotModel.LEAK, + "modelFriendlyName": "Leak Detector", + "func": process_leak, + "manufacturer_id": 2409, + }, "y": { "modelName": SwitchbotModel.KEYPAD, "modelFriendlyName": "Keypad", "func": process_wokeypad, "manufacturer_id": 2409, }, + "Y": { + "modelName": SwitchbotModel.KEYPAD, + "modelFriendlyName": "Keypad", + "func": process_wokeypad, + "manufacturer_id": 2409, + }, "<": { "modelName": SwitchbotModel.RELAY_SWITCH_1PM, "modelFriendlyName": "Relay Switch 1PM", "func": process_relay_switch_1pm, "manufacturer_id": 2409, }, + b"\x1c": { + "modelName": SwitchbotModel.RELAY_SWITCH_1PM, + "modelFriendlyName": "Relay Switch 1PM", + "func": process_relay_switch_1pm, + "manufacturer_id": 2409, + }, ";": { "modelName": SwitchbotModel.RELAY_SWITCH_1, "modelFriendlyName": "Relay Switch 1", "func": process_relay_switch_common_data, "manufacturer_id": 2409, }, + b"\x1b": { + "modelName": SwitchbotModel.RELAY_SWITCH_1, + "modelFriendlyName": "Relay Switch 1", + "func": process_relay_switch_common_data, + "manufacturer_id": 2409, + }, "b": { "modelName": SwitchbotModel.REMOTE, "modelFriendlyName": "Remote", "func": process_woremote, "manufacturer_id": 89, }, + "B": { + "modelName": SwitchbotModel.REMOTE, + "modelFriendlyName": "Remote", + "func": process_woremote, + "manufacturer_id": 89, + }, ",": { "modelName": SwitchbotModel.ROLLER_SHADE, "modelFriendlyName": "Roller Shade", "func": process_worollershade, "manufacturer_id": 2409, }, + b"\x0c": { + "modelName": SwitchbotModel.ROLLER_SHADE, + "modelFriendlyName": "Roller Shade", + "func": process_worollershade, + "manufacturer_id": 2409, + }, "%": { "modelName": SwitchbotModel.HUBMINI_MATTER, "modelFriendlyName": "HubMini Matter", "func": process_hubmini_matter, "manufacturer_id": 2409, }, + b"\x05": { + "modelName": SwitchbotModel.HUBMINI_MATTER, + "modelFriendlyName": "HubMini Matter", + "func": process_hubmini_matter, + "manufacturer_id": 2409, + }, "~": { "modelName": SwitchbotModel.CIRCULATOR_FAN, "modelFriendlyName": "Circulator Fan", "func": process_fan, "manufacturer_id": 2409, }, + "^": { + "modelName": SwitchbotModel.CIRCULATOR_FAN, + "modelFriendlyName": "Circulator Fan", + "func": process_fan, + "manufacturer_id": 2409, + }, ".": { "modelName": SwitchbotModel.K20_VACUUM, "modelFriendlyName": "K20 Vacuum", "func": process_vacuum, "manufacturer_id": 2409, }, + b"\x0f": { + "modelName": SwitchbotModel.K20_VACUUM, + "modelFriendlyName": "K20 Vacuum", + "func": process_vacuum, + "manufacturer_id": 2409, + }, "z": { "modelName": SwitchbotModel.S10_VACUUM, "modelFriendlyName": "S10 Vacuum", "func": process_vacuum, "manufacturer_id": 2409, }, + "Z": { + "modelName": SwitchbotModel.S10_VACUUM, + "modelFriendlyName": "S10 Vacuum", + "func": process_vacuum, + "manufacturer_id": 2409, + }, "3": { "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM, "modelFriendlyName": "K10+ Pro Combo Vacuum", "func": process_vacuum, "manufacturer_id": 2409, }, + b"\x13": { + "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM, + "modelFriendlyName": "K10+ Pro Combo Vacuum", + "func": process_vacuum, + "manufacturer_id": 2409, + }, "}": { "modelName": SwitchbotModel.K10_VACUUM, "modelFriendlyName": "K10+ Vacuum", "func": process_vacuum_k, "manufacturer_id": 2409, }, + "]": { + "modelName": SwitchbotModel.K10_VACUUM, + "modelFriendlyName": "K10+ Vacuum", + "func": process_vacuum_k, + "manufacturer_id": 2409, + }, "(": { "modelName": SwitchbotModel.K10_PRO_VACUUM, "modelFriendlyName": "K10+ Pro Vacuum", "func": process_vacuum_k, "manufacturer_id": 2409, }, + b"\x08": { + "modelName": SwitchbotModel.K10_PRO_VACUUM, + "modelFriendlyName": "K10+ Pro Vacuum", + "func": process_vacuum_k, + "manufacturer_id": 2409, + }, "*": { "modelName": SwitchbotModel.AIR_PURIFIER, "modelFriendlyName": "Air Purifier", "func": process_air_purifier, "manufacturer_id": 2409, }, + b"\x0a": { + "modelName": SwitchbotModel.AIR_PURIFIER, + "modelFriendlyName": "Air Purifier", + "func": process_air_purifier, + "manufacturer_id": 2409, + }, "+": { "modelName": SwitchbotModel.AIR_PURIFIER, "modelFriendlyName": "Air Purifier", "func": process_air_purifier, "manufacturer_id": 2409, }, + b"\x0b": { + "modelName": SwitchbotModel.AIR_PURIFIER, + "modelFriendlyName": "Air Purifier", + "func": process_air_purifier, + "manufacturer_id": 2409, + }, "7": { "modelName": SwitchbotModel.AIR_PURIFIER_TABLE, "modelFriendlyName": "Air Purifier Table", "func": process_air_purifier, "manufacturer_id": 2409, }, + b"\x17": { + "modelName": SwitchbotModel.AIR_PURIFIER_TABLE, + "modelFriendlyName": "Air Purifier Table", + "func": process_air_purifier, + "manufacturer_id": 2409, + }, "8": { "modelName": SwitchbotModel.AIR_PURIFIER_TABLE, "modelFriendlyName": "Air Purifier Table", "func": process_air_purifier, "manufacturer_id": 2409, }, + b"\x18": { + "modelName": SwitchbotModel.AIR_PURIFIER_TABLE, + "modelFriendlyName": "Air Purifier Table", + "func": process_air_purifier, + "manufacturer_id": 2409, + }, b"\x00\x10\xb9\x40": { "modelName": SwitchbotModel.HUB3, "modelFriendlyName": "Hub3", "func": process_hub3, "manufacturer_id": 2409, }, + b"\x01\x10\xb9\x40": { + "modelName": SwitchbotModel.HUB3, + "modelFriendlyName": "Hub3", + "func": process_hub3, + "manufacturer_id": 2409, + }, "-": { "modelName": SwitchbotModel.LOCK_LITE, "modelFriendlyName": "Lock Lite", "func": process_locklite, "manufacturer_id": 2409, }, + b"\x0d": { + "modelName": SwitchbotModel.LOCK_LITE, + "modelFriendlyName": "Lock Lite", + "func": process_locklite, + "manufacturer_id": 2409, + }, b"\x00\x10\xa5\xb8": { "modelName": SwitchbotModel.LOCK_ULTRA, "modelFriendlyName": "Lock Ultra", "func": process_lock2, "manufacturer_id": 2409, }, + b"\x01\x10\xa5\xb8": { + "modelName": SwitchbotModel.LOCK_ULTRA, + "modelFriendlyName": "Lock Ultra", + "func": process_lock2, + "manufacturer_id": 2409, + }, ">": { "modelName": SwitchbotModel.GARAGE_DOOR_OPENER, "modelFriendlyName": "Garage Door Opener", "func": process_garage_door_opener, "manufacturer_id": 2409, }, + b"\x1e": { + "modelName": SwitchbotModel.GARAGE_DOOR_OPENER, + "modelFriendlyName": "Garage Door Opener", + "func": process_garage_door_opener, + "manufacturer_id": 2409, + }, "=": { "modelName": SwitchbotModel.RELAY_SWITCH_2PM, "modelFriendlyName": "Relay Switch 2PM", "func": process_relay_switch_2pm, "manufacturer_id": 2409, }, + b"\x1d": { + "modelName": SwitchbotModel.RELAY_SWITCH_2PM, + "modelFriendlyName": "Relay Switch 2PM", + "func": process_relay_switch_2pm, + "manufacturer_id": 2409, + }, b"\x00\x10\xd0\xb0": { "modelName": SwitchbotModel.FLOOR_LAMP, "modelFriendlyName": "Floor Lamp", "func": process_light, "manufacturer_id": 2409, }, + b"\x01\x10\xd0\xb0": { + "modelName": SwitchbotModel.FLOOR_LAMP, + "modelFriendlyName": "Floor Lamp", + "func": process_light, + "manufacturer_id": 2409, + }, b"\x00\x10\xd0\xb1": { "modelName": SwitchbotModel.STRIP_LIGHT_3, "modelFriendlyName": "Strip Light 3", "func": process_light, "manufacturer_id": 2409, }, + b"\x01\x10\xd0\xb1": { + "modelName": SwitchbotModel.STRIP_LIGHT_3, + "modelFriendlyName": "Strip Light 3", + "func": process_light, + "manufacturer_id": 2409, + }, "?": { "modelName": SwitchbotModel.PLUG_MINI_EU, "modelFriendlyName": "Plug Mini (EU)", "func": process_relay_switch_1pm, "manufacturer_id": 2409, }, + b"\x1f": { + "modelName": SwitchbotModel.PLUG_MINI_EU, + "modelFriendlyName": "Plug Mini (EU)", + "func": process_relay_switch_1pm, + "manufacturer_id": 2409, + }, b"\x00\x10\xd0\xb3": { "modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT, "modelFriendlyName": "RGBICWW Strip Light", "func": process_rgbic_light, "manufacturer_id": 2409, }, + b"\x01\x10\xd0\xb3": { + "modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT, + "modelFriendlyName": "RGBICWW Strip Light", + "func": process_rgbic_light, + "manufacturer_id": 2409, + }, b"\x00\x10\xd0\xb4": { "modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP, "modelFriendlyName": "RGBICWW Floor Lamp", "func": process_rgbic_light, "manufacturer_id": 2409, }, + b"\x01\x10\xd0\xb4": { + "modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP, + "modelFriendlyName": "RGBICWW Floor Lamp", + "func": process_rgbic_light, + "manufacturer_id": 2409, + }, b"\x00\x10\xfb\xa8": { "modelName": SwitchbotModel.K11_VACUUM, "modelFriendlyName": "K11+ Vacuum", "func": process_vacuum, "manufacturer_id": 2409, }, + b"\x01\x10\xfb\xa8": { + "modelName": SwitchbotModel.K11_VACUUM, + "modelFriendlyName": "K11+ Vacuum", + "func": process_vacuum, + "manufacturer_id": 2409, + }, b"\x00\x10\xf3\xd8": { "modelName": SwitchbotModel.CLIMATE_PANEL, "modelFriendlyName": "Climate Panel", "func": process_climate_panel, "manufacturer_id": 2409, }, + b"\x01\x10\xf3\xd8": { + "modelName": SwitchbotModel.CLIMATE_PANEL, + "modelFriendlyName": "Climate Panel", + "func": process_climate_panel, + "manufacturer_id": 2409, + }, b"\x00\x116@": { "modelName": SwitchbotModel.SMART_THERMOSTAT_RADIATOR, "modelFriendlyName": "Smart Thermostat Radiator", "func": process_smart_thermostat_radiator, "manufacturer_id": 2409, }, + b"\x01\x116@": { + "modelName": SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + "modelFriendlyName": "Smart Thermostat Radiator", + "func": process_smart_thermostat_radiator, + "manufacturer_id": 2409, + }, b"\x00\x10\xe0P": { "modelName": SwitchbotModel.S20_VACUUM, "modelFriendlyName": "S20 Vacuum", "func": process_vacuum, "manufacturer_id": 2409, }, + b"\x01\x10\xe0P": { + "modelName": SwitchbotModel.S20_VACUUM, + "modelFriendlyName": "S20 Vacuum", + "func": process_vacuum, + "manufacturer_id": 2409, + }, b"\x00\x10\xcc\xc8": { "modelName": SwitchbotModel.PRESENCE_SENSOR, "modelFriendlyName": "Presence Sensor", "func": process_presence_sensor, "manufacturer_id": 2409, }, + b"\x01\x10\xcc\xc8": { + "modelName": SwitchbotModel.PRESENCE_SENSOR, + "modelFriendlyName": "Presence Sensor", + "func": process_presence_sensor, + "manufacturer_id": 2409, + }, } -_SWITCHBOT_MODEL_TO_CHAR = { - model_data["modelName"]: model_chr - for model_chr, model_data in SUPPORTED_TYPES.items() -} +_SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict( + list +) +for model_chr, model_data in SUPPORTED_TYPES.items(): + _SWITCHBOT_MODEL_TO_CHAR[model_data["modelName"]].append(model_chr) MODELS_BY_MANUFACTURER_DATA: dict[int, list[tuple[str, SwitchbotSupportedType]]] = { mfr_id: [] for mfr_id in MFR_DATA_ORDER @@ -461,34 +782,57 @@ def parse_advertisement_data( ) -@lru_cache(maxsize=128) -def _parse_data( - _service_data: bytes | None, - _mfr_data: bytes | None, - _mfr_id: int | None = None, - _switchbot_model: SwitchbotModel | None = None, -) -> dict[str, Any] | None: - """Parse advertisement data.""" - _model = chr(_service_data[0] & 0b01111111) if _service_data else None +def _find_model_from_service_data(_service_data: bytes) -> str | bytes | None: + """Find model from service data.""" + char_model = chr(_service_data[0] & 0b01111111) + if char_model in SUPPORTED_TYPES: + return char_model - if _switchbot_model and _switchbot_model in _SWITCHBOT_MODEL_TO_CHAR: - _model = _SWITCHBOT_MODEL_TO_CHAR[_switchbot_model] + byte_model = bytes([_service_data[0] & 0b01111111]) + if byte_model in SUPPORTED_TYPES: + return byte_model - if not _model and _mfr_id and _mfr_id in MODELS_BY_MANUFACTURER_DATA: - for model_chr, model_data in MODELS_BY_MANUFACTURER_DATA[_mfr_id]: - if model_data.get("manufacturer_data_length") == len(_mfr_data): - _model = model_chr - break + return None - if _service_data and len(_service_data) > 5: - for s in (_service_data[-4:], _service_data[-5:-1]): - if s in SUPPORTED_TYPES: - _model = s - break - if not _model: +def _find_model_from_switchbot_model( + _switchbot_model: SwitchbotModel, +) -> str | bytes | None: + """Find model from switchbot model.""" + if _switchbot_model in _SWITCHBOT_MODEL_TO_CHAR: + return _SWITCHBOT_MODEL_TO_CHAR[_switchbot_model][0] + return None + + +def _find_model_from_manufacturer_data( + _mfr_id: int, _mfr_data: bytes | None +) -> str | bytes | None: + """Find model from manufacturer data.""" + if _mfr_id not in MODELS_BY_MANUFACTURER_DATA or _mfr_data is None: return None + for model_chr, model_data in MODELS_BY_MANUFACTURER_DATA[_mfr_id]: + expected_length = model_data.get("manufacturer_data_length") + if expected_length is not None and expected_length == len(_mfr_data): + return model_chr + return None + + +def _find_model_from_service_data_suffix(_service_data: bytes) -> str | bytes | None: + """Find model from service data suffix.""" + if len(_service_data) <= 5: + return None + + for s in (_service_data[-4:], _service_data[-5:-1]): + if s in SUPPORTED_TYPES: + return s + return None + + +def build_advertisement_data( + _model: str | bytes, _service_data: bytes | None, _mfr_data: bytes | None +) -> dict[str, Any]: + """Build advertisement data dictionary.""" _isEncrypted = bool(_service_data[0] & 0b10000000) if _service_data else False data = { "rawAdvData": _service_data, @@ -512,6 +856,34 @@ def _parse_data( return data +@lru_cache(maxsize=128) +def _parse_data( + _service_data: bytes | None, + _mfr_data: bytes | None, + _mfr_id: int | None = None, + _switchbot_model: SwitchbotModel | None = None, +) -> dict[str, Any] | None: + """Parse advertisement data.""" + _model = None + + if _service_data: + _model = _find_model_from_service_data(_service_data) + + if not _model and _switchbot_model: + _model = _find_model_from_switchbot_model(_switchbot_model) + + if not _model and _mfr_id: + _model = _find_model_from_manufacturer_data(_mfr_id, _mfr_data) + + if not _model and _service_data: + _model = _find_model_from_service_data_suffix(_service_data) + + if not _model: + return None + + return build_advertisement_data(_model, _service_data, _mfr_data) + + def populate_model_to_mac_cache(mac: str, model: SwitchbotModel) -> None: """Populate the model to MAC address cache.""" _MODEL_TO_MAC_CACHE[mac] = model diff --git a/switchbot/devices/device.py b/switchbot/devices/device.py index 14abf95a..3dadbeec 100644 --- a/switchbot/devices/device.py +++ b/switchbot/devices/device.py @@ -54,27 +54,56 @@ def _extract_region(userinfo: dict[str, Any]) -> str: API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = { "WoHand": SwitchbotModel.BOT, "WoCurtain": SwitchbotModel.CURTAIN, + "WoCurtain3": SwitchbotModel.CURTAIN, # Curtain3 "WoHumi": SwitchbotModel.HUMIDIFIER, + "WoHumi2": SwitchbotModel.EVAPORATIVE_HUMIDIFIER, "WoPlug": SwitchbotModel.PLUG_MINI, "WoPlugUS": SwitchbotModel.PLUG_MINI, "WoContact": SwitchbotModel.CONTACT_SENSOR, "WoStrip": SwitchbotModel.LIGHT_STRIP, - "WoSensorTH": SwitchbotModel.METER, "WoMeter": SwitchbotModel.METER, - "WoMeterPlus": SwitchbotModel.METER_PRO, + "WoMeterPlus": SwitchbotModel.METER, # Meter Plus "WoPresence": SwitchbotModel.MOTION_SENSOR, "WoBulb": SwitchbotModel.COLOR_BULB, "WoCeiling": SwitchbotModel.CEILING_LIGHT, + "WoCeilingPro": SwitchbotModel.CEILING_LIGHT, # Ceiling Light Pro "WoLock": SwitchbotModel.LOCK, + "WoLockPro": SwitchbotModel.LOCK_PRO, + "WoLockLite": SwitchbotModel.LOCK_LITE, "WoBlindTilt": SwitchbotModel.BLIND_TILT, "WoIOSensor": SwitchbotModel.IO_METER, # Outdoor Meter "WoButton": SwitchbotModel.REMOTE, # Remote button "WoLinkMini": SwitchbotModel.HUBMINI_MATTER, # Hub Mini + "WoFan2": SwitchbotModel.CIRCULATOR_FAN, + "WoHub2": SwitchbotModel.HUB2, + "WoRollerShade": SwitchbotModel.ROLLER_SHADE, + "WoAirPurifierJP": SwitchbotModel.AIR_PURIFIER, + "WoAirPurifierUS": SwitchbotModel.AIR_PURIFIER, + "WoAirPurifierJPPro": SwitchbotModel.AIR_PURIFIER_TABLE, + "WoAirPurifierUSPro": SwitchbotModel.AIR_PURIFIER_TABLE, + "WoSweeperMini": SwitchbotModel.K10_VACUUM, + "WoSweeperMiniPro": SwitchbotModel.K10_PRO_VACUUM, + "91AgWZ1n": SwitchbotModel.K10_PRO_COMBO_VACUUM, + "W1113000": SwitchbotModel.K11_VACUUM, + "sH5cQeLF": SwitchbotModel.K20_VACUUM, + "WoSweeperOrigin": SwitchbotModel.S10_VACUUM, + "W1106000": SwitchbotModel.S20_VACUUM, + "W1083000": SwitchbotModel.RELAY_SWITCH_1PM, + "W1083001": SwitchbotModel.RELAY_SWITCH_2PM, "W1083002": SwitchbotModel.RELAY_SWITCH_1, # Relay Switch 1 "W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant) - "W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3 - "W1106000": SwitchbotModel.S20_VACUUM, + "W1079001": SwitchbotModel.METER_PRO_C, "W1101000": SwitchbotModel.PRESENCE_SENSOR, + "W1091000": SwitchbotModel.LOCK_ULTRA, + "W1096000": SwitchbotModel.HUB3, + "W1083003": SwitchbotModel.GARAGE_DOOR_OPENER, + "W1102000": SwitchbotModel.FLOOR_LAMP, + "W1102001": SwitchbotModel.STRIP_LIGHT_3, + "W1102003": SwitchbotModel.RGBICWW_STRIP_LIGHT, + "W1102004": SwitchbotModel.RGBICWW_FLOOR_LAMP, + "W1104000": SwitchbotModel.PLUG_MINI_EU, + "W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR, + "W1111000": SwitchbotModel.CLIMATE_PANEL, } REQ_HEADER = "570f" diff --git a/tests/test_adv_parser.py b/tests/test_adv_parser.py index 406ef80d..6f6b3c84 100644 --- a/tests/test_adv_parser.py +++ b/tests/test_adv_parser.py @@ -119,8 +119,8 @@ def test_parse_advertisement_data_curtain_passive(): "deviceChain": 1, }, "isEncrypted": False, - "model": "c", - "modelFriendlyName": "Curtain", + "model": "{", + "modelFriendlyName": "Curtain 3", "modelName": SwitchbotModel.CURTAIN, }, device=ble_device, @@ -151,8 +151,8 @@ def test_parse_advertisement_data_curtain_passive_12_bytes(): "deviceChain": 1, }, "isEncrypted": False, - "model": "c", - "modelFriendlyName": "Curtain", + "model": "{", + "modelFriendlyName": "Curtain 3", "modelName": SwitchbotModel.CURTAIN, }, device=ble_device, @@ -422,8 +422,8 @@ def test_parse_advertisement_data_curtain3_passive(): "deviceChain": 1, }, "isEncrypted": False, - "model": "c", - "modelFriendlyName": "Curtain", + "model": "{", + "modelFriendlyName": "Curtain 3", "modelName": SwitchbotModel.CURTAIN, }, device=ble_device, @@ -901,8 +901,8 @@ def test_wosensor_passive_only(): "temperature": 24.6, }, "isEncrypted": False, - "model": "T", - "modelFriendlyName": "Meter", + "model": "i", + "modelFriendlyName": "Meter Plus", "modelName": SwitchbotModel.METER, "rawAdvData": None, }, @@ -2508,7 +2508,7 @@ def test_air_purifier_passive() -> None: "sequence_number": 161, }, "isEncrypted": False, - "model": "+", + "model": "*", "modelFriendlyName": "Air Purifier", "modelName": SwitchbotModel.AIR_PURIFIER, }, @@ -3518,6 +3518,20 @@ def test_humidifer_with_empty_data() -> None: "Presence Sensor", SwitchbotModel.PRESENCE_SENSOR, ), + AdvTestCase( + b"\xb0\xe9\xfeR\xdd\x84\x06d\x08\x97,\x00\x05", + b"\x14\x00d", + { + "battery": 100, + "fahrenheit": False, + "humidity": 44, + "temp": {"c": 23.8, "f": 74.84}, + "temperature": 23.8, + }, + b"\x14", + "Meter Pro", + SwitchbotModel.METER_PRO, + ), ], ) def test_adv_active(test_case: AdvTestCase) -> None: @@ -4086,8 +4100,43 @@ def test_parse_advertisement_with_mac_cache_curtain() -> None: result_with_cache = parse_advertisement_data(ble_device, adv_data) assert result_with_cache is not None assert result_with_cache.data["modelName"] == SwitchbotModel.CURTAIN - assert result_with_cache.data["modelFriendlyName"] == "Curtain" + assert result_with_cache.data["modelFriendlyName"] == "Curtain 3" assert result_with_cache.active is False # Clean up _MODEL_TO_MAC_CACHE.clear() + + +@pytest.mark.parametrize( + ("manufacturer_data", "service_data", "model"), + [ + (b"\xff\xff\xff\xff", b"\xff\xff\xff\xff", None), + (b"\xff\xff\xff\xff", b"\xff\xff\xff\xff", "F"), + (b"\xff\xff\xff\xff\xff\xff\xff", b"\xff\xff\xff\xff\xff\xff\xff\xff", None), + (None, None, None), + ], +) +def test_with_invalid_advertisement(manufacturer_data, service_data, model) -> None: + """Test with invalid advertisement data.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + adv_data = generate_advertisement_data( + manufacturer_data={2409: manufacturer_data}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": service_data}, + rssi=-97, + ) + result = parse_advertisement_data(ble_device, adv_data, model) + assert result is None + + +def test_with_special_manufacturer_data_length() -> None: + """Test with special manufacturer data length.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + adv_data = generate_advertisement_data( + manufacturer_data={741: b"\xacg\xb2\xcd\xfa\xbe"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\xff\x80\x00\xf9\x80Bc\x00" + }, + rssi=-97, + ) + result = parse_advertisement_data(ble_device, adv_data) + assert result.data["modelName"] == SwitchbotModel.HUMIDIFIER