PySwitchbot 0.68.4__tar.gz → 0.70.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
  4. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/setup.py +1 -1
  5. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/__init__.py +8 -1
  6. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parser.py +31 -1
  7. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/light_strip.py +12 -3
  8. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/__init__.py +3 -0
  9. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/light.py +11 -0
  10. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/ceiling_light.py +3 -1
  11. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/device.py +155 -24
  12. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/light_strip.py +167 -1
  13. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/relay_switch.py +1 -0
  14. pyswitchbot-0.70.0/switchbot/utils.py +24 -0
  15. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_adv_parser.py +194 -1
  16. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_ceiling_light.py +20 -1
  17. pyswitchbot-0.70.0/tests/test_device.py +379 -0
  18. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_relay_switch.py +1 -0
  19. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_strip_light.py +104 -57
  20. pyswitchbot-0.70.0/tests/test_utils.py +41 -0
  21. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/LICENSE +0 -0
  22. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/MANIFEST.in +0 -0
  23. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  24. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/requires.txt +0 -0
  25. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  26. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/README.md +0 -0
  27. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/pyproject.toml +0 -0
  28. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/setup.cfg +0 -0
  29. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/__init__.py +0 -0
  30. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  31. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  32. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/bot.py +0 -0
  33. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/bulb.py +0 -0
  34. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  35. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/contact.py +0 -0
  36. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/curtain.py +0 -0
  37. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/fan.py +0 -0
  38. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hub2.py +0 -0
  39. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hub3.py +0 -0
  40. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  41. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/humidifier.py +0 -0
  42. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/keypad.py +0 -0
  43. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/leak.py +0 -0
  44. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/lock.py +0 -0
  45. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/meter.py +0 -0
  46. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/motion.py +0 -0
  47. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/plug.py +0 -0
  48. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  49. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/remote.py +0 -0
  50. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  51. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/adv_parsers/vacuum.py +0 -0
  52. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/api_config.py +0 -0
  53. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/air_purifier.py +0 -0
  54. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/evaporative_humidifier.py +0 -0
  55. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/fan.py +0 -0
  56. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/hub2.py +0 -0
  57. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/hub3.py +0 -0
  58. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/const/lock.py +0 -0
  59. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/__init__.py +0 -0
  60. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/air_purifier.py +0 -0
  61. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/base_cover.py +0 -0
  62. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/base_light.py +0 -0
  63. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/blind_tilt.py +0 -0
  64. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/bot.py +0 -0
  65. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/bulb.py +0 -0
  66. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/contact.py +0 -0
  67. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/curtain.py +0 -0
  68. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  69. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/fan.py +0 -0
  70. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/humidifier.py +0 -0
  71. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/keypad.py +0 -0
  72. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/lock.py +0 -0
  73. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/meter.py +0 -0
  74. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/motion.py +0 -0
  75. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/plug.py +0 -0
  76. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/roller_shade.py +0 -0
  77. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/devices/vacuum.py +0 -0
  78. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/discovery.py +0 -0
  79. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/enum.py +0 -0
  80. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/helpers.py +0 -0
  81. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/switchbot/models.py +0 -0
  82. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_air_purifier.py +0 -0
  83. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_base_cover.py +0 -0
  84. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_blind_tilt.py +0 -0
  85. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_bulb.py +0 -0
  86. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_colormode_imports.py +0 -0
  87. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_curtain.py +0 -0
  88. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_encrypted_device.py +0 -0
  89. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_evaporative_humidifier.py +0 -0
  90. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_fan.py +0 -0
  91. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_helpers.py +0 -0
  92. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_hub2.py +0 -0
  93. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_hub3.py +0 -0
  94. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_lock.py +0 -0
  95. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_roller_shade.py +0 -0
  96. {pyswitchbot-0.68.4 → pyswitchbot-0.70.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.68.4
3
+ Version: 0.70.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.68.4
3
+ Version: 0.70.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -15,6 +15,7 @@ switchbot/discovery.py
15
15
  switchbot/enum.py
16
16
  switchbot/helpers.py
17
17
  switchbot/models.py
18
+ switchbot/utils.py
18
19
  switchbot/adv_parsers/__init__.py
19
20
  switchbot/adv_parsers/air_purifier.py
20
21
  switchbot/adv_parsers/blind_tilt.py
@@ -78,6 +79,7 @@ tests/test_bulb.py
78
79
  tests/test_ceiling_light.py
79
80
  tests/test_colormode_imports.py
80
81
  tests/test_curtain.py
82
+ tests/test_device.py
81
83
  tests/test_encrypted_device.py
82
84
  tests/test_evaporative_humidifier.py
83
85
  tests/test_fan.py
@@ -88,4 +90,5 @@ tests/test_lock.py
88
90
  tests/test_relay_switch.py
89
91
  tests/test_roller_shade.py
90
92
  tests/test_strip_light.py
93
+ tests/test_utils.py
91
94
  tests/test_vacuum.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.68.4",
23
+ version="0.70.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -36,11 +36,16 @@ from .devices.device import (
36
36
  SwitchbotDevice,
37
37
  SwitchbotEncryptedDevice,
38
38
  SwitchbotOperationError,
39
+ fetch_cloud_devices,
39
40
  )
