PySwitchbot 0.44.0__tar.gz → 0.45.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.44.0 → PySwitchbot-0.45.0}/PKG-INFO +1 -1
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/setup.py +1 -1
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/meter.py +5 -1
- PySwitchbot-0.45.0/switchbot/devices/base_cover.py +142 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/base_light.py +3 -4
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/blind_tilt.py +42 -13
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/curtain.py +24 -105
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/tests/test_adv_parser.py +24 -0
- PySwitchbot-0.45.0/tests/test_base_cover.py +152 -0
- PySwitchbot-0.45.0/tests/test_blind_tilt.py +240 -0
- PySwitchbot-0.45.0/tests/test_curtain.py +426 -0
- PySwitchbot-0.44.0/tests/test_curtain.py +0 -242
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/LICENSE +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/MANIFEST.in +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/README.md +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/setup.cfg +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/__init__.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parser.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/__init__.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/bot.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/bulb.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/contact.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/curtain.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/lock.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/motion.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/plug.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/api_config.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/const.py +2 -2
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/__init__.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/bot.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/bulb.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/ceiling_light.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/contact.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/device.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/humidifier.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/light_strip.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/lock.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/meter.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/motion.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/plug.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/discovery.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/enum.py +0 -0
- {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/models.py +0 -0
|
@@ -28,6 +28,7 @@ switchbot/adv_parsers/meter.py
|
|
|
28
28
|
switchbot/adv_parsers/motion.py
|
|
29
29
|
switchbot/adv_parsers/plug.py
|
|
30
30
|
switchbot/devices/__init__.py
|
|
31
|
+
switchbot/devices/base_cover.py
|
|
31
32
|
switchbot/devices/base_light.py
|
|
32
33
|
switchbot/devices/blind_tilt.py
|
|
33
34
|
switchbot/devices/bot.py
|
|
@@ -43,4 +44,6 @@ switchbot/devices/meter.py
|
|
|
43
44
|
switchbot/devices/motion.py
|
|
44
45
|
switchbot/devices/plug.py
|
|
45
46
|
tests/test_adv_parser.py
|
|
47
|
+
tests/test_base_cover.py
|
|
48
|
+
tests/test_blind_tilt.py
|
|
46
49
|
tests/test_curtain.py
|
|
@@ -26,13 +26,17 @@ def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str,
|
|
|
26
26
|
)
|
|
27
27
|
_temp_f = (_temp_c * 9 / 5) + 32
|
|
28
28
|
_temp_f = (_temp_f * 10) / 10
|
|
29
|
+
humidity = temp_data[2] & 0b01111111
|
|
30
|
+
|
|
31
|
+
if _temp_c == 0 and humidity == 0 and battery == 0:
|
|
32
|
+
return {}
|
|
29
33
|
|
|
30
34
|
_wosensorth_data = {
|
|
31
35
|
# Data should be flat, but we keep the original structure for now
|
|
32
36
|
"temp": {"c": _temp_c, "f": _temp_f},
|
|
33
37
|
"temperature": _temp_c,
|
|
34
38
|
"fahrenheit": bool(temp_data[2] & 0b10000000),
|
|
35
|
-
"humidity":
|
|
39
|
+
"humidity": humidity,
|
|
36
40
|
"battery": battery,
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..models import SwitchBotAdvertisement
|
|
9
|
+
from .device import REQ_HEADER, SwitchbotDevice, update_after_operation
|
|
10
|
+
|
|
11
|
+
# Cover keys
|
|
12
|
+
COVER_COMMAND = "4501"
|
|
13
|
+
|
|
14
|
+
# For second element of open and close arrs we should add two bytes i.e. ff00
|
|
15
|
+
# First byte [ff] stands for speed (00 or ff - normal, 01 - slow) *
|
|
16
|
+
# * Only for curtains 3. For other models use ff
|
|
17
|
+
# Second byte [00] is a command (00 - open, 64 - close)
|
|
18
|
+
POSITION_KEYS = [
|
|
19
|
+
f"{REQ_HEADER}{COVER_COMMAND}0101",
|
|
20
|
+
f"{REQ_HEADER}{COVER_COMMAND}05", # +speed
|
|
21
|
+
] # +actual_position
|
|
22
|
+
STOP_KEYS = [f"{REQ_HEADER}{COVER_COMMAND}0001", f"{REQ_HEADER}{COVER_COMMAND}00ff"]
|
|
23
|
+
|
|
24
|
+
COVER_EXT_SUM_KEY = f"{REQ_HEADER}460401"
|
|
25
|
+
COVER_EXT_ADV_KEY = f"{REQ_HEADER}460402"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SwitchbotBaseCover(SwitchbotDevice):
|
|
32
|
+
"""Representation of a Switchbot Cover devices for both curtains and tilt blinds."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, reverse: bool, *args: Any, **kwargs: Any) -> None:
|
|
35
|
+
"""Switchbot Cover device constructor."""
|
|
36
|
+
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self._reverse = reverse
|
|
39
|
+
self._settings: dict[str, Any] = {}
|
|
40
|
+
self.ext_info_sum: dict[str, Any] = {}
|
|
41
|
+
self.ext_info_adv: dict[str, Any] = {}
|
|
42
|
+
self._is_opening: bool = False
|
|
43
|
+
self._is_closing: bool = False
|
|
44
|
+
|
|
45
|
+
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
46
|
+
"""Send multiple commands to device.
|
|
47
|
+
|
|
48
|
+
Since we current have no way to tell which command the device
|
|
49
|
+
needs we send both.
|
|
50
|
+
"""
|
|
51
|
+
final_result = False
|
|
52
|
+
for key in keys:
|
|
53
|
+
result = await self._send_command(key)
|
|
54
|
+
final_result |= self._check_command_result(result, 0, {1})
|
|
55
|
+
return final_result
|
|
56
|
+
|
|
57
|
+
@update_after_operation
|
|
58
|
+
async def stop(self) -> bool:
|
|
59
|
+
"""Send stop command to device."""
|
|
60
|
+
return await self._send_multiple_commands(STOP_KEYS)
|
|
61
|
+
|
|
62
|
+
@update_after_operation
|
|
63
|
+
async def set_position(self, position: int, speed: int = 255) -> bool:
|
|
64
|
+
"""Send position command (0-100) to device. Speed 255 - normal, 1 - slow"""
|
|
65
|
+
position = (100 - position) if self._reverse else position
|
|
66
|
+
return await self._send_multiple_commands(
|
|
67
|
+
[
|
|
68
|
+
f"{POSITION_KEYS[0]}{position:02X}",
|
|
69
|
+
f"{POSITION_KEYS[1]}{speed:02X}{position:02X}",
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_position(self) -> Any:
|
|
75
|
+
"""Return current device position."""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
79
|
+
"""Get device basic settings."""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def get_extended_info_summary(self) -> dict[str, Any] | None:
|
|
83
|
+
"""Get extended info for all devices in chain."""
|
|
84
|
+
|
|
85
|
+
async def get_extended_info_adv(self) -> dict[str, Any] | None:
|
|
86
|
+
"""Get advance page info for device chain."""
|
|
87
|
+
|
|
88
|
+
_data = await self._send_command(key=COVER_EXT_ADV_KEY)
|
|
89
|
+
if not _data:
|
|
90
|
+
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if _data in (b"\x07", b"\x00"):
|
|
94
|
+
_LOGGER.error("%s: Unsuccessful, please try again", self.name)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
_state_of_charge = [
|
|
98
|
+
"not_charging",
|
|
99
|
+
"charging_by_adapter",
|
|
100
|
+
"charging_by_solar",
|
|
101
|
+
"fully_charged",
|
|
102
|
+
"solar_not_charging",
|
|
103
|
+
"charging_error",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
self.ext_info_adv["device0"] = {
|
|
107
|
+
"battery": _data[1],
|
|
108
|
+
"firmware": _data[2] / 10.0,
|
|
109
|
+
"stateOfCharge": _state_of_charge[_data[3]],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# If grouped curtain device present.
|
|
113
|
+
if _data[4]:
|
|
114
|
+
self.ext_info_adv["device1"] = {
|
|
115
|
+
"battery": _data[4],
|
|
116
|
+
"firmware": _data[5] / 10.0,
|
|
117
|
+
"stateOfCharge": _state_of_charge[_data[6]],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return self.ext_info_adv
|
|
121
|
+
|
|
122
|
+
def get_light_level(self) -> Any:
|
|
123
|
+
"""Return cached light level."""
|
|
124
|
+
# To get actual light level call update() first.
|
|
125
|
+
return self._get_adv_value("lightLevel")
|
|
126
|
+
|
|
127
|
+
def is_reversed(self) -> bool:
|
|
128
|
+
"""Return True if curtain position is opposite from SB data."""
|
|
129
|
+
return self._reverse
|
|
130
|
+
|
|
131
|
+
def is_calibrated(self) -> Any:
|
|
132
|
+
"""Return True curtain is calibrated."""
|
|
133
|
+
# To get actual light level call update() first.
|
|
134
|
+
return self._get_adv_value("calibration")
|
|
135
|
+
|
|
136
|
+
def is_opening(self) -> bool:
|
|
137
|
+
"""Return True if the curtain is opening."""
|
|
138
|
+
return self._is_opening
|
|
139
|
+
|
|
140
|
+
def is_closing(self) -> bool:
|
|
141
|
+
"""Return True if the curtain is closing."""
|
|
142
|
+
return self._is_closing
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
5
|
+
import time
|
|
4
6
|
from abc import abstractmethod
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
9
|
+
from ..models import SwitchBotAdvertisement
|
|
7
10
|
from .device import ColorMode, SwitchbotDevice
|
|
8
11
|
|
|
9
12
|
_LOGGER = logging.getLogger(__name__)
|
|
10
|
-
import asyncio
|
|
11
|
-
import time
|
|
12
|
-
|
|
13
|
-
from ..models import SwitchBotAdvertisement
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class SwitchbotBaseLight(SwitchbotDevice):
|
|
@@ -10,27 +10,27 @@ from switchbot.devices.device import (
|
|
|
10
10
|
update_after_operation,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from ..models import SwitchBotAdvertisement
|
|
14
|
+
from .base_cover import COVER_COMMAND, COVER_EXT_SUM_KEY, SwitchbotBaseCover
|
|
14
15
|
|
|
15
16
|
_LOGGER = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
BLIND_COMMAND = "4501"
|
|
19
19
|
OPEN_KEYS = [
|
|
20
|
-
f"{REQ_HEADER}{
|
|
21
|
-
f"{REQ_HEADER}{
|
|
20
|
+
f"{REQ_HEADER}{COVER_COMMAND}010132",
|
|
21
|
+
f"{REQ_HEADER}{COVER_COMMAND}05ff32",
|
|
22
22
|
]
|
|
23
23
|
CLOSE_DOWN_KEYS = [
|
|
24
|
-
f"{REQ_HEADER}{
|
|
25
|
-
f"{REQ_HEADER}{
|
|
24
|
+
f"{REQ_HEADER}{COVER_COMMAND}010100",
|
|
25
|
+
f"{REQ_HEADER}{COVER_COMMAND}05ff00",
|
|
26
26
|
]
|
|
27
27
|
CLOSE_UP_KEYS = [
|
|
28
|
-
f"{REQ_HEADER}{
|
|
29
|
-
f"{REQ_HEADER}{
|
|
28
|
+
f"{REQ_HEADER}{COVER_COMMAND}010164",
|
|
29
|
+
f"{REQ_HEADER}{COVER_COMMAND}05ff64",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class SwitchbotBlindTilt(
|
|
33
|
+
class SwitchbotBlindTilt(SwitchbotBaseCover, SwitchbotSequenceDevice):
|
|
34
34
|
"""Representation of a Switchbot Blind Tilt."""
|
|
35
35
|
|
|
36
36
|
# The position of the blind is saved returned with 0 = closed down, 50 = open and 100 = closed up.
|
|
@@ -43,23 +43,52 @@ class SwitchbotBlindTilt(SwitchbotCurtain, SwitchbotSequenceDevice):
|
|
|
43
43
|
|
|
44
44
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
45
45
|
"""Switchbot Blind Tilt/woBlindTilt constructor."""
|
|
46
|
-
super().__init__(*args, **kwargs)
|
|
47
|
-
|
|
48
46
|
self._reverse: bool = kwargs.pop("reverse_mode", False)
|
|
47
|
+
super().__init__(self._reverse, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def _set_parsed_data(
|
|
50
|
+
self, advertisement: SwitchBotAdvertisement, data: dict[str, Any]
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Set data."""
|
|
53
|
+
in_motion = data["inMotion"]
|
|
54
|
+
previous_tilt = self._get_adv_value("tilt")
|
|
55
|
+
new_tilt = data["tilt"]
|
|
56
|
+
self._update_motion_direction(in_motion, previous_tilt, new_tilt)
|
|
57
|
+
super()._set_parsed_data(advertisement, data)
|
|
58
|
+
|
|
59
|
+
def _update_motion_direction(
|
|
60
|
+
self, in_motion: bool, previous_tilt: int | None, new_tilt: int
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Update opening/closing status based on movement."""
|
|
63
|
+
if previous_tilt is None:
|
|
64
|
+
return
|
|
65
|
+
if in_motion is False:
|
|
66
|
+
self._is_closing = self._is_opening = False
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if new_tilt != previous_tilt:
|
|
70
|
+
self._is_opening = new_tilt > previous_tilt
|
|
71
|
+
self._is_closing = new_tilt < previous_tilt
|
|
49
72
|
|
|
50
73
|
@update_after_operation
|
|
51
74
|
async def open(self) -> bool:
|
|
52
75
|
"""Send open command."""
|
|
76
|
+
self._is_opening = True
|
|
77
|
+
self._is_closing = False
|
|
53
78
|
return await self._send_multiple_commands(OPEN_KEYS)
|
|
54
79
|
|
|
55
80
|
@update_after_operation
|
|
56
81
|
async def close_up(self) -> bool:
|
|
57
82
|
"""Send close up command."""
|
|
83
|
+
self._is_opening = False
|
|
84
|
+
self._is_closing = True
|
|
58
85
|
return await self._send_multiple_commands(CLOSE_UP_KEYS)
|
|
59
86
|
|
|
60
87
|
@update_after_operation
|
|
61
88
|
async def close_down(self) -> bool:
|
|
62
89
|
"""Send close down command."""
|
|
90
|
+
self._is_opening = False
|
|
91
|
+
self._is_closing = True
|
|
63
92
|
return await self._send_multiple_commands(CLOSE_DOWN_KEYS)
|
|
64
93
|
|
|
65
94
|
# The aim of this is to close to the nearest endpoint.
|
|
@@ -114,8 +143,8 @@ class SwitchbotBlindTilt(SwitchbotCurtain, SwitchbotSequenceDevice):
|
|
|
114
143
|
}
|
|
115
144
|
|
|
116
145
|
async def get_extended_info_summary(self) -> dict[str, Any] | None:
|
|
117
|
-
"""Get
|
|
118
|
-
_data = await self._send_command(key=
|
|
146
|
+
"""Get extended info for all devices in chain."""
|
|
147
|
+
_data = await self._send_command(key=COVER_EXT_SUM_KEY)
|
|
119
148
|
|
|
120
149
|
if not _data:
|
|
121
150
|
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
|
|
@@ -4,40 +4,35 @@ from __future__ import annotations
|
|
|
4
4
|
import logging
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from .device import REQ_HEADER,
|
|
10
|
-
|
|
11
|
-
# Curtain keys
|
|
12
|
-
CURTAIN_COMMAND = "4501"
|
|
7
|
+
from ..models import SwitchBotAdvertisement
|
|
8
|
+
from .base_cover import COVER_COMMAND, COVER_EXT_SUM_KEY, SwitchbotBaseCover
|
|
9
|
+
from .device import REQ_HEADER, update_after_operation
|
|
13
10
|
|
|
14
11
|
# For second element of open and close arrs we should add two bytes i.e. ff00
|
|
15
12
|
# First byte [ff] stands for speed (00 or ff - normal, 01 - slow) *
|
|
16
13
|
# * Only for curtains 3. For other models use ff
|
|
17
14
|
# Second byte [00] is a command (00 - open, 64 - close)
|
|
18
15
|
OPEN_KEYS = [
|
|
19
|
-
f"{REQ_HEADER}{
|
|
20
|
-
f"{REQ_HEADER}{
|
|
16
|
+
f"{REQ_HEADER}{COVER_COMMAND}010100",
|
|
17
|
+
f"{REQ_HEADER}{COVER_COMMAND}05", # +speed + "00"
|
|
21
18
|
]
|
|
22
19
|
CLOSE_KEYS = [
|
|
23
|
-
f"{REQ_HEADER}{
|
|
24
|
-
f"{REQ_HEADER}{
|
|
20
|
+
f"{REQ_HEADER}{COVER_COMMAND}010164",
|
|
21
|
+
f"{REQ_HEADER}{COVER_COMMAND}05", # +speed + "64"
|
|
25
22
|
]
|
|
26
23
|
POSITION_KEYS = [
|
|
27
|
-
f"{REQ_HEADER}{
|
|
28
|
-
f"{REQ_HEADER}{
|
|
24
|
+
f"{REQ_HEADER}{COVER_COMMAND}0101",
|
|
25
|
+
f"{REQ_HEADER}{COVER_COMMAND}05", # +speed
|
|
29
26
|
] # +actual_position
|
|
30
|
-
STOP_KEYS = [f"{REQ_HEADER}{
|
|
27
|
+
STOP_KEYS = [f"{REQ_HEADER}{COVER_COMMAND}0001", f"{REQ_HEADER}{COVER_COMMAND}00ff"]
|
|
31
28
|
|
|
32
|
-
CURTAIN_EXT_SUM_KEY = f"{REQ_HEADER}460401"
|
|
33
|
-
CURTAIN_EXT_ADV_KEY = f"{REQ_HEADER}460402"
|
|
34
29
|
CURTAIN_EXT_CHAIN_INFO_KEY = f"{REQ_HEADER}468101"
|
|
35
30
|
|
|
36
31
|
|
|
37
32
|
_LOGGER = logging.getLogger(__name__)
|
|
38
33
|
|
|
39
34
|
|
|
40
|
-
class SwitchbotCurtain(
|
|
35
|
+
class SwitchbotCurtain(SwitchbotBaseCover):
|
|
41
36
|
"""Representation of a Switchbot Curtain."""
|
|
42
37
|
|
|
43
38
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -51,13 +46,11 @@ class SwitchbotCurtain(SwitchbotDevice):
|
|
|
51
46
|
# and position = 100 equals open. The parameter is default set to True so that
|
|
52
47
|
# the definition of position is the same as in Home Assistant.
|
|
53
48
|
|
|
54
|
-
super().__init__(*args, **kwargs)
|
|
55
49
|
self._reverse: bool = kwargs.pop("reverse_mode", True)
|
|
50
|
+
super().__init__(self._reverse, *args, **kwargs)
|
|
56
51
|
self._settings: dict[str, Any] = {}
|
|
57
52
|
self.ext_info_sum: dict[str, Any] = {}
|
|
58
53
|
self.ext_info_adv: dict[str, Any] = {}
|
|
59
|
-
self._is_opening: bool = False
|
|
60
|
-
self._is_closing: bool = False
|
|
61
54
|
|
|
62
55
|
def _set_parsed_data(
|
|
63
56
|
self, advertisement: SwitchBotAdvertisement, data: dict[str, Any]
|
|
@@ -69,18 +62,6 @@ class SwitchbotCurtain(SwitchbotDevice):
|
|
|
69
62
|
self._update_motion_direction(in_motion, previous_position, new_position)
|
|
70
63
|
super()._set_parsed_data(advertisement, data)
|
|
71
64
|
|
|
72
|
-
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
73
|
-
"""Send multiple commands to device.
|
|
74
|
-
|
|
75
|
-
Since we current have no way to tell which command the device
|
|
76
|
-
needs we send both.
|
|
77
|
-
"""
|
|
78
|
-
final_result = False
|
|
79
|
-
for key in keys:
|
|
80
|
-
result = await self._send_command(key)
|
|
81
|
-
final_result |= self._check_command_result(result, 0, {1})
|
|
82
|
-
return final_result
|
|
83
|
-
|
|
84
65
|
@update_after_operation
|
|
85
66
|
async def open(self, speed: int = 255) -> bool:
|
|
86
67
|
"""Send open command. Speed 255 - normal, 1 - slow"""
|
|
@@ -103,19 +84,16 @@ class SwitchbotCurtain(SwitchbotDevice):
|
|
|
103
84
|
async def stop(self) -> bool:
|
|
104
85
|
"""Send stop command to device."""
|
|
105
86
|
self._is_opening = self._is_closing = False
|
|
106
|
-
return await
|
|
87
|
+
return await super().stop()
|
|
107
88
|
|
|
108
89
|
@update_after_operation
|
|
109
90
|
async def set_position(self, position: int, speed: int = 255) -> bool:
|
|
110
91
|
"""Send position command (0-100) to device. Speed 255 - normal, 1 - slow"""
|
|
111
|
-
|
|
112
|
-
self._update_motion_direction(
|
|
113
|
-
|
|
114
|
-
[
|
|
115
|
-
f"{POSITION_KEYS[0]}{position:02X}",
|
|
116
|
-
f"{POSITION_KEYS[1]}{speed:02X}{position:02X}",
|
|
117
|
-
]
|
|
92
|
+
direction_adjusted_position = (100 - position) if self._reverse else position
|
|
93
|
+
self._update_motion_direction(
|
|
94
|
+
True, self._get_adv_value("position"), direction_adjusted_position
|
|
118
95
|
)
|
|
96
|
+
return await super().set_position(position, speed)
|
|
119
97
|
|
|
120
98
|
def get_position(self) -> Any:
|
|
121
99
|
"""Return cached position (0-100) of Curtain."""
|
|
@@ -168,8 +146,8 @@ class SwitchbotCurtain(SwitchbotDevice):
|
|
|
168
146
|
self._is_closing = new_position < previous_position
|
|
169
147
|
|
|
170
148
|
async def get_extended_info_summary(self) -> dict[str, Any] | None:
|
|
171
|
-
"""Get
|
|
172
|
-
_data = await self._send_command(key=
|
|
149
|
+
"""Get extended info for all devices in chain."""
|
|
150
|
+
_data = await self._send_command(key=COVER_EXT_SUM_KEY)
|
|
173
151
|
|
|
174
152
|
if not _data:
|
|
175
153
|
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
|
|
@@ -184,78 +162,19 @@ class SwitchbotCurtain(SwitchbotDevice):
|
|
|
184
162
|
"touchToOpen": bool(_data[1] & 0b01000000),
|
|
185
163
|
"light": bool(_data[1] & 0b00100000),
|
|
186
164
|
"openDirection": (
|
|
187
|
-
"left_to_right" if _data[1] & 0b00010000
|
|
165
|
+
"left_to_right" if _data[1] & 0b00010000 else "right_to_left"
|
|
188
166
|
),
|
|
189
167
|
}
|
|
190
168
|
|
|
191
169
|
# if grouped curtain device present.
|
|
192
170
|
if _data[2] != 0:
|
|
193
171
|
self.ext_info_sum["device1"] = {
|
|
194
|
-
"openDirectionDefault": not bool(_data[
|
|
195
|
-
"touchToOpen": bool(_data[
|
|
196
|
-
"light": bool(_data[
|
|
172
|
+
"openDirectionDefault": not bool(_data[2] & 0b10000000),
|
|
173
|
+
"touchToOpen": bool(_data[2] & 0b01000000),
|
|
174
|
+
"light": bool(_data[2] & 0b00100000),
|
|
197
175
|
"openDirection": (
|
|
198
|
-
"left_to_right" if _data[
|
|
176
|
+
"left_to_right" if _data[2] & 0b00010000 else "right_to_left"
|
|
199
177
|
),
|
|
200
178
|
}
|
|
201
179
|
|
|
202
180
|
return self.ext_info_sum
|
|
203
|
-
|
|
204
|
-
async def get_extended_info_adv(self) -> dict[str, Any] | None:
|
|
205
|
-
"""Get advance page info for device chain."""
|
|
206
|
-
|
|
207
|
-
_data = await self._send_command(key=CURTAIN_EXT_ADV_KEY)
|
|
208
|
-
if not _data:
|
|
209
|
-
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
|
|
210
|
-
return None
|
|
211
|
-
|
|
212
|
-
if _data in (b"\x07", b"\x00"):
|
|
213
|
-
_LOGGER.error("%s: Unsuccessful, please try again", self.name)
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
|
-
_state_of_charge = [
|
|
217
|
-
"not_charging",
|
|
218
|
-
"charging_by_adapter",
|
|
219
|
-
"charging_by_solar",
|
|
220
|
-
"fully_charged",
|
|
221
|
-
"solar_not_charging",
|
|
222
|
-
"charging_error",
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
self.ext_info_adv["device0"] = {
|
|
226
|
-
"battery": _data[1],
|
|
227
|
-
"firmware": _data[2] / 10.0,
|
|
228
|
-
"stateOfCharge": _state_of_charge[_data[3]],
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
# If grouped curtain device present.
|
|
232
|
-
if _data[4]:
|
|
233
|
-
self.ext_info_adv["device1"] = {
|
|
234
|
-
"battery": _data[4],
|
|
235
|
-
"firmware": _data[5] / 10.0,
|
|
236
|
-
"stateOfCharge": _state_of_charge[_data[6]],
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return self.ext_info_adv
|
|
240
|
-
|
|
241
|
-
def get_light_level(self) -> Any:
|
|
242
|
-
"""Return cached light level."""
|
|
243
|
-
# To get actual light level call update() first.
|
|
244
|
-
return self._get_adv_value("lightLevel")
|
|
245
|
-
|
|
246
|
-
def is_reversed(self) -> bool:
|
|
247
|
-
"""Return True if curtain position is opposite from SB data."""
|
|
248
|
-
return self._reverse
|
|
249
|
-
|
|
250
|
-
def is_calibrated(self) -> Any:
|
|
251
|
-
"""Return True curtain is calibrated."""
|
|
252
|
-
# To get actual light level call update() first.
|
|
253
|
-
return self._get_adv_value("calibration")
|
|
254
|
-
|
|
255
|
-
def is_opening(self) -> bool:
|
|
256
|
-
"""Return True if the curtain is opening."""
|
|
257
|
-
return self._is_opening
|
|
258
|
-
|
|
259
|
-
def is_closing(self) -> bool:
|
|
260
|
-
"""Return True if the curtain is closing."""
|
|
261
|
-
return self._is_closing
|
|
@@ -940,6 +940,30 @@ def test_wosensor_passive_only():
|
|
|
940
940
|
)
|
|
941
941
|
|
|
942
942
|
|
|
943
|
+
def test_wosensor_active_zero_data():
|
|
944
|
+
"""Test parsing wosensor with active data but all values are zero."""
|
|
945
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
946
|
+
adv_data = generate_advertisement_data(
|
|
947
|
+
manufacturer_data={},
|
|
948
|
+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\x00\x00\x00\x00"},
|
|
949
|
+
tx_power=-127,
|
|
950
|
+
rssi=-50,
|
|
951
|
+
)
|
|
952
|
+
result = parse_advertisement_data(ble_device, adv_data)
|
|
953
|
+
assert result == SwitchBotAdvertisement(
|
|
954
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
955
|
+
data={
|
|
956
|
+
"data": {},
|
|
957
|
+
"isEncrypted": False,
|
|
958
|
+
"model": "T",
|
|
959
|
+
"rawAdvData": b"T\x00\x00\x00\x00\x00",
|
|
960
|
+
},
|
|
961
|
+
device=ble_device,
|
|
962
|
+
rssi=-50,
|
|
963
|
+
active=True,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
|
|
943
967
|
def test_woiosensor_passive_and_active():
|
|
944
968
|
"""Test parsing woiosensor as passive with active data as well."""
|
|
945
969
|
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|