Add Boneco devices

I think I have made progress but not sure

Iā€™m stuck on trying to bypass the handshake and stop it from disconnecting. Any suggestions?

Start with bleakclient.start_notify(CHARACTERISTIC_AUTH, callback) and def callback(sender: int, data: bytearray) -> None.
Your callback should check that sender == 0x026 and

  • Save ā€œnonceā€ if len(data) == 20 and data[0] == 1
  • Save ā€œauth level/stepā€ from data[1] if data[0] == 4 and data[2] == 2
  • Save ā€œdevice keyā€ from data[3:19] if data[0:3] == b'\x06\x00\x00'

After getting ā€œnonceā€ you should also call bleakclient.start_notify(CHARACTERISTIC_AUTH_AND_SERVICE, callback2) and start calling bleakclient.write_gatt_char(CHARACTERISTIC_AUTH_AND_SERVICE, some_data) for moving between auth states

callback2 just checks that sender == 0x029 and data[1] & 1 == 1

CHARACTERISTIC_AUTH = "fdce2347-1013-4120-b919-1dbb32a2d132"
CHARACTERISTIC_AUTH_AND_SERVICE = "fdce2348-1013-4120-b919-1dbb32a2d132"

Auth states: GOT_NONCE ā†’ CONFIRM_WAITING (here you press button on device) ā†’ CONFIRMED ā†’ GOT_DEVICE_KEY ā†’ AUTH_SUCCESS

im just getting this error

Started notifications for AUTH characteristic
callback invoked with sender: fdce2347-1013-4120-b919-1dbb32a2d132 (Handle: 38): Unknown, data: 0101c88b5474b03218a41249a5d396afcca20000
callback invoked with sender: fdce2347-1013-4120-b919-1dbb32a2d132 (Handle: 38): Unknown, data: 0400010000000000000000000000000000000000

with this script

import asyncio
from bleak import BleakClient

CHARACTERISTIC_AUTH = "fdce2347-1013-4120-b919-1dbb32a2d132"
CHARACTERISTIC_AUTH_AND_SERVICE = "fdce2348-1013-4120-b919-1dbb32a2d132"

DEVICE_MAC_ADDRESS = "E1:A6:44:55:39:CA"

# Global variables to store the data and state
nonce = None
auth_level_step = None
device_key = None
auth_state = "INIT"

def callback(sender: str, data: bytearray) -> None:
    global nonce, auth_level_step, device_key, auth_state
    print(f"callback invoked with sender: {sender}, data: {data.hex()}")

    if sender == CHARACTERISTIC_AUTH:
        if len(data) == 20 and data[0] == 1:
            nonce = data
            auth_state = "GOT_NONCE"
            print("Nonce saved:", nonce.hex())
            asyncio.create_task(start_notify_auth_and_service())
        elif data[0] == 4 and data[2] == 2:
            auth_level_step = data[1]
            auth_state = "CONFIRM_WAITING"
            print("Auth level/step saved:", auth_level_step)
        elif data[0:3] == b'\x06\x00\x00':
            device_key = data[3:19]
            auth_state = "GOT_DEVICE_KEY"
            print("Device key saved:", device_key.hex())
            auth_state = "AUTH_SUCCESS"
            print("Authentication successful")

def callback2(sender: str, data: bytearray) -> None:
    print(f"callback2 invoked with sender: {sender}, data: {data.hex()}")
    if sender == CHARACTERISTIC_AUTH_AND_SERVICE and (data[1] & 1) == 1:
        print("Callback2 triggered with valid data.")
        # Here you can implement the logic to move between authentication states

async def start_notify_auth_and_service():
    print("Connecting to start AUTH_AND_SERVICE notifications...")
    async with BleakClient(DEVICE_MAC_ADDRESS) as client:
        await client.start_notify(CHARACTERISTIC_AUTH_AND_SERVICE, callback2)
        some_data = bytearray([0x01, 0x02, 0x03])  # Replace this with actual data as needed
        await asyncio.sleep(1)  # Allow time for notifications to stabilize
        await client.write_gatt_char(CHARACTERISTIC_AUTH_AND_SERVICE, some_data)
        print("Started notifications for AUTH_AND_SERVICE characteristic")

