PySwitchbot 2.0.1__tar.gz → 2.2.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 (118) hide show
  1. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PKG-INFO +1 -1
  2. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PySwitchbot.egg-info/SOURCES.txt +2 -0
  4. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/setup.py +1 -1
  5. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/__init__.py +16 -1
  6. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parser.py +81 -3
  7. pyswitchbot-2.2.0/switchbot/adv_parsers/_sensor_th.py +36 -0
  8. pyswitchbot-2.2.0/switchbot/adv_parsers/fan.py +55 -0
  9. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/light_strip.py +15 -0
  10. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/meter.py +3 -21
  11. pyswitchbot-2.2.0/switchbot/adv_parsers/weather_station.py +40 -0
  12. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/__init__.py +17 -1
  13. pyswitchbot-2.2.0/switchbot/const/fan.py +62 -0
  14. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/light.py +1 -0
  15. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/air_purifier.py +1 -26
  16. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/art_frame.py +2 -26
  17. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/device.py +17 -3
  18. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/evaporative_humidifier.py +2 -27
  19. pyswitchbot-2.2.0/switchbot/devices/fan.py +226 -0
  20. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/keypad_vision.py +1 -1
  21. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/light_strip.py +58 -49
  22. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/lock.py +37 -18
  23. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/relay_switch.py +4 -48
  24. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/roller_shade.py +55 -17
  25. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/smart_thermostat_radiator.py +1 -26
  26. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_adv_parser.py +310 -0
  27. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_air_purifier.py +5 -23
  28. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_art_frame.py +3 -24
  29. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_encrypted_device.py +49 -4
  30. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_evaporative_humidifier.py +6 -23
  31. pyswitchbot-2.2.0/tests/test_fan.py +621 -0
  32. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_lock.py +76 -21
  33. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_relay_switch.py +9 -30
  34. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_roller_shade.py +162 -2
  35. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_smart_thermostat_radiator.py +6 -24
  36. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_strip_light.py +119 -42
  37. pyswitchbot-2.0.1/switchbot/adv_parsers/fan.py +0 -33
  38. pyswitchbot-2.0.1/switchbot/const/fan.py +0 -14
  39. pyswitchbot-2.0.1/switchbot/devices/fan.py +0 -103
  40. pyswitchbot-2.0.1/tests/test_fan.py +0 -177
  41. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/LICENSE +0 -0
  42. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/MANIFEST.in +0 -0
  43. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  44. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PySwitchbot.egg-info/requires.txt +0 -0
  45. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  46. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/README.md +0 -0
  47. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/pyproject.toml +0 -0
  48. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/setup.cfg +0 -0
  49. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/__init__.py +0 -0
  50. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  51. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/art_frame.py +0 -0
  52. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  53. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/bot.py +0 -0
  54. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/bulb.py +0 -0
  55. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  56. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/climate_panel.py +0 -0
  57. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/contact.py +0 -0
  58. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/curtain.py +0 -0
  59. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/hub2.py +0 -0
  60. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/hub3.py +0 -0
  61. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  62. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/humidifier.py +0 -0
  63. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/keypad.py +0 -0
  64. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
  65. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/leak.py +0 -0
  66. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/lock.py +0 -0
  67. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/motion.py +0 -0
  68. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/plug.py +0 -0
  69. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
  70. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  71. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/remote.py +0 -0
  72. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  73. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
  74. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/adv_parsers/vacuum.py +0 -0
  75. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/api_config.py +0 -0
  76. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/air_purifier.py +0 -0
  77. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/climate.py +0 -0
  78. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/evaporative_humidifier.py +0 -0
  79. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/hub2.py +0 -0
  80. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/hub3.py +0 -0
  81. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/lock.py +0 -0
  82. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/const/presence_sensor.py +0 -0
  83. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/__init__.py +0 -0
  84. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/base_cover.py +0 -0
  85. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/base_light.py +0 -0
  86. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/blind_tilt.py +0 -0
  87. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/bot.py +0 -0
  88. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/bulb.py +0 -0
  89. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/ceiling_light.py +0 -0
  90. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/contact.py +0 -0
  91. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/curtain.py +0 -0
  92. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/humidifier.py +0 -0
  93. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/keypad.py +0 -0
  94. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/meter.py +0 -0
  95. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/meter_pro.py +0 -0
  96. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/motion.py +0 -0
  97. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/plug.py +0 -0
  98. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/devices/vacuum.py +0 -0
  99. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/discovery.py +0 -0
  100. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/enum.py +0 -0
  101. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/helpers.py +0 -0
  102. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/models.py +0 -0
  103. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/switchbot/utils.py +0 -0
  104. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_base_cover.py +0 -0
  105. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_blind_tilt.py +0 -0
  106. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_bulb.py +0 -0
  107. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_ceiling_light.py +0 -0
  108. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_colormode_imports.py +0 -0
  109. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_curtain.py +0 -0
  110. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_device.py +0 -0
  111. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_discovery_callback.py +0 -0
  112. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_helpers.py +0 -0
  113. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_hub2.py +0 -0
  114. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_hub3.py +0 -0
  115. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_keypad_vision.py +0 -0
  116. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_meter_pro.py +0 -0
  117. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_utils.py +0 -0
  118. {pyswitchbot-2.0.1 → pyswitchbot-2.2.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 2.0.1
3
+ Version: 2.2.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: 2.0.1
3
+ Version: 2.2.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
@@ -17,6 +17,7 @@ switchbot/helpers.py
17
17
  switchbot/models.py
18
18
  switchbot/utils.py
19
19
  switchbot/adv_parsers/__init__.py
20
+ switchbot/adv_parsers/_sensor_th.py
20
21
  switchbot/adv_parsers/air_purifier.py
21
22
  switchbot/adv_parsers/art_frame.py
22
23
  switchbot/adv_parsers/blind_tilt.py
@@ -45,6 +46,7 @@ switchbot/adv_parsers/remote.py
45
46
  switchbot/adv_parsers/roller_shade.py
46
47
  switchbot/adv_parsers/smart_thermostat_radiator.py
47
48
  switchbot/adv_parsers/vacuum.py
49
+ switchbot/adv_parsers/weather_station.py
48
50
  switchbot/const/__init__.py
49
51
  switchbot/const/air_purifier.py
50
52
  switchbot/const/climate.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="2.0.1",
23
+ version="2.2.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -18,16 +18,20 @@ from .const import (
18
18
  ClimateMode,
19
19
  ColorMode,
20
20
  FanMode,
21
+ HorizontalOscillationAngle,
21
22
  HumidifierAction,
22
23
  HumidifierMode,
23
24
  HumidifierWaterLevel,
24
25
  LockStatus,
26
+ NightLightState,
25
27
  SmartThermostatRadiatorMode,
28
+ StandingFanMode,
26
29
  StripLightColorMode,
27
30
  SwitchbotAccountConnectionError,
28
31
  SwitchbotApiError,
29
32
  SwitchbotAuthenticationError,
30
33
  SwitchbotModel,
34
+ VerticalOscillationAngle,
31
35
  )
