-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Apple Health metrics: toothbrushing as sessions per day and as minutes per day #556
base: master
Are you sure you want to change the base?
Changes from all commits
e67b50b
f624c9d
c8f97d6
4eea411
ad76d3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ public enum HealthKitCategory : String, CaseIterable { | |
case Heart = "Heart" | ||
case Mindfulness = "Mindfulness" | ||
case Nutrition = "Nutrition" | ||
case SelfCare = "Self Care" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These categories map to the categorizations in the Health app. Toothbrushing is under "Other". |
||
case Sleep = "Sleep" | ||
case Other = "Other Data" | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import Foundation | ||
import HealthKit | ||
|
||
/// tracks toothbrushing, in number of (decimal) minutes per day (daystamp) | ||
class ToothbrushingDailyMinutesHealthKitMetric: CategoryHealthKitMetric { | ||
private static let healthkitMetric = ["toothbrushing", "minutes-per-day"].joined(separator: "|") | ||
|
||
private init(humanText: String, | ||
databaseString: String, | ||
category: HealthKitCategory) { | ||
super.init(humanText: humanText, | ||
databaseString: databaseString, | ||
category: category, | ||
hkSampleType: HKObjectType.categoryType(forIdentifier: .toothbrushingEvent)!) | ||
} | ||
|
||
override func units(healthStore : HKHealthStore) async throws -> HKUnit { | ||
HKUnit.second() | ||
} | ||
|
||
static func make() -> ToothbrushingDailyMinutesHealthKitMetric { | ||
.init(humanText: "Teethbrushing (in seconds per day)", | ||
databaseString: healthkitMetric, | ||
category: HealthKitCategory.SelfCare) | ||
} | ||
krugerk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
override func valueInAppropriateUnits(rawValue: Double) -> Double { | ||
// raw seconds into minutes | ||
rawValue / 60 | ||
} | ||
|
||
override func recentDataPoints(days: Int, deadline: Int, healthStore: HKHealthStore) async throws -> [any BeeDataPoint] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than this, can probably just implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was implemented for the purpose of specifying the healthkitMetric in the comment and yes, converting seconds to minutes. |
||
try await super.recentDataPoints(days: days, deadline: deadline, healthStore: healthStore) | ||
.map { | ||
NewDataPoint(requestid: $0.requestid, | ||
daystamp: $0.daystamp, | ||
value: $0.value, | ||
comment: "Auto-entered via Apple Health (\(Self.healthkitMetric))") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Part of BeeSwift. Copyright Beeminder | ||
|
||
import Foundation | ||
import HealthKit | ||
|
||
/// tracks toothbrushing, in number of sessions per day (daystamp) | ||
class ToothbrushingDailySessionsHealthKitMetric: CategoryHealthKitMetric { | ||
private static let healthkitMetric = ["toothbrushing", "sessions-per-day"].joined(separator: "|") | ||
|
||
private init(humanText: String, | ||
databaseString: String, | ||
category: HealthKitCategory) { | ||
super.init(humanText: humanText, | ||
databaseString: databaseString, | ||
category: category, | ||
hkSampleType: HKObjectType.categoryType(forIdentifier: .toothbrushingEvent)!) | ||
} | ||
|
||
override func units(healthStore : HKHealthStore) async throws -> HKUnit { | ||
.count() | ||
} | ||
|
||
static func make() -> ToothbrushingDailySessionsHealthKitMetric { | ||
.init(humanText: "Teethbrushing (in sessions per day)", | ||
databaseString: healthkitMetric, | ||
category: HealthKitCategory.SelfCare) | ||
} | ||
|
||
override func recentDataPoints(days: Int, deadline: Int, healthStore: HKHealthStore) async throws -> [any BeeDataPoint] { | ||
let todayDaystamp = Daystamp.now(deadline: deadline) | ||
let startDaystamp = todayDaystamp - days | ||
|
||
let predicate = HKQuery.predicateForSamples(withStart: startDaystamp.start(deadline: deadline), | ||
end: todayDaystamp.end(deadline: deadline)) | ||
|
||
let samples = try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<[HKSample], Error>) in | ||
let query = HKSampleQuery(sampleType: sampleType(), | ||
predicate: predicate, | ||
limit: HKObjectQueryNoLimit, | ||
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)], | ||
resultsHandler: { (query, samples, error) in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else if let samples { | ||
continuation.resume(returning: samples) | ||
} else { | ||
continuation.resume(throwing: HealthKitError("HKSampleQuery did not return samples")) | ||
} | ||
}) | ||
healthStore.execute(query) | ||
}) | ||
.compactMap { $0 as? HKCategorySample } | ||
|
||
let calendar = Calendar.autoupdatingCurrent | ||
let groupedByDay = Dictionary(grouping: samples, by: { sample in | ||
calendar.startOfDay(for: sample.startDate) | ||
}) | ||
|
||
let dailyCounts = groupedByDay | ||
.map { ($0, $1.count) } | ||
.sorted { $0.0 < $1.0 } | ||
|
||
let datapoints = dailyCounts.map({ (date, numberOfEntries) in | ||
let daystamp = Daystamp(fromDate: date, deadline: deadline) | ||
let requestID = "apple-heath-" + daystamp.description | ||
|
||
return NewDataPoint(requestid: requestID, | ||
daystamp: daystamp, | ||
value: NSNumber(value: numberOfEntries), | ||
comment: "Auto-entered via Apple Health (\(Self.healthkitMetric))") | ||
}) | ||
|
||
return datapoints | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to have multiple top level metrics for the same underlying metric. There are lots of metrics where this could make sense, and the list risks getting far too long.
The way I think this should work:
Note this will require some tweaks to the sync code to allow multiple data points per day to be synced correctly. A while back the app started including requestid with these points to help facilitate this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Customizing
aggday
is possible with a custom goal. A typical do more (hustler) goal type will have its how to aggregate the day set to sum.