async def main():
    print(f"Connecting to device {DEVICE_MAC_ADDRESS}...")
    async with BleakClient(DEVICE_MAC_ADDRESS) as client:
        await client.start_notify(CHARACTERISTIC_AUTH, callback)
        print("Started notifications for AUTH characteristic")

        while auth_state != "AUTH_SUCCESS":
            await asyncio.sleep(1)
            if auth_state == "GOT_NONCE":
                print("Press the button on the device to confirm...")

if __name__ == "__main__":
    asyncio.run(main())

Can you please share your code? I am new to bluetooth communication to be true, but want to try it.

I think you have a mistake in sender == CHARACTERISTIC_AUTH you should use something like str(sender).split(' ')[0] == CHARACTERISTIC_AUTH

I updated the script but still couldnā€™t get it to work

import asyncio
from bleak import BleakClient
from bleak.exc import BleakError, BleakDBusError

CHARACTERISTIC_AUTH = "fdce2347-1013-4120-b919-1dbb32a2d132"
CHARACTERISTIC_AUTH_AND_SERVICE = "fdce2348-1013-4120-b919-1dbb32a2d132"

DEVICE_MAC_ADDRESS = "E1:A6:44:55:39:CA"

# Global variables to store the data and state
nonce = None
auth_level_step = None
device_key = None
auth_state = "INIT"

def callback(sender: str, data: bytearray) -> None:
    global nonce, auth_level_step, device_key, auth_state
    sender_str = str(sender).split(' ')[0]
    print(f"callback invoked with sender: {sender_str}, data: {data.hex()}")

    if sender_str == CHARACTERISTIC_AUTH:
        if len(data) == 20 and data[0] == 1:
            nonce = data
            auth_state = "GOT_NONCE"
            print("Nonce saved:", nonce.hex())
            asyncio.create_task(start_notify_auth_and_service())
        elif data[0] == 4 and data[2] == 2:
            auth_level_step = data[1]
            auth_state = "CONFIRM_WAITING"
            print("Auth level/step saved:", auth_level_step)
        elif data[0:3] == b'\x06\x00\x00':
            device_key = data[3:19]
            auth_state = "GOT_DEVICE_KEY"
            print("Device key saved:", device_key.hex())
            auth_state = "AUTH_SUCCESS"
            print("Authentication successful")

def callback2(sender: str, data: bytearray) -> None:
    sender_str = str(sender).split(' ')[0]
    print(f"callback2 invoked with sender: {sender_str}, data: {data.hex()}")
    if sender_str == CHARACTERISTIC_AUTH_AND_SERVICE and (data[1] & 1) == 1:
        print("Callback2 triggered with valid data.")
        # Implement the logic to move between authentication states

async def start_notify_auth_and_service():
    retry_attempts = 3
    for attempt in range(retry_attempts):
        try:
            print("Connecting to start AUTH_AND_SERVICE notifications...")
            async with BleakClient(DEVICE_MAC_ADDRESS) as client:
                await client.start_notify(CHARACTERISTIC_AUTH_AND_SERVICE, callback2)
                some_data = bytearray([0x01, 0x02, 0x03])  # Replace this with actual data as needed
                await asyncio.sleep(1)  # Allow time for notifications to stabilize
                await client.write_gatt_char(CHARACTERISTIC_AUTH_AND_SERVICE, some_data)
                print("Started notifications for AUTH_AND_SERVICE characteristic")
                break
        except BleakDBusError as e:
            print(f"Failed to start notifications (attempt {attempt + 1}): {e}")
            await asyncio.sleep(2)
        except BleakError as e:
            print(f"BLE error (attempt {attempt + 1}): {e}")
            await asyncio.sleep(2)

async def main():
    print(f"Connecting to device {DEVICE_MAC_ADDRESS}...")
    try:
        async with BleakClient(DEVICE_MAC_ADDRESS) as client:
            await client.start_notify(CHARACTERISTIC_AUTH, callback)
            print("Started notifications for AUTH characteristic")

            while auth_state != "AUTH_SUCCESS":
                await asyncio.sleep(1)
                if auth_state == "GOT_NONCE":
                    print("Press the button on the device to confirm...")
    except BleakDBusError as e:
        print(f"Failed to connect to device: {e}")
    except BleakError as e:
        print(f"BLE error: {e}")