40
41
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
41
42
  from .devices.fan import SwitchbotFan
42
43
  from .devices.humidifier import SwitchbotHumidifier
43
- from .devices.light_strip import SwitchbotLightStrip, SwitchbotStripLight3
44
+ from .devices.light_strip import (
45
+ SwitchbotLightStrip,
46
+ SwitchbotRgbicLight,
47
+ SwitchbotStripLight3,
48
+ )
44
49
  from .devices.lock import SwitchbotLock
45
50
  from .devices.plug import SwitchbotPlugMini
46
51
  from .devices.relay_switch import (
@@ -92,6 +97,7 @@ __all__ = [
92
97
  "SwitchbotPlugMini",
93
98
  "SwitchbotRelaySwitch",
94
99
  "SwitchbotRelaySwitch2PM",
100
+ "SwitchbotRgbicLight",
95
101
  "SwitchbotRollerShade",
96
102
  "SwitchbotStripLight3",
97
103
  "SwitchbotSupportedType",
@@ -99,6 +105,7 @@ __all__ = [
99
105
  "SwitchbotVacuum",
100
106
  "close_stale_connections",
101
107
  "close_stale_connections_by_address",
108
+ "fetch_cloud_devices",
102
109
  "get_device",
103
110
  "parse_advertisement_data",
104
111
  ]
@@ -24,7 +24,7 @@ from .adv_parsers.hubmini_matter import process_hubmini_matter
24
24
  from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
25
25
  from .adv_parsers.keypad import process_wokeypad
26
26
  from .adv_parsers.leak import process_leak
27
- from .adv_parsers.light_strip import process_light, process_wostrip
27
+ from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
28
28
  from .adv_parsers.lock import (
29
29
  process_lock2,
30
30
  process_locklite,
@@ -45,6 +45,7 @@ from .adv_parsers.roller_shade import process_worollershade
45
45
  from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
46
46
  from .const import SwitchbotModel
47
47
  from .models import SwitchBotAdvertisement
48
+ from .utils import format_mac_upper
48
49
 
49
50
  _LOGGER = logging.getLogger(__name__)
50
51
 
@@ -54,6 +55,8 @@ SERVICE_DATA_ORDER = (
54
55
  )
55
56
  MFR_DATA_ORDER = (2409, 741, 89)
56
57
 
58
+ _MODEL_TO_MAC_CACHE: dict[str, SwitchbotModel] = {}
59
+
57
60
 
58
61
  class SwitchbotSupportedType(TypedDict):
59
62
  """Supported type of Switchbot."""
@@ -343,6 +346,24 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
343
346
  "func": process_light,
344
347
  "manufacturer_id": 2409,
345
348
  },
349
+ "?": {
350
+ "modelName": SwitchbotModel.PLUG_MINI_EU,
351
+ "modelFriendlyName": "Plug Mini (EU)",
352
+ "func": process_relay_switch_1pm,
353
+ "manufacturer_id": 2409,
354
+ },
355
+ b"\x00\x10\xd0\xb3": {
356
+ "modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT,
357
+ "modelFriendlyName": "RGBICWW Strip Light",
358
+ "func": process_rgbic_light,
359
+ "manufacturer_id": 2409,
360
+ },
361
+ b"\x00\x10\xd0\xb4": {
362
+ "modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP,
363
+ "modelFriendlyName": "RGBICWW Floor Lamp",
364
+ "func": process_rgbic_light,
365
+ "manufacturer_id": 2409,
366
+ },
346
367
  }
347
368
 
348
369
  _SWITCHBOT_MODEL_TO_CHAR = {
@@ -365,6 +386,10 @@ def parse_advertisement_data(
365
386
  model: SwitchbotModel | None = None,
366
387
  ) -> SwitchBotAdvertisement | None:
367
388
  """Parse advertisement data."""
389
+ upper_mac = format_mac_upper(device.address)
390
+ if model is None and upper_mac in _MODEL_TO_MAC_CACHE:
391
+ model = _MODEL_TO_MAC_CACHE[upper_mac]
392
+
368
393
  service_data = advertisement_data.service_data
369
394
 
370
395
  _service_data = None
@@ -452,3 +477,8 @@ def _parse_data(
452
477
  )
453
478
 
454
479
  return data
480
+
481
+
482
+ def populate_model_to_mac_cache(mac: str, model: SwitchbotModel) -> None:
483
+ """Populate the model to MAC address cache."""
484
+ _MODEL_TO_MAC_CACHE[mac] = model
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import struct
5
+ from ..helpers import _UNPACK_UINT16_BE
6
6
 
7
7
 
8
8
  def process_wostrip(
@@ -21,12 +21,21 @@ def process_wostrip(
21
21
  }
22
22
 
23
23
 
24
- def process_light(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
24
+ def process_light(
25
+ data: bytes | None, mfr_data: bytes | None, cw_offset: int = 16
26
+ ) -> dict[str, bool | int]:
25
27
  """Support for strip light 3 and floor lamp."""
26
28
  common_data = process_wostrip(data, mfr_data)
27
29
  if not common_data:
28
30
  return {}
29
31
 
30
- light_data = {"cw": struct.unpack(">H", mfr_data[16:18])[0]}
32
+ light_data = {"cw": _UNPACK_UINT16_BE(mfr_data, cw_offset)[0]}
31
33
 
32
34
  return common_data | light_data
35
+
36
+
37
+ def process_rgbic_light(
38
+ data: bytes | None, mfr_data: bytes | None
39
+ ) -> dict[str, bool | int]:
40
+ """Support for RGBIC lights."""
41
+ return process_light(data, mfr_data, cw_offset=10)
@@ -93,6 +93,9 @@ class SwitchbotModel(StrEnum):
93
93
  RELAY_SWITCH_2PM = "Relay Switch 2PM"
94
94
  STRIP_LIGHT_3 = "Strip Light 3"
95
95
  FLOOR_LAMP = "Floor Lamp"
96
+ PLUG_MINI_EU = "Plug Mini (EU)"
97
+ RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
98
+ RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
96
99
 
97
100
 
98
101
  __all__ = [
@@ -31,4 +31,15 @@ class CeilingLightColorMode(Enum):
31
31
  UNKNOWN = 10
32
32
 
33
33
 
34
+ class RGBICStripLightColorMode(Enum):
35
+ SEGMENTED = 1
36
+ RGB = 2
37
+ SCENE = 3
38
+ MUSIC = 4
39
+ CONTROLLER = 5
40
+ COLOR_TEMP = 6
41
+ EFFECT = 7
42
+ UNKNOWN = 10
43
+
44
+
34
45
  DEFAULT_COLOR_TEMP = 4001
@@ -37,7 +37,9 @@ class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
37
37
  @property
38
38
  def color_mode(self) -> ColorMode:
39
39
  """Return the current color mode."""
40
- device_mode = CeilingLightColorMode(self._get_adv_value("color_mode") or 10)
40
+ device_mode = CeilingLightColorMode(
41
+ value if (value := self._get_adv_value("color_mode")) is not None else 10
42
+ )
41
43
  return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
42
44
 
43
45
  @update_after_operation
@@ -24,6 +24,7 @@ from bleak_retry_connector import (
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,9 +38,43 @@ from ..const import (
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 @@ class SwitchbotBaseDevice:
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 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
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 @@ class SwitchbotSequenceDevice(SwitchbotDevice):
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)
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from bleak.backends.device import BLEDevice
6
6
 
7
7
  from ..const import SwitchbotModel
8
- from ..const.light import ColorMode, StripLightColorMode
8
+ from ..const.light import ColorMode, RGBICStripLightColorMode, StripLightColorMode
9
9
  from .base_light import SwitchbotSequenceBaseLight
10
10
  from .device import SwitchbotEncryptedDevice
11
11
 
@@ -18,6 +18,15 @@ _STRIP_LIGHT_COLOR_MODE_MAP = {
18
18
  StripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
19
19
  StripLightColorMode.UNKNOWN: ColorMode.OFF,
20
20
  }
21
+ _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
22
+ RGBICStripLightColorMode.SEGMENTED: ColorMode.EFFECT,
23
+ RGBICStripLightColorMode.RGB: ColorMode.RGB,
24
+ RGBICStripLightColorMode.SCENE: ColorMode.EFFECT,
25
+ RGBICStripLightColorMode.MUSIC: ColorMode.EFFECT,
26
+ RGBICStripLightColorMode.CONTROLLER: ColorMode.EFFECT,
27
+ RGBICStripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
28
+ RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
29
+ }
21
30
  LIGHT_STRIP_CONTROL_HEADER = "570F4901"
22
31
  COMMON_EFFECTS = {
23
32
  "christmas": [
@@ -97,6 +106,122 @@ COMMON_EFFECTS = {
97
106
  "570F490701000503600C2B35040C",
98
107
  ],
99
108
  }
109
+ RGBIC_EFFECTS = {
110
+ "romance": [
111
+ "570F490D01350100FF10EE",
112
+ "570F490D0363",
113
+ ],
114
+ "energy": [
115
+ "570F490D01000300ED070F34FF14FFE114",
116
+ "570F490D03FA",
117
+ ],
118
+ "heartbeat": [
119
+ "570F490D01020400FFDEADFE90FDFF9E3D",
120
+ "570F490D01020403FCBAFD",
121
+ "570F490D03FA",
122
+ ],
123
+ "party": [
124
+ "570F490D01030400FF8A47FF524DFF4DEE",
125
+ "570F490D010304034DFF8C",
126
+ "570F490D03FA",
127
+ ],
128
+ "dynamic": [
129
+ "570F490D010403004DFFFB4DFF4FFFBF4D",
130
+ "570F490D03FA",
131
+ ],
132
+ "mystery": [
133
+ "570F490D01050300F660F6F6D460C6F660",
134
+ "570F490D03FA",
135
+ ],
136
+ "lightning": [
137
+ "570F490D01340100FFD700",
138
+ "570F490D03FA",
139
+ ],
140
+ "rock": [
141
+ "570F490D01090300B0F6606864FCFFBC3D",
142
+ "570F490D03FA",
143
+ ],
144
+ "starlight": [
145
+ "570F490D010A0100FF8C00",
146
+ "570F490D0363",
147
+ ],
148
+ "valentine_day": [
149
+ "570F490D010C0300FDE0FFFFCC8AD7FF8A",
150
+ "570F490D03FA",
151
+ ],
152
+ "dream": [
153
+ "570F490D010E0300A3E5FF73F019FFA8E5",
154
+ "570F490D03FA",
155
+ ],
156
+ "alarm": [
157
+ "570F490D013E0100FF0000",
158
+ "570F490D03FA",
159
+ ],
160
+ "fireworks": [
161
+ "570F490D01110300FFAA33FFE233FF5CDF",
162
+ "570F490D03FA",
163
+ ],
164
+ "waves": [
165
+ "570F490D013D01001E90FF",
166
+ "570F490D03FA",
167
+ ],
168
+ "christmas": [
169
+ "570F490D01380400DC143C228B22DAA520",
170
+ "570F490D0363",
171
+ "570F490D0138040332CD32",
172
+ "570F490D0363",
173
+ ],
174
+ "rainbow": [
175
+ "570F490D01160600FF0000FF7F00FFFF00",
176
+ "570F490D03FA",
177
+ "570F490D0116060300FF000000FF9400D3",
178
+ "570F490D03FA",
179
+ ],
180
+ "game": [
181
+ "570F490D011A0400D05CFF668FFFFFEFD5",
182
+ "570F490D0363",
183
+ "570F490D011A0403FFC55C",
184
+ "570F490D0363",
185
+ ],
186
+ "halloween": [
187
+ "570F490D01320300FF8C009370DB32CD32",
188
+ "570F490D0364",
189
+ ],
190
+ "meditation": [
191
+ "570F490D013502001E90FF9370DB",
192
+ "570F490D0364",
193
+ ],
194
+ "starlit_sky": [
195
+ "570F490D010D010099C8FF",
196
+ "570F490D0364",
197
+ ],
198
+ "sleep": [
199
+ "570F490D01370300FF8C002E4E3E3E3E5E",
200
+ "570F490D0364",
201
+ ],
202
+ "movie": [
203
+ "570F490D013602001919704B0082",
204
+ "570F490D0364",
205
+ ],
206
+ "sunrise": [
207
+ "570F490D013F0200FFD700FF4500",
208
+ "570F490D03FA",
209
+ "570F490D03FA",
210
+ ],
211
+ "sunset": [
212
+ "570F490D01390300FF4500FFA500483D8B",
213
+ "570F490D0363",
214
+ "570F490D0363",
215
+ ],
216
+ "new_year": [
217
+ "570F490D013F0300FF0000FFD700228B22",
218
+ "570F490D0364",
219
+ ],
220
+ "cherry_blossom": [
221
+ "570F490D01400200FFB3C1FF69B4",
222
+ "570F490D0364",
223
+ ],
224
+ }
100
225
 
101
226
 
102
227
  class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
@@ -177,3 +302,44 @@ class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
177
302
  def color_modes(self) -> set[ColorMode]:
178
303
  """Return the supported color modes."""
179
304
  return {ColorMode.RGB, ColorMode.COLOR_TEMP}
305
+
306
+
307
+ class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
308
+ """Support for Switchbot RGBIC lights."""
309
+
310
+ _effect_dict = RGBIC_EFFECTS
311
+
312
+ def __init__(
313
+ self,
314
+ device: BLEDevice,
315
+ key_id: str,
316
+ encryption_key: str,
317
+ interface: int = 0,
318
+ model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
319
+ **kwargs: Any,
320
+ ) -> None:
321
+ super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
322
+
323
+ @classmethod
324
+ async def verify_encryption_key(
325
+ cls,
326
+ device: BLEDevice,
327
+ key_id: str,
328
+ encryption_key: str,
329
+ model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
330
+ **kwargs: Any,
331
+ ) -> bool:
332
+ return await super().verify_encryption_key(
333
+ device, key_id, encryption_key, model, **kwargs
334
+ )
335
+
336
+ @property
337
+ def color_modes(self) -> set[ColorMode]:
338
+ """Return the supported color modes."""
339
+ return {ColorMode.RGB, ColorMode.COLOR_TEMP}
340
+
341
+ @property
342
+ def color_mode(self) -> ColorMode:
343
+ """Return the current color mode."""
344
+ device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
345
+ return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
@@ -128,6 +128,7 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
128
128
  if self._model in (
129
129
  SwitchbotModel.RELAY_SWITCH_1PM,
130
130
  SwitchbotModel.RELAY_SWITCH_2PM,
131
+ SwitchbotModel.PLUG_MINI_EU,
131
132
  ):
132
133
  if channel is None:
133
134
  adv_data["voltage"] = self._get_adv_value("voltage") or 0
@@ -0,0 +1,24 @@
1
+ """Utility functions for switchbot."""
2
+
3
+ from functools import lru_cache
4
+
5
+
6
+ @lru_cache(maxsize=512)
7
+ def format_mac_upper(mac: str) -> str:
8
+ """Format the mac address string to uppercase with colons."""
9
+ to_test = mac
10
+
11
+ if len(to_test) == 17 and to_test.count(":") == 5:
12
+ return to_test.upper()
13
+
14
+ if len(to_test) == 17 and to_test.count("-") == 5:
15
+ to_test = to_test.replace("-", "")
16
+ elif len(to_test) == 14 and to_test.count(".") == 2:
17
+ to_test = to_test.replace(".", "")
18
+
19
+ if len(to_test) == 12:
20
+ # no : included
21
+ return ":".join(to_test.upper()[i : i + 2] for i in range(0, 12, 2))
22
+
23
+ # Not sure how formatted, return original
24
+ return mac.upper()