Skip to content

Commit

Permalink
Comments in the ticket VIT-1827
Browse files Browse the repository at this point in the history
  • Loading branch information
RuiAAPeres committed Oct 17, 2022
1 parent 24fc920 commit 511085e
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 5 deletions.
1 change: 1 addition & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/VitalCore.xcscheme
Expand Up @@ -30,6 +30,7 @@
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
Expand Down
Expand Up @@ -30,6 +30,7 @@
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
Expand Down
170 changes: 170 additions & 0 deletions Sources/VitalHealthKit/HealthKit/HealthKitReads.swift
Expand Up @@ -880,3 +880,173 @@ private func isLongerThan30Minutes(firstDate: Date, secondDate: Date) -> Bool {
let longerThanThirtyMinutes = diff > 60 * 30
return longerThanThirtyMinutes
}

private func orderByDate(_ values: [QuantitySample]) -> [QuantitySample] {
return values.sorted { $0.startDate < $1.startDate }
}

func splitPerBundle(_ values: [QuantitySample]) -> [[QuantitySample]] {
var temp: [String: [QuantitySample]] = ["na": []]

for value in values {
if let bundle = value.sourceBundle {
if temp[bundle] != nil {
var copy = temp[bundle]!
copy.append(value)
temp[bundle] = copy
} else {
temp[bundle] = [value]
}
} else {
var copy = temp["na"]!
copy.append(value)
temp["na"] = copy
}
}

var outcome: [[QuantitySample]] = [[]]

for key in temp.keys {
let samples = temp[key]!
outcome.append(samples)
}

return outcome
}

private func _accumulate(_ values: [QuantitySample], interval: Int, calendar: Calendar) -> [QuantitySample] {
let ordered = orderByDate(values)

return ordered.reduce(into: []) { newValues, newValue in
if let lastValue = newValues.last {

let lastValueHour = calendar.component(.hour, from: lastValue.startDate)
let newValueHour = calendar.component(.hour, from: newValue.startDate)

guard lastValueHour == newValueHour else {
newValues.append(newValue)
return
}

let lastValueBucket = Int(calendar.component(.minute, from: lastValue.startDate) / interval)
let newValueBucket = Int(calendar.component(.minute, from: newValue.startDate) / interval)

guard lastValueBucket == newValueBucket else {
newValues.append(newValue)
return
}

var lastValue = newValues.removeLast()
lastValue.value = lastValue.value + newValue.value
lastValue.endDate = newValue.endDate

newValues.append(lastValue)

} else {
newValues.append(newValue)
}
}
}

func accumulate(_ values: [QuantitySample], interval: Int = 15, calendar: Calendar) -> [QuantitySample] {
let split = splitPerBundle(values)
let outcome = split.flatMap {
_accumulate($0, interval: interval, calendar: calendar)
}

return orderByDate(outcome)
}


private func _average(_ values: [QuantitySample], calendar: Calendar) -> [QuantitySample] {
let ordered = orderByDate(values)

guard ordered.count > 2 else {
return values
}

/// We want to preserve these values, instead of averaging them.
var min: QuantitySample? = nil
var max: QuantitySample? = nil
// var copy = ordered

for value in values {
if let minValue = min {
if value.value <= minValue.value {
min = value
}
} else {
min = value
}

if let maxValue = max {
if value.value >= maxValue.value {
max = value
}
} else {
max = value
}
}

class Payload {
var samples: [QuantitySample] = []
var total: Double = 0
var totalGrouped: Double = 0
var count = 0
}

var outcome: [QuantitySample] = ordered.reduce(into: Payload()) { payload, newValue in
payload.count = payload.count + 1

if let lastValue = payload.samples.last {

let time = calendar.date(byAdding: .second, value: 5, to: lastValue.startDate)!
if time >= newValue.startDate {
var lastValue = payload.samples.removeLast()

payload.total = payload.total + newValue.value
payload.totalGrouped = payload.totalGrouped + 1
lastValue.endDate = newValue.endDate

if payload.count == ordered.count {
lastValue.value = payload.total / payload.totalGrouped
}

payload.samples.append(lastValue)
} else {
var lastValue = payload.samples.removeLast()
lastValue.value = payload.total / payload.totalGrouped
payload.samples.append(lastValue)

payload.total = newValue.value
payload.totalGrouped = 1

payload.samples.append(newValue)
}

} else {
payload.total = newValue.value
payload.totalGrouped = payload.totalGrouped + 1
payload.samples.append(newValue)
}
}.samples

if let max = max {
outcome.append(max)
}

if let min = min {
outcome.append(min)
}

return outcome
}

