Releases: mcdax/walkingpad-controller
v0.4.11
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
Fixed
- WiLink: surface unilateral disconnects.
WiLinkControllernow 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. Theconnectedproperty also consults the live client state. Without this,_disconnect_callbackswere never fired on legacy devices and_connectedstayedTrueforever after a real drop. WalkingPadController.disconnect(): don't skip teardown on stale cache. Previously gated the entire disconnect on the cached_connectedbool; 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_speeddocstring trimmed of stale "the user will need to set the desired speed once the belt is running" comment — the code already issuesset_target_speedafterstart().
Docs
- README clarifies that the vendor pre-amble is sent for any device exposing the
d18d2c10-…characteristic (not MC-21-specific).
v0.4.9
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).
-
_pauseddeceleration race in_on_treadmill_data. The previous version cleared_pausedon every Treadmill Data frame withspeed > 0. Pause sets the flag at speed ≈ 2.5; the ~5–10 s deceleration to zero emits dozens ofspeed > 0frames, each clearing the flag, so by the time speed reached 0 the_pausedbranch was dead and belt_state derived as STOPPED. The flag is now set/cleared exclusively by_on_machine_statusin response to FM Status events;_on_treadmill_dataonly reads it. -
"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_eventon 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 StatusSTOPPED_OR_PAUSED + PAUSEarrives as the first event. The ack waiter got woken (sopause()returned True), but_pausedwas never set, andbelt_statederived 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
Added
-
BeltState.PAUSED(= 2) on the public enum. Distinguishes "session paused, will resume" from "session ended" — the device emits FM StatusSTOPPED_OR_PAUSEDwith the PAUSE param (0x02) when the user pauses (viapause()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.FTMSControllernow 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_pausedis 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
Added
-
WalkingPadController.pause()— exposes the existing FTMS pause path (STOP_OR_PAUSEopcode0x08withPAUSE = 0x02) on the unified controller. Pause leaves the session counters (time, distance, calories, steps) intact, so a subsequentstart()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()sendsSTOP = 0x01which 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 tostop().
Refs walkingpad-controller#2.
v0.4.6
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
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
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
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
Durationconstants 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_pointnow 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_statusnow handles0x05(Target Speed Changed) and0x06(Target Inclination Changed). AddsFitnessMachineStatusOpcodeenum.
Capabilities & metadata
- Read Software Revision String (
0x2A28) during connect; expose asFTMSController.firmware_versionandWalkingPadController.firmware_version. - Add
KingSmithMode/KingSmithStatus/KingSmithTreadmillStatusenums decoded from KS Fit's AOT snapshot, for callers that want named values instead of raw integers.
Tests
- Fix
tests/test_real_device.pyto clear the disconnect latch after a successfulconnect()(BleakClient retries duringconnect()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
Fixed
WalkingPadController.connectednow 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 raisesBleakErrorwhen 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 rejectingSTART_OR_RESUMEbecause 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.