Skip to content

Wellness Stack: Garmin / Eight Sleep / Home Assistant Improvements

Date: 2026-04-28 Owner: the Opus worker spawned to do this Parent session: n8n-telegram_Opus47 (Building The Brain) Estimated effort: 2-4h, depending on what you find

STATUS, done 2026-04-28. All 5 build steps + cleanup complete. Pivoted Eight Sleep to HA native integration, expanded Garmin (HRV/stages/SpO2), rebuilt dashboard, added Notion Wellness Daily life-coach DB, anomaly flagging in morning brief, brain wellness tools. See reference_wellness_pollers.md and reference_wellness_daily_notion.md for the post-pivot architecture.

CORRECTION: the user_id mapping in this doc below is swapped. Verified against actual sleep durations on 2026-04-28: - Justin = right side, user_id c78dce09aa6c4e638dd92bd86a002302 - Krystal = left side, user_id b5ce51a3d34e4b25a053f57ec42af514

The cron poller's STABLE_SIDE comments and the dashboard labels were correct all along.


The Goal

Tighten Justin's wellness data pipeline, make the Eight Sleep + Garmin → Home Assistant → Context API → morning brief flow rock-solid and richer. There are known soft-spots (side detection, missing HRV from Garmin, anomalous Eight Sleep data points) plus probably a richer HA wellness dashboard he'd benefit from. Find what's broken, propose fixes with reasoning, then execute on his green-light.

This is investigation + fixes, not a single specific task. You decide what to attack first based on what you find.


Architecture (read this first)

Eight Sleep API ─┐
                 │  (cron poller, ~every 4h)
      scripts/integrations/eight-sleep/poll.py
                 │  POSTs JSON metrics
            Home Assistant (CT 100, 192.168.86.70)
                 │  exposes as REST sensors
      infra/context-api/scripts/ha_poller.py  (cron every 15 min)
            facts_wellness table (data/context.db)
            GET /context?about=wellness  (Context API at 127.0.0.1:7358)
            scripts/morning-brief.py (cron 12:00 UTC = 7am CT)
            push.sh updates → @jw_updates_bot

Garmin Connect API ─┐ (same pattern)
      scripts/integrations/garmin/poll.py
              Home Assistant (REST sensors)
              ha_poller → facts_wellness → /context → brief

The pattern: HA is the source of truth. Every wellness data point must flow through HA so the Context API can pick it up. If you add a new metric, you write it to HA, then make sure the ha_poller normalizer recognizes it.


Known issues (start here)

1. Eight Sleep, side detection is flaky

Looking at logs/integrations/eight-sleep.log, Justin's user_id (b5ce51a3d34e4b25a053f57ec42af514) appears on different side values night to night: solo, left, right. Sometimes data is on the "away" side instead. The downstream Context API DB groups by side, which means morning brief has to "find the side that has data", fragile.

Fix direction: the poller should ALWAYS write Justin's metrics keyed by his user_id, not the side label. Or normalize at the HA layer so HA exposes one stable sensor.eight_sleep_justin_* regardless of which side the bed assigned that night. Recommend the latter, keeps downstream code simple.

His user_id: b5ce51a3d34e4b25a053f57ec42af514. The other user_id (c78dce09aa6c4e638dd92bd86a002302) is Krystal, exclude from Justin's wellness sensors.

2. Eight Sleep, anomalous data points

Recent logs show many nights with "sleep_score": null, "sleep_duration_min": null: Justin presumably wasn't sleeping in his Eight Sleep bed those nights (couch, travel, etc.). Today's data: sleep_score: 26, duration: 47 min (he was in bed 00:07–00:55 only). That's real but misleading.

Fix direction: when sleep_score < 50 or duration < 4 hours, mark the data as "incomplete" rather than displaying as today's primary number. Brief should fall back to Garmin's number in that case. Garmin captured 85/85 sleep score for the same night because it sees the actual sleep elsewhere.

3. Garmin: HRV missing entirely

The Garmin poller captures training_readiness, body_battery, stress_avg, sleep_score, sleep_duration_min, rhr, steps, distance_km, calories_*, intensity_minutes, but no HRV. Garmin has HRV (via the garmin-connect Python SDK or the unofficial garth lib, whichever the poller uses). Adding it means the brief can show HRV from Garmin if Eight Sleep is missing.

Files to read: scripts/integrations/garmin/poll.py. Check what library it uses. Add HRV (and possibly sleep stages: deep/light/rem) to the metric set.

4. Home Assistant, wellness dashboard quality

Per memory: there's a /wellness-dash Lovelace view at http://192.168.86.70:8123/lovelace/wellness-dash. Audit it:

  • Are all the wellness sensors visible there?
  • Are sleep/HRV/stress/readiness/body battery rendered with charts (not just current value)?
  • Are there gauges, sparklines, history?
  • Is it phone-responsive?

