Skip to content

feat: add ultrahuman provider#323

Draft
gudvin-byte wants to merge 20 commits intothe-momentum:mainfrom
gudvin-byte:feat/add-ultrahuman-provider
Draft

feat: add ultrahuman provider#323
gudvin-byte wants to merge 20 commits intothe-momentum:mainfrom
gudvin-byte:feat/add-ultrahuman-provider

Conversation

@gudvin-byte
Copy link

Description

Adds support for Ultrahuman Ring Air wearable device, implementing OAuth authentication and 24/7 data synchronization including sleep sessions (with stages: awake, light, deep, REM), recovery metrics, and activity samples (heart rate, temperature, movement). The implementation follows the existing provider pattern used by Garmin, Whoop, and others.

Related Issue

N/A (new provider implementation)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Checklist

General

  • My code follows the project's code style
  • I have performed a self-review of my code
  • I have added tests that prove my fix/feature works (if applicable)
  • New and existing tests pass locally

Backend Changes

  • uv run ruff check passes
  • uv run ruff format --check passes
  • uv run ty check passes

Frontend Changes

  • pnpm run lint passes
  • pnpm run format:check passes
  • pnpm run build succeeds

Testing Instructions

Steps to test:

  1. Configure Ultrahuman OAuth credentials in backend/config/.env (add ULTRAHUMAN_CLIENT_ID, ULTRAHUMAN_CLIENT_SECRET, ULTRAHUMAN_REDIRECT_URI)
  2. Important: Set ULTRAHUMAN_REDIRECT_URI to https:// URL (not http://), e.g., https://yourdomain.com/widget.connect
  3. Run migrations: make migrate
  4. Start services: docker compose up -d
  5. Navigate to https://yourdomain.com and create/connect an Ultrahuman account
  6. Trigger data sync for the Ultrahuman connection
  7. Verify sleep sessions are saved: GET /api/v1/users/{user_id}/events/sleep?start_date={date}&end_date={date}
  8. Verify timeseries data: GET /api/v1/users/{user_id}/timeseries?start_time={iso}&end_time={iso}&types=heart_rate,temperature

Expected behavior:

  • OAuth flow redirects to Ultrahuman authorization and creates an active connection
  • Data sync successfully fetches and stores sleep sessions with efficiency scores and stage breakdown
  • Timeseries samples (heart rate, temperature, movement) are saved to the database
  • Sleep API returns sessions with correct efficiency_percent and stages (awake_minutes, light_minutes, deep_minutes, rem_minutes)

Screenshots

N/A (backend-only changes)

Additional Notes

  • Provider follows existing patterns from Garmin/Whoop implementations
  • Uses Ultrahuman Partner API: https://partner.ultrahuman.com/api/partners/v1
  • OAuth uses client_secret in request body (not PKCE)
  • Comprehensive test suite includes 35 tests covering OAuth, data normalization, and full integration flows
  • Includes verification script at backend/scripts/verify_ultrahuman_integration.py for end-to-end testing

Implementation Caveats

  • HTTPS redirect URI required: Ultrahuman API only accepts HTTPS redirect URIs; http:// will be rejected
  • Unique sample IDs: Timeseries samples don't have unique IDs from Ultrahuman API; we rely on database unique constraint (user_id, provider_name, series_type, recorded_at) to prevent duplicates
  • Sleep efficiency fallback: If sleep efficiency is missing from quick_metrics["sleep_efic"], we fall back to the top-level sleep_efficiency field
  • Data type compatibility: normalize_activity_samples method signature differs from the base template (expects dict instead of list) due to Ultrahuman's response structure; this is handled internally and doesn't affect the public API

@bartmichalak
Copy link
Contributor

Hello @gudvin-byte, thanks for your contribution! Our team will do a review after the weekend.

PS Are you already on our Discord? If not, be sure to join!
https://discord.gg/qrcfFnNE6H

@bartmichalak
Copy link
Contributor

bartmichalak commented Jan 17, 2026

And in the meantime you can fix code quality issues :)

@KaliszS
Copy link
Collaborator

KaliszS commented Jan 17, 2026

And in the meantime you can fix code quality issues :)

The fastest method would be uv run pre-commit run --all-files from backend directory imo.

@gudvin-byte
Copy link
Author

Hello guys! I've joined your discord channel. Code quality checks also passed ^^

@bartmichalak bartmichalak changed the title Feat/add ultrahuman provider feat: add ultrahuman provider Jan 21, 2026
Copy link
Collaborator

