Skip to content

Add Apple Health ring goals and stand hours sensors - #6044

Open
chewrocca wants to merge 8 commits into
home-assistant:mainfrom
chewrocca:feat/healthkit-activity-rings
Open

chewrocca wants to merge 8 commits into
home-assistant:mainfrom
chewrocca:feat/healthkit-activity-rings

Conversation

@chewrocca

@chewrocca chewrocca commented Oct 11, 2026 •

Copy link
Copy Markdown

AI Policy

Select exactly one option that describes AI usage in this contribution:

  • I have not used AI for this contribution.
  • AI assistance was used for this contribution.
  • AI fully generated the code for this contribution, but I've reviewed and understood it before submitting and will respond without AI during review.

Summary

Adds four opt-in Apple Health sensors read from the day's HKActivitySummary, the object behind the Fitness app's rings, so a Home Assistant dashboard can draw the rings with the user's real goals instead of guessed ones. Move and Exercise progress already exist as the sample-based Active Energy and Exercise Time sensors; this adds what those can't provide: the daily goals, and stand hours, which have no quantity type to sum.

Sensor unique_id Unit State class
Move Goal health_active_energy_burned_goal kcal measurement
Exercise Goal health_apple_exercise_time_goal min, device class duration measurement
Stand Hours health_apple_stand_hours hours total_increasing
Stand Goal health_apple_stand_hours_goal hours measurement

Each goal's ID extends the ID of the sensor it is the goal for, and shares its unit and device class. They sit in the Activity section of Settings > Sensors > Apple Health Sensors, start off like every other Apple Health sensor, are read-only, and are read only during normal sensor updates. Existing sensors are untouched. Like the sleep stages and VO2 Max already in the catalog, three of them depend on an Apple Watch; without one they read unavailable, and their rows carry a "Requires Apple Watch" badge.

Closes the request in https://github.com/orgs/home-assistant/discussions/5132.

How it works:

  • One new aggregation, .activitySummary(field), beside cumulativeSum, mostRecent and sleep. HealthKitActivityRing and HealthKitActivitySummaryField are plain data like the rest of the catalog; the HealthKit mapping lives in their +HealthKit files, following the sleep metrics' layout.
  • The query reads the summary for the local calendar day, built from the app calendar's era/year/month/day with the calendar attached, as HealthKit requires.
  • A ring HealthKit isn't tracking has no goal (exerciseTimeGoal and standHoursGoal are optional). Its sensors read as unavailable rather than a zero that was never measured. The Move goal additionally checks activityMoveMode, so a Move ring set to move time is never reported as kilocalories.
  • Goals read the latest summary within a 7-day lookback rather than today's only, so they don't drop to unavailable between midnight and the day's first sample. Stand Hours reads today's only.
  • The activity summary type can't be observed or background-delivered. Stand Hours observes the Apple stand hour sample type instead; goals observe nothing, since changing a goal writes no sample, and refresh on periodic updates. A goal alone adds only the summary type to the permission sheet.
  • Fixes the update signaler, which compared HealthKit type identifiers to decide whether to re-observe; metrics sharing one identifier could never add their own observed types. It now compares the set of enabled metrics. Covered by a test.
  • The "When app is open" badge became SensorNoteBadge, one capsule view with a preset per fact, reused for "Requires Apple Watch".

Tested on an iPhone 16 Pro Max with a paired Apple Watch: all four values match the Fitness app in the app and in Home Assistant, and the goals held through midnight with no gap (details and history graphs in the comments below).

Screenshots

Recorded snapshots of the badge rows, light and dark:

Requires Apple Watch rows, light Requires Apple Watch rows, dark

The rows on a phone, from a device build:

Ring sensor rows on an iPhone

Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#1449

Any other notes

  • The docs PR is stacked on Update Localized Strings #1409, which adds the Apple Health section it extends; only its last commit is new.
  • Each enabled ring sensor runs its own summary query per update, as each sleep sensor runs its own sample query today; a shared per-update cache would help both and is left as a follow-up.
  • Codecov's patch target is missed only on the HealthKit query calls themselves, which can't execute in unit tests, same as the existing code in that file.