if __name__ == "__main__":
    asyncio.run(main())

Any news on that? have two h700 as well

No did you figure it out ?

Hi folks. I have successfully deduced the auth mechanism for boneco BLE devices after much much effort both packet capturing the BLE comms from my F235 fans and also spoofing a device within a BLE GATT server I control to observe how the app reponds to certain sequences in real time.

I am a software engineer by trade, but this was a very fun project for me that had me learning about an area Iā€™ve not really explored previously. But it was also one that required me to graze on it over a long period.

I have successfully built a home assistant custom integration that can use the device key and control them as well as read back values. This is in development, more later.

It works for my device and should work for all F235 devices. Itā€™s quite possible the auth approach is the same for all of their models ā€“ but I have no way to know without owning one. Iā€™d say that the other fan they do, F225 is probably the same as looks more or the less same from the outside.

Since some devices mentioned here have different functions, I suspect at the very least the way the actual device data itself is constructed differently. But the hard part here was the auth so if its the same as these other ones, this will be a big breakthrough for those too.

Regardless the door will be open for others to use the info for further work on other models. If the auth mechanism works for those too, the second part about the format of the actual device metrics/control should be easier to solve on a per model basis. If someone worked with me and had relevant packet captures Iā€™m sure I could look into it. Even better if you can borrow me a device.

So where is it? Well, its janky at the moment as you need to supply the device key. Actually implementing the ā€œpairā€ mechanism to grab the key itself is out of reach at the moment or requires more of my time. I achieved my goal of interoping the fan with home assistant, but I also did that in part with a nrf52840 dongle that allowed me to capture the key. So at least on initial release you will need the means to capture it which needs a device capable of passive Bluetooth packet capture. Once you have that though, there are some easy to follow steps to grab it.

Iā€™m also looking into, for a further release, easy tools to be able to capture the key without this special hardware by spoofing/proxying the device with common consumer BT hardware. This method doesnā€™t need passive capture or special ā€œsniffersā€. But also the nrf52840 is really cheap anyways. And to clarify, the key capture step is a one-time thing you perform on a personal computer. You donā€™t need this special stuff on an ongoing basis.

Ok so wheres the initial release?

Iā€™m close. Give me a week or two. I originally did all of this on ESPHome but have since engaged in a rebuild as a first class HA integration to eliminate needing an ESP32 device. If you needed range (home assistant too far from device to connect direct), you could then just use an esphome Bluetooth proxy optionally.

4 Likes

Also Iā€™d just like to add, I just noticed on the other posts in this thread the GATT UUIDs are the same as the ones referenced in my complete solution for the F235! This bodes well for the possibility they have the same auth mechanisms :slight_smile:

Any updates Adam ?

Yep im still working on it and making progress! Taking a bit longer makng it prod ready than expected just because of time, but I am actively working on it.

Sweet, let me know if need any help!

Anymore updates ?

@Pretzel0000 Iā€™ve updated my script and made a library so you can check it with your device

@adamscybot You can use the library for getting key user friendly, similar to mobile app

1 Like

This is amazing thank you!!! I cant seem to connect to the device though (RROR:pyboneco.client:F235 (6C7BAC08-5EEA-5157-80A4-FCEC0D080CC4): Canā€™t auth. Exiting)

Checked your logs in issue, added a fix

Thanks, works fine with my W400 unit!

I had to read their FAQ on how to put the device into the pairing mode, grant my macOS terminal rights to use bluetooth, and sit right in front of the unit before finally succeeding.

  • Press the large black button on the device for at least 3 seconds until the LEDs flash blue or ā€œbtā€ appears on the device display, depending on the model.
  • If pairing is successful, the app indicates that the connection still needs to be confirmed on the device.
  • To confirm, briefly press the black button again and the device is paired with your smartphone.

Others might also find the example at Error can't authenticate Ā· Issue #1 Ā· DeKaN/pyboneco Ā· GitHub useful once paired successfully.

Just incase anyone else want it I have edited the example to add control functions

import asyncio
import json
import logging

from bleak import BleakClient

from pyboneco.auth import BonecoAuth
from pyboneco.client import BonecoClient
from pyboneco.enums import AuthState, ModeStatus, OperationMode

