pyftms

PyFTMS - Bluetooth Fitness Machine Service async client library

PyFTMS is a Python client library for the FTMS service, which is a standard for fitness equipment with a Bluetooth interface. Bleak is used as the Bluetooth library. Currently four main types of fitness machines are supported:

  1. Treadmill
  2. Cross Trainer (Elliptical Trainer)
  3. Rower (Rowing Machine)
  4. Indoor Bike (Spin Bike)

Step Climber and Stair Climber machines are not supported due to incomplete protocol information and low popularity.

Requirments

  1. bleak
  2. bleak-retry-connector

Install it from PyPI

pip install pyftms

Usage

Please read API documentation.

 1# Copyright 2024, Sergey Dudanov
 2# SPDX-License-Identifier: Apache-2.0
 3
 4"""
 5.. include:: ../../README.md
 6"""
 7
 8from .client import (
 9    ControlEvent,
10    DeviceInfo,
11    FitnessMachine,
12    FtmsCallback,
13    MachineType,
14    MovementDirection,
15    NotFitnessMachineError,
16    PropertiesManager,
17    SettingRange,
18    SetupEvent,
19    SetupEventData,
20    SpinDownEvent,
21    SpinDownEventData,
22    UpdateEvent,
23    UpdateEventData,
24    discover_ftms_devices,
25    get_client,
26    get_client_from_address,
27    get_machine_type_from_service_data,
28)
29from .client.backends import FtmsEvents
30from .client.machines import CrossTrainer, IndoorBike, Rower, Treadmill
31from .models import (
32    IndoorBikeSimulationParameters,
33    ResultCode,
34    SpinDownControlCode,
35    SpinDownSpeedData,
36    SpinDownStatusCode,
37    TrainingStatusCode,
38)
39
40__all__ = [
41    "discover_ftms_devices",
42    "get_client",
43    "get_client_from_address",
44    "get_machine_type_from_service_data",
45    "FitnessMachine",
46    "CrossTrainer",
47    "IndoorBike",
48    "Treadmill",
49    "Rower",
50    "FtmsCallback",
51    "FtmsEvents",
52    "MachineType",
53    "UpdateEvent",
54    "SetupEvent",
55    "ControlEvent",
56    "NotFitnessMachineError",
57    "SetupEventData",
58    "UpdateEventData",
59    "MovementDirection",
60    "IndoorBikeSimulationParameters",
61    "DeviceInfo",
62    "SettingRange",
63    "ResultCode",
64    "PropertiesManager",
65    "TrainingStatusCode",
66    # Spin-Down
67    "SpinDownEvent",
68    "SpinDownEventData",
69    "SpinDownSpeedData",
70    "SpinDownControlCode",
71    "SpinDownStatusCode",
72]
async def discover_ftms_devices( discover_time: float = 10, **kwargs: Any) -> AsyncIterator[tuple[bleak.backends.device.BLEDevice, MachineType]]:
 85async def discover_ftms_devices(
 86    discover_time: float = 10,
 87    **kwargs: Any,
 88) -> AsyncIterator[tuple[BLEDevice, MachineType]]:
 89    """
 90    Discover FTMS devices.
 91
 92    Parameters:
 93    - `discover_time` - Discover time. Defaults to 10s.
 94    - `**kwargs` - Additional keyword arguments for backwards compatibility.
 95
 96    Return:
 97    - `AsyncIterator[tuple[BLEDevice, MachineType]]` async generator of `BLEDevice` and `MachineType` tuples.
 98    """
 99
100    devices: set[str] = set()
101
102    async with BleakScanner(
103        service_uuids=[normalize_uuid_str(FTMS_UUID)],
104        kwargs=kwargs,
105    ) as scanner:
106        try:
107            async with asyncio.timeout(discover_time):
108                async for dev, adv in scanner.advertisement_data():
109                    if dev.address in devices:
110                        continue
111
112                    try:
113                        machine_type = get_machine_type_from_service_data(adv)
114
115                    except NotFitnessMachineError:
116                        continue
117
118                    devices.add(dev.address)
119
120                    _LOGGER.debug(
121                        " #%d - %s: address='%s', name='%s'",
122                        len(devices),
123                        machine_type.name,
124                        dev.address,
125                        dev.name,
126                    )
127
128                    yield dev, machine_type
129
130        except asyncio.TimeoutError:
131            pass

Discover FTMS devices.

Parameters:

  • discover_time - Discover time. Defaults to 10s.
  • **kwargs - Additional keyword arguments for backwards compatibility.

Return:

  • AsyncIterator[tuple[BLEDevice, MachineType]] async generator of BLEDevice and MachineType tuples.
def get_client( ble_device: bleak.backends.device.BLEDevice, adv_or_type: bleak.backends.scanner.AdvertisementData | MachineType, *, timeout: float = 2, on_ftms_event: Optional[Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]] = None, on_disconnect: DisconnectCallback | None = None, **kwargs: Any) -> FitnessMachine:
42def get_client(
43    ble_device: BLEDevice,
44    adv_or_type: AdvertisementData | MachineType,
45    *,
46    timeout: float = 2,
47    on_ftms_event: FtmsCallback | None = None,
48    on_disconnect: DisconnectCallback | None = None,
49    **kwargs: Any,
50) -> FitnessMachine:
51    """
52    Creates an `FitnessMachine` instance from [Bleak](https://bleak.readthedocs.io/) discovered
53    information: device and advertisement data. Instead of advertisement data, the `MachineType` can be used.
54
55    Parameters:
56    - `ble_device` - [BLE device](https://bleak.readthedocs.io/en/latest/api/index.html#bleak.backends.device.BLEDevice).
57    - `adv_or_type` - Service [advertisement data](https://bleak.readthedocs.io/en/latest/backends/index.html#bleak.backends.scanner.AdvertisementData) or `MachineType`.
58    - `timeout` - Control operation timeout. Defaults to 2.0s.
59    - `on_ftms_event` - Callback for receiving fitness machine events.
60    - `on_disconnect` - Disconnection callback.
61    - `**kwargs` - Additional keyword arguments for backwards compatibility.
62
63    Return:
64    - `FitnessMachine` instance.
65    """
66
67    adv_data = None
68
69    if isinstance(adv_or_type, AdvertisementData):
70        adv_data = adv_or_type
71        adv_or_type = get_machine_type_from_service_data(adv_or_type)
72
73    cls = get_machine(adv_or_type)
74
75    return cls(
76        ble_device,
77        adv_data,
78        timeout=timeout,
79        on_ftms_event=on_ftms_event,
80        on_disconnect=on_disconnect,
81        kwargs=kwargs,
82    )

Creates an FitnessMachine instance from Bleak discovered information: device and advertisement data. Instead of advertisement data, the MachineType can be used.

Parameters:

  • ble_device - BLE device.
  • adv_or_type - Service advertisement data or MachineType.
  • timeout - Control operation timeout. Defaults to 2.0s.
  • on_ftms_event - Callback for receiving fitness machine events.
  • on_disconnect - Disconnection callback.
  • **kwargs - Additional keyword arguments for backwards compatibility.

Return:

async def get_client_from_address( address: str, *, scan_timeout: float = 10, timeout: float = 2, on_ftms_event: Optional[Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]] = None, on_disconnect: DisconnectCallback | None = None, **kwargs: Any) -> FitnessMachine:
134async def get_client_from_address(
135    address: str,
136    *,
137    scan_timeout: float = 10,
138    timeout: float = 2,
139    on_ftms_event: FtmsCallback | None = None,
140    on_disconnect: DisconnectCallback | None = None,
141    **kwargs: Any,
142) -> FitnessMachine:
143    """
144    Scans for fitness machine with specified BLE address. On success creates and return an `FitnessMachine` instance.
145
146    Parameters:
147    - `address` - The Bluetooth address of the device on this machine (UUID on macOS).
148    - `scan_timeout` - Scanning timeout. Defaults to 10.0s.
149    - `timeout` - Control operation timeout. Defaults to 2.0s.
150    - `on_ftms_event` - Callback for receiving fitness machine events.
151    - `on_disconnect` - Disconnection callback.
152    - `**kwargs` - Additional keyword arguments for backwards compatibility.
153
154    Return:
155    - `FitnessMachine` instance if device found successfully.
156    """
157
158    async for dev, machine_type in discover_ftms_devices(
159        scan_timeout, kwargs=kwargs
160    ):
161        if dev.address.lower() == address.lower():
162            return get_client(
163                dev,
164                machine_type,
165                timeout=timeout,
166                on_ftms_event=on_ftms_event,
167                on_disconnect=on_disconnect,
168                kwargs=kwargs,
169            )
170
171    raise BleakDeviceNotFoundError(address)

Scans for fitness machine with specified BLE address. On success creates and return an FitnessMachine instance.

Parameters:

  • address - The Bluetooth address of the device on this machine (UUID on macOS).
  • scan_timeout - Scanning timeout. Defaults to 10.0s.
  • timeout - Control operation timeout. Defaults to 2.0s.
  • on_ftms_event - Callback for receiving fitness machine events.
  • on_disconnect - Disconnection callback.
  • **kwargs - Additional keyword arguments for backwards compatibility.

Return:

def get_machine_type_from_service_data( adv_data: bleak.backends.scanner.AdvertisementData) -> MachineType:
52def get_machine_type_from_service_data(
53    adv_data: AdvertisementData,
54) -> MachineType:
55    """Returns fitness machine type from Bluetooth advertisement data.
56
57    Parameters:
58        adv_data: Bluetooth [advertisement data](https://bleak.readthedocs.io/en/latest/backends/index.html#bleak.backends.scanner.AdvertisementData).
59
60    Returns:
61        Fitness machine type.
62    """
63
64    data = adv_data.service_data.get(normalize_uuid_str(FTMS_UUID))
65
66    if data is None or not (2 <= len(data) <= 3):
67        raise NotFitnessMachineError(data)
68
69    # Reading mandatory `Flags` and `Machine Type`.
70    # `Machine Type` bytes may be reversed on some machines or be a just one
71    # byte (it's bug), so I logically ORed them.
72    try:
73        mt = functools.reduce(operator.or_, data[1:])
74        mf, mt = MachineFlags(data[0]), MachineType(mt)
75
76    except ValueError:
77        raise NotFitnessMachineError(data)
78
79    if mf and mt:
80        return mt
81
82    raise NotFitnessMachineError(data)

Returns fitness machine type from Bluetooth advertisement data.

Parameters: adv_data: Bluetooth advertisement data.

Returns: Fitness machine type.