@KaliszS KaliszS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! Integration seems to be done correctly (well done), but could you provide some screenshoots of how it works? Our frontend would be the best, just to check if it integrates correctly. None of developers has an access to superhuman device, so right now we cannot check it on our own sadly.

@@ -0,0 +1,410 @@
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is required. I would removed that script.

@@ -0,0 +1,338 @@
# Ultrahuman Testing Agent
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's am LLM artifact, also to remove :)

@@ -0,0 +1,198 @@
# Ultrahuman Provider - ngrok Setup Guide

## Overview
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another file which shouldn't be generated here

gudvin-byte and others added 18 commits January 23, 2026 01:09
…mentation

- Change authorization URL to https://auth.ultrahuman.com/authorize
- Update token URL to /api/partners/oauth/token
- Change API base URL to /api/partners/v1
- Update all data endpoints to use /user_data/ prefix
  - /user_data/user_info (was /v1/profile)
  - /user_data/metrics (was /v1/ring/metrics)
  - /user_data/sleep (candidate - was /v1/ring/sleep)
  - /user_data/recovery (candidate - was /v1/ring/recovery)
- Add cgm_data to default scope for Continuous Glucose Monitoring
- Update documentation in .env.example and ngrok_setup.md

Based on official Ultrahuman OAuth 2.0 Provider Documentation.
This commit completes the Ultrahuman provider integration after extensive
testing with real credentials and fixing all API endpoint discrepancies.

## Changes Made

### API Endpoint Fixes
- Fix authorization URL: /authorize → /authorise (British spelling)
  - Corrects 404 error when navigating to OAuth authorization
- Update token URL to /api/partners/oauth/token
- Change API base URL to /api/partners/v1
- Update data endpoints:
  - /user_data/user_info (was /v1/profile)
  - /user_data/metrics (was /v1/ring/metrics)
  - Removed /user_data/sleep (returns 404 - not available)
  - Removed /user_data/recovery (returns 404 - not available)

### Configuration Updates
- Add cgm_data to default scope for Continuous Glucose Monitoring
- Update ultrahuman_redirect_uri to ngrok HTTPS URL
- Update .env.example with correct scope and URL documentation

### Code Improvements
- Remove sleep and recovery from load_and_save_all() method
- Update docstring to clarify available endpoints (metrics only)
- Add sync_data router to v1/__init__.py for proper routing

### Documentation Updates
- Update ngrok_setup.md with correct Ultrahuman API endpoints
- Document OAuth URLs, token URLs, and data endpoints

## Testing Results

Tested with real Ultrahuman device connection and credentials:

✅ Working:
- OAuth authorization flow
- Token exchange
- /user_data/user_info endpoint (200 OK)
- /user_data/metrics endpoint (200 OK) - HR, HRV, temperature, steps

❌ Not Available:
- /user_data/sleep endpoint (404 - not documented in Ultrahuman API)
- /user_data/recovery endpoint (404 - not documented in Ultrahuman API)

Note: Ultrahuman only provides ring metrics and user info via their API.
Sleep and recovery data may be included in the metrics response structure,
but are not available as separate endpoints.

## Files Modified
- backend/app/services/providers/ultrahuman/oauth.py
- backend/app/services/providers/ultrahuman/strategy.py
- backend/app/services/providers/ultrahuman/data_247.py
- backend/app/config.py
- backend/config/.env
- backend/config/.env.example
- backend/app/api/routes/v1/__init__.py
- ngrok_setup.md

Based on official Ultrahuman OAuth 2.0 Provider Documentation
and successful testing with real device connection.
This commit completes the Ultrahuman Ring Air provider implementation
with full OAuth 2.0 support and 24/7 data synchronization.

## Implementation

### OAuth Integration
- UltrahumanOAuth class implementing BaseOAuthTemplate
- Authorization: https://auth.ultrahuman.com/authorise
- Token exchange: https://partner.ultrahuman.com/api/partners/oauth/token
- Support for PKCE: false (not required by Ultrahuman)
- Authentication method: BODY (credentials in request body)
- User profile fetching via /user_data/user_info endpoint
- Token refresh support

### Provider Strategy
- UltrahumanStrategy implementing BaseProviderStrategy
- Integration with UltrahumanOAuth and Ultrahuman247Data
- API base URL: https://partner.ultrahuman.com/api/partners/v1
- Provider icon: ultrahuman.svg

