Skip to content
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

Tidepool Merge #23

Open
wants to merge 29 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f4f3c4d
[COASTAL-1291] plugin identifier is no longer class property (#92)
nhamming Sep 25, 2023
9975401
Merge fixes
ps2 Sep 26, 2023
a8dd5ab
Merge remote-tracking branch 'origin/dev' into ps2/LOOP-4735/cgm-even…
ps2 Sep 27, 2023
6555a71
Merge pull request #93 from tidepool-org/ps2/LOOP-4735/cgm-event-store
ps2 Sep 27, 2023
6cf2c22
added testflight configuration (#94)
nhamming Oct 10, 2023
103fb13
Update test for api change
ps2 Oct 24, 2023
d630579
Merge pull request #95 from tidepool-org/ps2/LOOP-4665/algo-recommend…
ps2 Oct 24, 2023
9fd01cf
Temporary preset activations moved out of LoopSettings. (#96)
ps2 Dec 19, 2023
5d582ad
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
ea6095f
[LOOP-4788] Fix Unit Tests for iOS 17
Camji55 Jan 16, 2024
2b8dc51
LOOP-4781 Types moved to LoopAlgorithm (#98)
ps2 Mar 5, 2024
bbc2c54
[LOOP-4801] adding pump inoperable (#101)
nhamming Jun 7, 2024
a723ade
LOOP-1169 - Upload device logs (#100)
ps2 Jun 10, 2024
0b9dbdb
Upload temporary presets (#103)
ps2 Aug 27, 2024
5caeee3
LOOP-4098 - Specify correct duration for basal segments (#104)
ps2 Sep 13, 2024
e192e32
[LOOP-5035] report time zone changes (#105)
nhamming Sep 18, 2024
b98c311
LOOP-5065 Device log upload fixes (#106)
ps2 Sep 18, 2024
78ed7b7
[LOOP-5035] Pump event time zone sync (#107)
nhamming Sep 23, 2024
54f12b5
LOOP-5071 Overlay basal and automation history (#108)
ps2 Oct 2, 2024
dae1169
Merge tidepool/dev
ps2 Oct 5, 2024
e7ad5fe
Use continue button during onboarding (#110)
ps2 Oct 25, 2024
ab70ef9
Merge tidepool/dev
ps2 Oct 27, 2024
65e5c77
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
98545f0
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
8c72563
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
da6df5b
[LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSetti…
Camji55 Oct 29, 2024
342c8e9
[PAL-818] plugin dependency (#112)
nhamming Oct 30, 2024
1c15789
Merge tidepool/dev
ps2 Oct 31, 2024
2cd32ca
Fix deletion bug with manual injection bolus
ps2 Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions TidepoolService.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions TidepoolServiceKit/DeviceLogUploader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// DeviceLogUploader.swift
// TidepoolServiceKit
//
// Created by Pete Schwamb on 5/28/24.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import os.log
import LoopKit
import TidepoolKit

/// Periodically uploads device logs in hourly chunks to backend
actor DeviceLogUploader {
private let log = OSLog(category: "DeviceLogUploader")

private let api: TAPI

private var delegate: RemoteDataServiceDelegate?

private var logChunkDuration = TimeInterval(hours: 1)

private let backfillLimitInterval = TimeInterval(days: 2)


func setDelegate(_ delegate: RemoteDataServiceDelegate?) {
self.delegate = delegate
}

init(api: TAPI) {
self.api = api

Task {
await main()
}
}

func main() async {
var nextLogStart: Date?

// Start upload loop
while true {
if nextLogStart == nil {
do {
nextLogStart = try await getMostRecentUploadEndTime()
} catch {
log.error("Unable to fetch device log metadata: %{public}@", String(describing: error))
}
}

if nextLogStart != nil {
let nextLogEnd = nextLogStart!.addingTimeInterval(logChunkDuration)
let timeUntilNextUpload = nextLogEnd.timeIntervalSinceNow
if timeUntilNextUpload > 0 {
log.debug("Waiting %{public}@s until next upload", String(timeUntilNextUpload))
try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
}
do {
try await upload(from: nextLogStart!, to: nextLogEnd)
nextLogStart = nextLogEnd
} catch {
log.error("Upload failed: %{public}@", String(describing: error))
// Upload failed, retry in 5 minutes.
try? await Task.sleep(nanoseconds: TimeInterval(minutes: 5).nanoseconds)
}
} else {
// Haven't been able to talk to backend to find any previous log uploads. Retry in 15 minutes.
try? await Task.sleep(nanoseconds: TimeInterval(minutes: 15).nanoseconds)
}
}
}

func getMostRecentUploadEndTime() async throws -> Date {
var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
uploadMetadata.sort { a, b in
return a.endAtTime < b.endAtTime
}
if let lastEnd = uploadMetadata.last?.endAtTime {
return lastEnd
} else {
// No previous uploads found in last two days
return Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)
}
}

func upload(from start: Date, to end: Date) async throws {
if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
if logs.count > 0 {
let data = logs.map({
entry in
TDeviceLogEntry(
type: entry.type.tidepoolType,
managerIdentifier: entry.managerIdentifier,
deviceIdentifier: entry.deviceIdentifier ?? "unknown",
timestamp: entry.timestamp,
message: entry.message
)
})
let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
log.debug("Uploaded %d entries from %{public}@ to %{public}@", logs.count, String(describing: start), String(describing: end))
log.debug("metadata: %{public}@", String(describing: metatdata))
} else {
log.debug("No device log entries from %{public}@ to %{public}@", String(describing: start), String(describing: end))
}
}
}
}

extension TimeInterval {
var nanoseconds: UInt64 {
return UInt64(self * 1e+9)
}
}

extension DeviceLogEntryType {
var tidepoolType: TDeviceLogEntry.TDeviceLogEntryType {
switch self {
case .send:
return .send
case .receive:
return .receive
case .error:
return .error
case .delegate:
return .delegate
case .delegateResponse:
return .delegateResponse
case .connection:
return .connection
}
}
}
205 changes: 198 additions & 7 deletions TidepoolServiceKit/Extensions/DoseEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import LoopKit
import TidepoolKit
import LoopAlgorithm
import HealthKit

/*
DoseEntry
Expand Down Expand Up @@ -82,8 +84,8 @@ extension DoseEntry: IdentifiableDatum {
payload["deliveredUnits"] = datumBasalDeliveredUnits

var datum = TAutomatedBasalDatum(time: datumTime,
duration: !isMutable ? datumDuration : 0,
expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
duration: datumDuration,
expectedDuration: nil,
rate: datumScheduledBasalRate,
scheduleName: StoredSettings.activeScheduleNameDefault,
insulinFormulation: datumInsulinFormulation)
Expand Down Expand Up @@ -209,8 +211,8 @@ extension DoseEntry: IdentifiableDatum {
payload["deliveredUnits"] = deliveredUnits

var datum = TAutomatedBasalDatum(time: datumTime,
duration: !isMutable ? datumDuration : 0,
expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
duration: datumDuration,
expectedDuration: datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
rate: datumRate,
scheduleName: StoredSettings.activeScheduleNameDefault,
insulinFormulation: datumInsulinFormulation)
Expand Down Expand Up @@ -299,9 +301,11 @@ extension DoseEntry {
case .basal:
return [datumSelector(for: TScheduledBasalDatum.self)]
case .bolus:
if manuallyEntered {
return [datumSelector(for: TInsulinDatum.self)]
} else if automatic != true {
// TODO: revert to using .insulin datum type once fully supported in Tidepool frontend
// if manuallyEntered {
// return [datumSelector(for: TInsulinDatum.self)]
// } else if automatic != true {
if automatic != true {
return [datumSelector(for: TNormalBolusDatum.self)]
} else {
return [datumSelector(for: TAutomatedBolusDatum.self)]
Expand Down Expand Up @@ -347,3 +351,190 @@ extension TNormalBolusDatum: TypedDatum {
extension TInsulinDatum: TypedDatum {
static var resolvedType: String { TDatum.DatumType.insulin.rawValue }
}

extension DoseEntry {

/// Annotates a dose with the context of a history of scheduled basal rates
///
/// If the dose crosses a schedule boundary, it will be split into multiple doses so each dose has a
/// single scheduled basal rate.
///
/// - Parameter basalHistory: The history of basal schedule values to apply. Only schedule values overlapping the dose should be included.
/// - Returns: An array of annotated doses
fileprivate func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {

guard type == .tempBasal || type == .suspend, !basalHistory.isEmpty else {
return [self]
}

if type == .suspend {
guard value == 0 else {
preconditionFailure("suspend with non-zero delivery")
}
} else {
guard unit != .units else {
preconditionFailure("temp basal without rate unsupported")
}
}

if isMutable {
var newDose = self
let basal = basalHistory.first!
newDose.scheduledBasalRate = HKQuantity(unit: .internationalUnitsPerHour, doubleValue: basal.value)
return [newDose]
}

var doses: [DoseEntry] = []

for (index, basalItem) in basalHistory.enumerated() {
let startDate: Date
let endDate: Date

if index == 0 {
startDate = self.startDate
} else {
startDate = basalItem.startDate
}

if index == basalHistory.count - 1 {
endDate = self.endDate
} else {
endDate = basalHistory[index + 1].startDate
}

let segmentStartDate = max(startDate, self.startDate)
let segmentEndDate = max(startDate, min(endDate, self.endDate))
let segmentDuration = segmentEndDate.timeIntervalSince(segmentStartDate)
let segmentPortion = (segmentDuration / duration)

var annotatedDose = self
annotatedDose.startDate = segmentStartDate
annotatedDose.endDate = segmentEndDate
annotatedDose.scheduledBasalRate = HKQuantity(unit: .internationalUnitsPerHour, doubleValue: basalItem.value)

if let deliveredUnits {
annotatedDose.deliveredUnits = deliveredUnits * segmentPortion
}

doses.append(annotatedDose)
}

if doses.count > 1 {
for (index, dose) in doses.enumerated() {
if let originalIdentifier = dose.syncIdentifier, index>0 {
doses[index].syncIdentifier = originalIdentifier + "\(index+1)/\(doses.count)"
}
}
}

return doses
}

}


extension Collection where Element == DoseEntry {

/// Annotates a sequence of dose entries with the configured basal history
///
/// Doses which cross time boundaries in the basal rate schedule are split into multiple entries.
///
/// - Parameter basalHistory: A history of basal rates covering the timespan of these doses.
/// - Returns: An array of annotated dose entries
public func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {
var annotatedDoses: [DoseEntry] = []

for dose in self {
let basalItems = basalHistory.filterDateRange(dose.startDate, dose.endDate)
annotatedDoses += dose.annotated(with: basalItems)
}

return annotatedDoses
}


/// Assigns an automation status to any dose where automation is not already specified
///
/// - Parameters:
/// - automationHistory: A history of automation periods.
/// - Returns: An array of doses, with the automation flag set based on automation history. Doses will be split if the automation state changes mid-dose.

public func overlayAutomationHistory(
_ automationHistory: [AbsoluteScheduleValue<Bool>]
) -> [DoseEntry] {

guard count > 0 else {
return []
}

var newEntries = [DoseEntry]()

var automation = automationHistory

// Assume automation if doses start before automationHistory
if let firstAutomation = automation.first, firstAutomation.startDate > first!.startDate {
automation.insert(AbsoluteScheduleValue(startDate: first!.startDate, endDate: firstAutomation.startDate, value: true), at: 0)
}

// Overlay automation periods
func annotateDoseWithAutomation(dose: DoseEntry) {

var addedCount = 0
for period in automation {
if period.endDate > dose.startDate && period.startDate < dose.endDate {
var newDose = dose

if dose.isMutable {
newDose.automatic = period.value
newEntries.append(newDose)
return
}

newDose.startDate = Swift.max(period.startDate, dose.startDate)
newDose.endDate = Swift.min(period.endDate, dose.endDate)
if let delivered = dose.deliveredUnits {
newDose.deliveredUnits = newDose.duration / dose.duration * delivered
}
newDose.automatic = period.value
if addedCount > 0 {
newDose.syncIdentifier = "\(dose.syncIdentifierAsString)\(addedCount+1)"
}
newEntries.append(newDose)
addedCount += 1
}
}
if addedCount == 0 {
// automation history did not cover dose; mark automatic as default
var newDose = dose
newDose.automatic = true
newEntries.append(newDose)
}
}

for dose in self {
switch dose.type {
case .tempBasal, .basal, .suspend:
if dose.automatic == nil {
annotateDoseWithAutomation(dose: dose)
} else {
newEntries.append(dose)
}
default:
newEntries.append(dose)
break
}
}
return newEntries
}

}

extension DoseEntry {
var simpleDesc: String {
let seconds = Int(duration)
let automatic = automatic?.description ?? "na"
return "\(startDate) (\(seconds)s) - \(type) - isMutable:\(isMutable) automatic:\(automatic) value:\(value) delivered:\(String(describing: deliveredUnits)) scheduled:\(String(describing: scheduledBasalRate)) syncId:\(String(describing: syncIdentifier))"
}
}


1 change: 1 addition & 0 deletions TidepoolServiceKit/Extensions/InsulinType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import LoopKit
import TidepoolKit
import LoopAlgorithm

extension InsulinType {
var datum: TInsulinDatum.Formulation {
Expand Down
Loading