Skip to content

Releases: mcdax/walkingpad-controller

v0.4.11

10 May 09:46

Choose a tag to compare

Fixed

  • Use `bleak_retry_connector.establish_connection()` for FTMS connect. Replaces the hand-rolled `BleakClient(...).connect()` calls in both `FTMSController.connect()` and `_detect_protocol_from_probe()`. The retry-connector is HA-tuned: it handles ESPHome BT-proxy slot management, scanner re-discovery, and the `ESP_GATT_CONN_FAIL_ESTABLISH` failure mode KingSmith treadmills hit at marginal RSSI through proxies. Addresses the `BleakClient.connect() called without bleak-retry-connector` warning Bleak has been printing.

Added

  • `bleak-retry-connector>=3.0.0` as a runtime dependency (already pre-installed inside Home Assistant; standalone users pull it from PyPI).

v0.4.10

10 May 09:11

Choose a tag to compare

Fixed

  • WiLink: surface unilateral disconnects. WiLinkController now hooks ph4_walkingpad's BleakClient disconnect callback, so a unilateral BLE drop (range loss, firmware reboot, phone-app stealing the link) propagates to upper layers. The connected property also consults the live client state. Without this, _disconnect_callbacks were never fired on legacy devices and _connected stayed True forever after a real drop.
  • WalkingPadController.disconnect(): don't skip teardown on stale cache. Previously gated the entire disconnect on the cached _connected bool; now also checks backend liveness so a real Bleak client can still be closed when the firmware unilaterally dropped the link (which flipped our cache to False before disconnect could run).
  • set_speed docstring trimmed of stale "the user will need to set the desired speed once the belt is running" comment — the code already issues set_target_speed after start().

Docs

  • README clarifies that the vendor pre-amble is sent for any device exposing the d18d2c10-… characteristic (not MC-21-specific).

v0.4.9

10 May 09:03

Choose a tag to compare

Fixed

Two regressions where belt_state derived as STOPPED after a successful pause instead of PAUSED. Both were found by running tests/stress_dynamic.py against a real KS-HD-Z1D and verified by the pause_resume scenario invariants (16/16 OK after the fixes; ~50% before).

  1. _paused deceleration race in _on_treadmill_data. The previous version cleared _paused on every Treadmill Data frame with speed > 0. Pause sets the flag at speed ≈ 2.5; the ~5–10 s deceleration to zero emits dozens of speed > 0 frames, each clearing the flag, so by the time speed reached 0 the _paused branch was dead and belt_state derived as STOPPED. The flag is now set/cleared exclusively by _on_machine_status in response to FM Status events; _on_treadmill_data only reads it.

  2. "First 2ADA event after subscribe" filter dropped genuine ack state. The defensive filter that drops the first 2ADA event (added because the firmware replays its prior state on CCCD-subscribe, which would flap last_fm_event on every reconnect) over-filtered when the user's first command after a fresh connect produced an ack as the first event — e.g. pause() right after a reconnect → FM Status STOPPED_OR_PAUSED + PAUSE arrives as the first event. The ack waiter got woken (so pause() returned True), but _paused was never set, and belt_state derived as STOPPED. Heuristic: if the first event matches _status_ack_expected_opcode, treat as the real ack and apply state. Otherwise skip as before.

Refs walkingpad-controller#2.

v0.4.8

10 May 07:45

Choose a tag to compare

Added

  • BeltState.PAUSED (= 2) on the public enum. Distinguishes "session paused, will resume" from "session ended" — the device emits FM Status STOPPED_OR_PAUSED with the PAUSE param (0x02) when the user pauses (via pause() from 0.4.7, the phone app, or the physical remote), and counters (time, distance, calories, steps) survive across the pause. Previously the library reported `STOPPED` in both cases, so callers couldn't tell them apart.

    FTMSController now tracks an internal `_paused` flag set by the FM Status handler and consulted by the Treadmill Data parser, with this derivation:

    speed > 0           -> ACTIVE   (also clears _paused)
    speed = 0 + paused  -> PAUSED
    speed = 0 + ~paused -> STOPPED
    

    _paused is cleared on STARTED_OR_RESUMED, STOPPED_OR_PAUSED+STOP, safety-key stop, every speed > 0 frame, and on disconnect.

Refs walkingpad-controller#2.

v0.4.7

10 May 07:10

Choose a tag to compare

Added

  • WalkingPadController.pause() — exposes the existing FTMS pause path (STOP_OR_PAUSE opcode 0x08 with PAUSE = 0x02) on the unified controller. Pause leaves the session counters (time, distance, calories, steps) intact, so a subsequent start() resumes from where the user left off — same behaviour as the phone app's stop button and the physical remote.

    This matters for UI semantics: a "Stop" button in HA / a phone app should usually pause, not full-stop. The existing stop() sends STOP = 0x01 which fully ends the session and resets counters; that path is still available for the rare cases where a hard reset is wanted.

    WiLink protocol has no separate pause opcode; on those devices pause() logs a warning and falls back to stop().

Refs walkingpad-controller#2.

v0.4.6

06 May 20:10

Choose a tag to compare