### 24/7 Data Synchronization
- Ultrahuman247Data implementing Base247DataTemplate
- Activity samples: HR, HRV, temperature, steps via /user_data/metrics
- Note: Sleep and recovery endpoints not available in Ultrahuman API
  (Tested - both return 404)
- Date-based data fetching (YYYY-MM-DD format)
- Normalization to platform schema
- Database persistence for heart_rate, hrv, temperature, samples

### Factory Registration
- Added Ultrahuman to ProviderFactory
- Registered in ProviderName enum (ULTRAHUMAN)

### Configuration
- ULTRAHUMAN_CLIENT_ID environment variable
- ULTRAHUMAN_CLIENT_SECRET environment variable
- ULTRAHUMAN_REDIRECT_URI environment variable (HTTPS required)
- ULTRAHUMAN_DEFAULT_SCOPE: "ring_data cgm_data profile"

### API Endpoints
- OAuth authorize: /api/v1/oauth/ultrahuman/authorize
- OAuth callback: /api/v1/oauth/ultrahuman/callback
- Sync endpoint: /api/v1/ultrahuman/users/{user_id}/sync
- Providers list: Updated to include Ultrahuman

### Documentation
- ngrok_setup.md: Complete setup guide with Ultrahuman specifics
- .env.example: All Ultrahuman environment variables documented
- Inline code documentation with API endpoint references

### Testing
- Unit tests for OAuth flow
- Unit tests for provider strategy
- Unit tests for 24/7 data handling
- Tested with real Ultrahuman device connection
- Verified OAuth flow works end-to-end
- Confirmed metrics endpoint returns valid data

## Files Created
- backend/app/services/providers/ultrahuman/strategy.py
- backend/app/services/providers/ultrahuman/oauth.py
- backend/app/services/providers/ultrahuman/data_247.py
- backend/app/services/providers/ultrahuman/__init__.py
- backend/app/static/provider-icons/ultrahuman.svg
- backend/tests/providers/ultrahuman/test_ultrahuman_oauth.py
- backend/tests/providers/ultrahuman/test_ultrahuman_strategy.py
- backend/tests/providers/ultrahuman/test_ultrahuman_data_247.py

## Files Modified
- backend/app/schemas/oauth.py: Added ULTRAHUMAN to ProviderName enum
- backend/app/services/providers/factory.py: Registered Ultrahuman
- backend/app/config.py: Added Ultrahuman OAuth settings
- backend/config/.env.example: Added Ultrahuman documentation
- ngrok_setup.md: Added Ultrahuman setup guide and API endpoints
- backend/app/api/routes/v1/__init__.py: Added sync_data router
- backend/tests/providers/test_provider_factory.py: Updated for Ultrahuman

## API Documentation References
- Ultrahuman OAuth 2.0 Provider Documentation
- Ultrahuman Partnership API: https://vision.ultrahuman.com/developer-docs

## Known Limitations
- Sleep data: Not available as separate endpoint (404)
- Recovery data: Not available as separate endpoint (404)
- Only ring metrics (HR, HRV, temperature, steps) are available
  via /user_data/metrics endpoint

Verified with real Ultrahuman Ring Air device and credentials.
This commit updates core application files to register and configure
the new Ultrahuman Ring Air provider.

## Changes

### OAuth Schema
- Add ULTRAHUMAN to ProviderName enum (value: "ultrahuman")

### Provider Factory
- Import UltrahumanStrategy
- Add case in get_provider() to return UltrahumanStrategy
- Support for Ultrahuman provider instantiation

### Configuration
- Add ultrahuman_client_id environment variable
- Add ultrahuman_client_secret environment variable (SecretStr)
- Add ultrahuman_redirect_uri environment variable
- Add ultrahuman_default_scope: "ring_data cgm_data profile"
- Update .env.example with Ultrahuman variables documentation

### Provider Strategy
- UltrahumanStrategy with API base URL configuration
- Returns "https://partner.ultrahuman.com/api/partners/v1"

### Tests
- Add Ultrahuman import to test_provider_factory.py
- Add test_get_provider_ultrahuman() test
- Add test_provider_ultrahuman_has_oauth() test
- Add test_provider_ultrahuman_has_data_247() test

### Documentation
- Update ngrok_setup.md with Ultrahuman-specific setup guide
- Document Ultrahuman API endpoints and OAuth flow
- Add Ultrahuman to provider list in setup documentation

## Files Modified
- backend/app/schemas/oauth.py
- backend/app/services/providers/factory.py
- backend/app/services/providers/ultrahuman/strategy.py
- backend/app/config.py
- backend/config/.env.example
- backend/tests/providers/test_provider_factory.py
- ngrok_setup.md

All changes support the new Ultrahuman Ring Air provider integration.
- Update OAuth endpoints in tests to match implementation
- Update API base URL in strategy tests
- Fix unreachable code in data_247.py load_and_save_all
- Initialize potentially unbound variable time_in_bed_seconds
- Add missing external_device_mapping_id parameter
- Add default handling for start/end times
- Fix return type of load_and_save_all to match type hint
Switch from deprecated /user_data/sleep endpoint to /user_data/metrics. Parse 'Sleep' type objects from the metrics response to fix 404 errors and missing data.
- Fix sleep stage data extraction to read from sleep_stages array instead of quick_metrics
- Fix sleep efficiency parsing to read from sleep_efic in quick_metrics
- Improve error logging for activity sample failures (debug -> warning level)
- Add recorded_at_str initialization before try block to prevent unbound variable
- Add comprehensive testing guide for Ultrahuman integration

This fixes issues where:
- All sleep stages were showing 0 minutes
- Sleep efficiency was NULL
- Activity samples (HR, HRV, temperature, steps) were being saved but errors weren't visible

Test results after fix:
- Sleep stages now correctly populated (54, 319, 69, 49 minutes for deep, light, REM, awake)
- Sleep efficiency scores present (90%, 93%, 91%)
- 1817 activity samples saved across 4 types
- All API endpoints returning correct data
- Add comprehensive test fixtures for Ultrahuman data formats (sleep, recovery, activity samples)
- Fix unit tests to use correct API response structure (quick_metrics, sleep_stages, Unix timestamps)
- Add end-to-end integration tests with real API calls
- Add verification script (verify_ultrahuman_integration.py) with 7 automated checks
- Fix UserConnectionFactory to handle both 'provider' and 'provider_name' kwargs
- Update project documentation with Ultrahuman testing guide

Test Results:
- 21/21 unit tests passed (data_247, oauth, strategy)
- 7/7 verification checks passed
- Real integration test: 8 sleep sessions + 3,193 activity samples synced successfully
- Remove overly long comment in data_247.py (line length >120)
- Fix ExternalDeviceMappingFactory calls to use provider_name instead of provider
The normalize_activity_samples method expects a list of dicts with 'type' and 'values' keys, matching the format used in load_and_save_all. The test was passing a dict where keys were types, causing AttributeError: 'str' object has no attribute 'get'.

This fixes the failing CI test: TestUltrahumanActivitySamples::test_normalize_activity_samples
Replace real API calls with mocked responses in integration tests to make them deterministic. Distinguish between fatal auth errors (401/403) and recoverable errors in _fetch_daily_metrics. Add error tracking to load_and_save_all returning failed_days count and error list. Implement proper abstract methods from Base247DataTemplate.
Change test date range to match fixture data (2024-01-15 to 2024-01-17) so the assertion that sleep records fall within the requested date range will pass.
@gudvin-byte gudvin-byte force-pushed the feat/add-ultrahuman-provider branch from a98a842 to d16aa45 Compare January 23, 2026 00:09
@gudvin-byte gudvin-byte marked this pull request as draft January 23, 2026 00:13
@bartmichalak
Copy link
Contributor

@gudvin-byte are you still working on this? PR is marked as a draft.

@gudvin-byte
Copy link
Author

@gudvin-byte are you still working on this? PR is marked as a draft.

Yes, still in progress. Fine tuning small last small bits

@bartmichalak
Copy link
Contributor

Sure, thanks for confirmation!

@bartmichalak
Copy link
Contributor

@gudvin-byte hey, wanted to check how's this going?

We'd love to add support for this provider. I think there are just a few small things left to address here - any chance you could pick them up soon?

@gudvin-byte
Copy link
Author

Hey @bartmichalak Had to make a break for another priorities, but will work on it this weekend. Need to polish the normalization layer

@KaliszS
Copy link
Collaborator

KaliszS commented Feb 19, 2026

@gudvin-byte nobody likes conflicts, so I've resolved them for you (especially it happened because of our changes in main branch).

But now you need to fetch changes before you do the next commit in order to preserve that merge.

@bartmichalak
Copy link
Contributor

@gudvin-byte are you still planning to finish this integration?
I'm currently waiting for a shipment from Ultrahuman, and once it arrives I'll be able to verify the integration and finish up whatever's left to implement, if you don't have the bandwidth.

We'd love to get this integration live :)

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.

3 participants