Skip to content

Commit f5ebd04

Browse files
authored
Workaround stale advertisement data while connected to bots (#102)
1 parent 211e32c commit f5ebd04

File tree

2 files changed

+82
-16
lines changed

2 files changed

+82
-16
lines changed

switchbot/devices/bot.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
"""Library to handle connection with Switchbot."""
22
from __future__ import annotations
33

4+
import logging
45
from typing import Any
56

6-
from .device import DEVICE_SET_EXTENDED_KEY, DEVICE_SET_MODE_KEY, SwitchbotDevice
7+
from .device import (
8+
DEVICE_SET_EXTENDED_KEY,
9+
DEVICE_SET_MODE_KEY,
10+
SwitchbotDeviceOverrideStateDuringConnection,
11+
)
12+
13+
_LOGGER = logging.getLogger(__name__)
14+
15+
BOT_COMMAND_HEADER = "5701"
716

817
# Bot keys
9-
PRESS_KEY = "570100"
10-
ON_KEY = "570101"
11-
OFF_KEY = "570102"
12-
DOWN_KEY = "570103"
13-
UP_KEY = "570104"
18+
PRESS_KEY = f"{BOT_COMMAND_HEADER}00"
19+
ON_KEY = f"{BOT_COMMAND_HEADER}01"
20+
OFF_KEY = f"{BOT_COMMAND_HEADER}02"
21+
DOWN_KEY = f"{BOT_COMMAND_HEADER}03"
22+
UP_KEY = f"{BOT_COMMAND_HEADER}04"
1423

1524

16-
class Switchbot(SwitchbotDevice):
25+
class Switchbot(SwitchbotDeviceOverrideStateDuringConnection):
1726
"""Representation of a Switchbot."""
1827

1928
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -28,12 +37,24 @@ async def update(self, interface: int | None = None) -> None:
2837
async def turn_on(self) -> bool:
2938
"""Turn device on."""
3039
result = await self._send_command(ON_KEY)
31-
return self._check_command_result(result, 0, {1, 5})
40+
ret = self._check_command_result(result, 0, {1, 5})
41+
self._override_adv_data = {"isOn": True}
42+
_LOGGER.debug(
43+
"%s: Turn on result: %s -> %s", self.name, result, self._override_adv_data
44+
)
45+
self._fire_callbacks()
46+
return ret
3247

3348
async def turn_off(self) -> bool:
3449
"""Turn device off."""
3550
result = await self._send_command(OFF_KEY)
36-
return self._check_command_result(result, 0, {1, 5})
51+
ret = self._check_command_result(result, 0, {1, 5})
52+
self._override_adv_data = {"isOn": False}
53+
_LOGGER.debug(
54+
"%s: Turn off result: %s -> %s", self.name, result, self._override_adv_data
55+
)
56+
self._fire_callbacks()
57+
return ret
3758

3859
async def hand_up(self) -> bool:
3960
"""Raise device arm."""
@@ -79,12 +100,12 @@ async def get_basic_info(self) -> dict[str, Any] | None:
79100
"holdSeconds": _data[10],
80101
}
81102

82-
def switch_mode(self) -> Any:
103+
def switch_mode(self) -> bool | None:
83104
"""Return true or false from cache."""
84105
# To get actual position call update() first.
85106
return self._get_adv_value("switchMode")
86107

87-
def is_on(self) -> Any:
108+
def is_on(self) -> bool | None:
88109
"""Return switch state from cache."""
89110
# To get actual position call update() first.
90111
value = self._get_adv_value("isOn")

switchbot/devices/device.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _sb_uuid(comms_type: str = "service") -> UUID | str:
7676
WRITE_CHAR_UUID = _sb_uuid(comms_type="tx")
7777

7878

79-
class SwitchbotDevice:
79+
class SwitchbotBaseDevice:
8080
"""Base Representation of a Switchbot Device."""
8181

8282
def __init__(
@@ -349,6 +349,12 @@ def get_address(self) -> str:
349349
def _get_adv_value(self, key: str) -> Any:
350350
"""Return value from advertisement data."""
351351
if self._override_adv_data and key in self._override_adv_data:
352+
_LOGGER.debug(
353+
"%s: Using override value for %s: %s",
354+
self.name,
355+
key,
356+
self._override_adv_data[key],
357+
)
352358
return self._override_adv_data[key]
353359
if not self._sb_adv_data:
354360
return None
@@ -362,9 +368,6 @@ def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> No
362368
"""Update device data from advertisement."""
363369
# Only accept advertisements if the data is not missing
364370
# if we already have an advertisement with data
365-
if advertisement.data.get("data") or not self._sb_adv_data.data.get("data"):
366-
self._sb_adv_data = advertisement
367-
self._override_adv_data = None
368371
if self._device and ble_device_has_changed(self._device, advertisement.device):
369372
self._cached_services = None
370373
self._device = advertisement.device
@@ -432,9 +435,51 @@ def _check_command_result(
432435
)
433436
return result[index] in values
434437

438+
def _set_advertisement_data(self, advertisement: SwitchBotAdvertisement) -> None:
439+
"""Set advertisement data."""
440+
if advertisement.data.get("data") or not self._sb_adv_data.data.get("data"):
441+
self._sb_adv_data = advertisement
442+
self._override_adv_data = None
443+
444+
445+
class SwitchbotDevice(SwitchbotBaseDevice):
446+
"""Base Representation of a Switchbot Device.
447+
448+
This base class consumes the advertisement data during connection. If the device
449+
sends stale advertisement data while connected, use
450+
SwitchbotDeviceOverrideStateDuringConnection instead.
451+
"""
452+
453+
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
454+
"""Update device data from advertisement."""
455+
super().update_from_advertisement(advertisement)
456+
self._set_advertisement_data(advertisement)
457+
458+
459+
class SwitchbotDeviceOverrideStateDuringConnection(SwitchbotBaseDevice):
460+
"""Base Representation of a Switchbot Device.
461+
462+
This base class ignores the advertisement data during connection and uses the
463+
data from the device instead.
464+
"""
465+
466+
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
467+
super().update_from_advertisement(advertisement)
468+
if self._client and self._client.is_connected:
469+
# We do not consume the advertisement data if we are connected
470+
# to the device. This is because the advertisement data is not
471+
# updated when the device is connected for some devices.
472+
_LOGGER.debug("%s: Ignore advertisement data during connection", self.name)
473+
return
474+
self._set_advertisement_data(advertisement)
475+
435476

436477
class SwitchbotSequenceDevice(SwitchbotDevice):
437-
"""A Switchbot sequence device."""
478+
"""A Switchbot sequence device.
479+
480+
This class must not use SwitchbotDeviceOverrideStateDuringConnection because
481+
it needs to know when the sequence_number has changed.
482+
"""
438483

439484
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
440485
"""Update device data from advertisement."""

0 commit comments

Comments
 (0)