diff --git a/README.md b/README.md index 5a84cdb3e06de8327a991c14b4c639a449d2dee3..788e0d5dd4f2317fcf14994f71854a537b4e8ca9 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,13 @@ bluetooth smart home remote. It is written in Python 3, originally for use with [Home Assistant](https://www.home-assistant.io/). -# Status - -This is currently pre-alpha status. It is not usable. - # Usage ## Scanning for Turn Touch devices +**Note:** Scanning requires root privileges on Linux. To avoid this, skip +to the next section and connect to the device without scanning. + ```python import turntouch @@ -26,9 +25,103 @@ device = turntouch.scan(only_one=True)[0] devices = turntouch.scan(timeout=60) ``` -`turntouch.scan()` returns a list of `turntouch.TurnTouch` objects. - -## Interacting with a Turn Touch device +`turntouch.scan()` returns a list of `turntouch.TurnTouch` objects. A connection +is automatically opened to each device, so it is ready to use. `turntouch.TurnTouch` is a subclass of [`bluepy.btle.Peripheral`](http://ianharvey.github.io/bluepy-doc/peripheral.html). + +## Interacting with a Turn Touch device + +```python +import turntouch + +# Connect to a device by MAC address +tt = turntouch.TurnTouch('c0:ff:ee:c0:ff:ee') + +# Read the device nickname +print(tt.name) + +# Update the device nickname (max. 32 characters) +tt.name = 'Living Room Remote' +``` + +## Listening for button presses + +```python +from turntouch import TurnTouch, DefaultButtonPressHandler + +class MyHandler(DefaultButtonPressHandler): + def button_north(self): + print("Up button pressed.") + def button_east_double_tap(self): + print("Right button double-tapped.") + def button_south_hold(self): + print("Down button held.") + +tt = TurnTouch('c0:ff:ee:c0:ff:ee') +tt.set_handler(MyHandler()) +tt.listen_forever() + +# One-liner alternative (same as listen_forever) +TurnTouch('c0:ff:ee:c0:ff:ee', handler=MyHandler(), listen=True) +``` + +## More advanced usage + +Here's a more complex example, triggering some existing functions. + +```python +import turntouch + +# Define a handler +class MyFancyHandler(turntouch.DefaultButtonPressHandler): + + def __init__(some_object, other_function): + """Use the __init__ method to pass references to parts of your code, + such as objects, methods, or variables.""" + self.thing_1 = some_object + self.other_func = other_function + + def button_any(press_type): + """Special handler which is fired for ALL button presses. + `press_type` is an instance of turntouch.PressType.""" + if press_type.name == "North": + self.thing_1.some_method() + elif press_type.name in ["South", "East", "West"]: + self.thing_1.other_method() + else: + self.other_func() + + def button_south_hold(): + print("You can combine per-button handlers with button_any!") + + +# Instantiate the handler, passing some application data into it +my_handler = MyFancyHandler(some_object_from_my_application, a_function) + +# Scan until we find a device +devices = [] +while not devices: + devices = turntouch.scan(only_one=True) +tt = devices[0] + +# Assign the handler to your device. +tt.set_handler(my_handler) + +tt.listen_forever() +``` + +## Listening for just one button press + +If you don't want the listener to run forever, do this: + +```python +tt = TurnTouch('c0:ff:ee:c0:ff:ee', handler=SomeHandler) +tt.listen() # Will return as soon as one button is pressed. +``` + +## Error handling + +Connection failures will raise `turntouch.TurnTouchException`. You may want to +catch and ignore this exception to retry connecting. diff --git a/turntouch/__init__.py b/turntouch/__init__.py index a97771c842f71f838f72ea1587d80d90de63f8e9..206f038643f9b54d655dc566393e35858c58dbe9 100644 --- a/turntouch/__init__.py +++ b/turntouch/__init__.py @@ -7,6 +7,74 @@ from typing import List, Union logger = logging.getLogger('TurnTouch') +class PressType: + """A type of button press.""" + def __init__(self, data: bytes, name: str, function_name: str, + multi: bool = False): + self.data = data + self.name = name + self.function_name = function_name + self.multi = multi + + def __repr__(self): + return '<PressType name="{name}">'.format(name=self.name) + + +class DefaultButtonPressHandler: + """A callback handler class for button press events. + Create a subclass of this class to define your button press behavior.""" + + def button_any(self, press_type: PressType = None): pass + + def button_off(self): pass + + def button_north(self): pass + + def button_north_double_tap(self): pass + + def button_north_hold(self): pass + + def button_east(self): pass + + def button_east_double_tap(self): pass + + def button_east_hold(self): pass + + def button_west(self): pass + + def button_west_double_tap(self): pass + + def button_west_hold(self): pass + + def button_south(self): pass + + def button_south_double_tap(self): pass + + def button_south_hold(self): pass + + def button_multi_north_east(self): pass + + def button_multi_north_west(self): pass + + def button_multi_north_south(self): pass + + def button_multi_east_west(self): pass + + def button_multi_east_south(self): pass + + def button_multi_west_south(self): pass + + def button_multi_north_east_west(self): pass + + def button_multi_north_east_south(self): pass + + def button_multi_north_west_south(self): pass + + def button_multi_east_west_south(self): pass + + def button_multi_north_east_west_south(self): pass + + class TurnTouch(btle.Peripheral): """A Turn Touch smart home remote.""" @@ -14,27 +82,78 @@ class TurnTouch(btle.Peripheral): BATTERY_LEVEL_CHARACTERISTIC_UUID = '2a19' DEVICE_NAME_CHARACTERISTIC_UUID = '99c31526-dc4f-41b1-bb04-4e4deb81fadd' DEVICE_NAME_LENGTH = 32 + PRESS_TYPES = { + 0xFF00: PressType(0xFF00, 'Off', 'button_off'), + + 0xFE00: PressType(0xFE00, 'North', 'button_north'), + 0xEF00: PressType(0xEF00, 'North double tap', + 'button_north_double_tap'), + 0xFEFF: PressType(0xFEFF, 'North hold', 'button_north_hold'), + + 0xFD00: PressType(0xFD00, 'East', 'button_east'), + 0xDF00: PressType(0xDF00, 'East double tap', 'button_east_double_tap'), + 0xFDFF: PressType(0xFDFF, 'East hold', 'button_east_hold'), + + 0xFB00: PressType(0xFB00, 'West', 'button_west'), + 0xBF00: PressType(0xBF00, 'West double tap', 'button_west_double_tap'), + 0xFBFF: PressType(0xFBFF, 'West hold', 'button_west_hold'), + + 0xF700: PressType(0xF700, 'South', 'button_south'), + 0x7F00: PressType(0x7F00, 'South double tap', + 'button_south_double_tap'), + 0xF7FF: PressType(0xF7FF, 'South hold', 'button_south_hold'), + + 0xFC00: PressType(0xFC00, 'Multi North East', + 'button_multi_north_east', True), + 0xFA00: PressType(0xFA00, 'Multi North West', + 'button_multi_north_west', True), + 0xF600: PressType(0xF600, 'Multi North South', + 'button_multi_north_south', True), + 0xF900: PressType(0xF900, 'Multi East West', + 'button_multi_east_west', True), + 0xF500: PressType(0xF500, 'Multi East South', + 'button_multi_east_south', True), + 0xF300: PressType(0xF300, 'Multi West South', + 'button_multi_west_south', True), + + 0xF800: PressType(0xF800, 'Multi North East West', + 'button_multi_north_east_west', True), + 0xF400: PressType(0xF400, 'Multi North East South', + 'button_multi_north_east_south', True), + 0xF200: PressType(0xF200, 'Multi North West South', + 'button_multi_north_west_south', True), + 0xF100: PressType(0xF100, 'Multi East West South', + 'button_multi_east_west_south', True), + + 0xF000: PressType(0xF000, 'Multi North East West South', + 'button_multi_north_east_west_south', True), + } def __init__(self, - device_address: Union[str, btle.ScanEntry], - interface: int = None, - enable_notifications: bool = True): + address: Union[str, btle.ScanEntry], + handler: DefaultButtonPressHandler = None, + listen: bool = False, + interface: int = None): """Connect to the Turn Touch remote. Set appropriate address type (overriding btle default). - :param device_address Union[str, btle.ScanEntry]: + :param address Union[str, btle.ScanEntry]: MAC address (or btle.ScanEntry object) of this device - :param interface int: Index of the bluetooth device (eg. 0 for hci0) - :param enable_notifications bool: Start listening for button presses""" + :param listen bool: Start listening for button presses + :param interface int: Index of the bluetooth device (eg. 0 for hci0)""" try: - logger.info("Connecting to device {address}...'".format( - address=self.addr)) + logger.info("Connecting to device {address}...".format( + address=address)) super(TurnTouch, self).__init__( - device_address, btle.ADDR_TYPE_RANDOM, interface) - logger.info("Successfully connected to device {address}.'".format( + address, btle.ADDR_TYPE_RANDOM, interface) + logger.info("Successfully connected to device {address}.".format( address=self.addr)) except btle.BTLEException: raise TurnTouchException("Failed to connect to device {address}" - .format(address=device_address)) + .format(address=address)) + self.withDelegate(self.NotificationDelegate(turn_touch=self)) + self.set_handler(handler) + if listen: + self.listen_forever() @property def name(self) -> str: @@ -58,6 +177,56 @@ class TurnTouch(btle.Peripheral): logger.debug("Set name for device {address} to '{name}'".format( address=self.addr, name=name_bytes)) + def set_handler(self, handler: DefaultButtonPressHandler = None): + """Set the button press handler class for this remote.""" + self._handler = handler or DefaultButtonPressHandler + + def listen(self, timeout: int = 0, only_one: bool = True): + """Listen for a button press event. + Will listen indefinitely if `only_one` is False.""" + self._enable_notifications() + if only_one: + self.waitForNotifications(timeout) + else: + while True: + self.waitForNotifications(timeout) + self._enable_notifications(enable=False) + + def listen_forever(self): + """Listen for button press events indefinitely.""" + self.listen(only_one=False) + + def _enable_notifications(self, enabled=True): + """Tell the remote to start sending button press notifications.""" + notification_handle = self.getCharacteristics( + uuid=self.BUTTON_STATUS_CHARACTERISTIC_UUID)[0].getHandle() + notification_enable_handle = notification_handle + 1 + logger.debug("{action} notifications for device {address}...".format( + action="Enabling" if enabled else "Disabling", address=self.addr)) + self.writeCharacteristic(notification_enable_handle, + bytes([0x01 if enabled else 0x00, 0x00]), + withResponse=True) + logger.debug("Notifications {action} for device {address}.".format( + action="enabled" if enabled else "disabled", address=self.addr)) + + class NotificationDelegate(btle.DefaultDelegate): + """Handle callbacks for notifications from the device.""" + def __init__(self, turn_touch): + """Retain a reference to the calling object.""" + self.turn_touch = turn_touch + + def handleNotification(self, cHandle, data): + """Call the appropriate button press handler method(s).""" + logger.debug("Got notification {notification}".format( + notification=data)) + type_int = int.from_bytes(data, byteorder='big') + # Call the generic (any button) callback. + self.turn_touch._handler.button_any( + self.turn_touch.PRESS_TYPES.get(type_int)) + # Call the button-specific callback + getattr(self.turn_touch._handler, + self.turn_touch.PRESS_TYPES.get(type_int).function_name)() + class TurnTouchException(Exception): """An error related to the Turn Touch bluetooth smart home remote."""