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:
- Treadmill
- Cross Trainer (Elliptical Trainer)
- Rower (Rowing Machine)
- Indoor Bike (Spin Bike)
Step Climber and Stair Climber machines are not supported due to incomplete protocol information and low popularity.
Requirments
bleak
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]
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 ofBLEDevice
andMachineType
tuples.
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 orMachineType
.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:
FitnessMachine
instance.
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:
FitnessMachine
instance if device found successfully.
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.
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.
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)
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
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.
143 @property 144 def rssi(self) -> int | None: 145 """RSSI.""" 146 return self.get_property("rssi")
RSSI.
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
154 def set_disconnect_callback(self, cb: DisconnectCallback): 155 """Set disconnect callback.""" 156 self._disconnect_cb = cb
Set disconnect callback.
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.
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.
179 @property 180 def address(self) -> str: 181 """Bluetooth address.""" 182 183 return self._device.address
Bluetooth address.
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.
193 @property 194 def device_info(self) -> DeviceInfo: 195 """Device Information.""" 196 197 return self._device_info
Device Information.
199 @property 200 def machine_type(self) -> MachineType: 201 """Machine type.""" 202 203 return self._machine_type
Machine type.
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.
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.
231 @cached_property 232 def supported_settings(self) -> list[str]: 233 """Supported settings.""" 234 235 return ControlModel._get_features(self._m_settings)
Supported settings.
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.
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.
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.
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.
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.
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.
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
.
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: %
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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.
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
.
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.
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.
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
.
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
.
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
.
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
.
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.
323class UpdateEvent(NamedTuple): 324 """Update Event.""" 325 326 event_id: Literal["update"] 327 """Always `update`.""" 328 event_data: UpdateEventData 329 """`UpdateEvent` data."""
Update Event.
Create new instance of UpdateEvent(event_id, event_data)
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.
Create new instance of SetupEvent(event_id, event_data, event_source)
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.
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.
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.
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 """
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.
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.
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
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.
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.
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.
55 def get_property(self, name: str) -> Any: 56 """Get property by name.""" 57 return self._properties.get(name)
Get property by name.
59 def get_setting(self, name: str) -> Any: 60 """Get setting by name.""" 61 return self._settings.get(name)
Get setting by name.
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.
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.
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.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
178 @property 179 def inclination(self) -> float: 180 """ 181 Inclination. 182 183 Units: `%`. 184 """ 185 return self.get_property(c.INCLINATION)
Inclination.
Units: %
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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
.
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.
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
.
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
.
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
.
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
.
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: %
.
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
.
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
.
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
.
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
.
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
.
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.
Speed Outside of Control Region - Low (increase speed to return to controllable region).
Speed Outside of Control Region - High (decrease speed to return to controllable region).
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.
Create new instance of SpinDownEvent(event_id, event_data)
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."""
SpinDownEvent
data.
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
.
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.
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.