homebridge-peloton 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/LICENSE +190 -0
- package/README.md +180 -0
- package/config.schema.json +266 -0
- package/dist/accessories/attention-sensor.d.ts +18 -0
- package/dist/accessories/attention-sensor.js +38 -0
- package/dist/accessories/attention-sensor.js.map +1 -0
- package/dist/accessories/common.d.ts +33 -0
- package/dist/accessories/common.js +61 -0
- package/dist/accessories/common.js.map +1 -0
- package/dist/accessories/fast-polling-switch.d.ts +17 -0
- package/dist/accessories/fast-polling-switch.js +34 -0
- package/dist/accessories/fast-polling-switch.js.map +1 -0
- package/dist/accessories/trigger-sensor.d.ts +24 -0
- package/dist/accessories/trigger-sensor.js +56 -0
- package/dist/accessories/trigger-sensor.js.map +1 -0
- package/dist/api/peloton-api.d.ts +147 -0
- package/dist/api/peloton-api.js +209 -0
- package/dist/api/peloton-api.js.map +1 -0
- package/dist/auth/peloton-auth.d.ts +160 -0
- package/dist/auth/peloton-auth.js +775 -0
- package/dist/auth/peloton-auth.js.map +1 -0
- package/dist/config.d.ts +96 -0
- package/dist/config.js +227 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +67 -0
- package/dist/platform.js +257 -0
- package/dist/platform.js.map +1 -0
- package/dist/poller/poller.d.ts +259 -0
- package/dist/poller/poller.js +732 -0
- package/dist/poller/poller.js.map +1 -0
- package/dist/poller/rules.d.ts +101 -0
- package/dist/poller/rules.js +210 -0
- package/dist/poller/rules.js.map +1 -0
- package/dist/scripts/auth-probe.d.mts +15 -0
- package/dist/scripts/auth-probe.mjs +381 -0
- package/dist/scripts/auth-probe.mjs.map +1 -0
- package/dist/settings.d.ts +12 -0
- package/dist/settings.js +23 -0
- package/dist/settings.js.map +1 -0
- package/dist/store/account-connect.d.ts +71 -0
- package/dist/store/account-connect.js +169 -0
- package/dist/store/account-connect.js.map +1 -0
- package/dist/store/account-store.d.ts +93 -0
- package/dist/store/account-store.js +193 -0
- package/dist/store/account-store.js.map +1 -0
- package/dist/ui/handlers.d.ts +170 -0
- package/dist/ui/handlers.js +381 -0
- package/dist/ui/handlers.js.map +1 -0
- package/dist/ui/server.d.ts +12 -0
- package/dist/ui/server.js +45 -0
- package/dist/ui/server.js.map +1 -0
- package/homebridge-ui/public/accounts.js +394 -0
- package/homebridge-ui/public/api.js +51 -0
- package/homebridge-ui/public/app.js +619 -0
- package/homebridge-ui/public/copy.js +241 -0
- package/homebridge-ui/public/dom.js +286 -0
- package/homebridge-ui/public/index.css +938 -0
- package/homebridge-ui/public/index.html +13 -0
- package/homebridge-ui/public/model.js +493 -0
- package/homebridge-ui/public/peloton-banner.png +0 -0
- package/homebridge-ui/public/polling.js +78 -0
- package/homebridge-ui/public/settings.js +121 -0
- package/homebridge-ui/public/triggers.js +290 -0
- package/homebridge-ui/server.js +8 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to homebridge-peloton are listed here. The format follows Keep a Changelog, and the project follows semantic versioning from 0.1.0-beta.1.
|
|
4
|
+
|
|
5
|
+
## 0.1.0-beta.1 (2026-09-11)
|
|
6
|
+
|
|
7
|
+
First beta, to the npm beta tag. Build 4 of 4: banner, README, release prep, and the last Chrome pass findings. Build 3 of 4: config schema, settings page, UI server, device matching by platform. Build 2 of 4: config, rules, poller, accessories, platform wiring. Build 1 of 4: scaffold, auth module, account store, fixtures, tests.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- The regenerated banner (`assets/peloton-banner.png` and the copy under `homebridge-ui/public/`) with the release tagline "HomeKit sensors driven by Peloton workouts: workout in progress and heart-rate zones."
|
|
12
|
+
- README rewritten for release: requirements, install, the setup walkthrough with the settings page screenshots, browser sign-in, how it works, privacy and what is stored where, troubleshooting, development, and credits.
|
|
13
|
+
- The settings page (`homebridge-ui/public/`), plain HTML, CSS, and ES modules served by the Homebridge UI: Accounts with cards, status pills, avatars, the inline connect panel with email and password and the Sign in with browser fallback, Add account, and the membership's device names; Triggers as Workout and Heart-rate zone cards with badges, activity chips, the add chooser, Duplicate and Remove; Polling with the Fast polling switch block, the intervals, the live request estimate, and the dismissible automation callout; Settings with Name, Debug logging, and Advanced holding the auto-off minutes, the Attention needed sensor, the daily check-in time, and Restore from backup. Validation on blur, a sticky summary box that gates Save, the blocking Polling error, the reconnect banner, the unsaved-changes bar, empty and first-run states, the phone layout, and both host themes follow design/README.md.
|
|
14
|
+
- The settings UI server (`src/ui/`, started by `homebridge-ui/server.js`): /status, /connect, /browser/start, /browser/finish, /test, /household, /remove, and /avatar over the same account store, auth, and API modules the platform uses. Errors carry an auth stage or `api` with the HTTP status; tokens never leave the server; config.json is never written by it. Profile photos are proxied with a 24 hour cache.
|
|
15
|
+
- `connectWithTokens` in the shared connect flow for the browser sign-in path, and the removal of a household profile once a configured account connects as that member.
|
|
16
|
+
- `config.schema.json` with the full SPEC section 6 shape, the section 2 defaults, the settings page copy, and the validation constraints, marked as using the custom UI.
|
|
17
|
+
- Account write-back from the page: id, email, display name, and userId from the store on save; the password only when entered on the page, else the stored one unchanged.
|
|
18
|
+
- Test suites for the schema (ajv, a dev dependency), the UI server handlers with a fake store and fake fetch, the page model, and platform reconciliation by id.
|
|
19
|
+
- README walkthrough for the settings page: connect the owner, members connect, add a trigger, set up the fast polling automation, and the browser sign-in fallback with the Back button instruction.
|
|
20
|
+
- Config parsing and validation (`src/config.ts`) with the SPEC defaults: invalid values are clamped or defaulted with one warn line each, and missing or duplicate ids are generated for the run with a warn line asking for a save from the settings page.
|
|
21
|
+
- Trigger rules (`src/poller/rules.ts`): workout detectability, matching by who, activities, and device through a device_type map learned from attached devices, zone bounds in SPEC priority order with rounded-down defaults, zone from a sample, a hold helper with a hold time in each direction, and the workout sensor's hold after end.
|
|
22
|
+
- The poller (`src/poller/poller.ts`): standby, scanning, locked, and released states with staggered polling, lock-on to the first workout in progress, following it by id so a synced import cannot end it, the performance graph only while a zone trigger targets the locked account, backoff after three consecutive failures, refresh on 401, and account drop with an attention event on invalid_grant. The Fast polling switch with its auto-off timer anchored to the later of switch-on and the last workout end, and the daily check-in that refreshes tokens, zones, household profiles, and devices.
|
|
23
|
+
- Accessories (`src/accessories/`): trigger sensors as OccupancySensor or Switch with the service swapped in place when the kind changes, the Fast polling switch, and the optional Attention needed sensor, with UUIDs from ids and the SPEC accessory information fields.
|
|
24
|
+
- Platform wiring: config load, account store, device map from stored household devices, accessory registration with orphan cleanup, poller events connected to the accessories, startup skip lines for accounts that are not connected, and a clean stop on Homebridge shutdown.
|
|
25
|
+
- Startup sign-in and re-login (`src/store/account-connect.ts`): at startup the platform signs in, one after another, each configured account that has email and password but no stored sign-in, saves the tokens, fills the profile from `/api/me`, and settles the owner and household from subscriptions; after an invalid_grant drop it signs in once more when config has the password. Log lines "Connected {name} (@{username})" and "{name}: sign-in failed at stage {stage}, HTTP {status}; use the settings page to connect".
|
|
26
|
+
- Test suites for config, rules, poller, accessories, and platform with a fake clock and scheduler and a route-based fixture-replaying fetch.
|
|
27
|
+
- Project scaffold: TypeScript strict, ESLint, node:test, lint, build, and test scripts, CI on Node 20, 22, and 24, and a release workflow that publishes through npm trusted publishing (pre-release to the beta tag, latest release to latest), with the NPM_TOKEN secret for the first publish until the trusted publisher is registered.
|
|
28
|
+
- Stub Peloton platform that loads config and logs "Peloton platform loaded".
|
|
29
|
+
- Auth module (`src/auth/peloton-auth.ts`): headless Auth0 Universal Login with PKCE, refresh with token rotation, browser sign-in fallback (`browserStart` and `browserFinish`), an in-file cookie jar, HTML form parsing that reads the action and hidden inputs by name, `AuthError` with stage, status, and OAuth code, and an opt-in redacted debug dump of the login pages.
|
|
30
|
+
- API wrappers (`src/api/peloton-api.ts`) for me, subscriptions, latest workout, and performance graph, returning typed objects with only the fields SPEC lists and throwing `ApiError` with the HTTP status on non-2xx.
|
|
31
|
+
- Account store (`src/store/account-store.ts`): directory 0700, files 0600, atomic writes through a temp file and rename, and `withValidToken` that refreshes within 6 hours of expiry or on 401, persists the rotated refresh token before returning, and marks the account reconnect_needed on invalid_grant.
|
|
32
|
+
- Synthesised, sanitised fixtures under `fixtures/auth` and `fixtures/api`.
|
|
33
|
+
- Test suites for auth, api, and store with a fake fetch that replays fixtures in sequence and records requests.
|
|
34
|
+
- Standalone auth probe for the Pi (`dist/scripts/auth-probe.mjs`) with login, refresh, browser, me, and workout commands.
|
|
35
|
+
- `getWorkout(workoutId)` in the API wrappers for GET /api/workout/{id}, and the workout type now carries `isPelotonOriginatedWorkout`, `is3pFitFeedWorkout`, `platform`, `pelotonId`, and `ride` (id, title, duration, instructor id when present). Fixtures for the single-workout call and for a run synced from a third-party fitness feed, which detection ignores.
|
|
36
|
+
- Probe `graph <workoutId>` command that prints the metric slugs, sample counts, and heart-rate zone bounds of one workout's performance graph.
|
|
37
|
+
- CLAUDE.md with the working rules, README first pass, and this changelog.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- New trigger cards start with the Name prefilled, "Workout" or "Zone 4 or higher", instead of a placeholder. A Heart-rate zone card's name follows the chosen zone until the name is edited.
|
|
42
|
+
- package.json description and keywords for the npm listing (homebridge, homekit, workout, heart-rate, fitness alongside homebridge-plugin, supports-hap, and peloton).
|
|
43
|
+
- SPEC sections 10, 13, 14, 15, and 16 and the design README fold build 4 in; section 15 lists only what stays open after beta.1 and the clarification notes move to section 16.
|
|
44
|
+
- A trigger's device is `any`, `bike`, or `tread`, matched by the workout's platform (`home_bike` for the Bike and Bike+, `home_tread` for the Tread), the codes the Pi ride tests reported. The device_type map learned from attached_devices and the "device filtering is unavailable" log line are gone; device_type stays in the debug poll line. Any other device value becomes `any` with a warn line.
|
|
45
|
+
- The third-party import fixture is an Apple Health import (device_type `apple_health`, platform `iOS_app`), as the Pi reported it.
|
|
46
|
+
- SPEC sections 6, 7, 8.3, 9, 10, and 15 fold build 3 in; section 15 keeps the one open item.
|
|
47
|
+
- Attached devices from the subscriptions call carry `deviceType` when present, and the account store keeps `isOwner` and the owner's `devices` so the device map survives a restart. `withValidToken` takes a `forceRefresh` option for the daily check-in.
|
|
48
|
+
- README describes the config.json shape for a hand-edited setup and where sign-ins are stored.
|
|
49
|
+
- Verification detection no longer runs on the initial login page, whose bundled Lock library and text dictionary contain verification words for every account. It runs on the credentials response, the callback page, and the token error path only.
|
|
50
|
+
- The workout type no longer has a device id field: the second Pi probe confirmed that workouts carry none and that `device_type` is the hardware model code. `/api/me` `last_workout_at` is stale and is no longer the source for "last workout"; the latest workout's `created_at` is. The performance graph fixture carries the heart-rate zone bounds the probe confirmed on a 168 max. The probe prints the new workout fields and labels the stale `/api/me` value.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- The settings page iframe scrolled on its own by a few pixels and wheel scrolling stuck at the boundary: the Homebridge UI sizes the iframe to the body's scrollHeight, and the banner's top margin collapsed out of that measure. html and body are now margin 0 and overflow hidden, the root contains its margins, and the page asks the host to size the iframe after every render. Verified in headless Chromium against the host stylesheet in both themes and at phone width.
|
|
55
|
+
- Connect, Test, Add account, Edit, and Duplicate trigger rendered grey under the Homebridge UI's dark themes, whose stylesheet greys every link button with an important rule, while Remove stayed red. The page's link buttons and the callout link now keep the link colour in both themes.
|
|
56
|
+
- The Devices line was built from an attached_devices shape Peloton does not send. The live entries (second Chrome pass on the Pi) are `device_id`, `device_name`, `device_group`, and `last_attached_at`, with `device_name` null for the Guide and no `id`, `name`, or `device_type`, so the line listed nothing. The parser reads each entry as id, name or null, and group (bike, tread, guide); the store keeps that shape and the hardware family from `device_type` is gone; the line reads "Devices on this membership: Blue Door+ (bike), Tread (tread), Guide", each device as its name with its group after it, or the capitalised group alone for a device without a name. The fixture carries the live shape.
|
|
57
|
+
- Household profiles were named from `first_name` and `last_name`, which `shared_user_set` does not carry: the live entry has `name` as one string (with `username`, `id`, `image_url`, `is_profile_private`, `is_provisional`, `last_workout_at`, `total_workouts`, and `location`). Profiles take their display name from `name`, the username when it is empty.
|
|
58
|
+
- The membership was read only at connect and at the daily check-in, so a page opened after an upgrade showed the old names and devices until 03:00. /status now re-reads the owner's subscriptions when the stored household is older than one hour, records the read as `householdFetchedAt` on the owner's record, and keeps the stored household when the read fails.
|
|
59
|
+
- The settings page threw "crypto.randomUUID is not a function" on Add trigger and on Connect when the Homebridge UI is served over plain http, where the page is not a secure context. Ids now fall back to a version 4 UUID built from crypto.getRandomValues.
|
|
60
|
+
- Avatars never rendered: the /avatar proxy rejected any CDN answer whose content-type was not image/*, and S3 answers binary/octet-stream for an object stored without one. The image type now comes from the first bytes (JPEG, PNG, GIF, WebP) when the header does not say image/*. The page's request, the handler, the proxied fetch, and the img src were traced end to end in Chromium over plain http.
|
|
61
|
+
- The Devices line depended on every attached_devices entry carrying a string id and a name: a numeric or missing id folded devices into one, and a nameless device was listed as nothing. Devices are now stored on the owner's record at connect and at the daily check-in, keyed so none folds into another, and /status lists them all (the live entry shape and the line format are in the second Chrome pass entries above).
|
|
62
|
+
- Household profiles showed the username as the display name and were never updated after they were written. The profile is named from shared_user_set (its `name` field, see the second Chrome pass entries above), username otherwise, and every subscriptions read brings a stored profile up to date; a member's own connected record replaces a display name that was only the username with the profile's name from `/api/me`.
|
|
63
|
+
- "Last checked" showed a stale time right after a poll, because lastCheckedAt was written only at sign-in, refresh, and check-in. The poller now writes it on every successful poll of the account and /status reads it.
|
|
64
|
+
- A Workout trigger with activities unset showed "0 of 12 selected". Unset means all (SPEC section 6): every chip renders selected, Select all / none keep their meaning, and an all-selected set is saved as an empty list.
|
|
65
|
+
- Tests: the page renders under node:test on a small fake DOM (`test/helpers/fake-dom.mjs`) for the avatar, Devices line, chips, and id paths; handler, API, connect flow, and poller tests cover the rest.
|
|
66
|
+
- Headless login got past the credentials POST on the second Pi run and failed at stage callback with HTTP 302, because the module stopped at the first redirect that left auth.onepeloton.com and treated it as the callback. The live flow is six hops across auth.onepeloton.com and auth-orca.onepeloton.com before the redirect to members.onepeloton.com/callback. The module now follows every 3xx on any host, sends each host only its own cookies, never resends the form body, stops at the first Location that is the redirect URI (scheme, host, and path; a trailing slash tolerated, the query ignored), and allows up to 12 redirects. Fixtures carry the six-hop chain with its cookies per hop, and the single-hop shape is still accepted.
|
|
67
|
+
- Headless login failed at the credentials POST with HTTP 403 AnomalyDetected "Invalid state" on the first Pi run. The module now parses `window.injectedConfig` from the login page, checks that its code_challenge and nonce are ours, and posts every internalOptions value verbatim (Auth0's transaction state and `_csrf` among them) with the tenant and callback URL from the page. The confirmed tenant is "peloton-prod" and the connection "pelo-user-password". A 403 AnomalyDetected response maps to stage authorize so the settings page offers the browser fallback, and `access_denied` counts as a wrong password only when the description says so. The probe prints the tenant and connection it is about to use.
|
|
68
|
+
- The account store re-reads the record before refreshing, so a caller that loaded the record before another caller's rotation reuses that rotation instead of sending the retired refresh token to Auth0, which would have marked the account reconnect_needed for no reason.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Alex Rodriguez (arodbuilds) https://alex-rodriguez.com
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<img src="assets/peloton-banner.png" alt="Peloton: HomeKit sensors driven by Peloton workouts" width="100%">
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
verified-by-homebridge: this plugin has not been through Homebridge verification yet.
|
|
5
|
+
Do not claim it. Once the plugin is verified, replace this comment with the badge:
|
|
6
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/homebridge-peloton)
|
|
10
|
+
[](https://github.com/arodbuilds/homebridge-peloton/actions/workflows/build.yml)
|
|
11
|
+
|
|
12
|
+
A [Homebridge](https://homebridge.io) plugin that turns Peloton workouts into HomeKit sensors. It watches the Peloton accounts in your household and exposes trigger sensors that are on while a workout is in progress, or while a rider is at or above a heart-rate zone, so Home app automations can react when a workout starts, ends, or gets hard: dim the lights when the ride begins, turn on a fan at zone 4, bring the lights back when the class ends.
|
|
13
|
+
|
|
14
|
+
Not affiliated with or endorsed by Peloton Interactive. Uses Peloton's undocumented member API, which can change without notice.
|
|
15
|
+
|
|
16
|
+
> **Status:** Beta. Please report what works and what does not in the [issue tracker](https://github.com/arodbuilds/homebridge-peloton/issues).
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Requirements](#requirements)
|
|
21
|
+
- [Install](#install)
|
|
22
|
+
- [Setup](#setup)
|
|
23
|
+
- [1. Connect your accounts](#1-connect-your-accounts)
|
|
24
|
+
- [2. Add a Workout trigger](#2-add-a-workout-trigger)
|
|
25
|
+
- [3. Add a Heart-rate zone trigger](#3-add-a-heart-rate-zone-trigger)
|
|
26
|
+
- [4. Set up the fast polling automation](#4-set-up-the-fast-polling-automation)
|
|
27
|
+
- [5. Settings and Advanced](#5-settings-and-advanced)
|
|
28
|
+
- [Browser sign-in](#browser-sign-in)
|
|
29
|
+
- [How it works](#how-it-works)
|
|
30
|
+
- [Privacy and what is stored where](#privacy-and-what-is-stored-where)
|
|
31
|
+
- [Troubleshooting](#troubleshooting)
|
|
32
|
+
- [Development](#development)
|
|
33
|
+
- [Credits](#credits)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Homebridge 1.8 or 2.x, with the Homebridge UI for the settings page.
|
|
39
|
+
- Node 20 or later.
|
|
40
|
+
- A Peloton membership. Every profile you want to watch signs in with its own Peloton account; heart-rate zone triggers also need a heart-rate monitor paired to the workout.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
Search for "Peloton" under Plugins in the Homebridge UI and install the beta, or from a shell on the Homebridge host:
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
npm install -g homebridge-peloton@beta
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then open the plugin's settings from the Plugins page. Everything in the setup below happens on that page. The host's Save button writes config.json and Homebridge restarts the plugin.
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
### 1. Connect your accounts
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
Under Accounts, enter the email and password of the Peloton account that owns the membership and click Connect. The card flips to Connected, and every profile on the membership appears under it as its own card marked Not connected. A Devices line lists the hardware on the membership with the names from your Peloton account.
|
|
59
|
+
|
|
60
|
+
Each member connects their own profile before it is polled. Click Connect on the member's card and enter that member's Peloton email and password, or hand them the page. A member who would rather not type a password on the Homebridge host can use Sign in with browser instead, which stores no password (see [Browser sign-in](#browser-sign-in)).
|
|
61
|
+
|
|
62
|
+
The password is stored in your Homebridge config so the plugin can sign in again by itself if Peloton ever ends the session. The plugin keeps a session token and refreshes it, so signing in again is rare.
|
|
63
|
+
|
|
64
|
+
### 2. Add a Workout trigger
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
Under Triggers, click Add trigger and choose Workout. The sensor is on while a matching workout is in progress. Pick who it watches (anyone on the membership, or one member), the device (any, Bike, or Tread), and the activities that count; all twelve are selected to start with. Keep on after the workout ends holds the sensor on for a while after the class finishes, 90 seconds by default, so stacked classes do not turn your scene off between them.
|
|
69
|
+
|
|
70
|
+
Give the trigger a name (it starts as "Workout"), choose Occupancy sensor or Switch as its HomeKit kind, and click Done. Save, restart Homebridge, and the sensor appears in the Home app under the bridge. In Automations, choose the sensor as the trigger and pick what should happen when it turns on and when it turns off.
|
|
71
|
+
|
|
72
|
+
### 3. Add a Heart-rate zone trigger
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
Add trigger, then Heart-rate zone. The sensor is on while the member's heart rate is at or above the zone you choose. Zones come from that member's Peloton profile, and the hold time (20 seconds by default) keeps a short sprint from flicking the sensor on and off. The name follows the zone ("Zone 4 or higher") until you change it.
|
|
77
|
+
|
|
78
|
+
This trigger needs a heart-rate monitor paired to the workout. Without one the sensor stays off.
|
|
79
|
+
|
|
80
|
+
### 4. Set up the fast polling automation
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
The plugin creates a switch called Peloton fast polling. While it is on, the plugin checks every account every 10 seconds, locks onto the first workout it sees, and follows it to the end. While it is off, it polls at the standby interval, 120 seconds by default; 0 turns standby polling off entirely. The switch turns itself off after 120 minutes, counted from the later of turning on and the last workout ending.
|
|
85
|
+
|
|
86
|
+
Add two Home app automations:
|
|
87
|
+
|
|
88
|
+
1. When the gym light turns on, turn the Peloton fast polling switch on.
|
|
89
|
+
2. When the gym light turns off, turn the Peloton fast polling switch off.
|
|
90
|
+
|
|
91
|
+
Any trigger that means "a workout is about to start" works in place of the light: a time of day, a motion sensor, a scene. Without the automation the plugin still works, just at the standby interval, so a workout may be noticed up to two minutes after it starts.
|
|
92
|
+
|
|
93
|
+
### 5. Settings and Advanced
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
Settings holds the plugin name and Debug logging. Under Advanced:
|
|
98
|
+
|
|
99
|
+
- Fast polling switch turns off after: the auto-off period in minutes.
|
|
100
|
+
- Attention needed sensor: an occupancy sensor that is on while any account needs to be reconnected, for an automation that sends you a notification.
|
|
101
|
+
- Daily check-in time: once a day the plugin refreshes each account's session and heart-rate zones, even when nobody is working out.
|
|
102
|
+
- Restore from backup: loads a saved Peloton platform block, a whole config.json, or a saved draft of this page.
|
|
103
|
+
|
|
104
|
+
## Browser sign-in
|
|
105
|
+
|
|
106
|
+
If Peloton does not accept the sign-in from the plugin, or an account has extra verification turned on, the connect panel offers Sign in with browser:
|
|
107
|
+
|
|
108
|
+
1. Click Open Peloton sign-in. A new tab opens on Peloton's sign-in page. Sign in there. If you are already signed in, it jumps straight to your home page; that is fine.
|
|
109
|
+
2. Press your browser's Back button once. The address bar shows an address that starts with members.onepeloton.com/callback. Copy it.
|
|
110
|
+
3. Paste it into the field on the settings page and click Connect.
|
|
111
|
+
|
|
112
|
+
If Back does not show the callback address, long-press the Back button and pick the callback entry, or search your browser history for "callback". The address is single use and expires within minutes. If the page says the link expired or came from an earlier attempt, click Open Peloton sign-in again and use the new one.
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
- **Polling states.** With the Fast polling switch off and no workout in progress the plugin is in standby and polls each account at the standby interval. Switch on, it scans every account at the fast interval. The first account seen with a workout in progress locks the plugin onto that account: only that account is polled, at the fast interval, until the workout ends. Turning the switch off during a workout does not stop the plugin following it to the end.
|
|
117
|
+
- **Lock-on.** One workout is followed at a time. Other members' workouts are not polled while a workout is locked, so two riders on one membership should start the second ride after the first has ended, or use one Workout trigger for anyone.
|
|
118
|
+
- **Hold.** A Workout sensor stays on for the keep-on time after the class ends, and a new matching workout during that time keeps it on without a gap. A Heart-rate zone sensor changes only after the zone has held for the hold time, and turns off when the workout ends.
|
|
119
|
+
- **Third-party imports are ignored.** Runs and other activities synced into Peloton from Apple Health, Strava, or Fitbit never start or end a workout, even when they land at the top of your workout list mid-ride.
|
|
120
|
+
- **Configuration.** The settings page writes the platform block in config.json; the shape and every default are in [SPEC.md](SPEC.md) section 6 and in `config.schema.json`, for anyone who edits by hand.
|
|
121
|
+
|
|
122
|
+
## Privacy and what is stored where
|
|
123
|
+
|
|
124
|
+
- Session tokens live in `homebridge-peloton/accounts/` inside the Homebridge storage folder, one file per account with mode 0600, readable only by the Homebridge user. They are never written to config.json, the log, or the settings page.
|
|
125
|
+
- Passwords are stored in config.json only for accounts connected with email and password on the settings page, so the plugin can sign in again by itself. Accounts connected with Sign in with browser store no password.
|
|
126
|
+
- Profile photos are fetched by the plugin and proxied to the settings page; the page never talks to Peloton's servers itself.
|
|
127
|
+
- The log never contains tokens, passwords, emails, or Peloton's responses. Debug logging prints workout ids and statuses only.
|
|
128
|
+
- To remove everything for an account, click Remove on its card. To remove every stored sign-in, delete the `homebridge-peloton` folder in the Homebridge storage folder.
|
|
129
|
+
|
|
130
|
+
## Troubleshooting
|
|
131
|
+
|
|
132
|
+
- **Reconnect needed.** Peloton ended the session (a password change, a sign-out everywhere, or a session that expired). The card shows "Sign-in expired. Reconnect to resume polling." Click Reconnect and sign in again. If the account has a password in config the plugin also tries once by itself at the next start. The Attention needed sensor under Advanced turns on while any account is in this state.
|
|
133
|
+
- **Accounts with extra verification.** If Peloton asks for a verification code when the plugin signs in, the panel says so and offers Sign in with browser; complete the verification in the browser tab and paste the callback address. Please open an issue if you hit this, since it has not been seen on a household account yet.
|
|
134
|
+
- **"Peloton sign-in did not complete."** Peloton changed something about its login page and the headless sign-in no longer fits it. Use Sign in with browser to keep going, and open an issue with the output of the auth probe (see [Development](#development)); the fix lives in one file.
|
|
135
|
+
- **No heart-rate data.** The log says "no heart-rate data for this workout" once per workout when no heart-rate monitor is paired. The zone sensor stays off for that workout.
|
|
136
|
+
- **A workout is noticed late.** Without the fast polling automation the plugin polls at the standby interval. Set up the automation, or lower the standby interval under Polling.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
Node 20 or later. Install with `npm install`, then:
|
|
141
|
+
|
|
142
|
+
| Script | What it does |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `npm run lint` | ESLint over the source, the settings page, the tests, and the config, zero warnings allowed |
|
|
145
|
+
| `npm run build` | Compiles `src/` to `dist/` with TypeScript, including the UI server and the probe |
|
|
146
|
+
| `npm test` | Builds, then runs the node:test suites in `test/` against `dist/` and the settings page |
|
|
147
|
+
| `npm run watch` | Rebuilds on change and restarts a development Homebridge from `test/hbConfig` |
|
|
148
|
+
|
|
149
|
+
The network is always mocked in tests. Fixtures live under `fixtures/`, sanitised recordings of Peloton's login pages and API answers; see `fixtures/README.md` for what each file stands in for. The settings page is plain HTML, CSS, and ES modules under `homebridge-ui/public/`, served by the Homebridge UI with no build step; its server side is compiled from `src/ui/` and started by `homebridge-ui/server.js`. [SPEC.md](SPEC.md) is the source of truth for behaviour and [design/README.md](design/README.md) for the settings page.
|
|
150
|
+
|
|
151
|
+
### Auth probe
|
|
152
|
+
|
|
153
|
+
The probe runs the headless sign-in on its own, without Homebridge, which is the first thing to try when sign-in stops working. Build first, then run from a directory where a token file may be written:
|
|
154
|
+
|
|
155
|
+
```shell
|
|
156
|
+
npm run build
|
|
157
|
+
node dist/scripts/auth-probe.mjs login you@example.com # prompts for the password without echo
|
|
158
|
+
node dist/scripts/auth-probe.mjs login you@example.com --dump ./auth-dump
|
|
159
|
+
node dist/scripts/auth-probe.mjs refresh
|
|
160
|
+
node dist/scripts/auth-probe.mjs browser # prints the sign-in URL, waits for the pasted callback URL
|
|
161
|
+
node dist/scripts/auth-probe.mjs me
|
|
162
|
+
node dist/scripts/auth-probe.mjs workout --keys
|
|
163
|
+
node dist/scripts/auth-probe.mjs graph <workoutId> # metric slugs, sample counts, heart-rate zone bounds
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Tokens are stored in `./probe-tokens.json` with mode 0600 and are never printed. On failure the probe prints the auth stage and HTTP status only. With `--dump`, every HTML page of the login flow is written to the given directory with form values redacted, which is what to attach to an issue when the flow needs adjusting. Token responses are never written. Delete `probe-tokens.json` and the dump directory when done.
|
|
167
|
+
|
|
168
|
+
## Credits
|
|
169
|
+
|
|
170
|
+
The Peloton API is undocumented, and this plugin stands on the work of people who mapped it before:
|
|
171
|
+
|
|
172
|
+
- [peloton-to-garmin](https://github.com/philosowaffle/peloton-to-garmin) for the workout and performance graph endpoints.
|
|
173
|
+
- The [Home Assistant Peloton integration](https://github.com/edwork/homeassistant-peloton-sensor) for the polling approach and the workout status fields.
|
|
174
|
+
- [@dofek/peloton](https://github.com/Asherlc/dofek/tree/main/packages/peloton-client) for the research into Peloton's Auth0 sign-in flow.
|
|
175
|
+
|
|
176
|
+
Built by Alex Rodriguez. If this plugin is useful to you, say hello at [alex-rodriguez.com](https://alex-rodriguez.com/?ref=peloton#building).
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
Apache-2.0. Copyright 2026 Alex Rodriguez (arodbuilds).
|