Fix direction: rebuild the Lovelace view with rich cards (gauge for readiness, history graphs for HRV/stress/RHR over 7d/30d, today's steps, sleep score with breakdown). Use mushroom cards or apexcharts-card if available; install if not.

5. Context API, verify ingest

Check data/context.db table facts_wellness. Are there gaps? Are values landing where the poller expects? Run infra/context-api/.venv/bin/python infra/context-api/scripts/ha_poller.py once manually and watch what it writes vs reads.

Specifically check: morning brief now reads wellness.eight_sleep.<side>.{score, duration_min, hrv, rhr} and wellness.garmin.justin.{training_readiness, body_battery, stress_avg, steps, rhr}. Confirm those exact paths are populated.


Files you'll work with

Path What
scripts/integrations/eight-sleep/poll.py Eight Sleep poller (cron)
scripts/integrations/eight-sleep/ venv + state if any
scripts/integrations/garmin/poll.py Garmin poller (cron)
scripts/integrations/garmin/pyEight/ venv (despite the name; pyEight is mis-named in the Garmin folder per directory listing)
~/.forge-secrets/wellness.env API creds for both services (chmod 600)
infra/context-api/scripts/ha_poller.py Pulls HA sensor states into facts_wellness
infra/context-api/app/routes/context.py The /context endpoint (read to understand the response shape)
infra/context-api/app/normalizers/ Where source-specific normalizers go (this dir is empty per cleanup audit, fits here)
data/context.db SQLite, table facts_wellness
scripts/morning-brief.py Already fixed today (calendar window + wellness fields): DO NOT regress
Home Assistant UI http://192.168.86.70:8123 (token in ~/.forge-secrets/... per MEMORY.md HA API section)
memory/general/home-assistant.md Existing HA notes
MEMORY.md HA API section Long-lived token + entity inventory

How to investigate / what to look at

  1. Read the existing pollers fully. Understand: what library do they use, where do they write to HA, what's the sensor naming pattern.
  2. Hit HA directly with the long-lived token to list current sensor.eight_sleep_* and sensor.garmin_* entities. Spot what's there vs what should be.
  3. Check data/context.db facts_wellness for the last 14 days of rows. Identify gaps and metric coverage.
  4. Pull /context?about=wellness&window=7d with the token at ~/.forge-secrets/wellness.env (CONTEXT_API_TOKEN), see what the brain will actually receive.
  5. Open HA's /wellness-dash in a browser (Tailscale or LAN). Screenshot or describe what's there, what's missing.
  6. Decide priorities and propose the order of fixes back to the parent session before executing big changes (rebuilding the dashboard, adding HRV to Garmin poller, refactoring side-detection).

Don't do

  • Don't break the existing wellness data flow. It's running. Make changes additively where possible.
  • Don't restart forge-context-api repeatedly. It hits the DB; one restart per change is fine.
  • Don't commit Eight Sleep / Garmin credentials anywhere. They're in ~/.forge-secrets/wellness.env for a reason.
  • Don't touch morning-brief.py again. It was just fixed today (calendar window + Eight Sleep HRV/RHR fallback). Read it for understanding, but don't re-edit unless coordinating with the parent session.
  • Don't write a brand new poller from scratch. Improve the existing ones, they have working auth + retry + logging baked in.
  • Don't add new credentials to n8n unless asked, wellness data flows through HA, not n8n.

Likely fix sequence (your call to reorder based on findings)

  1. Audit + report, read all four files, hit HA + Context API, dump status to comms/inbox/parent-session-wellness-status.md. Get Justin's approval on what to attack first.
  2. Fix Eight Sleep side normalization at the HA layer, emit sensor.eight_sleep_justin_score, *_hrv, *_rhr, etc., regardless of which side label the API used.
  3. Add HRV (and ideally sleep stages) to Garmin poller, extend the metric set + write to HA + extend the ha_poller normalizer to ingest them into facts_wellness.
  4. Rebuild HA /wellness-dash with rich cards (gauges, sparklines, 7d/30d trends). Tailscale-reachable.
  5. Optional: anomaly flagging, when Eight Sleep duration < 4h, mark as "partial" and let the brief prefer Garmin's number.

Deliverables

  1. Status / audit report at comms/inbox/parent-session-wellness-status.md (BEFORE making changes, get Justin's approval)
  2. Fixes per the green-lit plan
  3. Updated reference doc(s):
  4. ~/.claude/projects/-home-justinwieb-forge/memory/reference_wellness_pollers.md, refresh if poller behavior changes
  5. New reference_ha_wellness_dashboard.md if you rebuild the dashboard
  6. MEMORY.md "Tools & Pipelines" entries updated for any new files/services
  7. Final completion summary appended to the same status file

Done when

  • Audit report written + Justin approved a plan
  • Eight Sleep data lands in HA + Context API regardless of which bedside was used overnight
  • Garmin HRV (and ideally sleep stages) flowing through to /context
  • HA /wellness-dash rebuilt with charts, gauges, 7d/30d trends, looks good on phone
  • morning-brief.py still works (no regression, eyeball-test by running it manually after changes)
  • Reference docs updated

Conversation pointers

  • Parent session: n8n-telegram_Opus47 (Building The Brain)
  • Today's work in parent: fixed morning-brief.py (calendar window + Eight Sleep HRV/RHR pull), see memory/daily/2026-04-28.md and memory/daily/2026-04-27.md checkpoints
  • Master vision: memory/handoffs/building-the-brain-life-os-inbox-2026-04-27.md (the brain that consumes this wellness data)
  • Wellness pollers reference: ~/.claude/projects/-home-justinwieb-forge/memory/reference_wellness_pollers.md
  • Context API reference: ~/.claude/projects/-home-justinwieb-forge/memory/reference_context_api.md
  • HA API access: ~/.claude/projects/-home-justinwieb-forge/memory/reference_ha_api.md
  • Sister handoffs (running tonight): mkdocs-build-2026-04-28.md (done), notion-scaffold-life-os-jwvr-2026-04-28.md (done), forge-cleanup-2026-04-28.md (cleanup worker: Phase 0 inventory done, awaiting greenlight)

When done, ping the parent session by writing to comms/inbox/parent-session-wellness-status.md and Justin will see it.