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.
Files changed (52) hide show
  1. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PKG-INFO +1 -1
  2. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/SOURCES.txt +3 -0
  4. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/setup.py +1 -1
  5. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/meter.py +5 -1
  6. PySwitchbot-0.45.0/switchbot/devices/base_cover.py +142 -0
  7. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/base_light.py +3 -4
  8. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/blind_tilt.py +42 -13
  9. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/curtain.py +24 -105
  10. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/tests/test_adv_parser.py +24 -0
  11. PySwitchbot-0.45.0/tests/test_base_cover.py +152 -0
  12. PySwitchbot-0.45.0/tests/test_blind_tilt.py +240 -0
  13. PySwitchbot-0.45.0/tests/test_curtain.py +426 -0
  14. PySwitchbot-0.44.0/tests/test_curtain.py +0 -242
  15. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/LICENSE +0 -0
  16. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/MANIFEST.in +0 -0
  17. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  18. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/requires.txt +0 -0
  19. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  20. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/README.md +0 -0
  21. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/setup.cfg +0 -0
  22. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/__init__.py +0 -0
  23. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parser.py +0 -0
  24. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/__init__.py +0 -0
  25. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  26. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/bot.py +0 -0
  27. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/bulb.py +0 -0
  28. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  29. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/contact.py +0 -0
  30. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/curtain.py +0 -0
  31. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/humidifier.py +0 -0
  32. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/light_strip.py +0 -0
  33. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/lock.py +0 -0
  34. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/motion.py +0 -0
  35. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/adv_parsers/plug.py +0 -0
  36. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/api_config.py +0 -0
  37. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/const.py +2 -2
  38. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/__init__.py +0 -0
  39. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/bot.py +0 -0
  40. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/bulb.py +0 -0
  41. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/ceiling_light.py +0 -0
  42. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/contact.py +0 -0
  43. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/device.py +0 -0
  44. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/humidifier.py +0 -0
  45. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/light_strip.py +0 -0
  46. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/lock.py +0 -0
  47. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/meter.py +0 -0
  48. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/motion.py +0 -0
  49. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/devices/plug.py +0 -0
  50. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/discovery.py +0 -0
  51. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/enum.py +0 -0
  52. {PySwitchbot-0.44.0 → PySwitchbot-0.45.0}/switchbot/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySwitchbot
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySwitchbot
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -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
@@ -11,7 +11,7 @@ setup(
11
11
  "boto3>=1.20.24",
12
12
  "requests>=2.28.1",
13
13
  ],
14
- version="0.44.0",
14
+ version="0.45.0",
15
15
  description="A library to communicate with Switchbot",
16
16
  author="Daniel Hjelseth Hoyer",
17
17
  url="https://github.com/Danielhiversen/pySwitchbot/",
@@ -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": temp_data[2] & 0b01111111,
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 .curtain import CURTAIN_EXT_SUM_KEY, SwitchbotCurtain
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}{BLIND_COMMAND}010132",
21
- f"{REQ_HEADER}{BLIND_COMMAND}05ff32",
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}{BLIND_COMMAND}010100",
25
- f"{REQ_HEADER}{BLIND_COMMAND}05ff00",
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}{BLIND_COMMAND}010164",
29
- f"{REQ_HEADER}{BLIND_COMMAND}05ff64",
28
+ f"{REQ_HEADER}{COVER_COMMAND}010164",
29
+ f"{REQ_HEADER}{COVER_COMMAND}05ff64",
30
30
  ]
31
31
 
32
32
 
33
- class SwitchbotBlindTilt(SwitchbotCurtain, SwitchbotSequenceDevice):
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 basic info for all devices in chain."""
118
- _data = await self._send_command(key=CURTAIN_EXT_SUM_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 switchbot.models import SwitchBotAdvertisement
8
-
9
- from .device import REQ_HEADER, SwitchbotDevice, update_after_operation
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}{CURTAIN_COMMAND}010100",
20
- f"{REQ_HEADER}{CURTAIN_COMMAND}05", # +speed + "00"
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}{CURTAIN_COMMAND}010164",
24
- f"{REQ_HEADER}{CURTAIN_COMMAND}05", # +speed + "64"
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}{CURTAIN_COMMAND}0101",
28
- f"{REQ_HEADER}{CURTAIN_COMMAND}05", # +speed
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}{CURTAIN_COMMAND}0001", f"{REQ_HEADER}{CURTAIN_COMMAND}00ff"]
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(SwitchbotDevice):
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 self._send_multiple_commands(STOP_KEYS)
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
- position = (100 - position) if self._reverse else position
112
- self._update_motion_direction(True, self._get_adv_value("position"), position)
113
- return await self._send_multiple_commands(
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 basic info for all devices in chain."""
172
- _data = await self._send_command(key=CURTAIN_EXT_SUM_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 == 1 else "right_to_left"
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[1] & 0b10000000),
195
- "touchToOpen": bool(_data[1] & 0b01000000),
196
- "light": bool(_data[1] & 0b00100000),
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[1] & 0b00010000 else "right_to_left"
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")