Преглед на файлове

Added device broadcast type and cloud type (#418)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Retha Runolfsson преди 1 ден
родител
ревизия
748dfe8c57
променени са 3 файла, в които са добавени 490 реда и са изтрити 40 реда
  1. 398 26
      switchbot/adv_parser.py
  2. 33 4
      switchbot/devices/device.py
  3. 59 10
      tests/test_adv_parser.py

+ 398 - 26
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 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "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 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "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 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "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

+ 33 - 4
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"

+ 59 - 10
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