From 75e34a5950c2ed994da42df80d4a8ffb20c83de3 Mon Sep 17 00:00:00 2001 From: dominikkarall Date: Tue, 6 Oct 2020 20:17:56 +0200 Subject: [PATCH 1/3] support multiple hci interfaces support keep connection alive support multiple callbacks --- eq3bt/connection.py | 71 +++++++++++++++++++++++++++++++++++---------- eq3bt/eq3btsmart.py | 4 +-- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/eq3bt/connection.py b/eq3bt/connection.py index 052a81b..2577594 100644 --- a/eq3bt/connection.py +++ b/eq3bt/connection.py @@ -4,6 +4,8 @@ """ import logging import codecs +import dbus +import re from bluepy import btle @@ -15,13 +17,36 @@ class BTLEConnection(btle.DefaultDelegate): """Representation of a BTLE Connection.""" - def __init__(self, mac): + def __init__(self, mac, keep_connected=False): """Initialize the connection.""" btle.DefaultDelegate.__init__(self) + self._ifaces = self.get_hci_ifaces() + self._iface_idx = 0 + self._conn = None self._mac = mac self._callbacks = {} + self._keep_connected = keep_connected + + def next_iface(self): + self._nr_conn_errors += 1 + self._iface_idx = (self._iface_idx + 1) % len(self._ifaces) + if self._nr_conn_errors >= len(self._ifaces)*2: + return False + return True + + def get_hci_ifaces(self): + iface_list = [] + bus = dbus.SystemBus() + manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") + objects = manager.GetManagedObjects() + for path, interfaces in objects.items(): + adapter = interfaces.get("org.bluez.Adapter1") + if adapter is None: + continue + iface_list.append(re.search(r'\d+$', path)[0]) + return iface_list def __enter__(self): """ @@ -29,24 +54,37 @@ def __enter__(self): :rtype: btle.Peripheral :return: """ - self._conn = btle.Peripheral() - self._conn.withDelegate(self) - _LOGGER.debug("Trying to connect to %s", self._mac) try: - self._conn.connect(self._mac) - except btle.BTLEException as ex: - _LOGGER.debug("Unable to connect to the device %s, retrying: %s", self._mac, ex) - try: - self._conn.connect(self._mac) - except Exception as ex2: - _LOGGER.debug("Second connection try to %s failed: %s", self._mac, ex2) - raise + conn_state = self._conn.getState() + except: + self._conn = None + + if self._conn is None or conn_state != "conn": + self._conn = btle.Peripheral() + self._conn.withDelegate(self) + self._nr_conn_errors = 0 + _LOGGER.debug("Trying to connect to %s", self._mac) + while True: + # try to connect with all ifaces + try: + self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) + break + except btle.BTLEException as ex: + _LOGGER.debug("Unable to connect to the device %s using iface %s, retrying: %s", self._mac, self._ifaces[self._iface_idx], ex) + try: + self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) + break + except Exception as ex2: + _LOGGER.debug("Second connection try to %s using ifaces %s failed: %s", self._mac, self._ifaces[self._iface_idx], ex2) + if self.next_iface() is False: + # tried all ifaces, raise exception + raise _LOGGER.debug("Connected to %s", self._mac) return self def __exit__(self, exc_type, exc_val, exc_tb): - if self._conn: + if self._conn and self._keep_connected is False: self._conn.disconnect() self._conn = None @@ -54,7 +92,8 @@ def handleNotification(self, handle, data): """Handle Callback from a Bluetooth (GATT) request.""" _LOGGER.debug("Got notification from %s: %s", handle, codecs.encode(data, 'hex')) if handle in self._callbacks: - self._callbacks[handle](data) + for callback in self._callbacks[handle]: + callback(data) @property def mac(self): @@ -63,7 +102,9 @@ def mac(self): def set_callback(self, handle, function): """Set the callback for a Notification handle. It will be called with the parameter data, which is binary.""" - self._callbacks[handle] = function + if handle not in self._callbacks: + self._callbacks[handle] = [] + self._callbacks[handle].append(function) def make_request(self, handle, value, timeout=DEFAULT_TIMEOUT, with_response=True): """Write a GATT Command without callback - not utf-8.""" diff --git a/eq3bt/eq3btsmart.py b/eq3bt/eq3btsmart.py index b510ce4..276b748 100644 --- a/eq3bt/eq3btsmart.py +++ b/eq3bt/eq3btsmart.py @@ -71,7 +71,7 @@ class TemperatureException(Exception): class Thermostat: """Representation of a EQ3 Bluetooth Smart thermostat.""" - def __init__(self, _mac, connection_cls=BTLEConnection): + def __init__(self, _mac, connection_cls=BTLEConnection, keep_connection=False): """Initialize the thermostat.""" self._target_temperature = Mode.Unknown @@ -94,7 +94,7 @@ def __init__(self, _mac, connection_cls=BTLEConnection): self._firmware_version = None self._device_serial = None - self._conn = connection_cls(_mac) + self._conn = connection_cls(_mac, keep_connection) self._conn.set_callback(PROP_NTFY_HANDLE, self.handle_notification) def __str__(self): From 43e6c5fbdd3de96cb2c9dc840e0dd947f2812db0 Mon Sep 17 00:00:00 2001 From: dominikkarall Date: Thu, 8 Oct 2020 19:03:00 +0200 Subject: [PATCH 2/3] remove get adapters add connect(iface) for keep_connection usage --- eq3bt/connection.py | 75 ++++++++++++++++----------------------------- eq3bt/eq3btsmart.py | 7 +++-- eq3bt/eq3cli.py | 5 +-- 3 files changed, 35 insertions(+), 52 deletions(-) diff --git a/eq3bt/connection.py b/eq3bt/connection.py index 2577594..bac1661 100644 --- a/eq3bt/connection.py +++ b/eq3bt/connection.py @@ -4,8 +4,6 @@ """ import logging import codecs -import dbus -import re from bluepy import btle @@ -17,36 +15,15 @@ class BTLEConnection(btle.DefaultDelegate): """Representation of a BTLE Connection.""" - def __init__(self, mac, keep_connected=False): + def __init__(self, mac, iface=None): """Initialize the connection.""" btle.DefaultDelegate.__init__(self) - self._ifaces = self.get_hci_ifaces() - self._iface_idx = 0 - + self._iface = iface + self._keep_connected = False self._conn = None self._mac = mac self._callbacks = {} - self._keep_connected = keep_connected - - def next_iface(self): - self._nr_conn_errors += 1 - self._iface_idx = (self._iface_idx + 1) % len(self._ifaces) - if self._nr_conn_errors >= len(self._ifaces)*2: - return False - return True - - def get_hci_ifaces(self): - iface_list = [] - bus = dbus.SystemBus() - manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") - objects = manager.GetManagedObjects() - for path, interfaces in objects.items(): - adapter = interfaces.get("org.bluez.Adapter1") - if adapter is None: - continue - iface_list.append(re.search(r'\d+$', path)[0]) - return iface_list def __enter__(self): """ @@ -54,33 +31,23 @@ def __enter__(self): :rtype: btle.Peripheral :return: """ - try: - conn_state = self._conn.getState() - except: - self._conn = None + conn_state = "conn" + if self._conn: + # connection active, check if still connected + try: + conn_state = self._conn.getState() + except (btle.BTLEInternalError, btle.BTLEDisconnectError): + # connection not active, set _conn object to None + self._conn = None if self._conn is None or conn_state != "conn": + # no active connection, connect now self._conn = btle.Peripheral() self._conn.withDelegate(self) - self._nr_conn_errors = 0 _LOGGER.debug("Trying to connect to %s", self._mac) - while True: - # try to connect with all ifaces - try: - self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) - break - except btle.BTLEException as ex: - _LOGGER.debug("Unable to connect to the device %s using iface %s, retrying: %s", self._mac, self._ifaces[self._iface_idx], ex) - try: - self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) - break - except Exception as ex2: - _LOGGER.debug("Second connection try to %s using ifaces %s failed: %s", self._mac, self._ifaces[self._iface_idx], ex2) - if self.next_iface() is False: - # tried all ifaces, raise exception - raise - - _LOGGER.debug("Connected to %s", self._mac) + self.connect(self._iface) + _LOGGER.debug("Connected to %s", self._mac) + return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -88,6 +55,18 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._conn.disconnect() self._conn = None + def connect(self, iface): + self._keep_connected = True + try: + self._conn.connect(self._mac, iface=iface) + except btle.BTLEException as ex: + _LOGGER.debug("Unable to connect to the device %s using iface %s, retrying: %s", self._mac, iface, ex) + try: + self._conn.connect(self._mac, iface=iface) + except Exception as ex2: + _LOGGER.debug("Second connection try to %s using ifaces %s failed: %s", self._mac, iface, ex2) + raise + def handleNotification(self, handle, data): """Handle Callback from a Bluetooth (GATT) request.""" _LOGGER.debug("Got notification from %s: %s", handle, codecs.encode(data, 'hex')) diff --git a/eq3bt/eq3btsmart.py b/eq3bt/eq3btsmart.py index 276b748..ab53774 100644 --- a/eq3bt/eq3btsmart.py +++ b/eq3bt/eq3btsmart.py @@ -71,7 +71,7 @@ class TemperatureException(Exception): class Thermostat: """Representation of a EQ3 Bluetooth Smart thermostat.""" - def __init__(self, _mac, connection_cls=BTLEConnection, keep_connection=False): + def __init__(self, _mac, connection_cls=BTLEConnection, iface=None): """Initialize the thermostat.""" self._target_temperature = Mode.Unknown @@ -94,7 +94,7 @@ def __init__(self, _mac, connection_cls=BTLEConnection, keep_connection=False): self._firmware_version = None self._device_serial = None - self._conn = connection_cls(_mac, keep_connection) + self._conn = connection_cls(_mac, iface) self._conn.set_callback(PROP_NTFY_HANDLE, self.handle_notification) def __str__(self): @@ -107,6 +107,9 @@ def __str__(self): self.mode_readable, away_end) + def connect(self, iface): + self._conn.connect(iface) + def _verify_temperature(self, temp): """Verifies that the temperature is valid. :raises TemperatureException: On invalid temperature. diff --git a/eq3bt/eq3cli.py b/eq3bt/eq3cli.py index 5801083..7d30ba7 100644 --- a/eq3bt/eq3cli.py +++ b/eq3bt/eq3cli.py @@ -21,16 +21,17 @@ def validate_mac(ctx, param, mac): @click.group(invoke_without_command=True) @click.option('--mac', envvar="EQ3_MAC", required=True, callback=validate_mac) +@click.option('--interface', default=None) @click.option('--debug/--normal', default=False) @click.pass_context -def cli(ctx, mac, debug): +def cli(ctx, mac, interface, debug): """ Tool to query and modify the state of EQ3 BT smart thermostat. """ if debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) - thermostat = Thermostat(mac) + thermostat = Thermostat(mac, iface=interface) thermostat.update() ctx.obj = thermostat From 75115f9b46e64375b753a0eed8a0ad787ac18a5b Mon Sep 17 00:00:00 2001 From: dominikkarall Date: Fri, 9 Oct 2020 19:31:14 +0200 Subject: [PATCH 3/3] add disconnect() fix keep_connected --- eq3bt/connection.py | 7 +++++++ eq3bt/eq3btsmart.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/eq3bt/connection.py b/eq3bt/connection.py index bac1661..fa6dbb3 100644 --- a/eq3bt/connection.py +++ b/eq3bt/connection.py @@ -57,6 +57,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): def connect(self, iface): self._keep_connected = True + self._connect(iface) + + def _connect(self, iface): try: self._conn.connect(self._mac, iface=iface) except btle.BTLEException as ex: @@ -66,6 +69,10 @@ def connect(self, iface): except Exception as ex2: _LOGGER.debug("Second connection try to %s using ifaces %s failed: %s", self._mac, iface, ex2) raise + + def disconnect(self): + self._conn.disconnect() + self._conn = None def handleNotification(self, handle, data): """Handle Callback from a Bluetooth (GATT) request.""" diff --git a/eq3bt/eq3btsmart.py b/eq3bt/eq3btsmart.py index ab53774..984245c 100644 --- a/eq3bt/eq3btsmart.py +++ b/eq3bt/eq3btsmart.py @@ -110,6 +110,9 @@ def __str__(self): def connect(self, iface): self._conn.connect(iface) + def disconnect(self): + self._conn.disconnect() + def _verify_temperature(self, temp): """Verifies that the temperature is valid. :raises TemperatureException: On invalid temperature.