Add Move Goal, Exercise Goal, Stand Hours and Stand Goal sensors, read
from the day's HKActivitySummary via HKActivitySummaryQuery. Move and
Exercise progress stay the existing sample-based Active Energy and
Exercise Time sensors; the goal IDs extend theirs.

A ring HealthKit isn't tracking comes back with a goal of zero: Exercise
and Stand without an Apple Watch, and the Move ring while set to move
time (checked via activityMoveMode). Those sensors read unavailable
rather than reporting a zero that was never measured. Zero progress
towards a set goal is reported as zero.

The activity summary type can't be observed or background-delivered, so
Stand Hours observes the stand hour sample type instead and requests it
alongside the summary type. Goals observe nothing: changing a goal
writes no sample, so they refresh on periodic updates.

Fix the update signaler to compare the set of enabled metrics rather
than their HealthKit type identifiers: metrics sharing one identifier
could otherwise never add their own observed types.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @chewrocca

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@chewrocca
chewrocca marked this pull request as ready for review October 11, 2026 20:12
Copilot AI balanced review requested due to automatic review settings October 11, 2026 20:12
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A new source file violates the repository’s one-top-level-type-per-file convention.

1 open finding
What changed in this PR

Adds opt-in Apple Health activity-ring goal and stand-hour sensors backed by daily HKActivitySummary data.

Changes:

  • Adds ring models, catalog entries, querying, permissions, and background observation.
  • Adds localized availability guidance.
  • Adds comprehensive catalog, mapping, and sensor tests.
File Description
Tests/​Shared/​Sensors/​HealthKitSensor.test.swift Tests observation updates and whole-number states.
Tests/​Shared/​Sensors/​HealthKitMetricCatalog.test.swift Validates ring catalog metadata and footers.
Tests/​Shared/​Sensors/​HealthKitActivityRing.test.swift Tests summary mapping, permissions, and calendar-day handling.
Sources/​Shared/​Resources/​Swiftgen/​Strings.swift Adds generated footer accessors.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitService.swift Queries summaries and configures read/observable types.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitSensorUpdateSignaler.swift Tracks enabled metrics by unique ID.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitSensor.swift Handles activity-summary query windows.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitMetricFooter.swift Adds ring availability footer types.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitMetricAggregation.swift Adds activity-summary aggregation.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitMetric+Activity.swift Defines the four new sensors.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitMetric.swift Documents summary-backed metrics.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitActivityRing+HealthKit.swift Maps summaries and observer sample types.
Sources/​Shared/​API/​Webhook/​Sensors/​Health/​HealthKitActivityRing.swift Defines ring and summary-field models.
Sources/​App/​Resources/​en.lproj/​Localizable.strings Adds English ring footer text.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/Shared/API/Webhook/Sensors/Health/HealthKitActivityRing.swift Outdated
Move HealthKitActivitySummaryField and its HealthKit extension into files
of their own, matching the one-type-per-file layout of the neighbouring
HealthKit metric files. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.78788% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.08%. Comparing base (4bdec75) to head (e87e3aa).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
.../API/Webhook/Sensors/Health/HealthKitService.swift 33.96% 35 Missing ⚠️

