2019-12-13 02:08:43 +01:00
|
|
|
import enum
|
2020-05-06 00:24:00 +02:00
|
|
|
import time
|
2023-02-05 23:10:35 +01:00
|
|
|
from typing import Any, Collection, Dict, List, Optional
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
from platypush.entities import EnumSwitchEntityManager
|
|
|
|
from platypush.entities.switches import EnumSwitch
|
2019-12-13 02:08:43 +01:00
|
|
|
from platypush.message.response.bluetooth import BluetoothScanResponse
|
2018-07-06 02:08:38 +02:00
|
|
|
from platypush.plugins import action
|
2019-12-13 02:08:43 +01:00
|
|
|
from platypush.plugins.bluetooth.ble import BluetoothBlePlugin
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2019-07-02 12:02:28 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
# pylint: disable=too-many-ancestors
|
|
|
|
class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
|
2018-05-08 15:51:47 +02:00
|
|
|
"""
|
2018-06-25 19:57:43 +02:00
|
|
|
Plugin to interact with a Switchbot (https://www.switch-bot.com/) device and
|
2021-05-10 18:43:00 +02:00
|
|
|
programmatically control switches over a Bluetooth interface.
|
2018-06-25 19:57:43 +02:00
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
See :class:`platypush.plugins.bluetooth.ble.BluetoothBlePlugin` for how to enable BLE permissions for
|
|
|
|
the platypush user (a simple solution may be to run it as root, but that's usually NOT a good idea).
|
2018-06-25 19:57:43 +02:00
|
|
|
|
|
|
|
Requires:
|
|
|
|
|
|
|
|
* **pybluez** (``pip install pybluez``)
|
|
|
|
* **gattlib** (``pip install gattlib``)
|
|
|
|
* **libboost** (on Debian ```apt-get install libboost-python-dev libboost-thread-dev``)
|
2021-05-10 18:43:00 +02:00
|
|
|
|
2018-05-08 15:51:47 +02:00
|
|
|
"""
|
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
uuid = 'cba20002-224d-11e6-9fb8-0002a5d5c51b'
|
|
|
|
handle = 0x16
|
|
|
|
|
|
|
|
class Command(enum.Enum):
|
|
|
|
"""
|
|
|
|
Base64 encoded commands
|
|
|
|
"""
|
2022-04-04 21:12:59 +02:00
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
# \x57\x01\x00
|
|
|
|
PRESS = 'VwEA'
|
|
|
|
# # \x57\x01\x01
|
|
|
|
ON = 'VwEB'
|
|
|
|
# # \x57\x01\x02
|
|
|
|
OFF = 'VwEC'
|
|
|
|
|
2022-04-04 21:12:59 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
interface=None,
|
|
|
|
connect_timeout=None,
|
|
|
|
scan_timeout=2,
|
|
|
|
devices=None,
|
|
|
|
**kwargs
|
|
|
|
):
|
2018-06-25 19:57:43 +02:00
|
|
|
"""
|
2019-12-13 02:08:43 +01:00
|
|
|
:param interface: Bluetooth interface to use (e.g. hci0) default: first available one
|
|
|
|
:type interface: str
|
2018-06-25 19:57:43 +02:00
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
:param connect_timeout: Timeout for the connection to the Switchbot device - default: None
|
2018-06-25 19:57:43 +02:00
|
|
|
:type connect_timeout: float
|
|
|
|
|
2019-12-14 15:56:58 +01:00
|
|
|
:param scan_timeout: Timeout for the scan operations
|
2018-06-25 19:57:43 +02:00
|
|
|
:type scan_timeout: float
|
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
:param devices: Devices to control, as a MAC address -> name map
|
2018-06-25 19:57:43 +02:00
|
|
|
:type devices: dict
|
|
|
|
"""
|
2021-04-06 21:10:48 +02:00
|
|
|
super().__init__(interface=interface, **kwargs)
|
2019-07-02 12:02:28 +02:00
|
|
|
|
2018-05-08 15:51:47 +02:00
|
|
|
self.connect_timeout = connect_timeout if connect_timeout else 5
|
|
|
|
self.scan_timeout = scan_timeout if scan_timeout else 2
|
2019-12-13 02:08:43 +01:00
|
|
|
self.configured_devices = devices or {}
|
2019-07-02 12:02:28 +02:00
|
|
|
self.configured_devices_by_name = {
|
2022-04-04 21:12:59 +02:00
|
|
|
name: addr for addr, name in self.configured_devices.items()
|
2019-07-02 12:02:28 +02:00
|
|
|
}
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
def _run(self, device: str, command: Command):
|
2022-04-04 21:12:59 +02:00
|
|
|
device = self.configured_devices_by_name.get(device, '')
|
2020-05-06 00:24:00 +02:00
|
|
|
n_tries = 1
|
|
|
|
|
|
|
|
try:
|
2022-04-04 21:12:59 +02:00
|
|
|
self.write(
|
|
|
|
device,
|
|
|
|
command.value,
|
|
|
|
handle=self.handle,
|
|
|
|
channel_type='random',
|
|
|
|
binary=True,
|
|
|
|
)
|
2020-05-06 00:24:00 +02:00
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
|
|
|
n_tries -= 1
|
|
|
|
|
|
|
|
if n_tries == 0:
|
|
|
|
raise e
|
|
|
|
time.sleep(5)
|
2019-07-02 12:02:28 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
return self.status(device)
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2018-05-08 15:51:47 +02:00
|
|
|
def press(self, device):
|
2018-06-25 19:57:43 +02:00
|
|
|
"""
|
|
|
|
Send a press button command to a device
|
|
|
|
|
|
|
|
:param device: Device name or address
|
|
|
|
:type device: str
|
|
|
|
"""
|
2019-12-13 02:08:43 +01:00
|
|
|
return self._run(device, self.Command.PRESS)
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2022-04-04 21:12:59 +02:00
|
|
|
def toggle(self, device, **_):
|
2019-07-02 12:02:28 +02:00
|
|
|
return self.press(device)
|
|
|
|
|
|
|
|
@action
|
2022-04-04 21:12:59 +02:00
|
|
|
def on(self, device, **_):
|
2018-06-25 19:57:43 +02:00
|
|
|
"""
|
|
|
|
Send a press-on button command to a device
|
|
|
|
|
|
|
|
:param device: Device name or address
|
|
|
|
:type device: str
|
|
|
|
"""
|
2019-12-13 02:08:43 +01:00
|
|
|
return self._run(device, self.Command.ON)
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2022-04-04 21:12:59 +02:00
|
|
|
def off(self, device, **_):
|
2018-06-25 19:57:43 +02:00
|
|
|
"""
|
|
|
|
Send a press-off button command to a device
|
|
|
|
|
|
|
|
:param device: Device name or address
|
|
|
|
:type device: str
|
|
|
|
"""
|
2019-12-13 02:08:43 +01:00
|
|
|
return self._run(device, self.Command.OFF)
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
@action
|
|
|
|
# pylint: disable=arguments-differ
|
|
|
|
def set_value(self, device: str, data: str, *_, **__):
|
|
|
|
"""
|
|
|
|
Entity-compatible ``set_value`` method to send a command to a device.
|
|
|
|
|
|
|
|
:param device: Device name or address
|
|
|
|
:param data: Command to send. Possible values are:
|
|
|
|
|
|
|
|
- ``on``: Press the button and remain in the pressed state.
|
|
|
|
- ``off``: Release a previously pressed button.
|
|
|
|
- ``press``: Press and release the button.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if data == 'on':
|
|
|
|
return self.on(device)
|
|
|
|
if data == 'off':
|
|
|
|
return self.off(device)
|
|
|
|
if data == 'press':
|
|
|
|
return self.press(device)
|
|
|
|
|
|
|
|
self.logger.warning('Unknown command for SwitchBot "%s": "%s"', device, data)
|
|
|
|
return None
|
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2022-04-04 21:12:59 +02:00
|
|
|
def scan(
|
|
|
|
self, interface: Optional[str] = None, duration: int = 10
|
|
|
|
) -> BluetoothScanResponse:
|
2019-07-02 12:02:28 +02:00
|
|
|
"""
|
|
|
|
Scan for available Switchbot devices nearby.
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
:param interface: Bluetooth interface to scan (default: default configured interface)
|
|
|
|
:param duration: Scan duration in seconds
|
2019-07-02 12:02:28 +02:00
|
|
|
"""
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
compatible_devices: Dict[str, Any] = {}
|
|
|
|
devices = (
|
|
|
|
super().scan(interface=interface, duration=duration).devices # type: ignore
|
|
|
|
)
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
for dev in devices:
|
|
|
|
try:
|
|
|
|
characteristics = [
|
2022-04-04 21:12:59 +02:00
|
|
|
chrc
|
2023-02-05 23:10:35 +01:00
|
|
|
for chrc in self.discover_characteristics( # type: ignore
|
2022-04-04 21:12:59 +02:00
|
|
|
dev['addr'],
|
|
|
|
channel_type='random',
|
|
|
|
wait=False,
|
|
|
|
timeout=self.scan_timeout,
|
|
|
|
).characteristics
|
2019-12-13 02:08:43 +01:00
|
|
|
if chrc.get('uuid') == self.uuid
|
|
|
|
]
|
|
|
|
|
|
|
|
if characteristics:
|
|
|
|
compatible_devices[dev['addr']] = None
|
2021-04-05 00:58:44 +02:00
|
|
|
except Exception as e:
|
2023-02-05 23:10:35 +01:00
|
|
|
self.logger.warning('Device scan error: %s', e)
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
self.publish_entities(compatible_devices)
|
2019-12-13 02:08:43 +01:00
|
|
|
return BluetoothScanResponse(devices=compatible_devices)
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
@action
|
|
|
|
def status(self, *_, **__) -> List[dict]:
|
|
|
|
self.publish_entities(self.configured_devices)
|
2019-07-02 12:02:28 +02:00
|
|
|
return [
|
|
|
|
{
|
|
|
|
'address': addr,
|
|
|
|
'id': addr,
|
|
|
|
'name': name,
|
|
|
|
'on': False,
|
|
|
|
}
|
|
|
|
for addr, name in self.configured_devices.items()
|
|
|
|
]
|
2018-06-25 19:57:43 +02:00
|
|
|
|
2023-02-05 23:10:35 +01:00
|
|
|
def transform_entities(self, entities: Collection[dict]) -> Collection[EnumSwitch]:
|
|
|
|
return [
|
|
|
|
EnumSwitch(
|
|
|
|
id=addr,
|
|
|
|
name=name,
|
|
|
|
value='on',
|
|
|
|
values=['on', 'off', 'press'],
|
|
|
|
is_write_only=True,
|
|
|
|
)
|
|
|
|
for addr, name in entities
|
|
|
|
]
|
2022-04-04 21:12:59 +02:00
|
|
|
|
2018-05-08 15:51:47 +02:00
|
|
|
|
2019-07-02 12:02:28 +02:00
|
|
|
# vim:sw=4:ts=4:et:
|