func average(_ values: [QuantitySample], calendar: Calendar) -> [QuantitySample] {
let split = splitPerBundle(values)
let outcome = split.flatMap {
_average($0, calendar: calendar)
}

return orderByDate(outcome)
}
71 changes: 66 additions & 5 deletions Sources/VitalHealthKit/HealthKit/VitalHealthKitClient.swift
Expand Up @@ -39,7 +39,7 @@ public class VitalHealthKitClient {

private let backgroundDeliveryEnabled: ProtectedBox<Bool> = .init(value: false)
let configuration: ProtectedBox<Configuration>

public var status: AnyPublisher<Status, Never> {
return _status.eraseToAnyPublisher()
}
Expand Down Expand Up @@ -67,7 +67,7 @@ public class VitalHealthKitClient {
public static func configure(
_ configuration: Configuration = .init()
) async {
await self.shared.setConfiguration(configuration: configuration)
await self.shared.setConfiguration(configuration: configuration)
}

public static func automaticConfiguration() async {
Expand Down Expand Up @@ -198,7 +198,7 @@ extension VitalHealthKitClient {

for sampleType in sampleTypes {
let query = HKObserverQuery(sampleType: sampleType, predicate: nil) {[weak self] query, handler, error in

guard error == nil else {
self?.logger?.error("Failed to background deliver for \(String(describing: sampleType)).")

Expand Down Expand Up @@ -251,7 +251,7 @@ extension VitalHealthKitClient {
_status.send(.syncingCompleted)
}
}

public enum SyncPayload {
case type(HKSampleType)
case resource(VitalResource)
Expand Down Expand Up @@ -346,9 +346,11 @@ extension VitalHealthKitClient {
/// Make sure the user has a connected source set up
try await vitalClient.checkConnectedSource(.appleHealthKit)

let transformedData = transform(data: data, calendar: Calendar.autoupdatingCurrent)

// Post data
try await vitalClient.post(
data,
transformedData,
stage,
.appleHealthKit,
TimeZone.autoupdatingCurrent
Expand Down Expand Up @@ -410,3 +412,62 @@ extension VitalHealthKitClient {
store.hasAskedForPermission(resource)
}
}


func transform(data: PostResourceData, calendar: Calendar) -> PostResourceData {
switch data {
case let .summary(.activity(patch)):
let activities = patch.activities.map { activity in
ActivityPatch.Activity(
activeEnergyBurned: accumulate(activity.activeEnergyBurned, calendar: calendar),
basalEnergyBurned: accumulate(activity.basalEnergyBurned, calendar: calendar),
steps: accumulate(activity.steps, calendar: calendar),
floorsClimbed: accumulate(activity.floorsClimbed, calendar: calendar),
distanceWalkingRunning: accumulate(activity.distanceWalkingRunning, calendar: calendar),
vo2Max: accumulate(activity.vo2Max, calendar: calendar)
)
}

return .summary(.activity(ActivityPatch(activities: activities)))

case let .summary(.workout(patch)):
let workouts = patch.workouts.map { workout in
WorkoutPatch.Workout(
id: workout.id,
startDate: workout.startDate,
endDate: workout.endDate,
sourceBundle: workout.sourceBundle,
productType: workout.productType,
sport: workout.sport,
calories: workout.calories,
distance: workout.distance,
heartRate: average(workout.heartRate, calendar: calendar),
respiratoryRate: average(workout.respiratoryRate, calendar: calendar)
)
}

return .summary(.workout(WorkoutPatch(workouts: workouts)))

case let.summary(.sleep(patch)):
let sleep = patch.sleep.map { sleep in
SleepPatch.Sleep.init(
id: sleep.id,
startDate: sleep.startDate,
endDate: sleep.endDate,
sourceBundle: sleep.sourceBundle,
productType: sleep.productType,
heartRate: average(sleep.heartRate, calendar: calendar),
restingHeartRate: average(sleep.restingHeartRate, calendar: calendar),
heartRateVariability: average(sleep.heartRateVariability, calendar: calendar),
oxygenSaturation: average(sleep.oxygenSaturation, calendar: calendar),
respiratoryRate: average(sleep.respiratoryRate, calendar: calendar)
)
}

return .summary(.sleep(SleepPatch(sleep: sleep)))
case .summary(.body), .summary(.profile):
return data
case .timeSeries:
return data
}
}

0 comments on commit 511085e

Please sign in to comment.