Skip to content

Commit 2fca8be

Browse files
authored
Added bot states and battery, refactored update methods. (#22)
* Added bot states and battery, refactored update methods. * Fixes * Add method to return battery. * Implemented suggestions * Fixed logic mistake on bot is_on status.
1 parent 1357c7c commit 2fca8be

File tree

3 files changed

+121
-39
lines changed

3 files changed

+121
-39
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ venv.bak/
102102

103103
# mypy
104104
.mypy_cache/
105+
switchbot/.vs/slnx.sqlite
106+
switchbot/.vs/switchbot/v16/.suo
107+
switchbot/.vs/VSWorkspaceState.json

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
name = 'PySwitchbot',
55
packages = ['switchbot'],
66
install_requires=['bluepy'],
7-
version = '0.10.0',
7+
version = '0.10.1',
88
description = 'A library to communicate with Switchbot',
99
author='Daniel Hjelseth Hoyer',
1010
url='https://github.com/Danielhiversen/pySwitchbot/',

switchbot/__init__.py

Lines changed: 117 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Library to handle connection with Switchbot"""
2-
import time
3-
42
import binascii
53
import logging
4+
import time
65

76
import bluepy
87

@@ -33,29 +32,34 @@
3332

3433
class SwitchbotDevice:
3534
# pylint: disable=too-few-public-methods
35+
# pylint: disable=too-many-instance-attributes
3636
"""Base Representation of a Switchbot Device."""
3737

3838
def __init__(self, mac, password=None, interface=None, **kwargs) -> None:
3939
self._interface = interface
4040
self._mac = mac
4141
self._device = None
42+
self._battery_percent = 0
4243
self._retry_count = kwargs.pop("retry_count", DEFAULT_RETRY_COUNT)
43-
self._time_between_update_command = kwargs.pop("time_between_update_command",
44-
DEFAULT_TIME_BETWEEN_UPDATE_COMMAND)
44+
self._time_between_update_command = kwargs.pop(
45+
"time_between_update_command", DEFAULT_TIME_BETWEEN_UPDATE_COMMAND
46+
)
4547
self._last_time_command_send = time.time()
4648
if password is None or password == "":
4749
self._password_encoded = None
4850
else:
49-
self._password_encoded = '%x' % (binascii.crc32(password.encode('ascii')) & 0xffffffff)
51+
self._password_encoded = "%x" % (
52+
binascii.crc32(password.encode("ascii")) & 0xFFFFFFFF
53+
)
5054

5155
def _connect(self) -> None:
5256
if self._device is not None:
5357
return
5458
try:
5559
_LOGGER.debug("Connecting to Switchbot...")
56-
self._device = bluepy.btle.Peripheral(self._mac,
57-
bluepy.btle.ADDR_TYPE_RANDOM,
58-
self._interface)
60+
self._device = bluepy.btle.Peripheral(
61+
self._mac, bluepy.btle.ADDR_TYPE_RANDOM, self._interface
62+
)
5963
_LOGGER.debug("Connected to Switchbot.")
6064
except bluepy.btle.BTLEException:
6165
_LOGGER.debug("Failed connecting to Switchbot.", exc_info=True)
@@ -91,8 +95,10 @@ def _writekey(self, key) -> bool:
9195
write_result = hand.write(binascii.a2b_hex(key), withResponse=True)
9296
self._last_time_command_send = time.time()
9397
if not write_result:
94-
_LOGGER.error("Sent command but didn't get a response from Switchbot confirming command was sent. "
95-
"Please check the Switchbot.")
98+
_LOGGER.error(
99+
"Sent command but didn't get a response from Switchbot confirming command was sent."
100+
" Please check the Switchbot."
101+
)
96102
else:
97103
_LOGGER.info("Successfully sent command to Switchbot (MAC: %s).", self._mac)
98104
return write_result
@@ -111,12 +117,54 @@ def _sendcommand(self, key, retry) -> bool:
111117
if send_success:
112118
return True
113119
if retry < 1:
114-
_LOGGER.error("Switchbot communication failed. Stopping trying.", exc_info=True)
120+
_LOGGER.error(
121+
"Switchbot communication failed. Stopping trying.", exc_info=True
122+
)
115123
return False
116-
_LOGGER.warning("Cannot connect to Switchbot. Retrying (remaining: %d)...", retry)
124+
_LOGGER.warning(
125+
"Cannot connect to Switchbot. Retrying (remaining: %d)...", retry
126+
)
117127
time.sleep(DEFAULT_RETRY_TIMEOUT)
118128
return self._sendcommand(key, retry - 1)
119129

130+
def get_servicedata(self, retry=DEFAULT_RETRY_COUNT, scan_timeout=5) -> bytearray:
131+
"""Get BTLE 16b Service Data,
132+
returns after the given timeout period in seconds."""
133+
devices = None
134+
135+
waiting_time = self._time_between_update_command - time.time()
136+
if waiting_time > 0:
137+
time.sleep(waiting_time)
138+
try:
139+
devices = bluepy.btle.Scanner().scan(scan_timeout)
140+
141+
except bluepy.btle.BTLEManagementError:
142+
_LOGGER.warning("Error updating Switchbot.", exc_info=True)
143+
144+
if devices is None:
145+
if retry < 1:
146+
_LOGGER.error(
147+
"Switchbot update failed. Stopping trying.", exc_info=True
148+
)
149+
return None
150+
151+
_LOGGER.warning(
152+
"Cannot update Switchbot. Retrying (remaining: %d)...", retry
153+
)
154+
time.sleep(DEFAULT_RETRY_TIMEOUT)
155+
return self.get_servicedata(retry - 1, scan_timeout)
156+
157+
for device in devices:
158+
if self._mac.lower() == device.addr.lower():
159+
for (adtype, _, value) in device.getScanData():
160+
if adtype == 22:
161+
service_data = value[4:].encode()
162+
service_data = binascii.unhexlify(service_data)
163+
164+
return service_data
165+
166+
return None
167+
120168
def get_mac(self) -> str:
121169
"""Returns the mac address of the device."""
122170
return self._mac
@@ -125,10 +173,40 @@ def get_min_time_update(self):
125173
"""Returns the first time an update can be executed."""
126174
return self._last_time_command_send + self._time_between_update_command
127175

176+
def get_battery_percent(self) -> int:
177+
"""Returns device battery level in percent."""
178+
return self._battery_percent
179+
128180

129181
class Switchbot(SwitchbotDevice):
130182
"""Representation of a Switchbot."""
131183

184+
def __init__(self, *args, **kwargs) -> None:
185+
self._is_on = None
186+
self._mode = None
187+
super().__init__(*args, **kwargs)
188+
189+
def update(self, scan_timeout=5) -> None:
190+
"""Updates the mode, battery percent and state of the device."""
191+
barray = self.get_servicedata(scan_timeout=scan_timeout)
192+
193+
if barray is None:
194+
return
195+
196+
_mode = barray[1] & 0b10000000 # 128 switch or 0 toggle
197+
if _mode != 0:
198+
self._mode = "switch"
199+
else:
200+
self._mode = "toggle"
201+
202+
_is_on = barray[1] & 0b01000000 # 64 on or 0 for off
203+
if _is_on == 0 and self._mode == "switch":
204+
self._is_on = True
205+
else:
206+
self._is_on = False
207+
208+
self._battery_percent = barray[2] & 0b01111111
209+
132210
def turn_on(self) -> bool:
133211
"""Turn device on."""
134212
return self._sendcommand(ON_KEY, self._retry_count)
@@ -141,6 +219,16 @@ def press(self) -> bool:
141219
"""Press command to device."""
142220
return self._sendcommand(PRESS_KEY, self._retry_count)
143221

222+
def switch_mode(self) -> str:
223+
"""Return Toggle or Switch from cache.
224+
Run update first."""
225+
return self._mode
226+
227+
def is_on(self) -> bool:
228+
"""Return switch state from cache.
229+
Run update first."""
230+
return self._is_on
231+
144232

145233
class SwitchbotCurtain(SwitchbotDevice):
146234
"""Representation of a Switchbot Curtain."""
@@ -150,23 +238,24 @@ def __init__(self, *args, **kwargs) -> None:
150238
The position of the curtain is saved in self._pos with 0 = open and 100 = closed.
151239
This is independent of the calibration of the curtain bot (Open left to right/
152240
Open right to left/Open from the middle).
153-
The parameter 'reverse_mode' reverse these values, if 'reverse_mode' = True, position = 0 equals close
241+
The parameter 'reverse_mode' reverse these values,
242+
if 'reverse_mode' = True, position = 0 equals close
154243
and position = 100 equals open. The parameter is default set to True so that
155244
the definition of position is the same as in Home Assistant."""
156-
self._reverse = kwargs.pop('reverse_mode', True)
245+
self._reverse = kwargs.pop("reverse_mode", True)
157246
self._pos = 0
158247
self._light_level = 0
159-
self._battery_percent = 0
248+
self._is_calibrated = 0
160249
super().__init__(*args, **kwargs)
161250

162251
def open(self) -> bool:
163252
"""Send open command."""
164-
self._pos = (100 if self._reverse else 0)
253+
self._pos = 100 if self._reverse else 0
165254
return self._sendcommand(OPEN_KEY, self._retry_count)
166255

167256
def close(self) -> bool:
168257
"""Send close command."""
169-
self._pos = (0 if self._reverse else 100)
258+
self._pos = 0 if self._reverse else 100
170259
return self._sendcommand(CLOSE_KEY, self._retry_count)
171260

172261
def stop(self) -> bool:
@@ -175,39 +264,29 @@ def stop(self) -> bool:
175264

176265
def set_position(self, position: int) -> bool:
177266
"""Send position command (0-100) to device."""
178-
position = ((100 - position) if self._reverse else position)
267+
position = (100 - position) if self._reverse else position
179268
self._pos = position
180269
hex_position = "%0.2X" % position
181270
return self._sendcommand(POSITION_KEY + hex_position, self._retry_count)
182271

183272
def update(self, scan_timeout=5) -> None:
184-
"""Updates the current position, battery percent and light level of the device.
185-
Returns after the given timeout period in seconds."""
186-
waiting_time = self.get_min_time_update() - time.time()
187-
if waiting_time > 0:
188-
time.sleep(waiting_time)
189-
devices = bluepy.btle.Scanner().scan(scan_timeout)
273+
"""Updates the current position, battery percent and light level of the device."""
274+
barray = self.get_servicedata(scan_timeout=scan_timeout)
190275

191-
for device in devices:
192-
if self.get_mac().lower() == device.addr.lower():
193-
for (adtype, _, value) in device.getScanData():
194-
if adtype == 22:
195-
barray = bytearray(value, 'ascii')
196-
self._battery_percent = int(barray[-6:-4], 16)
197-
position = max(min(int(barray[-4:-2], 16), 100), 0)
198-
self._pos = ((100 - position) if self._reverse else position)
199-
self._light_level = int(barray[-2:], 16)
276+
if barray is None:
277+
return
278+
279+
self._is_calibrated = barray[1] & 0b01000000
280+
self._battery_percent = barray[2] & 0b01111111
281+
position = max(min(barray[3] & 0b01111111, 100), 0)
282+
self._pos = (100 - position) if self._reverse else position
283+
self._light_level = (barray[4] >> 4) & 0b00001111 # light sensor level (1-10)
200284

201285
def get_position(self) -> int:
202286
"""Returns the current cached position (0-100), the actual position could vary.
203287
To get the actual position call update() first."""
204288
return self._pos
205289

206-
def get_battery_percent(self) -> int:
207-
"""Returns the current cached battery percent (0-100), the actual battery percent could vary.
208-
To get the actual battery percent call update() first."""
209-
return self._battery_percent
210-
211290
def get_light_level(self) -> int:
212291
"""Returns the current cached light level, the actual light level could vary.
213292
To get the actual light level call update() first."""

0 commit comments

Comments
 (0)