Skip to content

Commit fbd0cfc

Browse files
jesserockzBTMortonBen Mortonbdraco
authored
Initial Blind Tilt support (#181)
Co-authored-by: Ben Morton <[email protected]> Co-authored-by: Ben Morton <[email protected]> Co-authored-by: J. Nick Koston <[email protected]> closes #172
1 parent 0ece9cf commit fbd0cfc

File tree

6 files changed

+162
-1
lines changed

6 files changed

+162
-1
lines changed

switchbot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SwitchbotModel,
1212
)
1313
from .devices.base_light import SwitchbotBaseLight
14+
from .devices.blind_tilt import SwitchbotBlindTilt
1415
from .devices.bot import Switchbot
1516
from .devices.bulb import SwitchbotBulb
1617
from .devices.ceiling_light import SwitchbotCeilingLight
@@ -45,4 +46,5 @@
4546
"SwitchbotSupportedType",
4647
"SwitchbotModel",
4748
"SwitchbotLock",
49+
"SwitchbotBlindTilt",
4850
]

switchbot/adv_parser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from bleak.backends.device import BLEDevice
1010
from bleak.backends.scanner import AdvertisementData
1111

12+
from .adv_parsers.blind_tilt import process_woblindtilt
1213
from .adv_parsers.bot import process_wohand
1314
from .adv_parsers.bulb import process_color_bulb
1415
from .adv_parsers.ceiling_light import process_woceiling
@@ -126,6 +127,13 @@ class SwitchbotSupportedType(TypedDict):
126127
"func": process_wolock,
127128
"manufacturer_id": 2409,
128129
},
130+
"x": {
131+
"modelName": SwitchbotModel.BLIND_TILT,
132+
"modelFriendlyName": "Blind Tilt",
133+
"func": process_woblindtilt,
134+
"manufacturer_id": 2409,
135+
"manufacturer_data_length": 10,
136+
},
129137
}
130138