class FitnessMachine(abc.ABC, pyftms.PropertiesManager):
 46class FitnessMachine(ABC, PropertiesManager):
 47    """
 48    Base FTMS client.
 49
 50    Supports `async with ...` context manager.
 51    """
 52
 53    _machine_type: ClassVar[MachineType]
 54    """Machine type."""
 55
 56    _data_model: ClassVar[type[RealtimeData]]
 57    """Model of real-time training data."""
 58
 59    _data_uuid: ClassVar[str]
 60    """Notify UUID of real-time training data."""
 61
 62    _cli: BleakClient
 63
 64    _updater: DataUpdater
 65
 66    _device: BLEDevice
 67    _need_connect: bool
 68
 69    # Static device info
 70
 71    _device_info: DeviceInfo = {}
 72    _m_features: MachineFeatures = MachineFeatures(0)
 73    _m_settings: MachineSettings = MachineSettings(0)
 74    _settings_ranges: MappingProxyType[str, SettingRange] = MappingProxyType({})
 75
 76    def __init__(
 77        self,
 78        ble_device: BLEDevice,
 79        adv_data: AdvertisementData | None = None,
 80        *,
 81        timeout: float = 2.0,
 82        on_ftms_event: FtmsCallback | None = None,
 83        on_disconnect: DisconnectCallback | None = None,
 84        **kwargs: Any,
 85    ) -> None:
 86        super().__init__(on_ftms_event)
 87
 88        self._need_connect = False
 89        self._timeout = timeout
 90        self._disconnect_cb = on_disconnect
 91        self._kwargs = kwargs
 92
 93        self.set_ble_device_and_advertisement_data(ble_device, adv_data)
 94
 95        # Updaters
 96        self._updater = DataUpdater(self._data_model, self._on_event)
 97        self._controller = MachineController(self._on_event)
 98
 99    @classmethod
100    def _get_supported_properties(
101        cls, features: MachineFeatures = MachineFeatures(~0)
102    ) -> list[str]:
103        return cls._data_model._get_features(features)
104
105    async def __aenter__(self):
106        await self.connect()
107        return self
108
109    async def __aexit__(self, exc_type, exc, tb):
110        await self.disconnect()
111
112    # BLE SPECIFIC PROPERTIES
113
114    def set_ble_device_and_advertisement_data(
115        self, ble_device: BLEDevice, adv_data: AdvertisementData | None
116    ):
117        self._device = ble_device
118
119        if adv_data:
120            self._properties["rssi"] = adv_data.rssi
121
122            if self._cb:
123                self._cb(UpdateEvent("update", {"rssi": adv_data.rssi}))
124
125    @property
126    def unique_id(self) -> str:
127        """Unique ID"""
128
129        return self.device_info.get(
130            "serial_number", self.address.replace(":", "").lower()
131        )
132
133    @property
134    def need_connect(self) -> bool:
135        """Connection state latch. `True` if connection is needed."""
136        return self._need_connect
137
138    @need_connect.setter
139    def need_connect(self, value: bool) -> None:
140        """Connection state latch. `True` if connection is needed."""
141        self._need_connect = value
142
143    @property
144    def rssi(self) -> int | None:
145        """RSSI."""
146        return self.get_property("rssi")
147
148    @property
149    def name(self) -> str:
150        """Device name or BLE address"""
151
152        return self._device.name or self._device.address
153
154    def set_disconnect_callback(self, cb: DisconnectCallback):
155        """Set disconnect callback."""
156        self._disconnect_cb = cb
157
158    async def connect(self) -> None:
159        """
160        Opens a connection to the device. Reads necessary static information:
161        * Device Information (manufacturer, model, serial number, hardware and software versions);
162        * Supported features;
163        * Supported settings;
164        * Ranges of parameters settings.
165        """
166
167        self._need_connect = True
168
169        await self._connect()
170
171    async def disconnect(self) -> None:
172        """Disconnects from device."""
173
174        self._need_connect = False
175
176        if self.is_connected:
177            await self._cli.disconnect()
178
179    @property
180    def address(self) -> str:
181        """Bluetooth address."""
182
183        return self._device.address
184
185    @property
186    def is_connected(self) -> bool:
187        """Current connection status."""
188
189        return hasattr(self, "_cli") and self._cli.is_connected
190
191    # COMMON BASE PROPERTIES
192
193    @property
194    def device_info(self) -> DeviceInfo:
195        """Device Information."""
196
197        return self._device_info
198
199    @property
200    def machine_type(self) -> MachineType:
201        """Machine type."""
202
203        return self._machine_type
204
205    @cached_property
206    def supported_properties(self) -> list[str]:
207        """
208        Properties that supported by this machine.
209        Based on **Machine Features** report.
210
211        *May contain both meaningless properties and may not contain
212        some properties that are supported by the machine.*
213        """
214
215        x = self._get_supported_properties(self._m_features)
216
217        if self.training_status is not None:
218            x.append(c.TRAINING_STATUS)
219
220        return x
221
222    @cached_property
223    def available_properties(self) -> list[str]:
224        """All properties that *MAY BE* supported by this machine type."""
225
226        x = self._get_supported_properties()
227        x.append(c.TRAINING_STATUS)
228
229        return x
230
231    @cached_property
232    def supported_settings(self) -> list[str]:
233        """Supported settings."""
234
235        return ControlModel._get_features(self._m_settings)
236
237    @property
238    def supported_ranges(self) -> MappingProxyType[str, SettingRange]:
239        """Ranges of supported settings."""
240
241        return self._settings_ranges
242
243    def _on_disconnect(self, cli: BleakClient) -> None:
244        _LOGGER.debug("Client disconnected. Reset updaters states.")
245
246        del self._cli
247        self._updater.reset()
248        self._controller.reset()
249
250        if self._disconnect_cb:
251            self._disconnect_cb(self)
252
253    async def _connect(self) -> None:
254        if not self._need_connect or self.is_connected:
255            return
256
257        await close_stale_connections(self._device)
258
259        _LOGGER.debug("Initialization. Trying to establish connection.")
260
261        self._cli = await establish_connection(
262            client_class=BleakClient,
263            device=self._device,
264            name=self.name,
265            disconnected_callback=self._on_disconnect,
266            # we needed only two services: `Fitness Machine Service` and `Device Information Service`
267            services=[c.FTMS_UUID, DIS_UUID],
268            kwargs=self._kwargs,
269        )
270
271        _LOGGER.debug("Connection success.")
272
273        # Reading necessary static fitness machine information
274
275        if not self._device_info:
276            self._device_info = await read_device_info(self._cli)
277
278        if not self._m_features:
279            (
280                self._m_features,
281                self._m_settings,
282                self._settings_ranges,
283            ) = await read_features(self._cli, self._machine_type)
284
285        await self._controller.subscribe(self._cli)
286        await self._updater.subscribe(self._cli, self._data_uuid)
287
288    # COMMANDS
289
290    async def _write_command(
291        self, code: ControlCode | None = None, *args, **kwargs
292    ):
293        if self._need_connect:
294            await self._connect()
295            return await self._controller.write_command(
296                self._cli, code, timeout=self._timeout, **kwargs
297            )
298
299        return ResultCode.FAILED
300
301    async def reset(self) -> ResultCode:
302        """Initiates the procedure to reset the controllable settings of a fitness machine."""
303        return await self._write_command(ControlCode.RESET)
304
305    async def start_resume(self) -> ResultCode:
306        """Initiate the procedure to start or resume a training session."""
307        return await self._write_command(ControlCode.START_RESUME)
308
309    async def stop(self) -> ResultCode:
310        """Initiate the procedure to stop a training session."""
311        return await self._write_command(stop_pause=StopPauseCode.STOP)
312
313    async def pause(self) -> ResultCode:
314        """Initiate the procedure to pause a training session."""
315        return await self._write_command(stop_pause=StopPauseCode.PAUSE)
316
317    async def set_setting(self, setting_id: str, *args: Any) -> ResultCode:
318        """
319        Generic method of settings by ID.
320
321        **Methods for setting specific parameters.**
322        """
323
324        if setting_id not in self.supported_settings:
325            return ResultCode.NOT_SUPPORTED
326
327        if not args:
328            raise ValueError("No data to pass.")
329
330        if len(args) == 1:
331            args = args[0]
332
333        return await self._write_command(code=None, **{setting_id: args})
334
335    async def set_target_speed(self, value: float) -> ResultCode:
336        """
337        Sets target speed.
338
339        Units: `km/h`.
340        """
341        return await self.set_setting(c.TARGET_SPEED, value)
342
343    async def set_target_inclination(self, value: float) -> ResultCode:
344        """
345        Sets target inclination.
346
347        Units: `%`.
348        """
349        return await self.set_setting(c.TARGET_INCLINATION, value)
350
351    async def set_target_resistance(self, value: float) -> ResultCode:
352        """
353        Sets target resistance level.
354
355        Units: `unitless`.
356        """
357        return await self.set_setting(c.TARGET_RESISTANCE, value)
358
359    async def set_target_power(self, value: int) -> ResultCode:
360        """
361        Sets target power.
362
363        Units: `Watt`.
364        """
365        return await self.set_setting(c.TARGET_POWER, value)
366
367    async def set_target_heart_rate(self, value: int) -> ResultCode:
368        """
369        Sets target heart rate.
370
371        Units: `bpm`.
372        """
373        return await self.set_setting(c.TARGET_HEART_RATE, value)
374
375    async def set_target_energy(self, value: int) -> ResultCode:
376        """
377        Sets target expended energy.
378
379        Units: `kcal`.
380        """
381        return await self.set_setting(c.TARGET_ENERGY, value)
382
383    async def set_target_steps(self, value: int) -> ResultCode:
384        """
385        Sets targeted number of steps.
386
387        Units: `step`.
388        """
389        return await self.set_setting(c.TARGET_STEPS, value)
390
391    async def set_target_strides(self, value: int) -> ResultCode:
392        """
393        Sets targeted number of strides.
394
395        Units: `stride`.
396        """
397        return await self.set_setting(c.TARGET_STRIDES, value)
398
399    async def set_target_distance(self, value: int) -> ResultCode:
400        """
401        Sets targeted distance.
402
403        Units: `m`.
404        """
405        return await self.set_setting(c.TARGET_DISTANCE, value)
406
407    async def set_target_time(self, *value: int) -> ResultCode:
408        """
409        Set targeted training time.
410
411        Units: `s`.
412        """
413        return await self.set_setting(c.TARGET_TIME, *value)
414
415    async def set_indoor_bike_simulation(
416        self,
417        value: IndoorBikeSimulationParameters,
418    ) -> ResultCode:
419        """Set indoor bike simulation parameters."""
420        return await self.set_setting(c.INDOOR_BIKE_SIMULATION, value)
421
422    async def set_wheel_circumference(self, value: float) -> ResultCode:
423        """
424        Set wheel circumference.
425
426        Units: `mm`.
427        """
428        return await self.set_setting(c.WHEEL_CIRCUMFERENCE, value)
429
430    async def spin_down_start(self) -> ResultCode:
431        """
432        Start Spin-Down.
433
434        It can be sent either in response to a request to start Spin-Down, or separately.
435        """
436        return await self.set_setting(c.SPIN_DOWN, SpinDownControlCode.START)
437
438    async def spin_down_ignore(self) -> ResultCode:
439        """
440        Ignore Spin-Down.
441
442        It can be sent in response to a request to start Spin-Down.
443        """
444        return await self.set_setting(c.SPIN_DOWN, SpinDownControlCode.IGNORE)
445
446    async def set_target_cadence(self, value: float) -> ResultCode:
447        """
448        Set targeted cadence.
449
450        Units: `rpm`.
451        """
452        return await self.set_setting(c.TARGET_CADENCE, value)

Base FTMS client.

Supports async with ... context manager.

FitnessMachine( ble_device: bleak.backends.device.BLEDevice, adv_data: bleak.backends.scanner.AdvertisementData | None = None, *, timeout: float = 2.0, on_ftms_event: Optional[Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]] = None, on_disconnect: DisconnectCallback | None = None, **kwargs: Any)
76    def __init__(
77        self,
78        ble_device: BLEDevice,
79        adv_data: AdvertisementData | None = None,
80        *,
81        timeout: float = 2.0,
82        on_ftms_event: FtmsCallback | None = None,
83        on_disconnect: DisconnectCallback | None = None,
84        **kwargs: Any,
85    ) -> None:
86        super().__init__(on_ftms_event)
87
88        self._need_connect = False
89        self._timeout = timeout
90        self._disconnect_cb = on_disconnect
91        self._kwargs = kwargs
92
93        self.set_ble_device_and_advertisement_data(ble_device, adv_data)
94
95        # Updaters
96        self._updater = DataUpdater(self._data_model, self._on_event)
97        self._controller = MachineController(self._on_event)
def set_ble_device_and_advertisement_data( self, ble_device: bleak.backends.device.BLEDevice, adv_data: bleak.backends.scanner.AdvertisementData | None):
114    def set_ble_device_and_advertisement_data(
115        self, ble_device: BLEDevice, adv_data: AdvertisementData | None
116    ):
117        self._device = ble_device
118
119        if adv_data:
120            self._properties["rssi"] = adv_data.rssi
121
122            if self._cb:
123                self._cb(UpdateEvent("update", {"rssi": adv_data.rssi}))
unique_id: str
125    @property
126    def unique_id(self) -> str:
127        """Unique ID"""
128
129        return self.device_info.get(
130            "serial_number", self.address.replace(":", "").lower()
131        )

Unique ID

need_connect: bool
133    @property
134    def need_connect(self) -> bool:
135        """Connection state latch. `True` if connection is needed."""
136        return self._need_connect

Connection state latch. True if connection is needed.

rssi: int | None
143    @property
144    def rssi(self) -> int | None:
145        """RSSI."""
146        return self.get_property("rssi")

RSSI.

name: str
148    @property
149    def name(self) -> str:
150        """Device name or BLE address"""
151
152        return self._device.name or self._device.address

Device name or BLE address

def set_disconnect_callback(self, cb: DisconnectCallback):
154    def set_disconnect_callback(self, cb: DisconnectCallback):
155        """Set disconnect callback."""
156        self._disconnect_cb = cb

Set disconnect callback.

async def connect(self) -> None:
158    async def connect(self) -> None:
159        """
160        Opens a connection to the device. Reads necessary static information:
161        * Device Information (manufacturer, model, serial number, hardware and software versions);
162        * Supported features;
163        * Supported settings;
164        * Ranges of parameters settings.
165        """
166
167        self._need_connect = True
168
169        await self._connect()

Opens a connection to the device. Reads necessary static information:

  • Device Information (manufacturer, model, serial number, hardware and software versions);
  • Supported features;
  • Supported settings;
  • Ranges of parameters settings.
async def disconnect(self) -> None:
171    async def disconnect(self) -> None:
172        """Disconnects from device."""
173
174        self._need_connect = False
175
176        if self.is_connected:
177            await self._cli.disconnect()

Disconnects from device.

address: str
179    @property
180    def address(self) -> str:
181        """Bluetooth address."""
182
183        return self._device.address

Bluetooth address.

is_connected: bool
185    @property
186    def is_connected(self) -> bool:
187        """Current connection status."""
188
189        return hasattr(self, "_cli") and self._cli.is_connected

Current connection status.

device_info: DeviceInfo
193    @property
194    def device_info(self) -> DeviceInfo:
195        """Device Information."""
196
197        return self._device_info

Device Information.

machine_type: MachineType
199    @property
200    def machine_type(self) -> MachineType:
201        """Machine type."""
202
203        return self._machine_type

Machine type.

supported_properties: list[str]
205    @cached_property
206    def supported_properties(self) -> list[str]:
207        """
208        Properties that supported by this machine.
209        Based on **Machine Features** report.
210
211        *May contain both meaningless properties and may not contain
212        some properties that are supported by the machine.*
213        """
214
215        x = self._get_supported_properties(self._m_features)
216
217        if self.training_status is not None:
218            x.append(c.TRAINING_STATUS)
219
220        return x

Properties that supported by this machine. Based on Machine Features report.

May contain both meaningless properties and may not contain some properties that are supported by the machine.

available_properties: list[str]
222    @cached_property
223    def available_properties(self) -> list[str]:
224        """All properties that *MAY BE* supported by this machine type."""
225
226        x = self._get_supported_properties()
227        x.append(c.TRAINING_STATUS)
228
229        return x

All properties that MAY BE supported by this machine type.

supported_settings: list[str]
231    @cached_property
232    def supported_settings(self) -> list[str]:
233        """Supported settings."""
234
235        return ControlModel._get_features(self._m_settings)

Supported settings.

supported_ranges: mappingproxy[str, SettingRange]
237    @property
238    def supported_ranges(self) -> MappingProxyType[str, SettingRange]:
239        """Ranges of supported settings."""
240
241        return self._settings_ranges

Ranges of supported settings.

async def reset(self) -> ResultCode:
301    async def reset(self) -> ResultCode:
302        """Initiates the procedure to reset the controllable settings of a fitness machine."""
303        return await self._write_command(ControlCode.RESET)

Initiates the procedure to reset the controllable settings of a fitness machine.

async def start_resume(self) -> ResultCode:
305    async def start_resume(self) -> ResultCode:
306        """Initiate the procedure to start or resume a training session."""
307        return await self._write_command(ControlCode.START_RESUME)

Initiate the procedure to start or resume a training session.

async def stop(self) -> ResultCode:
309    async def stop(self) -> ResultCode:
310        """Initiate the procedure to stop a training session."""
311        return await self._write_command(stop_pause=StopPauseCode.STOP)

Initiate the procedure to stop a training session.

async def pause(self) -> ResultCode:
313    async def pause(self) -> ResultCode:
314        """Initiate the procedure to pause a training session."""
315        return await self._write_command(stop_pause=StopPauseCode.PAUSE)

Initiate the procedure to pause a training session.

async def set_setting( self, setting_id: str, *args: Any) -> ResultCode:
317    async def set_setting(self, setting_id: str, *args: Any) -> ResultCode:
318        """
319        Generic method of settings by ID.
320
321        **Methods for setting specific parameters.**
322        """
323
324        if setting_id not in self.supported_settings:
325            return ResultCode.NOT_SUPPORTED
326
327        if not args:
328            raise ValueError("No data to pass.")
329
330        if len(args) == 1:
331            args = args[0]
332
333        return await self._write_command(code=None, **{setting_id: args})

Generic method of settings by ID.

Methods for setting specific parameters.

async def set_target_speed(self, value: float) -> ResultCode:
335    async def set_target_speed(self, value: float) -> ResultCode:
336        """
337        Sets target speed.
338
339        Units: `km/h`.
340        """
341        return await self.set_setting(c.TARGET_SPEED, value)

Sets target speed.

Units: km/h.

async def set_target_inclination(self, value: float) -> ResultCode:
343    async def set_target_inclination(self, value: float) -> ResultCode:
344        """
345        Sets target inclination.
346
347        Units: `%`.
348        """
349        return await self.set_setting(c.TARGET_INCLINATION, value)

Sets target inclination.

Units: %.

async def set_target_resistance(self, value: float) -> ResultCode:
351    async def set_target_resistance(self, value: float) -> ResultCode:
352        """
353        Sets target resistance level.
354
355        Units: `unitless`.
356        """
357        return await self.set_setting(c.TARGET_RESISTANCE, value)

Sets target resistance level.

Units: unitless.

async def set_target_power(self, value: int) -> ResultCode:
359    async def set_target_power(self, value: int) -> ResultCode:
360        """
361        Sets target power.
362
363        Units: `Watt`.
364        """
365        return await self.set_setting(c.TARGET_POWER, value)

Sets target power.

Units: Watt.

async def set_target_heart_rate(self, value: int) -> ResultCode:
367    async def set_target_heart_rate(self, value: int) -> ResultCode:
368        """
369        Sets target heart rate.
370
371        Units: `bpm`.
372        """
373        return await self.set_setting(c.TARGET_HEART_RATE, value)

Sets target heart rate.

Units: bpm.

async def set_target_energy(self, value: int) -> ResultCode:
375    async def set_target_energy(self, value: int) -> ResultCode:
376        """
377        Sets target expended energy.
378
379        Units: `kcal`.
380        """
381        return await self.set_setting(c.TARGET_ENERGY, value)

Sets target expended energy.

Units: kcal.

async def set_target_steps(self, value: int) -> ResultCode:
383    async def set_target_steps(self, value: int) -> ResultCode:
384        """
385        Sets targeted number of steps.
386
387        Units: `step`.
388        """
389        return await self.set_setting(c.TARGET_STEPS, value)

Sets targeted number of steps.

Units: step.

async def set_target_strides(self, value: int) -> ResultCode:
391    async def set_target_strides(self, value: int) -> ResultCode:
392        """
393        Sets targeted number of strides.
394
395        Units: `stride`.
396        """
397        return await self.set_setting(c.TARGET_STRIDES, value)

Sets targeted number of strides.

Units: stride.

async def set_target_distance(self, value: int) -> ResultCode:
399    async def set_target_distance(self, value: int) -> ResultCode:
400        """
401        Sets targeted distance.
402
403        Units: `m`.
404        """
405        return await self.set_setting(c.TARGET_DISTANCE, value)

Sets targeted distance.

Units: m.

async def set_target_time(self, *value: int) -> ResultCode:
407    async def set_target_time(self, *value: int) -> ResultCode:
408        """
409        Set targeted training time.
410
411        Units: `s`.
412        """
413        return await self.set_setting(c.TARGET_TIME, *value)

Set targeted training time.

Units: s.

async def set_indoor_bike_simulation( self, value: IndoorBikeSimulationParameters) -> ResultCode:
415    async def set_indoor_bike_simulation(
416        self,
417        value: IndoorBikeSimulationParameters,
418    ) -> ResultCode:
419        """Set indoor bike simulation parameters."""
420        return await self.set_setting(c.INDOOR_BIKE_SIMULATION, value)

Set indoor bike simulation parameters.

async def set_wheel_circumference(self, value: float) -> ResultCode:
422    async def set_wheel_circumference(self, value: float) -> ResultCode:
423        """
424        Set wheel circumference.
425
426        Units: `mm`.
427        """
428        return await self.set_setting(c.WHEEL_CIRCUMFERENCE, value)

Set wheel circumference.

Units: mm.

async def spin_down_start(self) -> ResultCode:
430    async def spin_down_start(self) -> ResultCode:
431        """
432        Start Spin-Down.
433
434        It can be sent either in response to a request to start Spin-Down, or separately.
435        """
436        return await self.set_setting(c.SPIN_DOWN, SpinDownControlCode.START)

Start Spin-Down.

It can be sent either in response to a request to start Spin-Down, or separately.

async def spin_down_ignore(self) -> ResultCode:
438    async def spin_down_ignore(self) -> ResultCode:
439        """
440        Ignore Spin-Down.
441
442        It can be sent in response to a request to start Spin-Down.
443        """
444        return await self.set_setting(c.SPIN_DOWN, SpinDownControlCode.IGNORE)

Ignore Spin-Down.

It can be sent in response to a request to start Spin-Down.

async def set_target_cadence(self, value: float) -> ResultCode:
446    async def set_target_cadence(self, value: float) -> ResultCode:
447        """
448        Set targeted cadence.
449
450        Units: `rpm`.
451        """
452        return await self.set_setting(c.TARGET_CADENCE, value)

Set targeted cadence.

Units: rpm.

class CrossTrainer(pyftms.FitnessMachine):
13class CrossTrainer(FitnessMachine):
14    """
15    Cross Trainer (Elliptical Trainer).
16
17    Specific class of `FitnessMachine`.
18    """
19
20    _machine_type: ClassVar[MachineType] = MachineType.CROSS_TRAINER
21
22    _data_model: ClassVar[type[RealtimeData]] = CrossTrainerData
23
24    _data_uuid: ClassVar[str] = CROSS_TRAINER_DATA_UUID

Cross Trainer (Elliptical Trainer).

Specific class of FitnessMachine.

class IndoorBike(pyftms.FitnessMachine):
13class IndoorBike(FitnessMachine):
14    """
15    Indoor Bike (Spin Bike).
16
17    Specific class of `FitnessMachine`.
18    """
19
20    _machine_type: ClassVar[MachineType] = MachineType.INDOOR_BIKE
21
22    _data_model: ClassVar[type[RealtimeData]] = IndoorBikeData
23
24    _data_uuid: ClassVar[str] = INDOOR_BIKE_DATA_UUID

Indoor Bike (Spin Bike).

Specific class of FitnessMachine.

class Treadmill(pyftms.FitnessMachine):
13class Treadmill(FitnessMachine):
14    """
15    Treadmill.
16
17    Specific class of `FitnessMachine`.
18    """
19
20    _machine_type: ClassVar[MachineType] = MachineType.TREADMILL
21
22    _data_model: ClassVar[type[RealtimeData]] = TreadmillData
23
24    _data_uuid: ClassVar[str] = TREADMILL_DATA_UUID

Treadmill.

Specific class of FitnessMachine.

class Rower(pyftms.FitnessMachine):
13class Rower(FitnessMachine):
14    """
15    Rower (Rowing Machine).
16
17    Specific class of `FitnessMachine`.
18    """
19
20    _machine_type: ClassVar[MachineType] = MachineType.ROWER
21
22    _data_model: ClassVar[type[RealtimeData]] = RowerData
23
24    _data_uuid: ClassVar[str] = ROWER_DATA_UUID

Rower (Rowing Machine).

Specific class of FitnessMachine.

FtmsCallback = typing.Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]
class MachineType(enum.Flag):
29class MachineType(Flag):
30    """
31    Fitness Machine Type.
32
33    Included in the Advertisement Service Data.
34
35    Described in section **3.1.2: Fitness Machine Type Field**.
36    """
37
38    TREADMILL = auto()
39    """Treadmill Machine."""
40    CROSS_TRAINER = auto()
41    """Cross Trainer Machine."""
42    STEP_CLIMBER = auto()
43    """Step Climber Machine."""
44    STAIR_CLIMBER = auto()
45    """Stair Climber Machine."""
46    ROWER = auto()
47    """Rower Machine."""
48    INDOOR_BIKE = auto()
49    """Indoor Bike Machine."""

Fitness Machine Type.

Included in the Advertisement Service Data.

Described in section 3.1.2: Fitness Machine Type Field.

TREADMILL = <MachineType.TREADMILL: 1>

Treadmill Machine.

CROSS_TRAINER = <MachineType.CROSS_TRAINER: 2>

Cross Trainer Machine.

STEP_CLIMBER = <MachineType.STEP_CLIMBER: 4>

Step Climber Machine.

STAIR_CLIMBER = <MachineType.STAIR_CLIMBER: 8>

Stair Climber Machine.

ROWER = <MachineType.ROWER: 16>

Rower Machine.

INDOOR_BIKE = <MachineType.INDOOR_BIKE: 32>

Indoor Bike Machine.

class UpdateEvent(typing.NamedTuple):
323class UpdateEvent(NamedTuple):
324    """Update Event."""
325
326    event_id: Literal["update"]
327    """Always `update`."""
328    event_data: UpdateEventData
329    """`UpdateEvent` data."""

Update Event.

UpdateEvent( event_id: Literal['update'], event_data: UpdateEventData)

Create new instance of UpdateEvent(event_id, event_data)

event_id: Literal['update']

Always update.

event_data: UpdateEventData
class SetupEvent(typing.NamedTuple):
341class SetupEvent(NamedTuple):
342    """Setting Event."""
343
344    event_id: Literal["setup"]
345    """Always `setup`."""
346    event_data: SetupEventData
347    """`SetupEvent` data."""
348    event_source: ControlSource
349    """Reason of event."""

Setting Event.

SetupEvent( event_id: Literal['setup'], event_data: SetupEventData, event_source: Literal['callback', 'user', 'safety', 'other'])

Create new instance of SetupEvent(event_id, event_data, event_source)

event_id: Literal['setup']

Always setup.

event_data: SetupEventData

SetupEvent data.

event_source: Literal['callback', 'user', 'safety', 'other']

Reason of event.

class ControlEvent(typing.NamedTuple):
352class ControlEvent(NamedTuple):
353    """Control Event."""
354
355    event_id: ControlEvents
356    """One of: `start`, `stop`, `pause`, `reset`."""
357    event_source: ControlSource
358    """Reason of event."""

Control Event.

ControlEvent( event_id: Literal['start', 'stop', 'pause', 'reset'], event_source: Literal['callback', 'user', 'safety', 'other'])

Create new instance of ControlEvent(event_id, event_source)

event_id: Literal['start', 'stop', 'pause', 'reset']

One of: start, stop, pause, reset.

event_source: Literal['callback', 'user', 'safety', 'other']

Reason of event.

class NotFitnessMachineError(pyftms.client.errors.FtmsError):
15class NotFitnessMachineError(FtmsError):
16    """
17    An exception if the FTMS service is not supported by the Bluetooth device.
18
19    May be raised in `get_machine_type_from_service_data` and `get_client`
20    functions if advertisement data was passed as an argument.
21    """
22
23    def __init__(self, data: bytes | None = None) -> None:
24        if data is None:
25            reason = "No FTMS service data"
26        else:
27            reason = f"Wrong FTMS service data: '{data.hex(" ").upper()}'"
28
29        super().__init__(f"Device is not Fitness Machine. {reason}.")

An exception if the FTMS service is not supported by the Bluetooth device.

May be raised in get_machine_type_from_service_data and get_client functions if advertisement data was passed as an argument.

NotFitnessMachineError(data: bytes | None = None)
23    def __init__(self, data: bytes | None = None) -> None:
24        if data is None:
25            reason = "No FTMS service data"
26        else:
27            reason = f"Wrong FTMS service data: '{data.hex(" ").upper()}'"
28
29        super().__init__(f"Device is not Fitness Machine. {reason}.")
class SetupEventData(typing.TypedDict):
 32class SetupEventData(TypedDict, total=False):
 33    """`SetupEvent` data."""
 34
 35    indoor_bike_simulation: IndoorBikeSimulationParameters
 36    """Indoor Bike Simulation Parameters."""
 37    target_cadence: float
 38    """
 39    Targeted cadence.
 40
 41    Units: `rpm`.
 42    """
 43    target_distance: int
 44    """
 45    Targeted distance.
 46
 47    Units: `m`.
 48    """
 49    target_energy: int
 50    """
 51    Targeted expended energy.
 52
 53    Units: `kcal`.
 54    """
 55    target_heart_rate: int
 56    """
 57    Targeted heart rate.
 58
 59    Units: `bpm`.
 60    """
 61    target_inclination: float
 62    """
 63    Targeted inclination.
 64
 65    Units: `%`.
 66    """
 67    target_power: int
 68    """
 69    Targeted power.
 70
 71    Units: `Watt`.
 72    """
 73    target_resistance: float | int
 74    """
 75    Targeted resistance level.
 76
 77    Units: `unitless`.
 78    """
 79    target_speed: float
 80    """
 81    Targeted speed.
 82
 83    Units: `km/h`.
 84    """
 85    target_steps: int
 86    """
 87    Targeted number of steps.
 88
 89    Units: `step`.
 90    """
 91    target_strides: int
 92    """
 93    Targeted number of strides.
 94
 95    Units: `stride`.
 96    """
 97    target_time: tuple[int, ...]
 98    """
 99    Targeted training time.
100
101    Units: `s`.
102    """
103    wheel_circumference: float
104    """
105    Wheel circumference.
106
107    Units: `mm`.
108    """

SetupEvent data.

indoor_bike_simulation: IndoorBikeSimulationParameters

Indoor Bike Simulation Parameters.

target_cadence: float

Targeted cadence.

Units: rpm.

target_distance: int

Targeted distance.

Units: m.

target_energy: int

Targeted expended energy.

Units: kcal.

target_heart_rate: int

Targeted heart rate.

Units: bpm.

target_inclination: float

Targeted inclination.

Units: %.

target_power: int

Targeted power.

Units: Watt.

target_resistance: float | int

Targeted resistance level.

Units: unitless.

target_speed: float

Targeted speed.

Units: km/h.

target_steps: int

Targeted number of steps.

Units: step.

target_strides: int

Targeted number of strides.

Units: stride.

target_time: tuple[int, ...]

Targeted training time.

Units: s.

wheel_circumference: float

Wheel circumference.

Units: mm.

class UpdateEventData(typing.TypedDict):
111class UpdateEventData(TypedDict, total=False):
112    rssi: int
113    """RSSI."""
114
115    cadence_average: float
116    """
117    Average Cadence.
118
119    Units: `rpm`.
120    """
121    cadence_instant: float
122    """
123    Instantaneous Cadence.
124
125    Units: `rpm`.
126    """
127    distance_total: int
128    """
129    Total Distance.
130
131    Units: `m`.
132    """
133    elevation_gain_negative: int
134    """
135    Negative Elevation Gain.
136
137    Units: `m`.
138    """
139    elevation_gain_positive: int
140    """
141    Positive Elevation Gain.
142
143    Units: `m`.
144    """
145    energy_per_hour: int
146    """
147    Energy Per Hour.
148
149    Units: `kcal`.
150    """
151    energy_per_minute: int
152    """
153    Energy Per Minute.
154
155    Units: `kcal`.
156    """
157    energy_total: int
158    """
159    Total Energy.
160
161    Units: `kcal`.
162    """
163    force_on_belt: int
164    """
165    Force on Belt.
166
167    Units: `newton`.
168    """
169    heart_rate: int
170    """
171    Heart Rate.
172
173    Units: `bpm`.
174    """
175    inclination: float
176    """
177    Inclination.
178
179    Units: `%`.
180    """
181    metabolic_equivalent: float
182    """
183    Metabolic Equivalent.
184
185    Units: `meta`.
186    """
187    movement_direction: MovementDirection
188    """
189    Movement Direction.
190
191    Units: `MovementDirection`.
192    """
193    pace_average: float
194    """
195    Average Pace.
196
197    Units: `km/m`.
198    """
199    pace_instant: float
200    """
201    Instantaneous Pace.
202
203    Units: `km/m`.
204    """
205    power_average: int
206    """
207    Average Power.
208
209    Units: `Watt`.
210    """
211    power_instant: int
212    """
213    Instantaneous Power.
214
215    Units: `Watt`.
216    """
217    power_output: int
218    """
219    Power Output.
220
221    Units: `Watt`.
222    """
223    ramp_angle: float
224    """
225    Ramp Angle Setting.
226
227    Units: `degree`.
228    """
229    resistance_level: int | float
230    """
231    Resistance Level.
232
233    Units: `unitless`.
234    """
235    speed_average: float
236    """
237    Average Speed.
238
239    Units: `km/h`.
240    """
241    speed_instant: float
242    """
243    Instantaneous Speed.
244
245    Units: `km/h`.
246    """
247    split_time_average: int
248    """
249    Average Split Time.
250
251    Units: `s/500m`.
252    """
253    split_time_instant: int
254    """
255    Instantaneous Split Time.
256
257    Units: `s/500m`.
258    """
259    step_count: int
260    """
261    Step Count.
262
263    Units: `step`.
264    """
265    step_rate_average: int
266    """
267    Average Step Rate.
268
269    Units: `spm`.
270    """
271    step_rate_instant: int
272    """
273    Instantaneous Step Rate.
274
275    Units: `spm`.
276    """
277    stride_count: int
278    """
279    Stride Count.
280
281    Units: `unitless`.
282    """
283    stroke_count: int
284    """
285    Stroke Count.
286
287    Units: `unitless`.
288    """
289    stroke_rate_average: float
290    """
291    Average Stroke Rate.
292
293    Units: `spm`.
294    """
295    stroke_rate_instant: float
296    """
297    Instantaneous Stroke Rate.
298
299    Units: `spm`.
300    """
301    time_elapsed: int
302    """
303    Elapsed Time.
304
305    Units: `s`.
306    """
307    time_remaining: int
308    """
309    Remaining Time.
310
311    Units: `s`.
312    """
313    training_status: TrainingStatusCode
314    """
315    Training Status.
316    """
317    training_status_string: str
318    """
319    Training Status String.
320    """
rssi: int

RSSI.

cadence_average: float

Average Cadence.

Units: rpm.

cadence_instant: float

Instantaneous Cadence.

Units: rpm.

distance_total: int

Total Distance.

Units: m.

elevation_gain_negative: int

Negative Elevation Gain.

Units: m.

elevation_gain_positive: int

Positive Elevation Gain.

Units: m.

energy_per_hour: int

Energy Per Hour.

Units: kcal.

energy_per_minute: int

Energy Per Minute.

Units: kcal.

energy_total: int

Total Energy.

Units: kcal.

force_on_belt: int

Force on Belt.

Units: newton.

heart_rate: int

Heart Rate.

Units: bpm.

inclination: float

Inclination.

Units: %.

metabolic_equivalent: float

Metabolic Equivalent.

Units: meta.

movement_direction: MovementDirection

Movement Direction.

Units: MovementDirection.

pace_average: float

Average Pace.

Units: km/m.

pace_instant: float

Instantaneous Pace.

Units: km/m.

power_average: int

Average Power.

Units: Watt.

power_instant: int

Instantaneous Power.

Units: Watt.

power_output: int

Power Output.

Units: Watt.

ramp_angle: float

Ramp Angle Setting.

Units: degree.

resistance_level: int | float

Resistance Level.

Units: unitless.

speed_average: float

Average Speed.

Units: km/h.

speed_instant: float

Instantaneous Speed.

Units: km/h.

split_time_average: int

Average Split Time.

Units: s/500m.

split_time_instant: int

Instantaneous Split Time.

Units: s/500m.

step_count: int

Step Count.

Units: step.

step_rate_average: int

Average Step Rate.

Units: spm.

step_rate_instant: int

Instantaneous Step Rate.

Units: spm.

stride_count: int

Stride Count.

Units: unitless.

stroke_count: int

Stroke Count.

Units: unitless.

stroke_rate_average: float

Average Stroke Rate.

Units: spm.

stroke_rate_instant: float

Instantaneous Stroke Rate.

Units: spm.

time_elapsed: int

Elapsed Time.

Units: s.

time_remaining: int

Remaining Time.

Units: s.

training_status: TrainingStatusCode

Training Status.

training_status_string: str

Training Status String.

class MovementDirection(enum.IntEnum):
34class MovementDirection(IntEnum, boundary=STRICT):
35    """
36    Movement direction. Used by `CrossTrainer` machine only.
37
38    Described in section **4.5.1.1 Flags Field**.
39    """
40
41    FORWARD = False
42    """Move Forward"""
43    BACKWARD = True
44    """Move Backward"""

Movement direction. Used by CrossTrainer machine only.

Described in section 4.5.1.1 Flags Field.

FORWARD = <MovementDirection.FORWARD: 0>

Move Forward

BACKWARD = <MovementDirection.BACKWARD: 1>

Move Backward

@dc.dataclass(frozen=True)
class IndoorBikeSimulationParameters(pyftms.serializer.model.BaseModel):
26@dc.dataclass(frozen=True)
27class IndoorBikeSimulationParameters(BaseModel):
28    """
29    Indoor Bike Simulation Parameters
30
31    Described in section **4.16.2.18: Set Indoor Bike Simulation Parameters Procedure**.
32    """
33
34    wind_speed: float = dc.field(
35        metadata=model_meta(
36            format="s2.001",
37        )
38    )
39    """
40    Wind Speed.
41    
42    Units: `meters per second (mps)`.
43    """
44
45    grade: float = dc.field(
46        metadata=model_meta(
47            format="s2.01",
48        )
49    )
50    """
51    Grade.
52    
53    Units: `%`.
54    """
55
56    rolling_resistance: float = dc.field(
57        metadata=model_meta(
58            format="u1.0001",
59        )
60    )
61    """
62    Coefficient of Rolling Resistance.
63    
64    Units: `unitless`.
65    """
66
67    wind_resistance: float = dc.field(
68        metadata=model_meta(
69            format="u1.01",
70        )
71    )
72    """
73    Wind Resistance Coefficient.
74    
75    Units: `kilogram per meter (kg/m)`.
76    """

Indoor Bike Simulation Parameters

Described in section 4.16.2.18: Set Indoor Bike Simulation Parameters Procedure.

IndoorBikeSimulationParameters( wind_speed: float, grade: float, rolling_resistance: float, wind_resistance: float)
wind_speed: float

Wind Speed.

Units: meters per second (mps).

grade: float

Grade.

Units: %.

rolling_resistance: float

Coefficient of Rolling Resistance.

Units: unitless.

wind_resistance: float

Wind Resistance Coefficient.

Units: kilogram per meter (kg/m).

class DeviceInfo(typing.TypedDict):
24class DeviceInfo(TypedDict, total=False):
25    """Device Information"""
26
27    manufacturer: str
28    """Manufacturer"""
29    model: str
30    """Model"""
31    serial_number: str
32    """Serial Number"""
33    sw_version: str
34    """Software Version"""
35    hw_version: str
36    """Hardware Version"""

Device Information

manufacturer: str

Manufacturer

model: str

Model

serial_number: str

Serial Number

sw_version: str

Software Version

hw_version: str

Hardware Version

class SettingRange(typing.NamedTuple):
133class SettingRange(NamedTuple):
134    """Value range of settings parameter."""
135
136    min_value: float
137    """Minimum value. Included in the range."""
138    max_value: float
139    """Maximum value. Included in the range."""
140    step: float
141    """Step value."""

Value range of settings parameter.

SettingRange(min_value: float, max_value: float, step: float)

Create new instance of SettingRange(min_value, max_value, step)

min_value: float

Minimum value. Included in the range.

max_value: float

Maximum value. Included in the range.

step: float

Step value.

class ResultCode(enum.IntEnum):
 94class ResultCode(IntEnum, boundary=STRICT):
 95    """
 96    Result code of control operations.
 97
 98    Described in section **4.16.2.22 Procedure Complete**.
 99    """
100
101    SUCCESS = auto()
102    """Success."""
103
104    NOT_SUPPORTED = auto()
105    """Operation Not Supported."""
106
107    INVALID_PARAMETER = auto()
108    """Invalid Parameter."""
109
110    FAILED = auto()
111    """Operation Failed."""
112
113    NOT_PERMITTED = auto()
114    """Control Not Permitted."""

Result code of control operations.

Described in section 4.16.2.22 Procedure Complete.

SUCCESS = <ResultCode.SUCCESS: 1>

Success.

NOT_SUPPORTED = <ResultCode.NOT_SUPPORTED: 2>

Operation Not Supported.

INVALID_PARAMETER = <ResultCode.INVALID_PARAMETER: 3>

Invalid Parameter.

FAILED = <ResultCode.FAILED: 4>

Operation Failed.

NOT_PERMITTED = <ResultCode.NOT_PERMITTED: 5>

Control Not Permitted.

