Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ maintainers = [
]
license = "Apache-2.0"
readme = "README.md"
requires-python = ">=3.12,<3.14"
requires-python = ">=3.12,<3.15"
dependencies = [
"bleak >= 0.21",
"bleak-retry-connector >= 3.5",
Expand All @@ -34,6 +34,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]

[project.urls]
Expand Down
45 changes: 37 additions & 8 deletions src/pyftms/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from collections.abc import AsyncIterator
from typing import Any

from bleak import BleakScanner
from bleak import BleakClient, BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from bleak.exc import BleakDeviceNotFoundError
from bleak.exc import BleakError, BleakDeviceNotFoundError
from bleak.uuids import normalize_uuid_str

from .backends import (
Expand All @@ -32,6 +32,7 @@
MachineType,
MovementDirection,
SettingRange,
get_machine_type_from_gatt,
get_machine_type_from_service_data,
)

Expand Down Expand Up @@ -97,22 +98,50 @@ async def discover_ftms_devices(
"""

devices: set[str] = set()

async with BleakScanner(
service_uuids=[normalize_uuid_str(FTMS_UUID)],
kwargs=kwargs,
) as scanner:
ftms_uuid = normalize_uuid_str(FTMS_UUID)

# Scan without a service_uuids filter: on Linux/BlueZ the hardware-level
# UUID filter only matches devices that include FTMS data in the service
# data field of their advertisement. Some devices (e.g. Bodytone DU30)
# advertise the FTMS UUID in service_uuids but provide no service data, so
# they would be silently dropped. We therefore scan for all BLE devices
# and filter manually.
async with BleakScanner(**kwargs) as scanner:
try:
async with asyncio.timeout(discover_time):
async for dev, adv in scanner.advertisement_data():
if dev.address in devices:
continue

# Quick pre-filter: skip devices that have no FTMS UUID
# in either service_data or service_uuids.
if ftms_uuid not in adv.service_data and ftms_uuid not in (
adv.service_uuids or ()
):
continue

try:
machine_type = get_machine_type_from_service_data(adv)

except NotFitnessMachineError:
continue
# The device advertises the FTMS service UUID but does
# not include machine type in its service data (e.g.
# Bodytone DU30). Fall back to GATT characteristic
# inspection by briefly connecting to the device.
try:
Comment on lines 126 to +131
async with BleakClient(
dev, services=[FTMS_UUID]
) as cli:
Comment on lines +131 to +134
machine_type = await get_machine_type_from_gatt(
cli
)
except (BleakError, NotFitnessMachineError, OSError):
Comment on lines +132 to +138
_LOGGER.debug(
"Could not determine machine type for '%s' "
"via GATT fallback.",
dev.address,
)
continue
Comment on lines +138 to +144

devices.add(dev.address)

Expand Down
7 changes: 6 additions & 1 deletion src/pyftms/client/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
SettingRange,
read_features,
)
from .machine_type import MachineType, get_machine_type_from_service_data
from .machine_type import (
MachineType,
get_machine_type_from_gatt,
get_machine_type_from_service_data
Comment on lines +13 to +15
Comment on lines +13 to +15
)

__all__ = [
"DeviceInfo",
"get_machine_type_from_gatt",
"get_machine_type_from_service_data",
"MachineFeatures",
"MachineSettings",
Expand Down
37 changes: 36 additions & 1 deletion src/pyftms/client/properties/machine_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
import operator
from enum import Flag, auto

from bleak import BleakClient
from bleak.backends.scanner import AdvertisementData
from bleak.uuids import normalize_uuid_str

from ..const import FTMS_UUID
from ..const import (
CROSS_TRAINER_DATA_UUID,
FTMS_UUID,
INDOOR_BIKE_DATA_UUID,
ROWER_DATA_UUID,
TREADMILL_DATA_UUID,
)
from ..errors import NotFitnessMachineError


Expand Down Expand Up @@ -79,3 +86,31 @@ def get_machine_type_from_service_data(
return mt

raise NotFitnessMachineError(data)


async def get_machine_type_from_gatt(cli: BleakClient) -> MachineType:
"""Determines fitness machine type from connected GATT characteristics.

Used as a fallback when the device advertises the FTMS service UUID but
does not include machine type information in its advertisement service data
(e.g. Bodytone DU30).

Parameters:
cli: Connected `BleakClient` instance with FTMS service discovered.

Returns:
Fitness machine type.
"""

_UUID_TO_TYPE = (
(TREADMILL_DATA_UUID, MachineType.TREADMILL),
(CROSS_TRAINER_DATA_UUID, MachineType.CROSS_TRAINER),
(ROWER_DATA_UUID, MachineType.ROWER),
(INDOOR_BIKE_DATA_UUID, MachineType.INDOOR_BIKE),
)

for uuid, machine_type in _UUID_TO_TYPE:
if cli.services.get_characteristic(uuid) is not None:
return machine_type

raise NotFitnessMachineError()