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.
Files changed (69) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +190 -0
  3. package/README.md +180 -0
  4. package/config.schema.json +266 -0
  5. package/dist/accessories/attention-sensor.d.ts +18 -0
  6. package/dist/accessories/attention-sensor.js +38 -0
  7. package/dist/accessories/attention-sensor.js.map +1 -0
  8. package/dist/accessories/common.d.ts +33 -0
  9. package/dist/accessories/common.js +61 -0
  10. package/dist/accessories/common.js.map +1 -0
  11. package/dist/accessories/fast-polling-switch.d.ts +17 -0
  12. package/dist/accessories/fast-polling-switch.js +34 -0
  13. package/dist/accessories/fast-polling-switch.js.map +1 -0
  14. package/dist/accessories/trigger-sensor.d.ts +24 -0
  15. package/dist/accessories/trigger-sensor.js +56 -0
  16. package/dist/accessories/trigger-sensor.js.map +1 -0
  17. package/dist/api/peloton-api.d.ts +147 -0
  18. package/dist/api/peloton-api.js +209 -0
  19. package/dist/api/peloton-api.js.map +1 -0
  20. package/dist/auth/peloton-auth.d.ts +160 -0
  21. package/dist/auth/peloton-auth.js +775 -0
  22. package/dist/auth/peloton-auth.js.map +1 -0
  23. package/dist/config.d.ts +96 -0
  24. package/dist/config.js +227 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +9 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/platform.d.ts +67 -0
  30. package/dist/platform.js +257 -0
  31. package/dist/platform.js.map +1 -0
  32. package/dist/poller/poller.d.ts +259 -0
  33. package/dist/poller/poller.js +732 -0
  34. package/dist/poller/poller.js.map +1 -0
  35. package/dist/poller/rules.d.ts +101 -0
  36. package/dist/poller/rules.js +210 -0
  37. package/dist/poller/rules.js.map +1 -0
  38. package/dist/scripts/auth-probe.d.mts +15 -0
  39. package/dist/scripts/auth-probe.mjs +381 -0
  40. package/dist/scripts/auth-probe.mjs.map +1 -0
  41. package/dist/settings.d.ts +12 -0
  42. package/dist/settings.js +23 -0
  43. package/dist/settings.js.map +1 -0
  44. package/dist/store/account-connect.d.ts +71 -0
  45. package/dist/store/account-connect.js +169 -0
  46. package/dist/store/account-connect.js.map +1 -0
  47. package/dist/store/account-store.d.ts +93 -0
  48. package/dist/store/account-store.js +193 -0
  49. package/dist/store/account-store.js.map +1 -0
  50. package/dist/ui/handlers.d.ts +170 -0
  51. package/dist/ui/handlers.js +381 -0
  52. package/dist/ui/handlers.js.map +1 -0
  53. package/dist/ui/server.d.ts +12 -0
  54. package/dist/ui/server.js +45 -0
  55. package/dist/ui/server.js.map +1 -0
  56. package/homebridge-ui/public/accounts.js +394 -0
  57. package/homebridge-ui/public/api.js +51 -0
  58. package/homebridge-ui/public/app.js +619 -0
  59. package/homebridge-ui/public/copy.js +241 -0
  60. package/homebridge-ui/public/dom.js +286 -0
  61. package/homebridge-ui/public/index.css +938 -0
  62. package/homebridge-ui/public/index.html +13 -0
  63. package/homebridge-ui/public/model.js +493 -0
  64. package/homebridge-ui/public/peloton-banner.png +0 -0
  65. package/homebridge-ui/public/polling.js +78 -0
  66. package/homebridge-ui/public/settings.js +121 -0
  67. package/homebridge-ui/public/triggers.js +290 -0
  68. package/homebridge-ui/server.js +8 -0
  69. 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
+ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
7
+ -->
8
+
9
+ [![npm](https://img.shields.io/npm/v/homebridge-peloton/beta)](https://www.npmjs.com/package/homebridge-peloton)
10
+ [![Build, lint, and test](https://github.com/arodbuilds/homebridge-peloton/actions/workflows/build.yml/badge.svg)](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
+ ![The Accounts section: the owner connected, a household member not yet connected, and the browser sign-in panel open under the member's card](assets/screenshots/accounts.png)
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
+ ![A Workout trigger card: name, Occupancy sensor or Switch, who, device, the activity chips, and the keep-on time](assets/screenshots/trigger-workout.png)
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
+ ![A Heart-rate zone trigger card: the member, the zone, and the hold time](assets/screenshots/trigger-zone.png)
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
+ ![The Polling section: the Fast polling switch, the fast and standby intervals, and the automation callout](assets/screenshots/polling.png)
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
+ ![The Settings section with Advanced open: the auto-off period, the Attention needed sensor, the daily check-in time, and Restore from backup](assets/screenshots/settings.png)
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).