class PropertiesManager:
 14class PropertiesManager:
 15    """
 16    Based helper class for `FitnessMachine`. Implements access and caching of
 17    properties and settings.
 18
 19    Do not instantinate it.
 20    """
 21
 22    _cb: FtmsCallback | None
 23    """Event Callback function"""
 24
 25    _properties: UpdateEventData
 26    """Properties dictonary"""
 27
 28    _live_properties: set[str]
 29    """Properties dictonary"""
 30
 31    _settings: SetupEventData
 32    """Properties dictonary"""
 33
 34    def __init__(self, on_ftms_event: FtmsCallback | None = None) -> None:
 35        self._cb = on_ftms_event
 36        self._properties = {}
 37        self._live_properties = set()
 38        self._settings = {}
 39
 40    def set_callback(self, cb: FtmsCallback):
 41        self._cb = cb
 42
 43    def _on_event(self, e: FtmsEvents) -> None:
 44        """Real-time training data update handler."""
 45        if e.event_id == "update":
 46            self._properties |= e.event_data
 47            self._live_properties.update(
 48                k for k, v in e.event_data.items() if v
 49            )
 50        elif e.event_id == "setup":
 51            self._settings |= e.event_data
 52
 53        return self._cb and self._cb(e)
 54
 55    def get_property(self, name: str) -> Any:
 56        """Get property by name."""
 57        return self._properties.get(name)
 58
 59    def get_setting(self, name: str) -> Any:
 60        """Get setting by name."""
 61        return self._settings.get(name)
 62
 63    @property
 64    def properties(self) -> UpdateEventData:
 65        """Read-only updateable properties mapping."""
 66        return cast(UpdateEventData, MappingProxyType(self._properties))
 67
 68    @property
 69    def live_properties(self) -> tuple[str, ...]:
 70        """
 71        Living properties.
 72
 73        Properties that had a value other than zero at least once.
 74        """
 75        return tuple(self._live_properties)
 76
 77    @property
 78    def settings(self) -> SetupEventData:
 79        """Read-only updateable settings mapping."""
 80        return cast(SetupEventData, MappingProxyType(self._settings))
 81
 82    @property
 83    def training_status(self) -> TrainingStatusCode:
 84        return self.get_property(c.TRAINING_STATUS)
 85
 86    # REAL-TIME TRAINING DATA
 87
 88    @property
 89    def cadence_average(self) -> float:
 90        """
 91        Average Cadence.
 92
 93        Units: `rpm`.
 94        """
 95        return self.get_property(c.CADENCE_AVERAGE)
 96
 97    @property
 98    def cadence_instant(self) -> float:
 99        """
100        Instantaneous Cadence.
101
102        Units: `rpm`.
103        """
104        return self.get_property(c.CADENCE_INSTANT)
105
106    @property
107    def distance_total(self) -> int:
108        """
109        Total Distance.
110
111        Units: `m`.
112        """
113        return self.get_property(c.DISTANCE_TOTAL)
114
115    @property
116    def elevation_gain_negative(self) -> float:
117        """
118        Negative Elevation Gain.
119
120        Units: `m`.
121        """
122        return self.get_property(c.ELEVATION_GAIN_NEGATIVE)
123
124    @property
125    def elevation_gain_positive(self) -> float:
126        """
127        Positive Elevation Gain.
128
129        Units: `m`.
130        """
131        return self.get_property(c.ELEVATION_GAIN_POSITIVE)
132
133    @property
134    def energy_per_hour(self) -> int:
135        """
136        Energy Per Hour.
137
138        Units: `kcal/h`.
139        """
140        return self.get_property(c.ENERGY_PER_HOUR)
141
142    @property
143    def energy_per_minute(self) -> int:
144        """
145        Energy Per Minute.
146
147        Units: `kcal/min`.
148        """
149        return self.get_property(c.ENERGY_PER_MINUTE)
150
151    @property
152    def energy_total(self) -> int:
153        """
154        Total Energy.
155
156        Units: `kcal`.
157        """
158        return self.get_property(c.ENERGY_TOTAL)
159
160    @property
161    def force_on_belt(self) -> int:
162        """
163        Force on Belt.
164
165        Units: `newton`.
166        """
167        return self.get_property(c.FORCE_ON_BELT)
168
169    @property
170    def heart_rate(self) -> int:
171        """
172        Heart Rate.
173
174        Units: `bpm`.
175        """
176        return self.get_property(c.HEART_RATE)
177
178    @property
179    def inclination(self) -> float:
180        """
181        Inclination.
182
183        Units: `%`.
184        """
185        return self.get_property(c.INCLINATION)
186
187    @property
188    def metabolic_equivalent(self) -> float:
189        """
190        Metabolic Equivalent.
191
192        Units: `meta`.
193        """
194        return self.get_property(c.METABOLIC_EQUIVALENT)
195
196    @property
197    def movement_direction(self) -> MovementDirection:
198        """
199        Movement Direction.
200
201        Units: `MovementDirection`.
202        """
203        return self.get_property(c.MOVEMENT_DIRECTION)
204
205    @property
206    def pace_average(self) -> float:
207        """
208        Average Pace.
209
210        Units: `min/km`.
211        """
212        return self.get_property(c.PACE_AVERAGE)
213
214    @property
215    def pace_instant(self) -> float:
216        """
217        Instantaneous Pace.
218
219        Units: `min/km`.
220        """
221        return self.get_property(c.PACE_INSTANT)
222
223    @property
224    def power_average(self) -> int:
225        """
226        Average Power.
227
228        Units: `Watt`.
229        """
230        return self.get_property(c.POWER_AVERAGE)
231
232    @property
233    def power_instant(self) -> int:
234        """
235        Instantaneous Power.
236
237        Units: `Watt`.
238        """
239        return self.get_property(c.POWER_INSTANT)
240
241    @property
242    def power_output(self) -> int:
243        """
244        Power Output.
245
246        Units: `Watt`.
247        """
248        return self.get_property(c.POWER_OUTPUT)
249
250    @property
251    def ramp_angle(self) -> float:
252        """
253        Ramp Angle Setting.
254
255        Units: `degree`.
256        """
257        return self.get_property(c.RAMP_ANGLE)
258
259    @property
260    def resistance_level(self) -> int | float:
261        """
262        Resistance Level.
263
264        Units: `unitless`.
265        """
266        return self.get_property(c.RESISTANCE_LEVEL)
267
268    @property
269    def speed_average(self) -> float:
270        """
271        Average Speed.
272
273        Units: `km/h`.
274        """
275        return self.get_property(c.SPEED_AVERAGE)
276
277    @property
278    def speed_instant(self) -> float:
279        """
280        Instantaneous Speed.
281
282        Units: `km/h`.
283        """
284        return self.get_property(c.SPEED_INSTANT)
285
286    @property
287    def split_time_average(self) -> int:
288        """
289        Average Split Time.
290
291        Units: `s/500m`.
292        """
293        return self.get_property(c.SPLIT_TIME_AVERAGE)
294
295    @property
296    def split_time_instant(self) -> int:
297        """
298        Instantaneous Split Time.
299
300        Units: `s/500m`.
301        """
302        return self.get_property(c.SPLIT_TIME_INSTANT)
303
304    @property
305    def step_count(self) -> int:
306        """
307        Step Count.
308
309        Units: `step`.
310        """
311        return self.get_property(c.STEP_COUNT)
312
313    @property
314    def step_rate_average(self) -> int:
315        """
316        Average Step Rate.
317
318        Units: `step/min`.
319        """
320        return self.get_property(c.STEP_RATE_AVERAGE)
321
322    @property
323    def step_rate_instant(self) -> int:
324        """
325        Instantaneous Step Rate.
326
327        Units: `step/min`.
328        """
329        return self.get_property(c.STEP_RATE_INSTANT)
330
331    @property
332    def stride_count(self) -> int:
333        """
334        Stride Count.
335
336        Units: `unitless`.
337        """
338        return self.get_property(c.STRIDE_COUNT)
339
340    @property
341    def stroke_count(self) -> int:
342        """
343        Stroke Count.
344
345        Units: `unitless`.
346        """
347        return self.get_property(c.STROKE_COUNT)
348
349    @property
350    def stroke_rate_average(self) -> float:
351        """
352        Average Stroke Rate.
353
354        Units: `stroke/min`.
355        """
356        return self.get_property(c.STROKE_RATE_AVERAGE)
357
358    @property
359    def stroke_rate_instant(self) -> float:
360        """
361        Instantaneous Stroke Rate.
362
363        Units: `stroke/min`.
364        """
365        return self.get_property(c.STROKE_RATE_INSTANT)
366
367    @property
368    def time_elapsed(self) -> int:
369        """
370        Elapsed Time.
371
372        Units: `s`.
373        """
374        return self.get_property(c.TIME_ELAPSED)
375
376    @property
377    def time_remaining(self) -> int:
378        """
379        Remaining Time.
380
381        Units: `s`.
382        """
383        return self.get_property(c.TIME_REMAINING)
384
385    # SETTINGS
386
387    @property
388    def indoor_bike_simulation(self) -> IndoorBikeSimulationParameters:
389        """Indoor Bike Simulation Parameters."""
390        return self.get_setting(c.INDOOR_BIKE_SIMULATION)
391
392    @property
393    def target_cadence(self) -> float:
394        """
395        Targeted cadence.
396
397        Units: `rpm`.
398        """
399        return self.get_setting(c.TARGET_CADENCE)
400
401    @property
402    def target_distance(self) -> int:
403        """
404        Targeted distance.
405
406        Units: `m`.
407        """
408        return self.get_setting(c.TARGET_DISTANCE)
409
410    @property
411    def target_energy(self) -> int:
412        """
413        Targeted expended energy.
414
415        Units: `kcal`.
416        """
417        return self.get_setting(c.TARGET_ENERGY)
418
419    @property
420    def target_heart_rate(self) -> int:
421        """
422        Targeted heart rate.
423
424        Units: `bpm`.
425        """
426        return self.get_setting(c.TARGET_HEART_RATE)
427
428    @property
429    def target_inclination(self) -> float:
430        """
431        Targeted inclination.
432
433        Units: `%`.
434        """
435        return self.get_setting(c.TARGET_INCLINATION)
436
437    @property
438    def target_power(self) -> int:
439        """
440        Targeted power.
441
442        Units: `Watt`.
443        """
444        return self.get_setting(c.TARGET_POWER)
445
446    @property
447    def target_resistance(self) -> float:
448        """
449        Targeted resistance level.
450
451        Units: `unitless`.
452        """
453        return self.get_setting(c.TARGET_RESISTANCE)
454
455    @property
456    def target_speed(self) -> float:
457        """
458        Targeted speed.
459
460        Units: `km/h`.
461        """
462        return self.get_setting(c.TARGET_SPEED)
463
464    @property
465    def target_steps(self) -> int:
466        """
467        Targeted number of steps.
468
469        Units: `step`.
470        """
471        return self.get_setting(c.TARGET_STEPS)
472
473    @property
474    def target_strides(self) -> int:
475        """
476        Targeted number of strides.
477
478        Units: `stride`.
479        """
480        return self.get_setting(c.TARGET_STRIDES)
481
482    @property
483    def target_time(self) -> tuple[int, ...]:
484        """
485        Targeted training time.
486
487        Units: `s`.
488        """
489        return self.get_setting(c.TARGET_TIME)
490
491    @property
492    def wheel_circumference(self) -> float:
493        """
494        Wheel circumference.
495
496        Units: `mm`.
497        """
498        return self.get_setting(c.WHEEL_CIRCUMFERENCE)

Based helper class for FitnessMachine. Implements access and caching of properties and settings.

Do not instantinate it.

PropertiesManager( on_ftms_event: Optional[Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]] = None)
34    def __init__(self, on_ftms_event: FtmsCallback | None = None) -> None:
35        self._cb = on_ftms_event
36        self._properties = {}
37        self._live_properties = set()
38        self._settings = {}
def set_callback( self, cb: Callable[[UpdateEvent | SetupEvent | ControlEvent | SpinDownEvent], NoneType]):
40    def set_callback(self, cb: FtmsCallback):
41        self._cb = cb
def get_property(self, name: str) -> Any:
55    def get_property(self, name: str) -> Any:
56        """Get property by name."""
57        return self._properties.get(name)

Get property by name.

def get_setting(self, name: str) -> Any:
59    def get_setting(self, name: str) -> Any:
60        """Get setting by name."""
61        return self._settings.get(name)

Get setting by name.

properties: UpdateEventData
63    @property
64    def properties(self) -> UpdateEventData:
65        """Read-only updateable properties mapping."""
66        return cast(UpdateEventData, MappingProxyType(self._properties))

Read-only updateable properties mapping.

live_properties: tuple[str, ...]
68    @property
69    def live_properties(self) -> tuple[str, ...]:
70        """
71        Living properties.
72
73        Properties that had a value other than zero at least once.
74        """
75        return tuple(self._live_properties)

Living properties.

Properties that had a value other than zero at least once.

settings: SetupEventData
77    @property
78    def settings(self) -> SetupEventData:
79        """Read-only updateable settings mapping."""
80        return cast(SetupEventData, MappingProxyType(self._settings))

Read-only updateable settings mapping.

training_status: TrainingStatusCode
82    @property
83    def training_status(self) -> TrainingStatusCode:
84        return self.get_property(c.TRAINING_STATUS)
cadence_average: float
88    @property
89    def cadence_average(self) -> float:
90        """
91        Average Cadence.
92
93        Units: `rpm`.
94        """
95        return self.get_property(c.CADENCE_AVERAGE)

Average Cadence.

Units: rpm.

cadence_instant: float
 97    @property
 98    def cadence_instant(self) -> float:
 99        """
100        Instantaneous Cadence.
101
102        Units: `rpm`.
103        """
104        return self.get_property(c.CADENCE_INSTANT)

Instantaneous Cadence.

Units: rpm.

distance_total: int
106    @property
107    def distance_total(self) -> int:
108        """
109        Total Distance.
110
111        Units: `m`.
112        """
113        return self.get_property(c.DISTANCE_TOTAL)

Total Distance.

Units: m.

elevation_gain_negative: float
115    @property
116    def elevation_gain_negative(self) -> float:
117        """
118        Negative Elevation Gain.
119
120        Units: `m`.
121        """
122        return self.get_property(c.ELEVATION_GAIN_NEGATIVE)

Negative Elevation Gain.

Units: m.

elevation_gain_positive: float
124    @property
125    def elevation_gain_positive(self) -> float:
126        """
127        Positive Elevation Gain.
128
129        Units: `m`.
130        """
131        return self.get_property(c.ELEVATION_GAIN_POSITIVE)

Positive Elevation Gain.

Units: m.

energy_per_hour: int
133    @property
134    def energy_per_hour(self) -> int:
135        """
136        Energy Per Hour.
137
138        Units: `kcal/h`.
139        """
140        return self.get_property(c.ENERGY_PER_HOUR)

Energy Per Hour.

Units: kcal/h.

energy_per_minute: int
142    @property
143    def energy_per_minute(self) -> int:
144        """
145        Energy Per Minute.
146
147        Units: `kcal/min`.
148        """
149        return self.get_property(c.ENERGY_PER_MINUTE)

Energy Per Minute.

Units: kcal/min.

energy_total: int
151    @property
152    def energy_total(self) -> int:
153        """
154        Total Energy.
155
156        Units: `kcal`.
157        """
158        return self.get_property(c.ENERGY_TOTAL)

Total Energy.

Units: kcal.

force_on_belt: int
160    @property
161    def force_on_belt(self) -> int:
162        """
163        Force on Belt.
164
165        Units: `newton`.
166        """
167        return self.get_property(c.FORCE_ON_BELT)

Force on Belt.

Units: newton.

heart_rate: int
169    @property
170    def heart_rate(self) -> int:
171        """
172        Heart Rate.
173
174        Units: `bpm`.
175        """
176        return self.get_property(c.HEART_RATE)

Heart Rate.

Units: bpm.

inclination: float
178    @property
179    def inclination(self) -> float:
180        """
181        Inclination.
182
183        Units: `%`.
184        """
185        return self.get_property(c.INCLINATION)

Inclination.

Units: %.

metabolic_equivalent: float
187    @property
188    def metabolic_equivalent(self) -> float:
189        """
190        Metabolic Equivalent.
191
192        Units: `meta`.
193        """
194        return self.get_property(c.METABOLIC_EQUIVALENT)

Metabolic Equivalent.

Units: meta.

movement_direction: MovementDirection
196    @property
197    def movement_direction(self) -> MovementDirection:
198        """
199        Movement Direction.
200
201        Units: `MovementDirection`.
202        """
203        return self.get_property(c.MOVEMENT_DIRECTION)

Movement Direction.

Units: MovementDirection.

pace_average: float
205    @property
206    def pace_average(self) -> float:
207        """
208        Average Pace.
209
210        Units: `min/km`.
211        """
212        return self.get_property(c.PACE_AVERAGE)

Average Pace.

Units: min/km.

pace_instant: float
214    @property
215    def pace_instant(self) -> float:
216        """
217        Instantaneous Pace.
218
219        Units: `min/km`.
220        """
221        return self.get_property(c.PACE_INSTANT)

Instantaneous Pace.

Units: min/km.

power_average: int
223    @property
224    def power_average(self) -> int:
225        """
226        Average Power.
227
228        Units: `Watt`.
229        """
230        return self.get_property(c.POWER_AVERAGE)

Average Power.

Units: Watt.

power_instant: int
232    @property
233    def power_instant(self) -> int:
234        """
235        Instantaneous Power.
236
237        Units: `Watt`.
238        """
239        return self.get_property(c.POWER_INSTANT)

Instantaneous Power.

Units: Watt.

power_output: int
241    @property
242    def power_output(self) -> int:
243        """
244        Power Output.
245
246        Units: `Watt`.
247        """
248        return self.get_property(c.POWER_OUTPUT)

Power Output.

Units: Watt.

ramp_angle: float
250    @property
251    def ramp_angle(self) -> float:
252        """
253        Ramp Angle Setting.
254
255        Units: `degree`.
256        """
257        return self.get_property(c.RAMP_ANGLE)

Ramp Angle Setting.

Units: degree.

resistance_level: int | float
259    @property
260    def resistance_level(self) -> int | float:
261        """
262        Resistance Level.
263
264        Units: `unitless`.
265        """
266        return self.get_property(c.RESISTANCE_LEVEL)

Resistance Level.

Units: unitless.

speed_average: float
268    @property
269    def speed_average(self) -> float:
270        """
271        Average Speed.
272
273        Units: `km/h`.
274        """
275        return self.get_property(c.SPEED_AVERAGE)

Average Speed.

Units: km/h.

speed_instant: float
277    @property
278    def speed_instant(self) -> float:
279        """
280        Instantaneous Speed.
281
282        Units: `km/h`.
283        """
284        return self.get_property(c.SPEED_INSTANT)

Instantaneous Speed.

Units: km/h.

split_time_average: int
286    @property
287    def split_time_average(self) -> int:
288        """
289        Average Split Time.
290
291        Units: `s/500m`.
292        """
293        return self.get_property(c.SPLIT_TIME_AVERAGE)

Average Split Time.

Units: s/500m.

split_time_instant: int
295    @property
296    def split_time_instant(self) -> int:
297        """
298        Instantaneous Split Time.
299
300        Units: `s/500m`.
301        """
302        return self.get_property(c.SPLIT_TIME_INSTANT)

Instantaneous Split Time.

Units: s/500m.

step_count: int
304    @property
305    def step_count(self) -> int:
306        """
307        Step Count.
308
309        Units: `step`.
310        """
311        return self.get_property(c.STEP_COUNT)

Step Count.

Units: step.

step_rate_average: int
313    @property
314    def step_rate_average(self) -> int:
315        """
316        Average Step Rate.
317
318        Units: `step/min`.
319        """
320        return self.get_property(c.STEP_RATE_AVERAGE)

Average Step Rate.

Units: step/min.

step_rate_instant: int
322    @property
323    def step_rate_instant(self) -> int:
324        """
325        Instantaneous Step Rate.
326
327        Units: `step/min`.
328        """
329        return self.get_property(c.STEP_RATE_INSTANT)

Instantaneous Step Rate.

Units: step/min.

stride_count: int
331    @property
332    def stride_count(self) -> int:
333        """
334        Stride Count.
335
336        Units: `unitless`.
337        """
338        return self.get_property(c.STRIDE_COUNT)

Stride Count.

Units: unitless.

stroke_count: int
340    @property
341    def stroke_count(self) -> int:
342        """
343        Stroke Count.
344
345        Units: `unitless`.
346        """
347        return self.get_property(c.STROKE_COUNT)

Stroke Count.

Units: unitless.

stroke_rate_average: float
349    @property
350    def stroke_rate_average(self) -> float:
351        """
352        Average Stroke Rate.
353
354        Units: `stroke/min`.
355        """
356        return self.get_property(c.STROKE_RATE_AVERAGE)

Average Stroke Rate.

Units: stroke/min.

stroke_rate_instant: float
358    @property
359    def stroke_rate_instant(self) -> float:
360        """
361        Instantaneous Stroke Rate.
362
363        Units: `stroke/min`.
364        """
365        return self.get_property(c.STROKE_RATE_INSTANT)

Instantaneous Stroke Rate.

Units: stroke/min.

time_elapsed: int
367    @property
368    def time_elapsed(self) -> int:
369        """
370        Elapsed Time.
371
372        Units: `s`.
373        """
374        return self.get_property(c.TIME_ELAPSED)