❌ Your patch check has failed because the patch coverage (78.78%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6044      +/-   ##
==========================================
+ Coverage   54.81%   56.08%   +1.27%     
==========================================
  Files        1248     1259      +11     
  Lines       82776    83382     +606     
==========================================
+ Hits        45370    46765    +1395     
+ Misses      37406    36617     -789     
Files with missing lines Coverage Δ
.../App/Settings/Sensors/Health/HealthSensorRow.swift 100.00% <100.00%> (+100.00%) ⬆️
...es/App/Settings/Sensors/List/SensorNoteBadge.swift 100.00% <100.00%> (ø)
Sources/App/Settings/Sensors/List/SensorRow.swift 96.66% <100.00%> (ø)
...nsors/Health/HealthKitActivityRing+HealthKit.swift 100.00% <100.00%> (ø)
...alth/HealthKitActivitySummaryField+HealthKit.swift 100.00% <100.00%> (ø)
...Sensors/Health/HealthKitActivitySummaryField.swift 100.00% <100.00%> (ø)
...hook/Sensors/Health/HealthKitMetric+Activity.swift 100.00% <100.00%> (ø)
...d/API/Webhook/Sensors/Health/HealthKitMetric.swift 100.00% <100.00%> (ø)
...ok/Sensors/Health/HealthKitMetricAggregation.swift 100.00% <100.00%> (ø)
...Webhook/Sensors/Health/HealthKitMetricFooter.swift 100.00% <100.00%> (+100.00%) ⬆️
... and 3 more

... and 31 files with indirect coverage changes

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bgoncal bgoncal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, it's nicely scoped and the tests are thorough. The signaler fix is a good catch too.
Since three of the four sensors depend on a Watch, could you test on an iPhone paired with an Apple Watch before we merge? I'd especially like to see how the goals behave around midnight (see inline).

func goal(in summary: HKActivitySummary) -> HKQuantity {
switch self {
case .move: return summary.activeEnergyBurnedGoal
case .exercise: return summary.appleExerciseTimeGoal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

appleExerciseTimeGoal and appleStandHoursGoal are deprecated in favor of exerciseTimeGoal and standHoursGoal (iOS 16+, and we target 16.4). The new ones came with watchOS 9, when these goals became adjustable, so they're the ones to trust for a custom goal. They're also optional, which gives us a cleaner "not tracked" signal than a goal of 0. Could you switch to them and update the test fixture?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Switched to exerciseTimeGoal / standHoursGoal in c7d2677 and updated the test fixture. Thanks, I'd missed that those were deprecated.

if ring == .move, summary.activityMoveMode != .activeEnergy {
return false
}
return goal.doubleValue(for: unit) > 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With the optional goals, nil can mean "not tracked" for Exercise and Stand here, and the > 0 check only needs to stay for Move.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. nil is now the signal for Exercise and Stand. Move keeps the move-mode and > 0 checks since its energy goal is always present.

if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: summaries?.first)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

HKActivitySummaryQuery only returns days that already have a summary. Right after midnight, before the Watch syncs anything, I think all goal sensors would go unavailable and then come back, leaving a gap in their history every day. Can you confirm on a paired device? If it does happen, falling back to the most recent summary for goals would avoid it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll confirm this on a paired device as part of the test I describe below, but rather than leave the gap open until then I implemented the fallback you suggested: goals now read the latest summary within their 7-day lookback window, while progress still reads today's only. If a paired Watch turns out to pre-create the day's summary, the fallback is harmless. A test covers the two windows.

@home-assistant
home-assistant Bot marked this pull request as draft October 12, 2026 08:11
@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@bgoncal

bgoncal commented Oct 12, 2026

Copy link
Copy Markdown
Member

Please also create a documentation PR over this one home-assistant/companion.home-assistant#1409

Read exerciseTimeGoal and standHoursGoal instead of their deprecated
apple-prefixed predecessors. They're optional, so a missing goal is the
"not tracked" signal for Exercise and Stand; only Move, whose energy
goal is always present, still checks the move mode and a goal above 0.

A goal now reads the latest activity summary in its lookback window
rather than today's only, so it no longer drops to unavailable between
midnight and the day's first sample. Progress still reads today's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chewrocca

Copy link
Copy Markdown
Author

Thanks for the review. All three inline points are addressed in c7d2677, and the docs PR is up at home-assistant/companion.home-assistant#1449, stacked on #1409.

On device testing: I don't currently have an Apple developer setup, so I haven't been able to run this on a phone yet. I'll get one set up, install the build on an iPhone with a paired Watch, and monitor the four sensors for a day or two, with particular attention to what happens around midnight. I'll report back here with what I see.

On the Codecov patch check: the uncovered lines are the HealthKit query calls themselves, which can't execute in unit tests, same as the existing code in that file. Everything around them is covered.

@chewrocca
chewrocca marked this pull request as ready for review October 12, 2026 13:29
@home-assistant
home-assistant Bot requested a review from bgoncal October 12, 2026 13:29
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chewrocca

Copy link
Copy Markdown
Author

Device test done on an iPhone 16 Pro Max paired with an Apple Watch, over one evening and night.

Values: all four sensors matched the Fitness app exactly, both in the app's sensor list and in Home Assistant: Move Goal 610 kcal, Exercise Goal 30 min, Stand Hours 10 of 12 at the time of the screenshot, Stand Goal 12.

Overnight: the three goal sensors stayed flat through midnight with no gap. Stand Hours rose through the evening to 17, held overnight, and dropped to 1 at about 4:50 AM when the new day's first stand hour was recorded.

A caveat worth stating: the phone was locked overnight, and the existing code already keeps the previous value when HealthKit refuses a read on a locked device, so the midnight plateau is partly that rather than only the new fallback. An unlocked read between midnight and the first sample is covered by the lookback fallback and its unit test, but this night didn't exercise it. Happy to try to catch that case deliberately if you'd like.

One note on the test setup: this was a personal-team build, so it registers in HA under a generic device name; that's unrelated to the change.

Screenshots: the four overnight history graphs (Move Goal, Exercise Goal, Stand Goal, Stand Hours), the app's sensor rows, the HA sensor rows, and the Fitness app.

2026-10-13-history-move-goal 2026-10-13-history-exercise-goal 2026-10-13-history-stand-goal 2026-10-13-history-stand-hours 2026-10-12-test-build-ring-rows 2026-10-12-home-assistant-move-goal-row 2026-10-12-home-assistant-stand-rows 2026-10-12-fitness-app-rings

@bgoncal

bgoncal commented Oct 13, 2026

Copy link
Copy Markdown
Member

Instead of this repeated subtitle, can we have a badge "Requires Apple Watch"? same UI as the badge that exists now that says "When app is open" for sensors that only update when app is open

Replace the footer repeated under Exercise Goal, Stand Hours and Stand
Goal with a "Requires Apple Watch" badge in the same style as the
"When app is open" badge. That badge becomes SensorNoteBadge, one
capsule view with a preset for each fact, and HealthKitMetric gains a
requiresAppleWatch flag the health rows read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chewrocca

Copy link
Copy Markdown
Author

Done in bd27659. The three Watch-only rows now carry a "Requires Apple Watch" badge instead of the footer. I made the existing "When app is open" badge a shared SensorNoteBadge with a preset per fact so the two render identically; the SensorRow snapshot tests are unchanged. Move Goal keeps its one-line footer about move-time mode since that's a different point.

@bgoncal bgoncal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Whenever changing the UI please make snapshot tests

@home-assistant
home-assistant Bot marked this pull request as draft October 13, 2026 13:36
Cover the row with a value and switched off, the Requires Apple Watch
badge on and off, the two footers, and both presets of the shared note
badge, in light and dark.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

HealthKit authorization and background delivery behavior still needs validation on an iPhone paired with an Apple Watch.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

@bgoncal

bgoncal commented Oct 13, 2026

Copy link
Copy Markdown
Member

Please, rewrite the PR body following the PR template, link the documentation PR, I will then review the docs PR and we can merge

@chewrocca

Copy link
Copy Markdown
Author

ha. fair. Added in e5b6425: HealthSensorRowTests snapshots the row with a value and switched off, the "Requires Apple Watch" badge on and off, both footers, and the two presets of the shared SensorNoteBadge, in light and dark. The existing SensorRowTests snapshots are unchanged, which confirms the shared badge renders the same as before.

Recorded snapshot of the badge rows:

testRequiresAppleWatchRowViews light

And the same rows on the phone, from today's build:

Requires Apple Watch badges on the phone

@chewrocca
chewrocca marked this pull request as ready for review October 13, 2026 14:18
@home-assistant
home-assistant Bot requested a review from bgoncal October 13, 2026 14:18
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@bgoncal bgoncal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please, rewrite the PR body following the PR template, link the documentation PR, I will then review the docs PR and we can merge

@home-assistant
home-assistant Bot marked this pull request as draft October 13, 2026 14:19
@chewrocca

Copy link
Copy Markdown
Author

ha. fair. PR body rewritten on the template, with the light and dark snapshots under Screenshots and the docs PR linked: home-assistant/companion.home-assistant#1449 (stacked on #1409).

@chewrocca
chewrocca marked this pull request as ready for review October 13, 2026 14:28
@home-assistant
home-assistant Bot requested a review from bgoncal October 13, 2026 14:28
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@bgoncal

bgoncal commented Oct 13, 2026

Copy link
Copy Markdown
Member

AI is not allowed in PR communication, just a heads up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants