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_idb5ce51a3d34e4b25a053f57ec42af514The cron poller's
STABLE_SIDEcomments 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¶
- Read the existing pollers fully. Understand: what library do they use, where do they write to HA, what's the sensor naming pattern.
- Hit HA directly with the long-lived token to list current
sensor.eight_sleep_*andsensor.garmin_*entities. Spot what's there vs what should be. - Check
data/context.dbfacts_wellnessfor the last 14 days of rows. Identify gaps and metric coverage. - Pull /context?about=wellness&window=7d with the token at
~/.forge-secrets/wellness.env(CONTEXT_API_TOKEN), see what the brain will actually receive. - Open HA's
/wellness-dashin a browser (Tailscale or LAN). Screenshot or describe what's there, what's missing. - 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-apirepeatedly. It hits the DB; one restart per change is fine. - ❌ Don't commit Eight Sleep / Garmin credentials anywhere. They're in
~/.forge-secrets/wellness.envfor a reason. - ❌ Don't touch
morning-brief.pyagain. 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)¶
- 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. - 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. - 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. - Rebuild HA
/wellness-dashwith rich cards (gauges, sparklines, 7d/30d trends). Tailscale-reachable. - Optional: anomaly flagging, when Eight Sleep duration < 4h, mark as "partial" and let the brief prefer Garmin's number.
Deliverables¶
- Status / audit report at
comms/inbox/parent-session-wellness-status.md(BEFORE making changes, get Justin's approval) - Fixes per the green-lit plan
- Updated reference doc(s):
~/.claude/projects/-home-justinwieb-forge/memory/reference_wellness_pollers.md, refresh if poller behavior changes- New
reference_ha_wellness_dashboard.mdif you rebuild the dashboard - MEMORY.md "Tools & Pipelines" entries updated for any new files/services
- 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-dashrebuilt 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.mdandmemory/daily/2026-04-27.mdcheckpoints - 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.