PySwitchbot 0.59.0__tar.gz → 0.60.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-0.59.0 → pyswitchbot-0.60.0}/PKG-INFO +1 -1
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/PySwitchbot.egg-info/SOURCES.txt +4 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/setup.py +1 -1
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/__init__.py +4 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parser.py +7 -0
- pyswitchbot-0.60.0/switchbot/adv_parsers/fan.py +33 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/const/__init__.py +2 -0
- pyswitchbot-0.60.0/switchbot/const/fan.py +14 -0
- pyswitchbot-0.60.0/switchbot/devices/fan.py +116 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_adv_parser.py +94 -0
- pyswitchbot-0.60.0/tests/test_fan.py +172 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/LICENSE +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/README.md +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/pyproject.toml +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/setup.cfg +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/device.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_relay_switch.py +0 -0
- {pyswitchbot-0.59.0 → pyswitchbot-0.60.0}/tests/test_roller_shade.py +0 -0
|
@@ -22,6 +22,7 @@ switchbot/adv_parsers/bulb.py
|
|
|
22
22
|
switchbot/adv_parsers/ceiling_light.py
|
|
23
23
|
switchbot/adv_parsers/contact.py
|
|
24
24
|
switchbot/adv_parsers/curtain.py
|
|
25
|
+
switchbot/adv_parsers/fan.py
|
|
25
26
|
switchbot/adv_parsers/hub2.py
|
|
26
27
|
switchbot/adv_parsers/hubmini_matter.py
|
|
27
28
|
switchbot/adv_parsers/humidifier.py
|
|
@@ -37,6 +38,7 @@ switchbot/adv_parsers/remote.py
|
|
|
37
38
|
switchbot/adv_parsers/roller_shade.py
|
|
38
39
|
switchbot/const/__init__.py
|
|
39
40
|
switchbot/const/evaporative_humidifier.py
|
|
41
|
+
switchbot/const/fan.py
|
|
40
42
|
switchbot/const/hub2.py
|
|
41
43
|
switchbot/const/lock.py
|
|
42
44
|
switchbot/devices/__init__.py
|
|
@@ -50,6 +52,7 @@ switchbot/devices/contact.py
|
|
|
50
52
|
switchbot/devices/curtain.py
|
|
51
53
|
switchbot/devices/device.py
|
|
52
54
|
switchbot/devices/evaporative_humidifier.py
|
|
55
|
+
switchbot/devices/fan.py
|
|
53
56
|
switchbot/devices/humidifier.py
|
|
54
57
|
switchbot/devices/keypad.py
|
|
55
58
|
switchbot/devices/light_strip.py
|
|
@@ -64,6 +67,7 @@ tests/test_base_cover.py
|
|
|
64
67
|
tests/test_blind_tilt.py
|
|
65
68
|
tests/test_curtain.py
|
|
66
69
|
tests/test_evaporative_humidifier.py
|
|
70
|
+
tests/test_fan.py
|
|
67
71
|
tests/test_hub2.py
|
|
68
72
|
tests/test_relay_switch.py
|
|
69
73
|
tests/test_roller_shade.py
|
|
@@ -10,6 +10,7 @@ from bleak_retry_connector import (
|
|
|
10
10
|
|
|
11
11
|
from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
|
|
12
12
|
from .const import (
|
|
13
|
+
FanMode,
|
|
13
14
|
LockStatus,
|
|
14
15
|
SwitchbotAccountConnectionError,
|
|
15
16
|
SwitchbotApiError,
|
|
@@ -24,6 +25,7 @@ from .devices.ceiling_light import SwitchbotCeilingLight
|
|
|
24
25
|
from .devices.curtain import SwitchbotCurtain
|
|
25
26
|
from .devices.device import ColorMode, SwitchbotDevice, SwitchbotEncryptedDevice
|
|
26
27
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
28
|
+
from .devices.fan import SwitchbotFan
|
|
27
29
|
from .devices.humidifier import SwitchbotHumidifier
|
|
28
30
|
from .devices.light_strip import SwitchbotLightStrip
|
|
29
31
|
from .devices.lock import SwitchbotLock
|
|
@@ -35,6 +37,7 @@ from .models import SwitchBotAdvertisement
|
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
37
39
|
"ColorMode",
|
|
40
|
+
"FanMode",
|
|
38
41
|
"GetSwitchbotDevices",
|
|
39
42
|
"LockStatus",
|
|
40
43
|
"SwitchBotAdvertisement",
|
|
@@ -51,6 +54,7 @@ __all__ = [
|
|
|
51
54
|
"SwitchbotDevice",
|
|
52
55
|
"SwitchbotEncryptedDevice",
|
|
53
56
|
"SwitchbotEvaporativeHumidifier",
|
|
57
|
+
"SwitchbotFan",
|
|
54
58
|
"SwitchbotHumidifier",
|
|
55
59
|
"SwitchbotLightStrip",
|
|
56
60
|
"SwitchbotLock",
|
|
@@ -16,6 +16,7 @@ from .adv_parsers.bulb import process_color_bulb
|
|
|
16
16
|
from .adv_parsers.ceiling_light import process_woceiling
|
|
17
17
|
from .adv_parsers.contact import process_wocontact
|
|
18
18
|
from .adv_parsers.curtain import process_wocurtain
|
|
19
|
+
from .adv_parsers.fan import process_fan
|
|
19
20
|
from .adv_parsers.hub2 import process_wohub2
|
|
20
21
|
from .adv_parsers.hubmini_matter import process_hubmini_matter
|
|
21
22
|
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
|
|
@@ -230,6 +231,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
230
231
|
"func": process_hubmini_matter,
|
|
231
232
|
"manufacturer_id": 2409,
|
|
232
233
|
},
|
|
234
|
+
"~": {
|
|
235
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN,
|
|
236
|
+
"modelFriendlyName": "Circulator Fan",
|
|
237
|
+
"func": process_fan,
|
|
238
|
+
"manufacturer_id": 2409,
|
|
239
|
+
},
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Fan adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..const.fan import FanMode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_fan(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
9
|
+
"""Process fan services data."""
|
|
10
|
+
if mfr_data is None:
|
|
11
|
+
return {}
|
|
12
|
+
|
|
13
|
+
device_data = mfr_data[6:]
|
|
14
|
+
|
|
15
|
+
_seq_num = device_data[0]
|
|
16
|
+
_isOn = bool(device_data[1] & 0b10000000)
|
|
17
|
+
_mode = (device_data[1] & 0b01110000) >> 4
|
|
18
|
+
_mode = FanMode(_mode).name if 1 <= _mode <= 4 else None
|
|
19
|
+
_nightLight = (device_data[1] & 0b00001100) >> 2
|
|
20
|
+
_oscillate_left_and_right = bool(device_data[1] & 0b00000010)
|
|
21
|
+
_oscillate_up_and_down = bool(device_data[1] & 0b00000001)
|
|
22
|
+
_battery = device_data[2] & 0b01111111
|
|
23
|
+
_speed = device_data[3] & 0b01111111
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"sequence_number": _seq_num,
|
|
27
|
+
"isOn": _isOn,
|
|
28
|
+
"mode": _mode,
|
|
29
|
+
"nightLight": _nightLight,
|
|
30
|
+
"oscillating": _oscillate_left_and_right | _oscillate_up_and_down,
|
|
31
|
+
"battery": _battery,
|
|
32
|
+
"speed": _speed,
|
|
33
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from ..enum import StrEnum
|
|
6
|
+
from .fan import FanMode as FanMode
|
|
6
7
|
|
|
7
8
|
# Preserve old LockStatus export for backwards compatibility
|
|
8
9
|
from .lock import LockStatus as LockStatus
|
|
@@ -65,3 +66,4 @@ class SwitchbotModel(StrEnum):
|
|
|
65
66
|
EVAPORATIVE_HUMIDIFIER = "Evaporative Humidifier"
|
|
66
67
|
ROLLER_SHADE = "Roller Shade"
|
|
67
68
|
HUBMINI_MATTER = "HubMini Matter"
|
|
69
|
+
CIRCULATOR_FAN = "Circulator Fan"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..const.fan import FanMode
|
|
9
|
+
from .device import (
|
|
10
|
+
DEVICE_GET_BASIC_SETTINGS_KEY,
|
|
11
|
+
SwitchbotSequenceDevice,
|
|
12
|
+
update_after_operation,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
COMMAND_HEAD = "570f41"
|
|
19
|
+
COMMAND_TURN_ON = f"{COMMAND_HEAD}0101"
|
|
20
|
+
COMMAND_TURN_OFF = f"{COMMAND_HEAD}0102"
|
|
21
|
+
COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
|
|
22
|
+
COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
|
|
23
|
+
COMMAND_SET_MODE = {
|
|
24
|
+
FanMode.NORMAL.name: f"{COMMAND_HEAD}030101ff",
|
|
25
|
+
FanMode.NATURAL.name: f"{COMMAND_HEAD}030102ff",
|
|
26
|
+
FanMode.SLEEP.name: f"{COMMAND_HEAD}030103",
|
|
27
|
+
FanMode.BABY.name: f"{COMMAND_HEAD}030104",
|
|
28
|
+
}
|
|
29
|
+
COMMAND_SET_PERCENTAGE = f"{COMMAND_HEAD}0302" # +speed
|
|
30
|
+
COMMAND_GET_BASIC_INFO = "570f428102"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SwitchbotFan(SwitchbotSequenceDevice):
|
|
34
|
+
"""Representation of a Switchbot Circulator Fan."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, device, password=None, interface=0, **kwargs):
|
|
37
|
+
super().__init__(device, password, interface, **kwargs)
|
|
38
|
+
|
|
39
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
40
|
+
"""Get device basic settings."""
|
|
41
|
+
if not (_data := await self._get_basic_info(COMMAND_GET_BASIC_INFO)):
|
|
42
|
+
return None
|
|
43
|
+
if not (_data1 := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
_LOGGER.debug("data: %s", _data)
|
|
47
|
+
battery = _data[2] & 0b01111111
|
|
48
|
+
isOn = bool(_data[3] & 0b10000000)
|
|
49
|
+
oscillating = bool(_data[3] & 0b01100000)
|
|
50
|
+
_mode = _data[8] & 0b00000111
|
|
51
|
+
mode = FanMode(_mode).name if 1 <= _mode <= 4 else None
|
|
52
|
+
speed = _data[9]
|
|
53
|
+
firmware = _data1[2] / 10.0
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"battery": battery,
|
|
57
|
+
"isOn": isOn,
|
|
58
|
+
"oscillating": oscillating,
|
|
59
|
+
"mode": mode,
|
|
60
|
+
"speed": speed,
|
|
61
|
+
"firmware": firmware,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async def _get_basic_info(self, cmd: str) -> bytes | None:
|
|
65
|
+
"""Return basic info of device."""
|
|
66
|
+
_data = await self._send_command(key=cmd, retry=self._retry_count)
|
|
67
|
+
|
|
68
|
+
if _data in (b"\x07", b"\x00"):
|
|
69
|
+
_LOGGER.error("Unsuccessful, please try again")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
return _data
|
|
73
|
+
|
|
74
|
+
@update_after_operation
|
|
75
|
+
async def set_preset_mode(self, preset_mode: str) -> bool:
|
|
76
|
+
"""Send command to set fan preset_mode."""
|
|
77
|
+
return await self._send_command(COMMAND_SET_MODE[preset_mode])
|
|
78
|
+
|
|
79
|
+
@update_after_operation
|
|
80
|
+
async def set_percentage(self, percentage: int) -> bool:
|
|
81
|
+
"""Send command to set fan percentage."""
|
|
82
|
+
return await self._send_command(f"{COMMAND_SET_PERCENTAGE}{percentage:02X}")
|
|
83
|
+
|
|
84
|
+
@update_after_operation
|
|
85
|
+
async def set_oscillation(self, oscillating: bool) -> bool:
|
|
86
|
+
"""Send command to set fan oscillation"""
|
|
87
|
+
if oscillating:
|
|
88
|
+
return await self._send_command(COMMAND_START_OSCILLATION)
|
|
89
|
+
else:
|
|
90
|
+
return await self._send_command(COMMAND_STOP_OSCILLATION)
|
|
91
|
+
|
|
92
|
+
@update_after_operation
|
|
93
|
+
async def turn_on(self) -> bool:
|
|
94
|
+
"""Turn on the fan."""
|
|
95
|
+
return await self._send_command(COMMAND_TURN_ON)
|
|
96
|
+
|
|
97
|
+
@update_after_operation
|
|
98
|
+
async def turn_off(self) -> bool:
|
|
99
|
+
"""Turn off the fan."""
|
|
100
|
+
return await self._send_command(COMMAND_TURN_OFF)
|
|
101
|
+
|
|
102
|
+
def get_current_percentage(self) -> Any:
|
|
103
|
+
"""Return cached percentage."""
|
|
104
|
+
return self._get_adv_value("speed")
|
|
105
|
+
|
|
106
|
+
def is_on(self) -> bool | None:
|
|
107
|
+
"""Return fan state from cache."""
|
|
108
|
+
return self._get_adv_value("isOn")
|
|
109
|
+
|
|
110
|
+
def get_oscillating_state(self) -> Any:
|
|
111
|
+
"""Return cached oscillating."""
|
|
112
|
+
return self._get_adv_value("oscillating")
|
|
113
|
+
|
|
114
|
+
def get_current_mode(self) -> Any:
|
|
115
|
+
"""Return cached mode."""
|
|
116
|
+
return self._get_adv_value("mode")
|
|
@@ -2103,3 +2103,97 @@ def test_roller_shade_passive() -> None:
|
|
|
2103
2103
|
rssi=-97,
|
|
2104
2104
|
active=False,
|
|
2105
2105
|
)
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
def test_circulator_fan_active() -> None:
|
|
2109
|
+
"""Test parsing circulator fan with active data."""
|
|
2110
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2111
|
+
adv_data = generate_advertisement_data(
|
|
2112
|
+
manufacturer_data={2409: b"\xb0\xe9\xfeXY\xa8~LR9"},
|
|
2113
|
+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
|
|
2114
|
+
rssi=-97,
|
|
2115
|
+
)
|
|
2116
|
+
result = parse_advertisement_data(
|
|
2117
|
+
ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
|
|
2118
|
+
)
|
|
2119
|
+
assert result == SwitchBotAdvertisement(
|
|
2120
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2121
|
+
data={
|
|
2122
|
+
"rawAdvData": b"~\x00R",
|
|
2123
|
+
"data": {
|
|
2124
|
+
"sequence_number": 126,
|
|
2125
|
+
"isOn": False,
|
|
2126
|
+
"mode": "BABY",
|
|
2127
|
+
"nightLight": 3,
|
|
2128
|
+
"oscillating": False,
|
|
2129
|
+
"battery": 82,
|
|
2130
|
+
"speed": 57,
|
|
2131
|
+
},
|
|
2132
|
+
"isEncrypted": False,
|
|
2133
|
+
"model": "~",
|
|
2134
|
+
"modelFriendlyName": "Circulator Fan",
|
|
2135
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN,
|
|
2136
|
+
},
|
|
2137
|
+
device=ble_device,
|
|
2138
|
+
rssi=-97,
|
|
2139
|
+
active=True,
|
|
2140
|
+
)
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
def test_circulator_fan_passive() -> None:
|
|
2144
|
+
"""Test parsing circulator fan with passive data."""
|
|
2145
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2146
|
+
adv_data = generate_advertisement_data(
|
|
2147
|
+
manufacturer_data={2409: b"\xb0\xe9\xfeXY\xa8~LR9"},
|
|
2148
|
+
rssi=-97,
|
|
2149
|
+
)
|
|
2150
|
+
result = parse_advertisement_data(
|
|
2151
|
+
ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
|
|
2152
|
+
)
|
|
2153
|
+
assert result == SwitchBotAdvertisement(
|
|
2154
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2155
|
+
data={
|
|
2156
|
+
"rawAdvData": None,
|
|
2157
|
+
"data": {
|
|
2158
|
+
"sequence_number": 126,
|
|
2159
|
+
"isOn": False,
|
|
2160
|
+
"mode": "BABY",
|
|
2161
|
+
"nightLight": 3,
|
|
2162
|
+
"oscillating": False,
|
|
2163
|
+
"battery": 82,
|
|
2164
|
+
"speed": 57,
|
|
2165
|
+
},
|
|
2166
|
+
"isEncrypted": False,
|
|
2167
|
+
"model": "~",
|
|
2168
|
+
"modelFriendlyName": "Circulator Fan",
|
|
2169
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN,
|
|
2170
|
+
},
|
|
2171
|
+
device=ble_device,
|
|
2172
|
+
rssi=-97,
|
|
2173
|
+
active=False,
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def test_circulator_fan_with_empty_data() -> None:
|
|
2178
|
+
"""Test parsing circulator fan with empty data."""
|
|
2179
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
2180
|
+
adv_data = generate_advertisement_data(
|
|
2181
|
+
manufacturer_data={2409: None},
|
|
2182
|
+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
|
|
2183
|
+
rssi=-97,
|
|
2184
|
+
)
|
|
2185
|
+
result = parse_advertisement_data(
|
|
2186
|
+
ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
|
|
2187
|
+
)
|
|
2188
|
+
assert result == SwitchBotAdvertisement(
|
|
2189
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
2190
|
+
data={
|
|
2191
|
+
"rawAdvData": b"~\x00R",
|
|
2192
|
+
"data": {},
|
|
2193
|
+
"isEncrypted": False,
|
|
2194
|
+
"model": "~",
|
|
2195
|
+
},
|
|
2196
|
+
device=ble_device,
|
|
2197
|
+
rssi=-97,
|
|
2198
|
+
active=True,
|
|
2199
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from bleak.backends.device import BLEDevice
|
|
5
|
+
|
|
6
|
+
from switchbot import SwitchBotAdvertisement, SwitchbotModel
|
|
7
|
+
from switchbot.const.fan import FanMode
|
|
8
|
+
from switchbot.devices import fan
|
|
9
|
+
|
|
10
|
+
from .test_adv_parser import generate_ble_device
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_device_for_command_testing(init_data: dict | None = None):
|
|
14
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
15
|
+
fan_device = fan.SwitchbotFan(ble_device)
|
|
16
|
+
fan_device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
|
|
17
|
+
fan_device._send_command = AsyncMock()
|
|
18
|
+
fan_device.update = AsyncMock()
|
|
19
|
+
return fan_device
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None):
|
|
23
|
+
"""Set advertisement data with defaults."""
|
|
24
|
+
if init_data is None:
|
|
25
|
+
init_data = {}
|
|
26
|
+
|
|
27
|
+
return SwitchBotAdvertisement(
|
|
28
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
29
|
+
data={
|
|
30
|
+
"rawAdvData": b"~\x00R",
|
|
31
|
+
"data": {
|
|
32
|
+
"isOn": True,
|
|
33
|
+
"mode": "NORMAL",
|
|
34
|
+
"nightLight": 3,
|
|
35
|
+
"oscillating": False,
|
|
36
|
+
"battery": 60,
|
|
37
|
+
"speed": 50,
|
|
38
|
+
}
|
|
39
|
+
| init_data,
|
|
40
|
+
"isEncrypted": False,
|
|
41
|
+
"model": ",",
|
|
42
|
+
"modelFriendlyName": "Circulator Fan",
|
|
43
|
+
"modelName": SwitchbotModel.CIRCULATOR_FAN,
|
|
44
|
+
},
|
|
45
|
+
device=ble_device,
|
|
46
|
+
rssi=-80,
|
|
47
|
+
active=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
"response, expected",
|
|
54
|
+
[
|
|
55
|
+
(b"\x00", None),
|
|
56
|
+
(b"\x07", None),
|
|
57
|
+
(b"\x01\x02\x03", b"\x01\x02\x03"),
|
|
58
|
+
],
|
|
59
|
+
)
|
|
60
|
+
async def test__get_basic_info(response, expected):
|
|
61
|
+
fan_device = create_device_for_command_testing()
|
|
62
|
+
fan_device._send_command = AsyncMock(return_value=response)
|
|
63
|
+
result = await fan_device._get_basic_info(cmd="TEST_CMD")
|
|
64
|
+
assert result == expected
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
"basic_info,firmware_info", [(True, False), (False, True), (False, False)]
|
|
70
|
+
)
|
|
71
|
+
async def test_get_basic_info_returns_none(basic_info, firmware_info):
|
|
72
|
+
fan_device = create_device_for_command_testing()
|
|
73
|
+
|
|
74
|
+
async def mock_get_basic_info(arg):
|
|
75
|
+
if arg == fan.COMMAND_GET_BASIC_INFO:
|
|
76
|
+
return basic_info
|
|
77
|
+
elif arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
|
|
78
|
+
return firmware_info
|
|
79
|
+
|
|
80
|
+
fan_device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
|
|
81
|
+
|
|
82
|
+
assert await fan_device.get_basic_info() is None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
@pytest.mark.parametrize(
|
|
87
|
+
"basic_info,firmware_info,result",
|
|
88
|
+
[
|
|
89
|
+
(
|
|
90
|
+
bytearray(b"\x01\x02W\x82g\xf5\xde4\x01=dPP\x03\x14P\x00\x00\x00\x00"),
|
|
91
|
+
bytearray(b"\x01W\x0b\x17\x01"),
|
|
92
|
+
[87, True, False, "NORMAL", 61, 1.1],
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
bytearray(b"\x01\x02U\xc2g\xf5\xde4\x04+dPP\x03\x14P\x00\x00\x00\x00"),
|
|
96
|
+
bytearray(b"\x01U\x0b\x17\x01"),
|
|
97
|
+
[85, True, True, "BABY", 43, 1.1],
|
|
98
|
+
),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
async def test_get_basic_info(basic_info, firmware_info, result):
|
|
102
|
+
fan_device = create_device_for_command_testing()
|
|
103
|
+
|
|
104
|
+
async def mock_get_basic_info(arg):
|
|
105
|
+
if arg == fan.COMMAND_GET_BASIC_INFO:
|
|
106
|
+
return basic_info
|
|
107
|
+
elif arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
|
|
108
|
+
return firmware_info
|
|
109
|
+
|
|
110
|
+
fan_device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
|
|
111
|
+
|
|
112
|
+
info = await fan_device.get_basic_info()
|
|
113
|
+
assert info["battery"] == result[0]
|
|
114
|
+
assert info["isOn"] == result[1]
|
|
115
|
+
assert info["oscillating"] == result[2]
|
|
116
|
+
assert info["mode"] == result[3]
|
|
117
|
+
assert info["speed"] == result[4]
|
|
118
|
+
assert info["firmware"] == result[5]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_set_preset_mode():
|
|
123
|
+
fan_device = create_device_for_command_testing({"mode": "BABY"})
|
|
124
|
+
await fan_device.set_preset_mode("BABY")
|
|
125
|
+
assert fan_device.get_current_mode() == "BABY"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_set_set_percentage_with_speed_is_0():
|
|
130
|
+
fan_device = create_device_for_command_testing({"speed": 0, "isOn": False})
|
|
131
|
+
await fan_device.turn_off()
|
|
132
|
+
assert fan_device.get_current_percentage() == 0
|
|
133
|
+
assert fan_device.is_on() is False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_set_set_percentage():
|
|
138
|
+
fan_device = create_device_for_command_testing({"speed": 80})
|
|
139
|
+
await fan_device.set_percentage(80)
|
|
140
|
+
assert fan_device.get_current_percentage() == 80
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_set_not_oscillation():
|
|
145
|
+
fan_device = create_device_for_command_testing({"oscillating": False})
|
|
146
|
+
await fan_device.set_oscillation(False)
|
|
147
|
+
assert fan_device.get_oscillating_state() is False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_set_oscillation():
|
|
152
|
+
fan_device = create_device_for_command_testing({"oscillating": True})
|
|
153
|
+
await fan_device.set_oscillation(True)
|
|
154
|
+
assert fan_device.get_oscillating_state() is True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_turn_on():
|
|
159
|
+
fan_device = create_device_for_command_testing({"isOn": True})
|
|
160
|
+
await fan_device.turn_on()
|
|
161
|
+
assert fan_device.is_on() is True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
async def test_turn_off():
|
|
166
|
+
fan_device = create_device_for_command_testing({"isOn": False})
|
|
167
|
+
await fan_device.turn_off()
|
|
168
|
+
assert fan_device.is_on() is False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_get_modes():
|
|
172
|
+
assert FanMode.get_modes() == ["NORMAL", "NATURAL", "SLEEP", "BABY"]
|
|
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
|