0

I am using the python library pygatt to talk to govee bluetooth bulbs, and it is working fine, for only 1 bulb.

The problem with it is that it won't allow more than one connection at a time since it uses the command line GATTTool to communicate with the bulbs.

Another issue i have is that I only have A UUID to transmit information to the bulbs, and no handle as needed to be able to write with pygattlib.

I am looking at using another library similarly named, pygattlib that is similarly named, but uses its own c code for talking to bluetooth and so not being restricted to a single connection.

My problem with trying to use the pygattlib library is that I currently only have a UUID that is several characters long... and the write function for pygattlib uses a handle that is an integer, I believe 16 bits.

Any ideas how i can translate from the LONG UUID to the handle?

The libraries are:

https://github.com/chvolkmann/govee_btled

https://github.com/peplin/pygatt

The pygattlib below is what i want to use instead of the pygatt above, since it will allow more than one connection. the library below only uses 'handles' for writing to the bulbs.

https://github.com/oscaracena/pygattlib

Any ideas how to convert from UUID to handle?

thanks!

0

128-bit (or long) UUID is always the one that is actually used internally. If the GATT Service or GATT characteristic has been adopted by the Bluetooth SIG, then it can be written in the shortened 16-bit form. The base 128-bit number is: 0000xxxx-0000-1000-8000-00805F9B34FB and the x's get replaced with the 16-bit value.

Looking through the govee_btled, it seems the 128-bit UUID is:

UUID_CONTROL_CHARACTERISTIC = '00010203-0405-0607-0809-0a0b0c0d2b11'

I would be cautious of using either of the Bluetooth libraries you have linked to.

pygatt, as you say, uses the command line tool gatttool which was deprecated by in 2017. This is discussed in the library https://github.com/peplin/pygatt/issues/112

pygattlib uses HCI commands directly bypassing the Bluetooth Daemon running on your system. This is routinely done in the embedded space but considered bad practice on a general purpose system running Linux.

The recommended API for interacting with BlueZ is the D-Bus API and there is a pure Python implementation with pydbus

Looking at the bluetooth_led.py file, it doesn't seem like there are many commands that would need to match. I made it:

  • self._bt = bt_backend_cls()
  • self._bt.start()
  • self._bt.stop()
  • self._dev = self._bt.connect(self.mac)
  • self._dev.char_write(UUID_CONTROL_CHARACTERISTIC, frame)
  • self._dev.disconnect()

My thought would be that you could change the backend quite easily by importing a file like below (instead of import pygatt) and specifying bt_backend_cls to be PyDbusBackend class in that file. Apart from those two changes you shouldn't have to do any other changes.

from time import sleep
from pydbus import SystemBus

BLUEZ_SERVICE = 'org.bluez'
BLUEZ_DEV_IFACE = 'org.bluez.Device1'
BLUEZ_CHR_IFACE = 'org.bluez.GattCharacteristic1'

class PyDbusBackend:
    @classmethod
    def start(cls):
        """Mock function to match pygatt lib"""
        pass

    @classmethod
    def stop(cls):
        """Mock function to match pygatt lib"""
        pass

    @classmethod
    def connect(cls, device_address):
        """Create device object and connect"""
        device = PyDbusDevice(device_address)
        device.connect()
        return device


class PyDbusDevice:

    def __init__(self, address):
        self.bus = SystemBus()
        self.mngr = self.bus.get(BLUEZ_SERVICE, '/')
        self.dev_path = self._from_device_address(address)
        self.device = self.bus.get(BLUEZ_SERVICE, self.dev_path)

    def _from_device_address(self, addr):
        """Look up D-Bus object path from device address"""
        mng_objs = self.mngr.GetManagedObjects()
        for path in mng_objs:
            dev_addr = mng_objs[path].get(BLUEZ_DEV_IFACE, {}).get('Address', '')
            if addr.casefold() == dev_addr.casefold():
                return path

    def _from_gatt_uuid(self, uuid):
        """Look up D-Bus object path for characteristic UUID"""
        mng_objs = self.mngr.GetManagedObjects()
        for path in mng_objs:
            chr_uuid = mng_objs[path].get(BLUEZ_CHR_IFACE, {}).get('UUID')
            if path.startswith(self.dev_path) and chr_uuid == uuid.casefold():
                return path

    def connect(self):
        """
        Connect to device.
        Wait for GATT services to be resolved before returning
        """
        self.device.Connect()
        while not self.device.ServicesResolved:
            sleep(0.5)

    def disconnect(self):
        """Disconnect from device"""
        self.device.Disconnect()

    def char_write(self, uuid, value):
        """Write value to given GATT characteristic UUID"""
        char_path = self._from_gatt_uuid(uuid)
        char = self.bus.get(BLUEZ_SERVICE, char_path)
        char.WriteValue(value, {})

    def char_read(self, uuid):
        """Read value of given GATT characteristic UUID"""
        char_path = self._from_gatt_uuid(uuid)
        char = self.bus.get(BLUEZ_SERVICE, char_path)
        return char.ReadValue({})

Changing to this backend should then allow you to connect to multiple lights.

4
  • Thanks ukBaz! this solution is working great! As a follow up, how would the char_read() work in this ? In a similar manner to the char_write, only using char.ReadValue() Thanks in advance!
    – jr3us
    Jan 23 at 14:50
  • Additional info: I just tested using 2 of the bulbs at the same time, and it works very well!! Thanks again!
    – jr3us
    Jan 23 at 15:30
  • Glad it was helpful. Have updated the example with a char_read.
    – ukBaz
    Jan 23 at 15:50
  • Thank you sir! You're a scholar and a gentleman!
    – jr3us
    Jan 23 at 17:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.