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]
ifdata[0] == 4 and data[2] == 2
- Save ādevice keyā from
data[3:19]
ifdata[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.
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
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
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())