Skip to content

fix: update API calls for current Eight Sleep endpoints#31

Merged
omarshahine merged 3 commits intosteipete:mainfrom
omarshahine:fix/api-endpoint-updates
Apr 16, 2026
Merged

fix: update API calls for current Eight Sleep endpoints#31
omarshahine merged 3 commits intosteipete:mainfrom
omarshahine:fix/api-endpoint-updates

Conversation

@omarshahine
Copy link
Copy Markdown
Collaborator

@omarshahine omarshahine commented Apr 16, 2026

Summary

Initial diagnosis attached query params (metrics=all, specialization=all) to endpoints that no longer exist; live testing plus cross-check with the lukas-clarke/eight_sleep reference integration showed the modern Eight Sleep API has removed those endpoints entirely. Revised fixes below.

What changed

Issue Previous attempt Root cause Fix
#17 schedule list add specialization=all, fall back to /household/users/:id/schedule Both paths 404. Reference notes "Eight Sleep has migrated away from the routines API." Autopilot schedule lives as smart subfield of app-api.8slp.net/v1/users/:id/temperature Retarget schedule list to the smart-schedule endpoint; drop CRUD + next subcommands (API is gone)
#18 metrics summary / aggregate add metrics=all Both endpoints 404 "Cannot GET" — they are not in the reference integration anywhere; all sleep metrics go through /users/:id/trends Remove the commands
#19 device owner fall back to /devices/:id ownerId /devices/:id/owner doesn't exist; fallback matches reference Kept
#20 metrics trends --from/--to read flags from cobra, add tz Flag plumbing was correct but default --timezone local was sent verbatim; API requires a real IANA zone Resolve "local"time.Local.String() before querying

Closes #17, #18, #19, #20.

Files

File Change
internal/client/metrics.go Remove Summary/Aggregate; add resolveTZ helper; Trends now always sends a real IANA zone
internal/cmd/metrics.go Remove summary/aggregate subcommands
internal/client/schedules.go Replace CRUD with GetSmartSchedule targeting app-api.8slp.net/v1/users/:id/temperature
internal/cmd/schedule.go Keep schedule list (Autopilot schedule); remove create/update/delete/next

Net: +29 / −294.

Test plan (verified against a live pod)

  • eightctl metrics trends --from 2026-04-10 --to 2026-04-15 returns trend data with default --timezone
  • eightctl schedule list returns {bedTimeLevel, initialSleepLevel, finalSleepLevel}
  • eightctl device owner returns ownerId
  • eightctl metrics summary / aggregate no longer exist (removed rather than left broken)
  • go test ./... passes

Notes

This is a breaking change for anyone who scripted against metrics summary, metrics aggregate, or schedule create/update/delete/next — but those commands have been returning 404 since the API migration, so nothing that depends on them is actually working today.

🤖 Generated with Claude Code