Fixed

  • Connection stability on marginal-signal hardware. Removed Training Status (0x2AD3) from the default connect-path subscriptions. On a KS-HD-Z1D at -83 dBm RSSI:
    • Before: 0/5 reps held a 20 s idle window — the link dropped within 2-3 s of `connect()` returning.
    • After: 4/5 reps held the full 20 s.
    • Command flow: unchanged (12/12 ok in stop_start scenario).

The bisection (in `tests/probe_setup_killer.py`) showed extra CCCD writes during connect destabilise the link on weak signals. Pure GATT reads and the load-bearing `REQUEST_CONTROL` write are kept (the latter is required — KingSmith firmware silently rejects subsequent `SET_TARGET_SPEED` / `STOP` if `REQUEST_CONTROL` never happened, even though it ignores its own response).

Training Status was informational only (PRE_WORKOUT / MANUAL / etc); the library does not gate any decision on it. Callers that want it can subscribe lazily.

v0.4.5

05 May 09:59

Choose a tag to compare

Fixed

  • Stale 2ADA event on every reconnect. KingSmith firmware (KS-HD-Z1D verified, MC-21 likely the same) replays its current Fitness Machine Status the moment the CCCD is enabled — typically a leftover `STOPPED_OR_PAUSED` from before the client connected. The library now skips the very first 2ADA event after every subscribe, so `TreadmillStatus.last_fm_event` no longer flips to a misleading historical value within a second of every connect. The flag re-arms on disconnect, and pending command waiters still get woken if the skipped event matches the expected opcode (so command acknowledgement isn't lost).

v0.4.4

04 May 20:57

Choose a tag to compare

Added

Surface two new fields on TreadmillStatus so consumers can react to FTMS state-change events the library already subscribes to:

  • training_status — byte 1 of the 0x2AD3 notification (Bluetooth SIG FTMS standard enum). New `FTMSTrainingStatus` enum mirrors the 15 standard values (Idle / Warming Up / High-Intensity Interval / … / Manual Mode / Pre-Workout / Post-Workout).
  • last_fm_event — opcode of the most recent 0x2ADA notification. Maps to `FitnessMachineStatusOpcode`.

The status callback now fires whenever either changes, so downstream consumers (e.g. Home Assistant) get pushed updates the moment the device reports a state transition.

v0.4.3

04 May 20:50

Choose a tag to compare

Improvements from a deep reverse-engineering pass on KS Fit

This release applies findings from decompiling the KingSmith KS Fit app (Flutter/Dart AOT, via blutter). Full analysis lives in docs/ftms-protocol-reference.md and docs/ks-fit-reverse-engineering.md.

Connection setup

  • Added KS-MC21-, KS-SMC21C-, ZP-ZEALR1- to the FTMS name allowlist — the MC-21 family is now detected eagerly without a service-UUID probe.
  • Stagger CCCD subscriptions with 100/200/300 ms delays — the literal Duration constants used by KS Fit. KingSmith firmware silently drops CCCD writes that arrive in close succession.
  • Subscribe to Training Status (0x2AD3); KS Fit subscribes to it and we didn't.

Command acknowledgement

  • On the vendor pre-amble path (MC-21), most Control Point opcodes are acknowledged via a Fitness Machine Status (0x2ADA) event rather than a CP indication. _write_control_point now races the indication against the matching status event, returning success on whichever arrives first. Falls back to silent-accept on timeout (existing v0.4.1 behavior).
  • _on_machine_status now handles 0x05 (Target Speed Changed) and 0x06 (Target Inclination Changed). Adds FitnessMachineStatusOpcode enum.

Capabilities & metadata

  • Read Software Revision String (0x2A28) during connect; expose as FTMSController.firmware_version and WalkingPadController.firmware_version.
  • Add KingSmithMode / KingSmithStatus / KingSmithTreadmillStatus enums decoded from KS Fit's AOT snapshot, for callers that want named values instead of raw integers.

Tests

  • Fix tests/test_real_device.py to clear the disconnect latch after a successful connect() (BleakClient retries during connect() were leaking the callback, leading to spurious "TEST FAILED" later).

Verified

End-to-end against KS-HD-Z1D: scan → connect → start → set 2 km/h → observe 20s → stop → disconnect, with no regressions on the standard FTMS indication path. Firmware version V0.0.6 read successfully.

v0.4.2

04 May 19:28

Choose a tag to compare

Fixed

  • WalkingPadController.connected now defers to the active backend so it reflects the real BLE link state, not a cached bool that drifted when the firmware unilaterally dropped the connection.
  • FTMSController.connect() now raises BleakError when the link drops mid-setup (e.g. shortly after a previous abrupt disconnect — Bleak/BlueZ accepts the connect, but the device closes the link before service discovery completes). Previously it silently announced "Connected" and every subsequent command failed with no clear signal.
  • FTMSController.start() distinguishes between the device rejecting START_OR_RESUME because the belt is already running (success) and the write itself failing because the connection was lost (real failure). Previously both were treated as "belt already running", masking real disconnects.

Changed

  • Connect retries bumped 3 → 5 with 3s gap (was 2s), to ride out the post-disconnect window where Bleak/BlueZ briefly accepts a connect() against a device that's not actually ready to talk.

These changes don't introduce new device support; they make existing failures visible so callers (e.g. the HA integration) can react appropriately instead of seeing phantom "connected" state.