PySwitchbot 2.4.1__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.4.1 → pyswitchbot-2.6.0}/PKG-INFO +1 -1
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/pyproject.toml +1 -1
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/__init__.py +10 -1
- pyswitchbot-2.6.0/switchbot/__version__.py +1 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parser.py +30 -2
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/climate_panel.py +18 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/fan.py +49 -1
- pyswitchbot-2.6.0/switchbot/adv_parsers/remote.py +42 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/__init__.py +4 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/fan.py +17 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/device.py +2 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/fan.py +119 -0
- pyswitchbot-2.6.0/switchbot/devices/universal_remote.py +23 -0
- pyswitchbot-2.4.1/switchbot/__version__.py +0 -1
- pyswitchbot-2.4.1/switchbot/adv_parsers/remote.py +0 -23
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/LICENSE +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/README.md +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/_sensor_th.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/art_frame.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/keypad_vision.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/presence_sensor.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/adv_parsers/weather_station.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/climate.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/light.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/const/presence_sensor.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/art_frame.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/keypad_vision.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/meter_pro.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/enum.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-2.4.1 → pyswitchbot-2.6.0}/switchbot/models.py +0 -0
- {pyswitchbot-2.4.1 → 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 (
|
|
@@ -70,6 +75,7 @@ from .devices.relay_switch import (
|
|
|
70
75
|
)
|
|
71
76
|
from .devices.roller_shade import SwitchbotRollerShade
|
|
72
77
|
from .devices.smart_thermostat_radiator import SwitchbotSmartThermostatRadiator
|
|
78
|
+
from .devices.universal_remote import SwitchbotUniversalRemote
|
|
73
79
|
from .devices.vacuum import SwitchbotVacuum
|
|
74
80
|
from .discovery import GetSwitchbotDevices
|
|
75
81
|
from .models import SwitchBotAdvertisement
|
|
@@ -79,6 +85,7 @@ __all__ = [
|
|
|
79
85
|
"AirQualityLevel",
|
|
80
86
|
"BulbColorMode",
|
|
81
87
|
"CeilingLightColorMode",
|
|
88
|
+
"CirculatorFanProMode",
|
|
82
89
|
"ClimateAction",
|
|
83
90
|
"ClimateMode",
|
|
84
91
|
"ColorMode",
|
|
@@ -105,6 +112,7 @@ __all__ = [
|
|
|
105
112
|
"SwitchbotBulb",
|
|
106
113
|
"SwitchbotCandleWarmerLamp",
|
|
107
114
|
"SwitchbotCeilingLight",
|
|
115
|
+
"SwitchbotCirculatorFanPro",
|
|
108
116
|
"SwitchbotCurtain",
|
|
109
117
|
"SwitchbotDevice",
|
|
110
118
|
"SwitchbotEncryptedDevice",
|
|
@@ -131,6 +139,7 @@ __all__ = [
|
|
|
131
139
|
"SwitchbotStandingFan",
|
|
132
140
|
"SwitchbotStripLight3",
|
|
133
141
|
"SwitchbotSupportedType",
|
|
142
|
+
"SwitchbotUniversalRemote",
|
|
134
143
|
"SwitchbotVacuum",
|
|
135
144
|
"VerticalOscillationAngle",
|
|
136
145
|
"close_stale_connections",
|
|
@@ -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
|
|
@@ -51,7 +55,7 @@ from .adv_parsers.relay_switch import (
|
|
|
51
55
|
process_relay_switch_2pm,
|
|
52
56
|
process_relay_switch_common_data,
|
|
53
57
|
)
|
|
54
|
-
from .adv_parsers.remote import process_woremote
|
|
58
|
+
from .adv_parsers.remote import process_woremote, process_wouniversal_remote
|
|
55
59
|
from .adv_parsers.roller_shade import process_worollershade
|
|
56
60
|
from .adv_parsers.smart_thermostat_radiator import process_smart_thermostat_radiator
|
|
57
61
|
from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
|
|
@@ -402,6 +406,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
402
406
|
"func": process_woremote,
|
|
403
407
|
"manufacturer_id": 89,
|
|
404
408
|
},
|
|
409
|
+
"\x07": {
|
|
410
|
+
"modelName": SwitchbotModel.UNIVERSAL_REMOTE,
|
|
411
|
+
"modelFriendlyName": "Universal Remote",
|
|
412
|
+
"func": process_wouniversal_remote,
|
|
413
|
+
"manufacturer_id": 2409,
|
|
414
|
+
},
|
|
415
|
+
"'": {
|
|
416
|
+
"modelName": SwitchbotModel.UNIVERSAL_REMOTE,
|
|
417
|
+
"modelFriendlyName": "Universal Remote",
|
|
418
|
+
"func": process_wouniversal_remote,
|
|
419
|
+
"manufacturer_id": 2409,
|
|
420
|
+
},
|
|
405
421
|
",": {
|
|
406
422
|
"modelName": SwitchbotModel.ROLLER_SHADE,
|
|
407
423
|
"modelFriendlyName": "Roller Shade",
|
|
@@ -702,6 +718,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
702
718
|
"func": process_rgbicww_ceiling_light,
|
|
703
719
|
"manufacturer_id": 2409,
|
|
704
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
|
+
},
|
|
705
733
|
b"\x00\x10\xd0\xb7": {
|
|
706
734
|
"modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
|
|
707
735
|
"modelFriendlyName": "Permanent Outdoor Light",
|
|
@@ -28,6 +28,18 @@ def process_climate_panel(
|
|
|
28
28
|
pir_state = bool(mfr_data[15] & 0x80)
|
|
29
29
|
is_light = ((mfr_data[15] >> 1) & 0x03) == 0x02
|
|
30
30
|
|
|
31
|
+
# Keystate bytes report physical ON/OFF button presses. For each byte,
|
|
32
|
+
# Bit[7:5] is the press mode (0: power-on init, 1: single, 2: double,
|
|
33
|
+
# 3: long) and Bit[4:0] is a 1-30 cyclic counter that resets to 1 when the
|
|
34
|
+
# mode changes. A value of 0x00 is the power-on state and must not trigger.
|
|
35
|
+
# The raw byte is also exposed so consumers can watch it for any change.
|
|
36
|
+
on_keystate = mfr_data[13]
|
|
37
|
+
off_keystate = mfr_data[14]
|
|
38
|
+
on_keystate_mode = (on_keystate >> 5) & 0x07
|
|
39
|
+
on_keystate_counter = on_keystate & 0x1F
|
|
40
|
+
off_keystate_mode = (off_keystate >> 5) & 0x07
|
|
41
|
+
off_keystate_counter = off_keystate & 0x1F
|
|
42
|
+
|
|
31
43
|
result = {
|
|
32
44
|
"sequence_number": seq_number,
|
|
33
45
|
"isOn": isOn,
|
|
@@ -38,6 +50,12 @@ def process_climate_panel(
|
|
|
38
50
|
"humidity_alarm": humidity_alarm,
|
|
39
51
|
"motion_detected": pir_state,
|
|
40
52
|
"is_light": is_light,
|
|
53
|
+
"on_keystate": on_keystate,
|
|
54
|
+
"off_keystate": off_keystate,
|
|
55
|
+
"on_keystate_mode": on_keystate_mode,
|
|
56
|
+
"on_keystate_counter": on_keystate_counter,
|
|
57
|
+
"off_keystate_mode": off_keystate_mode,
|
|
58
|
+
"off_keystate_counter": off_keystate_counter,
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
_LOGGER.debug(
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Remote adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_woremote(
|
|
11
|
+
data: bytes | None, mfr_data: bytes | None
|
|
12
|
+
) -> dict[str, int | None]:
|
|
13
|
+
"""Process WoRemote adv data."""
|
|
14
|
+
if data is None or len(data) < 3:
|
|
15
|
+
return {
|
|
16
|
+
"battery": None,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_LOGGER.debug("data: %s", data.hex())
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"battery": data[2] & 0b01111111,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def process_wouniversal_remote(
|
|
27
|
+
data: bytes | None, mfr_data: bytes | None
|
|
28
|
+
) -> dict[str, bool | int | None]:
|
|
29
|
+
"""Process Universal Remote adv data."""
|
|
30
|
+
if mfr_data is None or len(mfr_data) < 8:
|
|
31
|
+
return {
|
|
32
|
+
"battery": None,
|
|
33
|
+
"charging": None,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
|
37
|
+
|
|
38
|
+
# mfr_data[7]: bit 7 is charging; bits 6-0 are battery percentage.
|
|
39
|
+
return {
|
|
40
|
+
"battery": mfr_data[7] & 0b01111111,
|
|
41
|
+
"charging": bool((mfr_data[7] >> 7) & 1),
|
|
42
|
+
}
|
|
@@ -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,
|
|
@@ -82,10 +83,12 @@ class SwitchbotModel(StrEnum):
|
|
|
82
83
|
RELAY_SWITCH_1PM = "Relay Switch 1PM"
|
|
83
84
|
RELAY_SWITCH_1 = "Relay Switch 1"
|
|
84
85
|
REMOTE = "WoRemote"
|
|
86
|
+
UNIVERSAL_REMOTE = "WoUniversalRemote"
|
|
85
87
|
EVAPORATIVE_HUMIDIFIER = "Evaporative Humidifier"
|
|
86
88
|
ROLLER_SHADE = "Roller Shade"
|
|
87
89
|
HUBMINI_MATTER = "HubMini Matter"
|
|
88
90
|
CIRCULATOR_FAN = "Circulator Fan"
|
|
91
|
+
CIRCULATOR_FAN_PRO = "Circulator Fan Pro"
|
|
89
92
|
STANDING_FAN = "Standing Fan"
|
|
90
93
|
K20_VACUUM = "K20 Vacuum"
|
|
91
94
|
S10_VACUUM = "S10 Vacuum"
|
|
@@ -134,6 +137,7 @@ __all__ = [
|
|
|
134
137
|
"AirQualityLevel",
|
|
135
138
|
"BulbColorMode",
|
|
136
139
|
"CeilingLightColorMode",
|
|
140
|
+
"CirculatorFanProMode",
|
|
137
141
|
"ClimateAction",
|
|
138
142
|
"ClimateMode",
|
|
139
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
|
|
|
@@ -74,6 +74,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
|
74
74
|
"WoBlindTilt": SwitchbotModel.BLIND_TILT,
|
|
75
75
|
"WoIOSensor": SwitchbotModel.IO_METER, # Outdoor Meter
|
|
76
76
|
"WoButton": SwitchbotModel.REMOTE, # Remote button
|
|
77
|
+
"WoUniversalRemote": SwitchbotModel.UNIVERSAL_REMOTE, # Universal Remote
|
|
77
78
|
"WoLinkMini": SwitchbotModel.HUBMINI_MATTER, # Hub Mini
|
|
78
79
|
"WoFan2": SwitchbotModel.CIRCULATOR_FAN,
|
|
79
80
|
"WoHub2": SwitchbotModel.HUB2,
|
|
@@ -104,6 +105,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
|
104
105
|
"W1102004": SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
|
105
106
|
"W1163000": SwitchbotModel.RGBICWW_LIGHT_BARS,
|
|
106
107
|
"W1162000": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
108
|
+
"W1160000": SwitchbotModel.CIRCULATOR_FAN_PRO,
|
|
107
109
|
"W1104000": SwitchbotModel.PLUG_MINI_EU,
|
|
108
110
|
"W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
|
|
109
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")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot Universal Remote."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .device import SwitchbotDevice
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SwitchbotUniversalRemote(SwitchbotDevice):
|
|
14
|
+
"""Representation of a Switchbot Universal Remote."""
|
|
15
|
+
|
|
16
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
17
|
+
"""Get device basic settings."""
|
|
18
|
+
if not (_data := await self._get_basic_info()):
|
|
19
|
+
return None
|
|
20
|
+
return {
|
|
21
|
+
"battery": _data[1],
|
|
22
|
+
"charging": bool(_data[12]),
|
|
23
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.4.1"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Remote adv parser."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
_LOGGER = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def process_woremote(
|
|
11
|
-
data: bytes | None, mfr_data: bytes | None
|
|
12
|
-
) -> dict[str, int | None]:
|
|
13
|
-
"""Process WoRemote adv data."""
|
|
14
|
-
if data is None or len(data) < 3:
|
|
15
|
-
return {
|
|
16
|
-
"battery": None,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_LOGGER.debug("data: %s", data.hex())
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
"battery": data[2] & 0b01111111,
|
|
23
|
-
}
|
|
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
|