Elapsed Time.

Units: s.

time_remaining: int
376    @property
377    def time_remaining(self) -> int:
378        """
379        Remaining Time.
380
381        Units: `s`.
382        """
383        return self.get_property(c.TIME_REMAINING)

Remaining Time.

Units: s.

indoor_bike_simulation: IndoorBikeSimulationParameters
387    @property
388    def indoor_bike_simulation(self) -> IndoorBikeSimulationParameters:
389        """Indoor Bike Simulation Parameters."""
390        return self.get_setting(c.INDOOR_BIKE_SIMULATION)

Indoor Bike Simulation Parameters.

target_cadence: float
392    @property
393    def target_cadence(self) -> float:
394        """
395        Targeted cadence.
396
397        Units: `rpm`.
398        """
399        return self.get_setting(c.TARGET_CADENCE)

Targeted cadence.

Units: rpm.

target_distance: int
401    @property
402    def target_distance(self) -> int:
403        """
404        Targeted distance.
405
406        Units: `m`.
407        """
408        return self.get_setting(c.TARGET_DISTANCE)

Targeted distance.

Units: m.

target_energy: int
410    @property
411    def target_energy(self) -> int:
412        """
413        Targeted expended energy.
414
415        Units: `kcal`.
416        """
417        return self.get_setting(c.TARGET_ENERGY)

Targeted expended energy.

Units: kcal.

target_heart_rate: int
419    @property
420    def target_heart_rate(self) -> int:
421        """
422        Targeted heart rate.
423
424        Units: `bpm`.
425        """
426        return self.get_setting(c.TARGET_HEART_RATE)

Targeted heart rate.

Units: bpm.

target_inclination: float
428    @property
429    def target_inclination(self) -> float:
430        """
431        Targeted inclination.
432
433        Units: `%`.
434        """
435        return self.get_setting(c.TARGET_INCLINATION)

Targeted inclination.

Units: %.

target_power: int
437    @property
438    def target_power(self) -> int:
439        """
440        Targeted power.
441
442        Units: `Watt`.
443        """
444        return self.get_setting(c.TARGET_POWER)

Targeted power.

Units: Watt.

target_resistance: float
446    @property
447    def target_resistance(self) -> float:
448        """
449        Targeted resistance level.
450
451        Units: `unitless`.
452        """
453        return self.get_setting(c.TARGET_RESISTANCE)

Targeted resistance level.

Units: unitless.

target_speed: float
455    @property
456    def target_speed(self) -> float:
457        """
458        Targeted speed.
459
460        Units: `km/h`.
461        """
462        return self.get_setting(c.TARGET_SPEED)

Targeted speed.

Units: km/h.

target_steps: int
464    @property
465    def target_steps(self) -> int:
466        """
467        Targeted number of steps.
468
469        Units: `step`.
470        """
471        return self.get_setting(c.TARGET_STEPS)

Targeted number of steps.

Units: step.

target_strides: int
473    @property
474    def target_strides(self) -> int:
475        """
476        Targeted number of strides.
477
478        Units: `stride`.
479        """
480        return self.get_setting(c.TARGET_STRIDES)

Targeted number of strides.

Units: stride.

target_time: tuple[int, ...]
482    @property
483    def target_time(self) -> tuple[int, ...]:
484        """
485        Targeted training time.
486
487        Units: `s`.
488        """
489        return self.get_setting(c.TARGET_TIME)

Targeted training time.

Units: s.

wheel_circumference: float
491    @property
492    def wheel_circumference(self) -> float:
493        """
494        Wheel circumference.
495
496        Units: `mm`.
497        """
498        return self.get_setting(c.WHEEL_CIRCUMFERENCE)

Wheel circumference.

Units: mm.

class TrainingStatusCode(enum.IntEnum):
27class TrainingStatusCode(IntEnum, boundary=STRICT):
28    """
29    Training Status.
30
31    Represents the current training state while a user is exercising.
32
33    Described in section **4.10.1.2: Training Status Field**.
34    """
35
36    OTHER = 0
37    """Other."""
38
39    IDLE = auto()
40    """Idle."""
41
42    WARMING_UP = auto()
43    """Warming Up."""
44
45    LOW_INTENSITY_INTERVAL = auto()
46    """Low Intensity Interval."""
47
48    HIGH_INTENSITY_INTERVAL = auto()
49    """High Intensity Interval."""
50
51    RECOVERY_INTERVAL = auto()
52    """Recovery Interval."""
53
54    ISOMETRIC = auto()
55    """Isometric."""
56
57    HEART_RATE_CONTROL = auto()
58    """Heart Rate Control."""
59
60    FITNESS_TEST = auto()
61    """Fitness Test."""
62
63    SPEED_TOO_LOW = auto()
64    """Speed Outside of Control Region - Low (increase speed to return to controllable region)."""
65
66    SPEED_TOO_HIGH = auto()
67    """Speed Outside of Control Region - High (decrease speed to return to controllable region)."""
68
69    COOL_DOWN = auto()
70    """Cool Down."""
71
72    WATT_CONTROL = auto()
73    """Watt Control."""
74
75    MANUAL_MODE = auto()
76    """Manual Mode (Quick Start)."""
77
78    PRE_WORKOUT = auto()
79    """Pre-Workout."""
80
81    POST_WORKOUT = auto()
82    """Post-Workout."""

Training Status.

Represents the current training state while a user is exercising.

Described in section 4.10.1.2: Training Status Field.

Other.

Idle.

WARMING_UP = <TrainingStatusCode.WARMING_UP: 2>

Warming Up.

LOW_INTENSITY_INTERVAL = <TrainingStatusCode.LOW_INTENSITY_INTERVAL: 3>

Low Intensity Interval.

HIGH_INTENSITY_INTERVAL = <TrainingStatusCode.HIGH_INTENSITY_INTERVAL: 4>

High Intensity Interval.

RECOVERY_INTERVAL = <TrainingStatusCode.RECOVERY_INTERVAL: 5>

Recovery Interval.

ISOMETRIC = <TrainingStatusCode.ISOMETRIC: 6>

Isometric.

HEART_RATE_CONTROL = <TrainingStatusCode.HEART_RATE_CONTROL: 7>

Heart Rate Control.

FITNESS_TEST = <TrainingStatusCode.FITNESS_TEST: 8>

Fitness Test.

SPEED_TOO_LOW = <TrainingStatusCode.SPEED_TOO_LOW: 9>

Speed Outside of Control Region - Low (increase speed to return to controllable region).

SPEED_TOO_HIGH = <TrainingStatusCode.SPEED_TOO_HIGH: 10>

Speed Outside of Control Region - High (decrease speed to return to controllable region).

COOL_DOWN = <TrainingStatusCode.COOL_DOWN: 11>

Cool Down.

WATT_CONTROL = <TrainingStatusCode.WATT_CONTROL: 12>

Watt Control.

MANUAL_MODE = <TrainingStatusCode.MANUAL_MODE: 13>

Manual Mode (Quick Start).

PRE_WORKOUT = <TrainingStatusCode.PRE_WORKOUT: 14>

Pre-Workout.

POST_WORKOUT = <TrainingStatusCode.POST_WORKOUT: 15>

Post-Workout.

class SpinDownEvent(typing.NamedTuple):
332class SpinDownEvent(NamedTuple):
333    """Spin Down Procedure Event."""
334
335    event_id: Literal["spin_down"]
336    """Always `spin_down`."""
337    event_data: SpinDownEventData
338    """`SpinDownEvent` data."""

Spin Down Procedure Event.

SpinDownEvent( event_id: Literal['spin_down'], event_data: SpinDownEventData)

Create new instance of SpinDownEvent(event_id, event_data)

event_id: Literal['spin_down']

Always spin_down.

event_data: SpinDownEventData
class SpinDownEventData(typing.TypedDict):
21class SpinDownEventData(TypedDict, total=False):
22    """`SpinDownEvent` data."""
23
24    target_speed: SpinDownSpeedData
25    """From fitness machine to client. Indicate successfully operation."""
26    code: SpinDownControlCode
27    """From client to fitness machine. START or IGNORE."""
28    status: SpinDownStatusCode
29    """From fitness machine to client."""
target_speed: SpinDownSpeedData

From fitness machine to client. Indicate successfully operation.

From client to fitness machine. START or IGNORE.

From fitness machine to client.

@dc.dataclass(frozen=True)
class SpinDownSpeedData(pyftms.serializer.model.BaseModel):
45@dc.dataclass(frozen=True)
46class SpinDownSpeedData(BaseModel):
47    """
48    Response Parameter when the Spin Down Procedure succeeds.
49
50    Described in section `4.16.2.20 Spin Down Control Procedure. Table 4.22`.
51    """
52
53    low: float = dc.field(
54        metadata=model_meta(
55            format="u2.01",
56        )
57    )
58    """
59    Target Speed Low.
60    
61    Units: `km/h`.
62    """
63
64    high: float = dc.field(
65        metadata=model_meta(
66            format="u2.01",
67        )
68    )
69    """
70    Target Speed High.
71    
72    Units: `km/h`.
73    """

Response Parameter when the Spin Down Procedure succeeds.

Described in section 4.16.2.20 Spin Down Control Procedure. Table 4.22.

SpinDownSpeedData(low: float, high: float)
low: float

Target Speed Low.

Units: km/h.

high: float

Target Speed High.

Units: km/h.

class SpinDownControlCode(enum.IntEnum):
31class SpinDownControlCode(IntEnum, boundary=STRICT):
32    """
33    Spin Down Control Code.
34
35    Described in section **4.16.2.20 Spin Down Control Procedure. Table 4.21**.
36    """
37
38    START = auto()
39    """Spin Down Start."""
40
41    IGNORE = auto()
42    """Spin Down Ignore."""

Spin Down Control Code.

Described in section 4.16.2.20 Spin Down Control Procedure. Table 4.21.

Spin Down Start.

Spin Down Ignore.

class SpinDownStatusCode(enum.IntEnum):
11class SpinDownStatusCode(IntEnum, boundary=STRICT):
12    """
13    Spin Down Status.
14
15    Described in section **4.17 Fitness Machine Status. Table 4.27**.
16    """
17
18    REQUESTED = auto()
19    """Spin Down Requested"""
20
21    SUCCESS = auto()
22    """Success"""
23
24    ERROR = auto()
25    """Error"""
26
27    STOP_PEDALING = auto()
28    """Stop Pedaling"""

Spin Down Status.

Described in section 4.17 Fitness Machine Status. Table 4.27.

REQUESTED = <SpinDownStatusCode.REQUESTED: 1>

Spin Down Requested

Success

Error

STOP_PEDALING = <SpinDownStatusCode.STOP_PEDALING: 4>

Stop Pedaling