131139
_SWITCHBOT_MODEL_TO_CHAR = {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Library to handle connection with Switchbot."""
2+
from __future__ import annotations
3+
4+
5+
def process_woblindtilt(
6+
data: bytes | None, mfr_data: bytes | None, reverse: bool = False
7+
) -> dict[str, bool | int]:
8+
"""Process woBlindTilt services data."""
9+
10+
if mfr_data is None:
11+
return {}
12+
13+
device_data = mfr_data[6:]
14+
15+
_tilt = max(min(device_data[2] & 0b01111111, 100), 0)
16+
_in_motion = bool(device_data[2] & 0b10000000)
17+
_light_level = (device_data[1] >> 4) & 0b00001111
18+
_calibrated = bool(device_data[1] & 0b00000001)
19+
20+
return {
21+
"calibration": _calibrated,
22+
"battery": data[2] & 0b01111111 if data else None,
23+
"inMotion": _in_motion,
24+
"tilt": (100 - _tilt) if reverse else _tilt,
25+
"lightLevel": _light_level,
26+
}

switchbot/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class SwitchbotModel(StrEnum):
3939
COLOR_BULB = "WoBulb"
4040
CEILING_LIGHT = "WoCeiling"
4141
LOCK = "WoLock"
42+
BLIND_TILT = "WoBlindTilt"
4243

4344

4445
class LockStatus(Enum):

switchbot/devices/blind_tilt.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Library to handle connection with Switchbot."""
2+
from __future__ import annotations
3+
4+
import logging
5+
from typing import Any
6+
7+
from switchbot.devices.device import REQ_HEADER, update_after_operation
8+
9+
from .curtain import CURTAIN_EXT_SUM_KEY, SwitchbotCurtain
10+
11+
_LOGGER = logging.getLogger(__name__)
12+
13+
14+
BLIND_COMMAND = "4501"
15+
OPEN_KEYS = [
16+
f"{REQ_HEADER}{BLIND_COMMAND}010132",
17+
f"{REQ_HEADER}{BLIND_COMMAND}05ff32",
18+
]
19+
CLOSE_DOWN_KEYS = [
20+
f"{REQ_HEADER}{BLIND_COMMAND}010100",
21+
f"{REQ_HEADER}{BLIND_COMMAND}05ff00",
22+
]
23+
CLOSE_UP_KEYS = [
24+
f"{REQ_HEADER}{BLIND_COMMAND}010164",
25+
f"{REQ_HEADER}{BLIND_COMMAND}05ff64",
26+
]
27+
28+
29+
class SwitchbotBlindTilt(SwitchbotCurtain):
30+
"""Representation of a Switchbot Blind Tilt."""
31+
32+
# The position of the blind is saved returned with 0 = closed down, 50 = open and 100 = closed up.
33+
# This is independent of the calibration of the blind.
34+
# The parameter 'reverse_mode' reverse these values,
35+
# if 'reverse_mode' = True, position = 0 equals closed up
36+
# and position = 100 equals closed down. The parameter is default set to False so that
37+
# the definition of position is the same as in Home Assistant.
38+
# This is opposite to the base class so needs to be overwritten.
39+
40+
def __init__(self, *args: Any, **kwargs: Any) -> None:
41+
"""Switchbot Blind Tilt/woBlindTilt constructor."""
42+
super().__init__(*args, **kwargs)
43+
44+
self._reverse: bool = kwargs.pop("reverse_mode", False)
45+
46+
@update_after_operation
47+
async def open(self) -> bool:
48+
"""Send open command."""
49+
return await self._send_multiple_commands(OPEN_KEYS)
50+
51+
@update_after_operation
52+
async def close_up(self) -> bool:
53+
"""Send close up command."""
54+
return await self._send_multiple_commands(CLOSE_UP_KEYS)
55+
56+
@update_after_operation
57+
async def close_down(self) -> bool:
58+
"""Send close down command."""
59+
return await self._send_multiple_commands(CLOSE_DOWN_KEYS)
60+
61+
# The aim of this is to close to the nearest endpoint.
62+
# If we're open upwards we close up, if we're open downwards we close down.
63+
# If we're in the middle we default to close down as that seems to be the app's preference.
64+
@update_after_operation
65+
async def close(self) -> bool:
66+
"""Send close command."""
67+
if self.get_position() > 50:
68+
return await self.close_up()
69+
else:
70+
return await self.close_down()
71+
72+
def get_position(self) -> Any:
73+
"""Return cached tilt (0-100) of Blind Tilt."""
74+
# To get actual tilt call update() first.
75+
return self._get_adv_value("tilt")
76+
77+
async def get_basic_info(self) -> dict[str, Any] | None:
78+
"""Get device basic settings."""
79+
if not (_data := await self._get_basic_info()):
80+
return None
81+
82+
_tilt = max(min(_data[6], 100), 0)
83+
return {
84+
"battery": _data[1],
85+
"firmware": _data[2] / 10.0,
86+
"light": bool(_data[4] & 0b00100000),
87+
"fault": bool(_data[4] & 0b00001000),
88+
"solarPanel": bool(_data[5] & 0b00001000),
89+
"calibration": bool(_data[5] & 0b00000100),
90+
"calibrated": bool(_data[5] & 0b00000100),
91+
"inMotion": bool(_data[5] & 0b00000011),
92+
"motionDirection": {
93+
"up": bool(_data[5] & (0b00000010 if self._reverse else 0b00000001)),
94+
"down": bool(_data[5] & (0b00000001 if self._reverse else 0b00000010)),
95+
},
96+
"tilt": (100 - _tilt) if self._reverse else _tilt,
97+
"timers": _data[7],
98+
}
99+
100+
async def get_extended_info_summary(self) -> dict[str, Any] | None:
101+
"""Get basic info for all devices in chain."""
102+
_data = await self._send_command(key=CURTAIN_EXT_SUM_KEY)
103+
104+
if not _data:
105+
_LOGGER.error("%s: Unsuccessful, no result from device", self.name)
106+
return None
107+
108+
if _data in (b"\x07", b"\x00"):
109+
_LOGGER.error("%s: Unsuccessful, please try again", self.name)
110+
return None
111+
112+
self.ext_info_sum["device0"] = {
113+
"light": bool(_data[1] & 0b00100000),
114+
}
115+
116+
return self.ext_info_sum

switchbot/discovery.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,17 @@ async def _get_devices_by_model(
8383
if adv.data.get("model") == model
8484
}
8585

86+
async def get_blind_tilts(self) -> dict[str, SwitchBotAdvertisement]:
87+
"""Return all WoBlindTilt/BlindTilts devices with services data."""
88+
regular_blinds = await self._get_devices_by_model("x")
89+
pairing_blinds = await self._get_devices_by_model("X")
90+
return {**regular_blinds, **pairing_blinds}
91+
8692
async def get_curtains(self) -> dict[str, SwitchBotAdvertisement]:
8793
"""Return all WoCurtain/Curtains devices with services data."""
88-
return await self._get_devices_by_model("c")
94+
regular_curtains = await self._get_devices_by_model("c")
95+
pairing_curtains = await self._get_devices_by_model("C")
96+
return {**regular_curtains, **pairing_curtains}
8997

9098
async def get_bots(self) -> dict[str, SwitchBotAdvertisement]:
9199
"""Return all WoHand/Bot devices with services data."""

0 commit comments

Comments
 (0)