PySwitchbot 2.5.0__tar.gz → 2.6.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.
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/PKG-INFO +1 -1
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/pyproject.toml +1 -1
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/__init__.py +8 -1
- pyswitchbot-2.6.0/switchbot/__version__.py +1 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parser.py +17 -1
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/fan.py +49 -1
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/__init__.py +3 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/fan.py +17 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/device.py +1 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/fan.py +119 -0
- pyswitchbot-2.5.0/switchbot/__version__.py +0 -1
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/LICENSE +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/README.md +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/_sensor_th.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/art_frame.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/climate_panel.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/adv_parsers/weather_station.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/climate.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/light.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/const/presence_sensor.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/art_frame.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/keypad_vision.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/meter_pro.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/universal_remote.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/enum.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/models.py +0 -0
- {pyswitchbot-2.5.0 → pyswitchbot-2.6.0}/switchbot/utils.py +0 -0
|
@@ -14,6 +14,7 @@ from .const import (
|
|
|
14
14
|
AirQualityLevel,
|
|
15
15
|
BulbColorMode,
|
|
16
16
|
CeilingLightColorMode,
|
|
17
|
+
CirculatorFanProMode,
|
|
17
18
|
ClimateAction,
|
|
18
19
|
ClimateMode,
|
|
19
20
|
ColorMode,
|
|
@@ -48,7 +49,11 @@ from .devices.device import (
|
|
|
48
49
|
fetch_cloud_devices,
|
|
49
50
|
)
|
|
50
51
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
51
|
-
from .devices.fan import
|
|
52
|
+
from .devices.fan import (
|
|
53
|
+
SwitchbotCirculatorFanPro,
|
|
54
|
+
SwitchbotFan,
|
|
55
|
+
SwitchbotStandingFan,
|
|
56
|
+
)
|
|
52
57
|
from .devices.humidifier import SwitchbotHumidifier
|
|
53
58
|
from .devices.keypad_vision import SwitchbotKeypadVision
|
|
54
59
|
from .devices.light_strip import (
|
|
@@ -80,6 +85,7 @@ __all__ = [
|
|
|
80
85
|
"AirQualityLevel",
|
|
81
86
|
"BulbColorMode",
|
|
82
87
|
"CeilingLightColorMode",
|
|
88
|
+
"CirculatorFanProMode",
|
|
83
89
|
"ClimateAction",
|
|
84
90
|
"ClimateMode",
|
|
85
91
|
"ColorMode",
|
|
@@ -106,6 +112,7 @@ __all__ = [
|
|
|
106
112
|
"SwitchbotBulb",
|
|
107
113
|
"SwitchbotCandleWarmerLamp",
|
|
108
114
|
"SwitchbotCeilingLight",
|
|
115
|
+
"SwitchbotCirculatorFanPro",
|
|
109
116
|
"SwitchbotCurtain",
|
|
110
117
|
"SwitchbotDevice",
|
|
111
118
|
"SwitchbotEncryptedDevice",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.6.0"
|
|
@@ -20,7 +20,11 @@ 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
|
|
23
|
+
from .adv_parsers.fan import (
|
|
24
|
+
process_circulator_fan_pro,
|
|
25
|
+
process_fan,
|
|
26
|
+
process_standing_fan,
|
|
27
|
+
)
|
|
24
28
|
from .adv_parsers.hub2 import process_wohub2
|
|
25
29
|
from .adv_parsers.hub3 import process_hub3
|
|
26
30
|
from .adv_parsers.hubmini_matter import process_hubmini_matter
|
|
@@ -714,6 +718,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
714
718
|
"func": process_rgbicww_ceiling_light,
|
|
715
719
|
"manufacturer_id": 2409,
|
|
716
720
|
},
|
|
721
|
+
b"\x00\x11\xb3@": {
|
|
722
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN_PRO,
|
|
723
|
+
"modelFriendlyName": "Circulator Fan Pro",
|
|
724
|
+
"func": process_circulator_fan_pro,
|
|
725
|
+
"manufacturer_id": 2409,
|
|
726
|
+
},
|
|
727
|
+
b"\x01\x11\xb3@": {
|
|
728
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN_PRO,
|
|
729
|
+
"modelFriendlyName": "Circulator Fan Pro",
|
|
730
|
+
"func": process_circulator_fan_pro,
|
|
731
|
+
"manufacturer_id": 2409,
|
|
732
|
+
},
|
|
717
733
|
b"\x00\x10\xd0\xb7": {
|
|
718
734
|
"modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
|
|
719
735
|
"modelFriendlyName": "Permanent Outdoor Light",
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from ..const.fan import FanMode, StandingFanMode
|
|
5
|
+
from ..const.fan import CirculatorFanProMode, FanMode, StandingFanMode
|
|
6
6
|
|
|
7
7
|
_FAN_MODE_MAP: dict[int, str] = {m.value: m.name.lower() for m in FanMode}
|
|
8
8
|
_STANDING_FAN_MODE_MAP: dict[int, str] = {
|
|
9
9
|
m.value: m.name.lower() for m in StandingFanMode
|
|
10
10
|
}
|
|
11
|
+
_CIRCULATOR_FAN_PRO_MODE_MAP: dict[int, str] = {
|
|
12
|
+
m.value: m.name.lower() for m in CirculatorFanProMode
|
|
13
|
+
}
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def _parse_fan(
|
|
@@ -60,3 +63,48 @@ def process_standing_fan(
|
|
|
60
63
|
) -> dict[str, bool | int | str | None]:
|
|
61
64
|
"""Process Standing Fan services data (modes 1-5; adds CUSTOM_NATURAL)."""
|
|
62
65
|
return _parse_fan(mfr_data, _STANDING_FAN_MODE_MAP, with_charging=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def process_circulator_fan_pro(
|
|
69
|
+
data: bytes | None, mfr_data: bytes | None
|
|
70
|
+
) -> dict[str, bool | int | str | None]:
|
|
71
|
+
"""
|
|
72
|
+
Process Circulator Fan Pro (W1160) advertisement.
|
|
73
|
+
|
|
74
|
+
The Pro shares the W1071 Modern Ceiling Fan broadcast layout, which differs
|
|
75
|
+
from the legacy Circulator Fan: battery and the fan-state byte are swapped.
|
|
76
|
+
The fan-state byte carries a two-level night light (bit2 = on/off, bit3 =
|
|
77
|
+
level: 0 high / 1 low). Byte offsets are relative to the manufacturer data,
|
|
78
|
+
after the leading 6-byte MAC.
|
|
79
|
+
"""
|
|
80
|
+
if mfr_data is None or len(mfr_data) < 10:
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
device_data = mfr_data[6:]
|
|
84
|
+
|
|
85
|
+
_seq_num = device_data[0]
|
|
86
|
+
_charging = bool(device_data[1] & 0b10000000)
|
|
87
|
+
_battery = device_data[1] & 0b01111111
|
|
88
|
+
_state = device_data[2]
|
|
89
|
+
_isOn = bool(_state & 0b10000000)
|
|
90
|
+
_mode = (_state & 0b01110000) >> 4
|
|
91
|
+
_night_light_on = bool(_state & 0b00000100)
|
|
92
|
+
# bit3: 0 = level 1 (high / bright), 1 = level 2 (low / dim)
|
|
93
|
+
_night_light_level = 2 if _state & 0b00001000 else 1
|
|
94
|
+
_oscillate_left_and_right = bool(_state & 0b00000010)
|
|
95
|
+
_oscillate_up_and_down = bool(_state & 0b00000001)
|
|
96
|
+
_speed = device_data[3] & 0b01111111
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"sequence_number": _seq_num,
|
|
100
|
+
"isOn": _isOn,
|
|
101
|
+
"mode": _CIRCULATOR_FAN_PRO_MODE_MAP.get(_mode),
|
|
102
|
+
"night_light_is_on": _night_light_on,
|
|
103
|
+
"night_light_level": _night_light_level if _night_light_on else 0,
|
|
104
|
+
"oscillating": _oscillate_left_and_right or _oscillate_up_and_down,
|
|
105
|
+
"oscillating_horizontal": _oscillate_left_and_right,
|
|
106
|
+
"oscillating_vertical": _oscillate_up_and_down,
|
|
107
|
+
"battery": _battery,
|
|
108
|
+
"charging": _charging,
|
|
109
|
+
"speed": _speed,
|
|
110
|
+
}
|
|
@@ -11,6 +11,7 @@ from .evaporative_humidifier import (
|
|
|
11
11
|
HumidifierWaterLevel,
|
|
12
12
|
)
|
|
13
13
|
from .fan import (
|
|
14
|
+
CirculatorFanProMode,
|
|
14
15
|
FanMode,
|
|
15
16
|
HorizontalOscillationAngle,
|
|
16
17
|
NightLightState,
|
|
@@ -87,6 +88,7 @@ class SwitchbotModel(StrEnum):
|
|
|
87
88
|
ROLLER_SHADE = "Roller Shade"
|
|
88
89
|
HUBMINI_MATTER = "HubMini Matter"
|
|
89
90
|
CIRCULATOR_FAN = "Circulator Fan"
|
|
91
|
+
CIRCULATOR_FAN_PRO = "Circulator Fan Pro"
|
|
90
92
|
STANDING_FAN = "Standing Fan"
|
|
91
93
|
K20_VACUUM = "K20 Vacuum"
|
|
92
94
|
S10_VACUUM = "S10 Vacuum"
|
|
@@ -135,6 +137,7 @@ __all__ = [
|
|
|
135
137
|
"AirQualityLevel",
|
|
136
138
|
"BulbColorMode",
|
|
137
139
|
"CeilingLightColorMode",
|
|
140
|
+
"CirculatorFanProMode",
|
|
138
141
|
"ClimateAction",
|
|
139
142
|
"ClimateMode",
|
|
140
143
|
"ColorMode",
|
|
@@ -26,6 +26,23 @@ class StandingFanMode(Enum):
|
|
|
26
26
|
return [mode.name.lower() for mode in cls]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class CirculatorFanProMode(Enum):
|
|
30
|
+
"""
|
|
31
|
+
Circulator Fan Pro (W1160) running modes.
|
|
32
|
+
|
|
33
|
+
Mode 0x04 is hurricane, not the baby mode of the legacy fan.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
NORMAL = 1
|
|
37
|
+
NATURAL = 2
|
|
38
|
+
SLEEP = 3
|
|
39
|
+
HURRICANE = 4
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_modes(cls) -> list[str]:
|
|
43
|
+
return [mode.name.lower() for mode in cls]
|
|
44
|
+
|
|
45
|
+
|
|
29
46
|
class NightLightState(Enum):
|
|
30
47
|
"""Standing Fan night-light command values."""
|
|
31
48
|
|
|
@@ -105,6 +105,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
|
105
105
|
"W1102004": SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
|
106
106
|
"W1163000": SwitchbotModel.RGBICWW_LIGHT_BARS,
|
|
107
107
|
"W1162000": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
108
|
+
"W1160000": SwitchbotModel.CIRCULATOR_FAN_PRO,
|
|
108
109
|
"W1104000": SwitchbotModel.PLUG_MINI_EU,
|
|
109
110
|
"W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
|
|
110
111
|
"W1111000": SwitchbotModel.CLIMATE_PANEL,
|
|
@@ -6,7 +6,9 @@ import logging
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Any, ClassVar
|
|
8
8
|
|
|
9
|
+
from ..const import SwitchbotModel
|
|
9
10
|
from ..const.fan import (
|
|
11
|
+
CirculatorFanProMode,
|
|
10
12
|
FanMode,
|
|
11
13
|
HorizontalOscillationAngle,
|
|
12
14
|
NightLightState,
|
|
@@ -15,6 +17,7 @@ from ..const.fan import (
|
|
|
15
17
|
)
|
|
16
18
|
from .device import (
|
|
17
19
|
DEVICE_GET_BASIC_SETTINGS_KEY,
|
|
20
|
+
SwitchbotEncryptedDevice,
|
|
18
21
|
SwitchbotSequenceDevice,
|
|
19
22
|
update_after_operation,
|
|
20
23
|
)
|
|
@@ -186,6 +189,11 @@ class SwitchbotFan(SwitchbotSequenceDevice):
|
|
|
186
189
|
"""Return cached mode."""
|
|
187
190
|
return self._get_adv_value("mode")
|
|
188
191
|
|
|
192
|
+
@property
|
|
193
|
+
def fan_modes(self) -> list[str]:
|
|
194
|
+
"""Return the supported preset (wind) modes for this device."""
|
|
195
|
+
return self._mode_enum.get_modes()
|
|
196
|
+
|
|
189
197
|
|
|
190
198
|
class SwitchbotStandingFan(SwitchbotFan):
|
|
191
199
|
"""Representation of a Switchbot Standing Fan (FAN2)."""
|
|
@@ -309,3 +317,114 @@ class SwitchbotStandingFan(SwitchbotFan):
|
|
|
309
317
|
def get_auto_recenter(self) -> bool | None:
|
|
310
318
|
"""Return cached auto-recenter (return-to-center) state."""
|
|
311
319
|
return self._get_adv_value("auto_recenter")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class SwitchbotCirculatorFanPro(SwitchbotEncryptedDevice, SwitchbotFan):
|
|
323
|
+
"""
|
|
324
|
+
Representation of a Switchbot Circulator Fan Pro (W1160).
|
|
325
|
+
|
|
326
|
+
The Pro uses extended commands (``57 0F <subcmd> …``) with a control-source
|
|
327
|
+
byte (0x29 = Home Assistant), wrapped in the encrypted command shell, so it
|
|
328
|
+
extends SwitchbotEncryptedDevice. Fan power uses subcommand 0x41 (open/close
|
|
329
|
+
sub-op 0x11); the night light uses subcommand 0x96 and supports on/off plus
|
|
330
|
+
a choice between two brightness levels via ``turn_on_light(low=...)``
|
|
331
|
+
(level 1 / bright or level 2 / dim).
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
_model = SwitchbotModel.CIRCULATOR_FAN_PRO
|
|
335
|
+
|
|
336
|
+
# Fan power: ext 0x0F, subcmd 0x41, 0x11 = power, 0x29 = control source
|
|
337
|
+
# (Home Assistant), byte5 0x01 = on / 0x00 = off / 0x02 = toggle.
|
|
338
|
+
_turn_on_command = "570f41112901"
|
|
339
|
+
_turn_off_command = "570f41112900"
|
|
340
|
+
# Preset mode: the 0x11 power command also carries the running mode in byte6
|
|
341
|
+
# (turning the fan on). The Pro's mode 0x04 is hurricane, not the legacy baby.
|
|
342
|
+
_mode_enum: ClassVar[type[Enum]] = CirculatorFanProMode
|
|
343
|
+
_command_set_mode: ClassVar[dict[str, str]] = {
|
|
344
|
+
mode.name.lower(): f"570f41112901{mode.value:02X}"
|
|
345
|
+
for mode in CirculatorFanProMode
|
|
346
|
+
}
|
|
347
|
+
# Night light: ext 0x0F, subcmd 0x96, byte3 0x0A, byte4 0x02, then a state
|
|
348
|
+
# byte: bit0 = on/off (0 off / 1 on), bit1 = level (0 high / 1 low).
|
|
349
|
+
# off = 0x00, on high = 0x01, on low = 0x03.
|
|
350
|
+
_night_light_command = "570f960a02{}"
|
|
351
|
+
# Oscillation: ext 0x0F, subcmd 0x02, 0x29 = control source (Home Assistant),
|
|
352
|
+
# then per-axis action bytes [horizontal, vertical] where 0x01 = start,
|
|
353
|
+
# 0x02 = stop, 0xFF = keep current. The Pro is dual-axis, so the all-axes
|
|
354
|
+
# start/stop variants toggle both axes at once.
|
|
355
|
+
_command_start_oscillation: ClassVar[str] = "570f4102290101"
|
|
356
|
+
_command_stop_oscillation: ClassVar[str] = "570f4102290202"
|
|
357
|
+
_command_start_horizontal_oscillation: ClassVar[str] = "570f41022901ff"
|
|
358
|
+
_command_stop_horizontal_oscillation: ClassVar[str] = "570f41022902ff"
|
|
359
|
+
_command_start_vertical_oscillation: ClassVar[str] = "570f410229ff01"
|
|
360
|
+
_command_stop_vertical_oscillation: ClassVar[str] = "570f410229ff02"
|
|
361
|
+
|
|
362
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
363
|
+
"""
|
|
364
|
+
Get device basic info.
|
|
365
|
+
|
|
366
|
+
The Pro carries all runtime state (fan + night light) in its
|
|
367
|
+
advertisement, so only the firmware is read here.
|
|
368
|
+
"""
|
|
369
|
+
if not (_data1 := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
|
|
370
|
+
return None
|
|
371
|
+
if len(_data1) <= 2:
|
|
372
|
+
return None
|
|
373
|
+
return {"firmware": _data1[2] / 10.0}
|
|
374
|
+
|
|
375
|
+
@update_after_operation
|
|
376
|
+
async def set_percentage(self, percentage: int) -> bool:
|
|
377
|
+
"""
|
|
378
|
+
Set the fan speed (1-100).
|
|
379
|
+
|
|
380
|
+
Speed lives in byte7 of the 0x11 power command and only applies in
|
|
381
|
+
direct mode, so this sends "on + direct mode + speed".
|
|
382
|
+
"""
|
|
383
|
+
percentage = max(1, min(100, percentage))
|
|
384
|
+
result = await self._send_command(f"570f4111290101{percentage:02X}")
|
|
385
|
+
return self._check_command_result(result, 0, {1})
|
|
386
|
+
|
|
387
|
+
@update_after_operation
|
|
388
|
+
async def set_horizontal_oscillation(self, oscillating: bool) -> bool:
|
|
389
|
+
"""Send command to set fan horizontal (left-right) oscillation only."""
|
|
390
|
+
cmd = (
|
|
391
|
+
self._command_start_horizontal_oscillation
|
|
392
|
+
if oscillating
|
|
393
|
+
else self._command_stop_horizontal_oscillation
|
|
394
|
+
)
|
|
395
|
+
result = await self._send_command(cmd)
|
|
396
|
+
return self._check_command_result(result, 0, {1})
|
|
397
|
+
|
|
398
|
+
@update_after_operation
|
|
399
|
+
async def set_vertical_oscillation(self, oscillating: bool) -> bool:
|
|
400
|
+
"""Send command to set fan vertical (up-down) oscillation only."""
|
|
401
|
+
cmd = (
|
|
402
|
+
self._command_start_vertical_oscillation
|
|
403
|
+
if oscillating
|
|
404
|
+
else self._command_stop_vertical_oscillation
|
|
405
|
+
)
|
|
406
|
+
result = await self._send_command(cmd)
|
|
407
|
+
return self._check_command_result(result, 0, {1})
|
|
408
|
+
|
|
409
|
+
@update_after_operation
|
|
410
|
+
async def turn_on_light(self, low: bool = False) -> bool:
|
|
411
|
+
"""Turn the night light on (low selects level 2 / dim, else level 1 / bright)."""
|
|
412
|
+
state = 0x03 if low else 0x01
|
|
413
|
+
result = await self._send_command(
|
|
414
|
+
self._night_light_command.format(f"{state:02X}")
|
|
415
|
+
)
|
|
416
|
+
return self._check_command_result(result, 0, {1})
|
|
417
|
+
|
|
418
|
+
@update_after_operation
|
|
419
|
+
async def turn_off_light(self) -> bool:
|
|
420
|
+
"""Turn the night light off."""
|
|
421
|
+
result = await self._send_command(self._night_light_command.format("00"))
|
|
422
|
+
return self._check_command_result(result, 0, {1})
|
|
423
|
+
|
|
424
|
+
def is_night_light_on(self) -> bool | None:
|
|
425
|
+
"""Return the cached night-light power state."""
|
|
426
|
+
return self._get_adv_value("night_light_is_on")
|
|
427
|
+
|
|
428
|
+
def get_night_light_level(self) -> int | None:
|
|
429
|
+
"""Return the cached night-light level (1 high, 2 low, 0 off)."""
|
|
430
|
+
return self._get_adv_value("night_light_level")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.5.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|