logging.basicConfig(level=logging.INFO)


async def device_control(boneco_client):
    while True:
        print("\nDevice Control Menu:")
        print("1. Turn On")
        print("2. Turn Off")
        print("3. Set Fan Speed (0-32)")
        print("4. Show Status")
        print("5. Exit")
        
        choice = input("Enter your choice (1-5): ")
        
        try:
            state = await boneco_client.get_state()
            
            if choice == "1":
                state._fan_mode = 0
                state.is_enabled = True
                await boneco_client.set_state(state)
                print("Device turned ON")
                
            elif choice == "2":
                state._fan_mode = 0
                state.is_enabled = False
                await boneco_client.set_state(state)
                print("Device turned OFF")
                
            elif choice == "3":
                try:
                    speed = int(input("Enter fan speed (0-32): "))
                    if 0 <= speed <= 32:
                        state._fan_mode = 0
                        if speed == 0:
                            # Turn device off if speed is 0
                            state.is_enabled = False
                            state.fan_level = 0
                            await boneco_client.set_state(state)
                            print("Device turned OFF")
                        else:
                            # Turn on and set speed for values 1-32
                            state.is_enabled = True
                            state.fan_level = speed
                            await boneco_client.set_state(state)
                            print(f"Device turned ON and fan speed set to {speed}")
                    else:
                        print("Invalid speed. Must be between 0-32")
                except ValueError:
                    print("Invalid input. Please enter a number")
                    
            elif choice == "4":
                info = await boneco_client.get_device_info()
                print(f"\nCurrent Status:")
                print(f"Power: {'ON' if state.is_enabled else 'OFF'}")
                print(f"Fan Speed: {state.fan_level}")
                print(f"Temperature: {info.temperature}Ā°C")
                print(f"Humidity: {info.humidity}%")
                
            elif choice == "5":
                print("Exiting device control")
                break
                
        except Exception as e:
            print(f"Error: {e}")

async def actions(auth: BonecoAuth):
    bleak_client = BleakClient(address_or_ble_device=auth.device)
    boneco_client = BonecoClient(bleak_client, auth)
    try:
        await boneco_client.connect()
        name = await boneco_client.get_device_name()
        print(f"Connected to: {name}")
        
        await device_control(boneco_client)
        
    finally:
        await boneco_client.disconnect()


def auth_state_callback(auth: BonecoAuth) -> None:
    print(
        f"Got new auth state: current={auth.current_state}, level={auth.current_auth_level}"
    )
    if auth.current_state == AuthState.CONFIRM_WAITING:
        print("Press button on device to confirm pairing")


async def find_device(address: str):
    scanned = await BonecoClient.find_boneco_devices()
    chosen = next((x for x in scanned.keys() if x.address == address), None)
    return chosen, scanned[chosen]


async def pair():
    scanned = await BonecoClient.find_boneco_devices()
    devices = list(scanned.keys())
    devices_text = "\n".join(
        [
            f"{n}) {value} (Pairing active = {scanned[value].pairing_active})"
            for n, value in enumerate(devices, start=1)
        ]
    )
    print(f"Scan results: \n{devices_text}\n")
    number = input(f"Choose device to pair [1-{len(scanned)}]: ")
    device = devices[int(number) - 1]
    advertisement = scanned[device]
    pairing_active = advertisement.pairing_active
    print(
        f'Chosen device "{device.name}" with address "{device.address}". Pairing active = {pairing_active}'
    )
    while not pairing_active:
        print("Put the device in pairing mode and press Enter")
        input()
        device, advertisement = await find_device(device.address)
        pairing_active = device and advertisement.pairing_active

    auth_data = BonecoAuth(device)
    auth_data.set_auth_state_callback(auth_state_callback)

    await actions(auth_data)


async def connect():
    print("Enter device json data")
    data = json.loads(input())
    device, advertisement = await find_device(data["address"])
    auth_data = BonecoAuth(device, data["key"])
    await actions(auth_data)


async def menu():
    choice = input("Choose between (1) pairing new device and (2) connecting existing device: ")
    if choice == "2":
        await connect()
    else:
        await pair()

if __name__ == "__main__":
    asyncio.run(menu())