32
36
  from .devices.air_purifier import SwitchbotAirPurifier
33
37
  from .devices.art_frame import SwitchbotArtFrame
@@ -44,12 +48,15 @@ from .devices.device import (
44
48
  fetch_cloud_devices,
45
49
  )
46
50
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
47
- from .devices.fan import SwitchbotFan
51
+ from .devices.fan import SwitchbotFan, SwitchbotStandingFan
48
52
  from .devices.humidifier import SwitchbotHumidifier
49
53
  from .devices.keypad_vision import SwitchbotKeypadVision
50
54
  from .devices.light_strip import (
55
+ SwitchbotCandleWarmerLamp,
51
56
  SwitchbotLightStrip,
57
+ SwitchbotPermanentOutdoorLight,
52
58
  SwitchbotRgbicLight,
59
+ SwitchbotRgbicNeonLight,
53
60
  SwitchbotStripLight3,
54
61
  )
55
62
  from .devices.lock import SwitchbotLock
@@ -76,11 +83,14 @@ __all__ = [
76
83
  "ColorMode",
77
84
  "FanMode",
78
85
  "GetSwitchbotDevices",
86
+ "HorizontalOscillationAngle",
79
87
  "HumidifierAction",
80
88
  "HumidifierMode",
81
89
  "HumidifierWaterLevel",
82
90
  "LockStatus",
91
+ "NightLightState",
83
92
  "SmartThermostatRadiatorMode",
93
+ "StandingFanMode",
84
94
  "StripLightColorMode",
85
95
  "SwitchBotAdvertisement",
86
96
  "Switchbot",
@@ -93,6 +103,7 @@ __all__ = [
93
103
  "SwitchbotBaseLight",
94
104
  "SwitchbotBlindTilt",
95
105
  "SwitchbotBulb",
106
+ "SwitchbotCandleWarmerLamp",
96
107
  "SwitchbotCeilingLight",
97
108
  "SwitchbotCurtain",
98
109
  "SwitchbotDevice",
@@ -108,17 +119,21 @@ __all__ = [
108
119
  "SwitchbotModel",
109
120
  "SwitchbotModel",
110
121
  "SwitchbotOperationError",
122
+ "SwitchbotPermanentOutdoorLight",
111
123
  "SwitchbotPlugMini",
112
124
  "SwitchbotPlugMini",
113
125
  "SwitchbotRelaySwitch",
114
126
  "SwitchbotRelaySwitch2PM",
115
127
  "SwitchbotRgbicLight",
128
+ "SwitchbotRgbicNeonLight",
116
129
  "SwitchbotRollerShade",
117
130
  "SwitchbotSmartThermostatRadiator",
131
+ "SwitchbotStandingFan",
118
132
  "SwitchbotStripLight3",
119
133
  "SwitchbotSupportedType",
120
134
  "SwitchbotSupportedType",
121
135
  "SwitchbotVacuum",
136
+ "VerticalOscillationAngle",
122
137
  "close_stale_connections",
123
138
  "close_stale_connections_by_address",
124
139
  "fetch_cloud_devices",
@@ -20,7 +20,7 @@ from .adv_parsers.ceiling_light import process_woceiling
20
20
  from .adv_parsers.climate_panel import process_climate_panel
21
21
  from .adv_parsers.contact import process_wocontact
22
22
  from .adv_parsers.curtain import process_wocurtain
23
- from .adv_parsers.fan import process_fan
23
+ from .adv_parsers.fan import process_fan, process_standing_fan
24
24
  from .adv_parsers.hub2 import process_wohub2
25
25
  from .adv_parsers.hub3 import process_hub3
26
26
  from .adv_parsers.hubmini_matter import process_hubmini_matter
@@ -28,7 +28,12 @@ from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohu
28
28
  from .adv_parsers.keypad import process_wokeypad
29
29
  from .adv_parsers.keypad_vision import process_keypad_vision, process_keypad_vision_pro
30
30
  from .adv_parsers.leak import process_leak
31
- from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
31
+ from .adv_parsers.light_strip import (
32
+ process_candle_warmer_lamp,
33
+ process_light,
34
+ process_rgbic_light,
35
+ process_wostrip,
36
+ )
32
37
  from .adv_parsers.lock import (
33
38
  process_lock2,
34
39
  process_locklite,
@@ -49,6 +54,7 @@ from .adv_parsers.remote import process_woremote
49
54
  from .adv_parsers.roller_shade import process_worollershade
50
55
  from .adv_parsers.smart_thermostat_radiator import process_smart_thermostat_radiator
51
56
  from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
57
+ from .adv_parsers.weather_station import process_weather_station
52
58
  from .const import SwitchbotModel
53
59
  from .models import SwitchBotAdvertisement
54
60
  from .utils import format_mac_upper
@@ -69,7 +75,7 @@ class SwitchbotSupportedType(TypedDict):
69
75
 
70
76
  modelName: SwitchbotModel
71
77
  modelFriendlyName: str
72
- func: Callable[[bytes, bytes | None], dict[str, bool | int]]
78
+ func: Callable[[bytes | None, bytes | None], dict[str, bool | int | str | None]]
73
79
  manufacturer_id: int | None
74
80
  manufacturer_data_length: int | None
75
81
 
@@ -611,6 +617,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
611
617
  "func": process_light,
612
618
  "manufacturer_id": 2409,
613
619
  },
620
+ b"\x00\x11\x22\xb8": {
621
+ "modelName": SwitchbotModel.CANDLE_WARMER_LAMP,
622
+ "modelFriendlyName": "Candle Warmer Lamp",
623
+ "func": process_candle_warmer_lamp,
624
+ "manufacturer_id": 2409,
625
+ },
626
+ b"\x01\x11\x22\xb8": {
627
+ "modelName": SwitchbotModel.CANDLE_WARMER_LAMP,
628
+ "modelFriendlyName": "Candle Warmer Lamp",
629
+ "func": process_candle_warmer_lamp,
630
+ "manufacturer_id": 2409,
631
+ },
614
632
  b"\x00\x10\xd0\xb1": {
615
633
  "modelName": SwitchbotModel.STRIP_LIGHT_3,
616
634
  "modelFriendlyName": "Strip Light 3",
@@ -659,6 +677,42 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
659
677
  "func": process_rgbic_light,
660
678
  "manufacturer_id": 2409,
661
679
  },
680
+ b"\x00\x10\xd0\xb7": {
681
+ "modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
682
+ "modelFriendlyName": "Permanent Outdoor Light",
683
+ "func": process_rgbic_light,
684
+ "manufacturer_id": 2409,
685
+ },
686
+ b"\x01\x10\xd0\xb7": {
687
+ "modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
688
+ "modelFriendlyName": "Permanent Outdoor Light",
689
+ "func": process_rgbic_light,
690
+ "manufacturer_id": 2409,
691
+ },
692
+ b"\x00\x10\xd0\xb5": {
693
+ "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
694
+ "modelFriendlyName": "RGBIC Neon Wire Rope Light",
695
+ "func": process_wostrip,
696
+ "manufacturer_id": 2409,
697
+ },
698
+ b"\x00\x10\xd0\xb6": {
699
+ "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
700
+ "modelFriendlyName": "RGBIC Neon Rope Light",
701
+ "func": process_wostrip,
702
+ "manufacturer_id": 2409,
703
+ },
704
+ b"\x01\x10\xd0\xb5": {
705
+ "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
706
+ "modelFriendlyName": "RGBIC Neon Wire Rope Light",
707
+ "func": process_wostrip,
708
+ "manufacturer_id": 2409,
709
+ },
710
+ b"\x01\x10\xd0\xb6": {
711
+ "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
712
+ "modelFriendlyName": "RGBIC Neon Rope Light",
713
+ "func": process_wostrip,
714
+ "manufacturer_id": 2409,
715
+ },
662
716
  b"\x00\x10\xfb\xa8": {
663
717
  "modelName": SwitchbotModel.K11_VACUUM,
664
718
  "modelFriendlyName": "K11+ Vacuum",
@@ -791,6 +845,30 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
791
845
  "func": process_wolock_pro,
792
846
  "manufacturer_id": 2409,
793
847
  },
848
+ b"\x00\x11\x07\x60": {
849
+ "modelName": SwitchbotModel.STANDING_FAN,
850
+ "modelFriendlyName": "Standing Fan",
851
+ "func": process_standing_fan,
852
+ "manufacturer_id": 2409,
853
+ },
854
+ b"\x01\x11\x07\x60": {
855
+ "modelName": SwitchbotModel.STANDING_FAN,
856
+ "modelFriendlyName": "Standing Fan",
857
+ "func": process_standing_fan,
858
+ "manufacturer_id": 2409,
859
+ },
860
+ b"\x00\x10\x53\xb0": {
861
+ "modelName": SwitchbotModel.WEATHER_STATION,
862
+ "modelFriendlyName": "Weather Station",
863
+ "func": process_weather_station,
864
+ "manufacturer_id": 2409,
865
+ },
866
+ b"\x01\x10\x53\xb0": {
867
+ "modelName": SwitchbotModel.WEATHER_STATION,
868
+ "modelFriendlyName": "Weather Station",
869
+ "func": process_weather_station,
870
+ "manufacturer_id": 2409,
871
+ },
794
872
  }
795
873
 
796
874
  _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
@@ -0,0 +1,36 @@
1
+ """Shared temperature/humidity decoding helpers for T/H sensors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..helpers import celsius_to_fahrenheit
8
+
9
+
10
+ def decode_temp_humidity(temp_data: bytes, battery: int | None) -> dict[str, Any]:
11
+ """
12
+ Decode temperature/humidity/fahrenheit-flag from a 3-byte payload.
13
+
14
+ Layout (bytes after company ID, for SwitchBot T/H sensors):
15
+ byte 0: bits[3:0] = temperature decimal (0.1 °C units)
16
+ byte 1: bit[7] = temperature sign (1 = positive), bits[6:0] = integer °C
17
+ byte 2: bit[7] = fahrenheit-display flag, bits[6:0] = humidity %
18
+ """
19
+ _temp_sign = 1 if temp_data[1] & 0b10000000 else -1
20
+ _temp_c = _temp_sign * (
21
+ (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
22
+ )
23
+ _temp_f = celsius_to_fahrenheit(_temp_c)
24
+ _temp_f = (_temp_f * 10) / 10
25
+ humidity = temp_data[2] & 0b01111111
26
+
27
+ if _temp_c == 0 and humidity == 0 and battery == 0:
28
+ return {}
29
+
30
+ return {
31
+ "temp": {"c": _temp_c, "f": _temp_f},
32
+ "temperature": _temp_c,
33
+ "fahrenheit": bool(temp_data[2] & 0b10000000),
34
+ "humidity": humidity,
35
+ "battery": battery,
36
+ }
@@ -0,0 +1,55 @@
1
+ """Fan adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..const.fan import FanMode, StandingFanMode
6
+
7
+ _FAN_MODE_MAP: dict[int, str] = {m.value: m.name.lower() for m in FanMode}
8
+ _STANDING_FAN_MODE_MAP: dict[int, str] = {
9
+ m.value: m.name.lower() for m in StandingFanMode
10
+ }
11
+
12
+
13
+ def _parse_fan(
14
+ mfr_data: bytes | None, mode_map: dict[int, str]
15
+ ) -> dict[str, bool | int | str | None]:
16
+ """Shared fan advertisement parse, parameterized on the mode map."""
17
+ if mfr_data is None:
18
+ return {}
19
+
20
+ device_data = mfr_data[6:]
21
+
22
+ _seq_num = device_data[0]
23
+ _isOn = bool(device_data[1] & 0b10000000)
24
+ _mode = (device_data[1] & 0b01110000) >> 4
25
+ _nightLight = (device_data[1] & 0b00001100) >> 2
26
+ _oscillate_left_and_right = bool(device_data[1] & 0b00000010)
27
+ _oscillate_up_and_down = bool(device_data[1] & 0b00000001)
28
+ _battery = device_data[2] & 0b01111111
29
+ _speed = device_data[3] & 0b01111111
30
+
31
+ return {
32
+ "sequence_number": _seq_num,
33
+ "isOn": _isOn,
34
+ "mode": mode_map.get(_mode),
35
+ "nightLight": _nightLight,
36
+ "oscillating": _oscillate_left_and_right or _oscillate_up_and_down,
37
+ "oscillating_horizontal": _oscillate_left_and_right,
38
+ "oscillating_vertical": _oscillate_up_and_down,
39
+ "battery": _battery,
40
+ "speed": _speed,
41
+ }
42
+
43
+
44
+ def process_fan(
45
+ data: bytes | None, mfr_data: bytes | None
46
+ ) -> dict[str, bool | int | str | None]:
47
+ """Process Circulator Fan services data (modes 1-4)."""
48
+ return _parse_fan(mfr_data, _FAN_MODE_MAP)
49
+
50
+
51
+ def process_standing_fan(
52
+ data: bytes | None, mfr_data: bytes | None
53
+ ) -> dict[str, bool | int | str | None]:
54
+ """Process Standing Fan services data (modes 1-5; adds CUSTOM_NATURAL)."""
55
+ return _parse_fan(mfr_data, _STANDING_FAN_MODE_MAP)
@@ -21,6 +21,21 @@ def process_wostrip(
21
21
  }
22
22
 
23
23
 
24
+ def process_candle_warmer_lamp(
25
+ data: bytes | None, mfr_data: bytes | None
26
+ ) -> dict[str, bool | int]:
27
+ """Process Candle Warmer Lamp services data."""
28
+ if mfr_data is None:
29
+ return {}
30
+ return {
31
+ "sequence_number": mfr_data[6],
32
+ "isOn": bool(mfr_data[7] & 0b10000000),
33
+ "brightness": mfr_data[7] & 0b01111111,
34
+ "delay": bool(mfr_data[8] & 0b10000000),
35
+ "network_state": (mfr_data[8] & 0b01110000) >> 4,
36
+ }
37
+
38
+
24
39
  def process_light(
25
40
  data: bytes | None, mfr_data: bytes | None, cw_offset: int = 16
26
41
  ) -> dict[str, bool | int]:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import struct
6
6
  from typing import Any
7
7
 
8
- from ..helpers import celsius_to_fahrenheit
8
+ from ._sensor_th import decode_temp_humidity
9
9
 
10
10
  CO2_UNPACK = struct.Struct(">H").unpack_from
11
11
 
@@ -13,7 +13,7 @@ CO2_UNPACK = struct.Struct(">H").unpack_from
13
13
  def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
14
14
  """Process woSensorTH/Temp sensor services data."""
15
15
  temp_data: bytes | None = None
16
- battery: bytes | None = None
16
+ battery: int | None = None
17
17
 
18
18
  if mfr_data:
19
19
  temp_data = mfr_data[8:11]
@@ -26,25 +26,7 @@ def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str,
26
26
  if not temp_data:
27
27
  return {}
28
28
 
29
- _temp_sign = 1 if temp_data[1] & 0b10000000 else -1
30
- _temp_c = _temp_sign * (
31
- (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
32
- )
33
- _temp_f = celsius_to_fahrenheit(_temp_c)
34
- _temp_f = (_temp_f * 10) / 10
35
- humidity = temp_data[2] & 0b01111111
36
-
37
- if _temp_c == 0 and humidity == 0 and battery == 0:
38
- return {}
39
-
40
- return {
41
- # Data should be flat, but we keep the original structure for now
42
- "temp": {"c": _temp_c, "f": _temp_f},
43
- "temperature": _temp_c,
44
- "fahrenheit": bool(temp_data[2] & 0b10000000),
45
- "humidity": humidity,
46
- "battery": battery,
47
- }
29
+ return decode_temp_humidity(temp_data, battery)
48
30
 
49
31
 
50
32
  def process_wosensorth_c(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
@@ -0,0 +1,40 @@
1
+ """Weather Station parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ._sensor_th import decode_temp_humidity
8
+
9
+
10
+ def process_weather_station(
11
+ data: bytes | None, mfr_data: bytes | None
12
+ ) -> dict[str, Any]:
13
+ """
14
+ Process Weather Station advertisement data.
15
+
16
+ Manufacturer data layout (mfr_id=2409, after company ID stripped by bleak):
17
+ Byte 0-5: MAC address
18
+ Byte 6: Sequence number
19
+ Byte 7: Battery (bit7=charging, bit6-0=level%)
20
+ Byte 8: Temp alarm(bit7-6), Humidity alarm(bit5-4), Temp decimal(bit3-0)
21
+ Byte 9: Temp sign(bit7: 0=neg,1=pos), Temp integer(bit6-0)
22
+ Byte 10: Fahrenheit flag(bit7), Humidity(bit6-0)
23
+ """
24
+ temp_data: bytes | None = None
25
+ battery: int | None = None
26
+
27
+ if mfr_data and len(mfr_data) >= 11:
28
+ temp_data = mfr_data[8:11]
29
+ battery = mfr_data[7] & 0b01111111
30
+
31
+ if data and len(data) >= 6:
32
+ if not temp_data:
33
+ temp_data = data[3:6]
34
+ if battery is None:
35
+ battery = data[2] & 0b01111111
36
+
37
+ if not temp_data:
38
+ return {}
39
+
40
+ return decode_temp_humidity(temp_data, battery)
@@ -10,7 +10,13 @@ from .evaporative_humidifier import (
10
10
  HumidifierMode,
11
11
  HumidifierWaterLevel,
12
12
  )
13
- from .fan import FanMode
13
+ from .fan import (
14
+ FanMode,
15
+ HorizontalOscillationAngle,
16
+ NightLightState,
17
+ StandingFanMode,
18
+ VerticalOscillationAngle,
19
+ )
14
20
  from .light import (
15
21
  BulbColorMode,
16
22
  CeilingLightColorMode,
@@ -80,6 +86,7 @@ class SwitchbotModel(StrEnum):
80
86
  ROLLER_SHADE = "Roller Shade"
81
87
  HUBMINI_MATTER = "HubMini Matter"
82
88
  CIRCULATOR_FAN = "Circulator Fan"
89
+ STANDING_FAN = "Standing Fan"
83
90
  K20_VACUUM = "K20 Vacuum"
84
91
  S10_VACUUM = "S10 Vacuum"
85
92
  K10_VACUUM = "K10+ Vacuum"
@@ -96,9 +103,13 @@ class SwitchbotModel(StrEnum):
96
103
  RELAY_SWITCH_2PM = "Relay Switch 2PM"
97
104
  STRIP_LIGHT_3 = "Strip Light 3"
98
105
  FLOOR_LAMP = "Floor Lamp"
106
+ CANDLE_WARMER_LAMP = "Candle Warmer Lamp"
99
107
  PLUG_MINI_EU = "Plug Mini (EU)"
100
108
  RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
101
109
  RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
110
+ PERMANENT_OUTDOOR_LIGHT = "Permanent Outdoor Light"
111
+ RGBIC_NEON_ROPE_LIGHT = "RGBIC Neon Rope Light"
112
+ RGBIC_NEON_WIRE_ROPE_LIGHT = "RGBIC Neon Wire Rope Light"
102
113
  K11_VACUUM = "K11+ Vacuum"
103
114
  CLIMATE_PANEL = "Climate Panel"
104
115
  SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator"
@@ -110,6 +121,7 @@ class SwitchbotModel(StrEnum):
110
121
  LOCK_VISION_PRO = "Lock Vision Pro"
111
122
  LOCK_VISION = "Lock Vision"
112
123
  LOCK_PRO_WIFI = "Lock Pro Wifi"
124
+ WEATHER_STATION = "Weather Station"
113
125
 
114
126
 
115
127
  __all__ = [
@@ -124,14 +136,18 @@ __all__ = [
124
136
  "ClimateMode",
125
137
  "ColorMode",
126
138
  "FanMode",
139
+ "HorizontalOscillationAngle",
127
140
  "HumidifierAction",
128
141
  "HumidifierMode",
129
142
  "HumidifierWaterLevel",
130
143
  "LockStatus",
144
+ "NightLightState",
131
145
  "SmartThermostatRadiatorMode",
146
+ "StandingFanMode",
132
147
  "StripLightColorMode",
133
148
  "SwitchbotAccountConnectionError",
134
149
  "SwitchbotApiError",
135
150
  "SwitchbotAuthenticationError",
136
151
  "SwitchbotModel",
152
+ "VerticalOscillationAngle",
137
153
  ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class FanMode(Enum):
7
+ NORMAL = 1
8
+ NATURAL = 2
9
+ SLEEP = 3
10
+ BABY = 4
11
+
12
+ @classmethod
13
+ def get_modes(cls) -> list[str]:
14
+ return [mode.name.lower() for mode in cls]
15
+
16
+
17
+ class StandingFanMode(Enum):
18
+ NORMAL = 1
19
+ NATURAL = 2
20
+ SLEEP = 3
21
+ BABY = 4
22
+ CUSTOM_NATURAL = 5
23
+
24
+ @classmethod
25
+ def get_modes(cls) -> list[str]:
26
+ return [mode.name.lower() for mode in cls]
27
+
28
+
29
+ class NightLightState(Enum):
30
+ """Standing Fan night-light command values."""
31
+
32
+ LEVEL_1 = 1
33
+ LEVEL_2 = 2
34
+ OFF = 3
35
+
36
+
37
+ class HorizontalOscillationAngle(Enum):
38
+ """
39
+ Horizontal oscillation angle command values.
40
+
41
+ For the horizontal axis the device byte is the same as the
42
+ user-facing angle in degrees.
43
+ """
44
+
45
+ ANGLE_30 = 30
46
+ ANGLE_60 = 60
47
+ ANGLE_90 = 90
48
+
49
+
50
+ class VerticalOscillationAngle(Enum):
51
+ """
52
+ Vertical oscillation angle command values.
53
+
54
+ The Standing Fan uses a different byte encoding on the vertical axis
55
+ than on the horizontal one. Byte 0x5A (decimal 90) is interpreted as
56
+ an axis halt, so 90° maps to byte 0x5F (95). 30° and 60° match their
57
+ degree values.
58
+ """
59
+
60
+ ANGLE_30 = 30
61
+ ANGLE_60 = 60
62
+ ANGLE_90 = 95
@@ -6,6 +6,7 @@ class ColorMode(Enum):
6
6
  COLOR_TEMP = 1
7
7
  RGB = 2
8
8
  EFFECT = 3
9
+ BRIGHTNESS = 4
9
10
 
10
11
 
11
12
  class StripLightColorMode(Enum):
@@ -5,8 +5,6 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import Any, ClassVar
7
7
 
8
- from bleak.backends.device import BLEDevice
9
-
10
8
  from ..adv_parsers.air_purifier import get_air_purifier_mode
11
9
  from ..const import SwitchbotModel
12
10
  from ..const.air_purifier import AirPurifierMode, AirQualityLevel
@@ -40,6 +38,7 @@ READ_LED_STATUS_COMMAND = "570f4d07"
40
38
  class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice):
41
39
  """Representation of a Switchbot Air Purifier."""
42
40
 
41
+ _model = SwitchbotModel.AIR_PURIFIER_US
43
42
  _turn_on_command = f"{COMMAND_HEAD}010100"
44
43
  _turn_off_command = f"{COMMAND_HEAD}010000"
45
44
  _open_child_lock_command = f"{COMMAND_HEAD}0301"
@@ -78,30 +77,6 @@ class SwitchbotAirPurifier(SwitchbotSequenceBaseLight, SwitchbotEncryptedDevice)
78
77
  }
79
78
  )
80
79
 
81
- def __init__(
82
- self,
83
- device: BLEDevice,
84
- key_id: str,
85
- encryption_key: str,
86
- interface: int = 0,
87
- model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
88
- **kwargs: Any,
89
- ) -> None:
90
- super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
91
-
92
- @classmethod
93
- async def verify_encryption_key(
94
- cls,
95
- device: BLEDevice,
96
- key_id: str,
97
- encryption_key: str,
98
- model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER_US,
99
- **kwargs: Any,
100
- ) -> bool:
101
- return await super().verify_encryption_key(
102
- device, key_id, encryption_key, model, **kwargs
103
- )
104
-
105
80
  @property
106
81
  def color_modes(self) -> set[ColorMode]:
107
82
  """Return the supported color modes."""
@@ -3,8 +3,6 @@
3
3
  import logging
4
4
  from typing import Any
5
5
 
6
- from bleak.backends.device import BLEDevice
7
-
8
6
  from ..const import SwitchbotModel
9
7
  from .device import (
10
8
  SwitchbotEncryptedDevice,
@@ -20,30 +18,8 @@ COMMAND_SET_IMAGE = "570F7A02{}"
20
18
  class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
21
19
  """Representation of a Switchbot Art Frame."""
22
20
 
23
- def __init__(
24
- self,
25
- device: BLEDevice,
26
- key_id: str,
27
- encryption_key: str,
28
- interface: int = 0,
29
- model: SwitchbotModel = SwitchbotModel.ART_FRAME,
30
- **kwargs: Any,
31
- ) -> None:
32
- super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
33
- self.response_flag = True
34
-
35
- @classmethod
36
- async def verify_encryption_key(
37
- cls,
38
- device: BLEDevice,
39
- key_id: str,
40
- encryption_key: str,
41
- model: SwitchbotModel = SwitchbotModel.ART_FRAME,
42
- **kwargs: Any,
43
- ) -> bool:
44
- return await super().verify_encryption_key(
45
- device, key_id, encryption_key, model, **kwargs
46
- )
21
+ _model = SwitchbotModel.ART_FRAME
22
+ response_flag: bool = True
47
23
 
48
24
  async def get_basic_info(self) -> dict[str, Any] | None:
49
25
  """Get device basic settings."""