omarshahine and others added 2 commits April 16, 2026 04:09
- metrics summary/aggregate: add required metrics=all param (steipete#18)
- metrics trends: read --from/--to from cmd flags, add tz param (steipete#20)
- device owner: fallback to device info when /owner returns 404 (steipete#19)
- schedule list: add specialization param, fallback to household endpoint (steipete#17)

Closes steipete#17, steipete#18, steipete#19, steipete#20

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verification against live API (and cross-check with lukas-clarke/eight_sleep
reference implementation) showed that the previous fixes added query params
to endpoints that no longer exist rather than reaching working paths:

- metrics summary/aggregate: /users/:id/metrics/{summary,aggregate} return
  404 "Cannot GET" regardless of params. The modern API has no equivalent;
  all sleep metrics flow through /users/:id/trends. Remove the commands.
- schedule CRUD: /users/:id/temperature/schedules and the household
  fallback both 404. Eight Sleep retired the routines/schedules API; the
  Autopilot schedule now lives as the `smart` subfield of
  app-api.8slp.net/v1/users/:id/temperature. Retarget `schedule list` to
  that endpoint and remove create/update/delete/next.
- metrics trends: endpoint is correct and --from/--to flag plumbing works,
  but the default --timezone "local" was sent verbatim as tz=local, which
  the API rejects (wants an IANA zone). Resolve "local" to time.Local
  before querying.
- device owner: fallback to /devices/:id ownerId already works; unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@omarshahine
Copy link
Copy Markdown
Collaborator Author

Self-review (automated agents)

Three specialized reviewers ran against commit 82fa5b6. Consolidated findings below — two are real, worth addressing before merge.

Important

1. Landmine: same tz=local bug exists in two other commands

The resolveTZ helper fixed metrics trends, but internal/cmd/sleep.go:31-34 and internal/cmd/sleep_range.go:39-42 still do if tz == "local" { tz = time.Local.String() } without guarding the "Local" sentinel (what time.Local.String() returns when zoneinfo is unavailable) or empty-string case. Same class of bug — they'll send literal tz=Local and get rejected. Should route those two call sites through the shared helper (export resolveTZ or move to a common package).

Per "no loose ends": this is the exact adjacent broken thing the commit should sweep.

2. Silent empty-success on missing smart field

GetSmartSchedule returns res.Smart, nil when the server omits or nulls the smart field. A user without Autopilot configured sees an empty row indistinguishable from a successful call with no data. Either return a sentinel error or have schedule list print "no Autopilot schedule configured" when nil.

3. Silent UTC fallback in resolveTZ

When time.Local.String() returns "Local" (no zoneinfo, e.g. some containers), the helper silently substitutes "UTC". A PST user gets UTC-bucketed trend data off by 8 hours with no indication. The fallback is reasonable but should warn to stderr when it triggers.

Nits

4. Comment bloat in schedules.go

The GetSmartSchedule doc comment narrates PR history ("Eight Sleep retired the routines/temperature-schedules CRUD API") rather than stating an invariant. It'll rot the moment the API reshapes again, and duplicates the URL that appAPIBaseURL + fmt.Sprintf already encode. Trim to one line or drop — the name and return type carry the meaning. The resolveTZ comment is the right shape (short, API-quirk WHY) — keep.

5. No tz validation at CLI boundary

resolveTZ doesn't call time.LoadLocation(tz) to pre-validate user input; we rely on the API rejecting it. Friendlier error surface if we validated first, but not required.

Positive

  • No silent-swallow patterns remain — the old ListSchedules err == nil fallback was the only offender and it's gone.
  • doURL signature matches the new call. appAPIBaseURL is test-swappable.
  • go build and go test ./... pass.
  • Clean removal of dead commands — no stale references to TemperatureSchedule, ListSchedules, or the metrics endpoints anywhere in the tree.
  • No shell-completion or man-page generator hard-codes the removed commands.

Recommendation

Before merging I should (a) fix the sleep.go/sleep_range.go landmine by sharing resolveTZ, and (b) surface the two silent behaviors (missing smart and UTC fallback). The comment nit can go with it.

- Move resolveTZ to internal/client/eightsleep.go so GetSleepDay also
  normalizes tz. Fixes the same `tz=local`/`tz=Local` rejection in
  `sleep day` and `sleep range` that the prior commit only fixed for
  `metrics trends`. CLI call sites in cmd/sleep.go and cmd/sleep_range.go
  now pass the raw timezone through.
- When resolveTZ falls back to UTC (system zoneinfo unavailable),
  emit a log.Warn so users see off-by-hours data flagged instead of
  silently trusting it.
- GetSmartSchedule returns ErrNoSmartSchedule when the server omits or
  nulls the `smart` field, so an unconfigured Autopilot surfaces as a
  clear CLI message instead of an empty-row success.
- Trim GetSmartSchedule doc comment to a single line stating the
  invariant, not PR history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@omarshahine omarshahine merged commit 22b405e into steipete:main Apr 16, 2026
2 checks passed
omarshahine pushed a commit to omarshahine/eightctl that referenced this pull request Apr 16, 2026
Aligns README with the command surface after steipete#31:
- schedule list is now the only schedule subcommand (CRUD + next retired)
- metrics summary / aggregate removed (endpoints gone upstream)
- Documents the tz=local/Local rejection and UTC fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
omarshahine pushed a commit to omarshahine/eightctl that referenced this pull request Apr 16, 2026
Aligns README with the command surface after steipete#31:
- schedule list is now the only schedule subcommand (CRUD + next retired)
- metrics summary / aggregate removed (endpoints gone upstream)
- Documents the tz=local/Local rejection and UTC fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
omarshahine added a commit that referenced this pull request Apr 16, 2026
Aligns README with the command surface after #31:
- schedule list is now the only schedule subcommand (CRUD + next retired)
- metrics summary / aggregate removed (endpoints gone upstream)
- Documents the tz=local/Local rejection and UTC fallback

Co-authored-by: Lobster <lobster@shahine.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

schedule list fails with specialization validation error (400)

1 participant