浏览代码

Merge branch 'dash_localized' into branch 'Crowdin'

Jon B.M 3 年之前
父节点
当前提交
f1018fd336
共有 100 个文件被更改,包括 2700 次插入1520 次删除
  1. 1 0
      Config.xcconfig
  2. 1 1
      Dependencies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj
  3. 1 1
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreGlucose.swift
  4. 4 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorData.swift
  5. 5 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterManager.swift
  6. 1 60
      Dependencies/LoopKit/.circleci/config.yml
  7. 1 7
      Dependencies/LoopKit/.travis.yml
  8. 0 1
      Dependencies/LoopKit/Cartfile
  9. 0 1
      Dependencies/LoopKit/Cartfile.resolved
  10. 5 4
      Dependencies/LoopKit/Extensions/Guardrail+Settings.swift
  11. 8 0
      Dependencies/LoopKit/Extensions/HKUnit.swift
  12. 18 16
      Dependencies/LoopKit/Extensions/NumberFormatter.swift
  13. 1 1
      Dependencies/LoopKit/LoopKit Example/Extensions/InsulinDeliveryTableViewController.swift
  14. 2 19
      Dependencies/LoopKit/LoopKit Example/Managers/DeviceDataManager.swift
  15. 37 80
      Dependencies/LoopKit/LoopKit Example/MasterViewController.swift
  16. 0 40
      Dependencies/LoopKit/LoopKit Example/ar.lproj/Localizable.strings
  17. 0 9
      Dependencies/LoopKit/LoopKit Example/ar.lproj/Main.strings
  18. 0 40
      Dependencies/LoopKit/LoopKit Example/ca.lproj/Localizable.strings
  19. 2 2
      Dependencies/LoopKit/LoopKit Example/de.lproj/Localizable.strings
  20. 0 1
      Dependencies/LoopKit/LoopKit Example/es.lproj/LaunchScreen.strings
  21. 0 9
      Dependencies/LoopKit/LoopKit Example/es.lproj/Main.strings
  22. 1 1
      Dependencies/LoopKit/LoopKit Example/fr.lproj/Localizable.strings
  23. 0 9
      Dependencies/LoopKit/LoopKit Example/fr.lproj/Main.strings
  24. 1 1
      Dependencies/LoopKit/LoopKit Example/he.lproj/Main.strings
  25. 0 9
      Dependencies/LoopKit/LoopKit Example/it.lproj/Main.strings
  26. 0 0
      Dependencies/LoopKit/LoopKit Example/ja.lproj/LaunchScreen.strings
  27. 0 0
      Dependencies/LoopKit/LoopKit Example/ja.lproj/Main.strings
  28. 4 4
      Dependencies/LoopKit/LoopKit Example/nb.lproj/Localizable.strings
  29. 0 9
      Dependencies/LoopKit/LoopKit Example/nb.lproj/Main.strings
  30. 3 3
      Dependencies/LoopKit/LoopKit Example/nl.lproj/Localizable.strings
  31. 0 9
      Dependencies/LoopKit/LoopKit Example/nl.lproj/Main.strings
  32. 0 9
      Dependencies/LoopKit/LoopKit Example/pl.lproj/Main.strings
  33. 0 1
      Dependencies/LoopKit/LoopKit Example/pt-BR.lproj/Localizable.strings
  34. 0 0
      Dependencies/LoopKit/LoopKit Example/ro.lproj/LaunchScreen.strings
  35. 0 0
      Dependencies/LoopKit/LoopKit Example/ro.lproj/Main.strings
  36. 3 3
      Dependencies/LoopKit/LoopKit Example/ru.lproj/Localizable.strings
  37. 0 9
      Dependencies/LoopKit/LoopKit Example/ru.lproj/Main.strings
  38. 0 1
      Dependencies/LoopKit/LoopKit Example/sk.lproj/LaunchScreen.strings
  39. 0 40
      Dependencies/LoopKit/LoopKit Example/sk.lproj/Localizable.strings
  40. 0 9
      Dependencies/LoopKit/LoopKit Example/sk.lproj/Main.strings
  41. 1 2
      Dependencies/LoopKit/LoopKit Example/sv.lproj/Localizable.strings
  42. 13 13
      Dependencies/LoopKit/LoopKit Example/tr.lproj/Localizable.strings
  43. 1 1
      Dependencies/LoopKit/LoopKit Example/tr.lproj/Main.strings
  44. 0 1
      Dependencies/LoopKit/LoopKit Example/uk.lproj/LaunchScreen.strings
  45. 0 40
      Dependencies/LoopKit/LoopKit Example/uk.lproj/Localizable.strings
  46. 0 9
      Dependencies/LoopKit/LoopKit Example/uk.lproj/Main.strings
  47. 0 1
      Dependencies/LoopKit/LoopKit Example/zh-Hans.lproj/LaunchScreen.strings
  48. 0 9
      Dependencies/LoopKit/LoopKit Example/zh-Hans.lproj/Main.strings
  49. 742 593
      Dependencies/LoopKit/LoopKit.xcodeproj/project.pbxproj
  50. 1 1
      Dependencies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  51. 14 0
      Dependencies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  52. 87 0
      Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme
  53. 76 0
      Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme
  54. 161 0
      Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme
  55. 63 9
      Dependencies/LoopKit/LoopKit/Alert.swift
  56. 46 0
      Dependencies/LoopKit/LoopKit/AnyCodableEquatable.swift
  57. 15 0
      Dependencies/LoopKit/LoopKit/AutomaticDosingStrategy.swift
  58. 76 0
      Dependencies/LoopKit/LoopKit/BluetoothProvider.swift
  59. 1 3
      Dependencies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataClass.swift
  60. 2 2
      Dependencies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataProperties.swift
  61. 28 16
      Dependencies/LoopKit/LoopKit/CarbKit/CarbStore.swift
  62. 4 5
      Dependencies/LoopKit/LoopKit/CarbKit/HKQuantitySample+CarbKit.swift
  63. 5 10
      Dependencies/LoopKit/LoopKit/CarbKit/StoredCarbEntry.swift
  64. 2 2
      Dependencies/LoopKit/LoopKit/CarbKit/SyncCarbObject.swift
  65. 67 0
      Dependencies/LoopKit/LoopKit/CorrectionRangeOverrides.swift
  66. 49 0
      Dependencies/LoopKit/LoopKit/DailyQuantitySchedule.swift
  67. 2 1
      Dependencies/LoopKit/LoopKit/DailyValueSchedule.swift
  68. 1 1
      Dependencies/LoopKit/LoopKit/DeliveryLimits.swift
  69. 10 0
      Dependencies/LoopKit/LoopKit/DeviceManager/AlertSoundPlayer.swift
  70. 96 17
      Dependencies/LoopKit/LoopKit/DeviceManager/CGMManager.swift
  71. 43 0
      Dependencies/LoopKit/LoopKit/DeviceManager/CodableDevice.swift
  72. 1 0
      Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLifecycleProgress.swift
  73. 5 2
      Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLog.xcdatamodeld/DeviceCommsLog.xcdatamodel/contents
  74. 9 2
      Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLog/PersistentDeviceLog.swift
  75. 7 34
      Dependencies/LoopKit/LoopKit/DeviceManager/DeviceManager.swift
  76. 4 5
      Dependencies/LoopKit/LoopKit/DeviceManager/DeviceStatusHighlight.swift
  77. 60 23
      Dependencies/LoopKit/LoopKit/DeviceManager/PumpManager.swift
  78. 30 80
      Dependencies/LoopKit/LoopKit/DeviceManager/PumpManagerStatus.swift
  79. 0 65
      Dependencies/LoopKit/LoopKit/DiagnosticLog.swift
  80. 1 1
      Dependencies/LoopKit/LoopKit/DosingDecisionObject+CoreDataClass.swift
  81. 18 0
      Dependencies/LoopKit/LoopKit/DosingDecisionObject+CoreDataProperties.swift
  82. 255 104
      Dependencies/LoopKit/LoopKit/DosingDecisionStore.swift
  83. 8 0
      Dependencies/LoopKit/LoopKit/Extensions/Double.swift
  84. 108 5
      Dependencies/LoopKit/LoopKit/GlucoseKit/CachedGlucoseObject+CoreDataClass.swift
  85. 26 1
      Dependencies/LoopKit/LoopKit/GlucoseKit/CachedGlucoseObject+CoreDataProperties.swift
  86. 12 0
      Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseCondition.swift
  87. 4 1
      Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseDisplayable.swift
  88. 8 0
      Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseSampleValue.swift
  89. 82 41
      Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseStore.swift
  90. 23 0
      Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseTrend.swift
  91. 54 0
      Dependencies/LoopKit/LoopKit/GlucoseKit/HKDevice+Encodable.swift
  92. 26 0
      Dependencies/LoopKit/LoopKit/GlucoseKit/HKQuantitySample+GlucoseKit.swift
  93. 24 2
      Dependencies/LoopKit/LoopKit/GlucoseKit/NewGlucoseSample.swift
  94. 81 4
      Dependencies/LoopKit/LoopKit/GlucoseKit/StoredGlucoseSample.swift
  95. 78 0
      Dependencies/LoopKit/LoopKit/GlucoseRange.swift
  96. 32 1
      Dependencies/LoopKit/LoopKit/GlucoseRangeSchedule.swift
  97. 25 1
      Dependencies/LoopKit/LoopKit/GlucoseSchedule.swift
  98. 11 0
      Dependencies/LoopKit/LoopKit/GlucoseThreshold.swift
  99. 3 3
      Dependencies/LoopKit/LoopKit/Guardrail.swift
  100. 0 0
      Dependencies/LoopKit/LoopKit/HealthKitSampleStore.swift

+ 1 - 0
Config.xcconfig

@@ -5,3 +5,4 @@ BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS
 APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroup
 
 #include? "ConfigOverride.xcconfig"
+#include? "../../ConfigOverride.xcconfig"

+ 1 - 1
Dependencies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj

@@ -274,9 +274,9 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 43CABE071C3506F100005705 /* Build configuration list for PBXNativeTarget "CGMBLEKit" */;
 			buildPhases = (
+				43CABDF01C3506F100005705 /* Headers */,
 				43CABDEE1C3506F100005705 /* Sources */,
 				43CABDEF1C3506F100005705 /* Frameworks */,
-				43CABDF01C3506F100005705 /* Headers */,
 				43CABDF11C3506F100005705 /* Resources */,
 			);
 			buildRules = (

+ 1 - 1
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreGlucose.swift

@@ -196,7 +196,7 @@ extension LibreGlucose {
                 timestamp: trend.date)
             // if sensor is ripped off body while transmitter is attached, values below 1 might be created
 
-            if glucose.unsmoothedGlucose > 0 {
+            if glucose.unsmoothedGlucose > 0 && glucose.unsmoothedGlucose <= 500 {
                 arr.append(glucose)
             }
 

+ 4 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorData.swift

@@ -90,6 +90,10 @@ public struct SensorData: Codable {
 
         return self.date.addingTimeInterval(TimeInterval(minutes: Double(self.minutesLeft)))
     }
+    
+    var sensorStartTime: Date? {
+            self.date.addingTimeInterval(-1*TimeInterval(minutes: Double(self.minutesSinceStart)))
+        }
 
     /// Sensor state (ready, failure, starting etc.)
     var state: SensorState {

+ 5 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterManager.swift

@@ -796,6 +796,11 @@ extension LibreTransmitterManager {
         proxy?.sensorData?.serialNumber ?? "n/a"
     }
 
+    public var sensorStartDate: Date? {
+            proxy?.sensorData?.sensorStartTime
+        }
+    
+    
     //cannot be called from managerQueue
     public var sensorAge: String {
         //proxy?.OnQueue_sensorData?.humanReadableSensorAge ?? "n/a"

+ 1 - 60
Dependencies/LoopKit/.circleci/config.yml

@@ -6,44 +6,6 @@ version: 2.1
 
 project_directory: &project_directory ~/project
 
-update_carthage: &update_carthage
-  name: Homebrew + Carthage Setup
-  command: |
-    if ! [ -x "$(command -v brew)" ]; then
-        # Install Homebrew
-        ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
-    fi
-
-    if brew ls carthage > /dev/null; then
-        brew upgrade carthage || echo "Continuing…"
-    else
-        brew install carthage
-    fi
-
-carthage_bootstrap: &carthage_bootstrap
-  name: Carthage Bootstrap
-  command: |
-    echo "Bootstrapping carthage dependencies"
-    unset LLVM_TARGET_TRIPLE_SUFFIX
-
-    if ! cmp -s Cartfile.Resolved Carthage/Cartfile.resolved; then
-      time ./Scripts/carthage.sh bootstrap --project-directory "$SRCROOT" --platform ios,watchos --cache-builds --verbose
-      cp Cartfile.resolved Carthage
-    else
-      echo "Carthage: not bootstrapping"
-    fi
-
-carthage_save_cache: &carthage_save_cache
-  name: Save Carthage Cache
-  key: carthage-v1-{{ .Branch }}-{{ checksum "Cartfile.resolved" }}
-  paths:
-    - Carthage
-
-carthage_restore_cache: &carthage_restore_cache
-  name: Restore Carthage Cache
-  keys:
-    - carthage-v1-{{ .Branch }}-{{ checksum "Cartfile.resolved" }}
-
 #
 # Jobs
 #
@@ -52,35 +14,15 @@ jobs:
   test:
     working_directory: *project_directory
     macos:
-      xcode: 12.2.0
+      xcode: 13.4.1
     steps:
       - checkout
-      - restore_cache: *carthage_restore_cache
-      - run: *update_carthage
-      - run: *carthage_bootstrap
       - run:
           name: Test
           command: |
             set -o pipefail && xcodebuild -project LoopKit.xcodeproj -scheme Shared build -destination 'name=iPhone 8' test | xcpretty
-      - save_cache: *carthage_save_cache
       - store_test_results:
           path: test_output
-
-  build-example:
-    working_directory: *project_directory
-    macos:
-      xcode: 12.2.0
-    steps:
-      - checkout
-      - restore_cache: *carthage_restore_cache
-      - run: *update_carthage
-      - run: *carthage_bootstrap
-      - run:
-          name: Build Example
-          command: |
-            set -o pipefail && xcodebuild -project LoopKit.xcodeproj -scheme "LoopKit Example" build -destination 'name=iPhone 8' CODE_SIGNING_ALLOWED=NO | xcpretty
-      - save_cache: *carthage_save_cache
-
 #
 # Workflows
 #
@@ -90,4 +32,3 @@ workflows:
   build_and_test:
     jobs:
       - test
-      - build-example

+ 1 - 7
Dependencies/LoopKit/.travis.yml

@@ -1,12 +1,6 @@
 language: objective-c
-osx_image: xcode12.2
+osx_image: xcode12.5
 
-cache:
-  directories:
-  - Carthage
-
-before_script:
-    - ./Scripts/carthage.sh bootstrap --cache-builds
 script:
    - set -o pipefail && xcodebuild -project LoopKit.xcodeproj -scheme Shared build -destination 'name=iPhone 8' test | xcpretty
    - set -o pipefail && xcodebuild -project LoopKit.xcodeproj -scheme "LoopKit Example" build -destination 'name=iPhone 8' CODE_SIGNING_ALLOWED=NO | xcpretty

+ 0 - 1
Dependencies/LoopKit/Cartfile

@@ -1 +0,0 @@
-github "i-schuetz/SwiftCharts" == 0.6.5

+ 0 - 1
Dependencies/LoopKit/Cartfile.resolved

@@ -1 +0,0 @@
-github "i-schuetz/SwiftCharts" "0.6.5"

+ 5 - 4
Dependencies/LoopKit/Extensions/Guardrail+Settings.swift

@@ -35,9 +35,10 @@ public extension Guardrail where Value == HKQuantity {
     }
     
     // Static "unconstrained" constant values before applying constraints
-    static let unconstrainedWorkoutCorrectionRange = Guardrail(absoluteBounds: 85...250,
-                                                               recommendedBounds: correctionRange.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...180,
-                                                               unit: .milligramsPerDeciliter)
+    static let unconstrainedWorkoutCorrectionRange = Guardrail(
+        absoluteBounds: correctionRange.absoluteBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...250,
+        recommendedBounds: correctionRange.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...180,
+        unit: .milligramsPerDeciliter)
     
     fileprivate static func workoutCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
                                                    suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
@@ -92,7 +93,7 @@ public extension Guardrail where Value == HKQuantity {
     )
 
     static func basalRate(supportedBasalRates: [Double]) -> Guardrail {
-        let scheduledBasalRateAbsoluteRange = 0.05...30.0
+        let scheduledBasalRateAbsoluteRange = 0.0...30.0
         let allowedBasalRates = supportedBasalRates.filter { scheduledBasalRateAbsoluteRange.contains($0) }
         return Guardrail(
             absoluteBounds: allowedBasalRates.first!...allowedBasalRates.last!,

+ 8 - 0
Dependencies/LoopKit/Extensions/HKUnit.swift

@@ -18,6 +18,14 @@ extension HKUnit {
         return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
     }()
 
+    static let milligramsPerDeciliterPerMinute: HKUnit = {
+        return HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
+    }()
+
+    static let millimolesPerLiterPerMinute: HKUnit = {
+        return HKUnit.millimolesPerLiter.unitDivided(by: .minute())
+    }()
+
     static let internationalUnitsPerHour: HKUnit = {
         return HKUnit.internationalUnit().unitDivided(by: .hour())
     }()

+ 18 - 16
Dependencies/LoopKit/Extensions/NumberFormatter.swift

@@ -15,33 +15,35 @@ extension NumberFormatter {
         return string(from: NSNumber(value: number))
     }
 
-    func string(from number: Double, unit: String, style: Formatter.UnitStyle = .medium) -> String? {
+    func string(from number: Double, unit: String, style: Formatter.UnitStyle = .medium, avoidLineBreaking: Bool = true) -> String? {
         guard let stringValue = string(from: number) else {
             return nil
         }
-
-        let format: String
+        
+        let separator: String
         switch style {
-        case .long, .medium:
-            format = LocalizedString(
-                "quantity-and-unit-space",
-                value: "%1$@ %2$@",
-                comment: "Format string for combining localized numeric value and unit with a space. (1: numeric value)(2: unit)"
-            )
+        case .long:
+            separator = " "
+        case .medium:
+            separator = avoidLineBreaking ? .nonBreakingSpace : " "
         case .short:
             fallthrough
         @unknown default:
-            format = LocalizedString(
-                "quantity-and-unit-tight",
-                value: "%1$@%2$@",
-                comment: "Format string for combining localized numeric value and unit without spacing. (1: numeric value)(2: unit)"
-            )
+            separator = avoidLineBreaking ? .wordJoiner : ""
         }
-
+        
+        let unit = avoidLineBreaking ? unit.replacingOccurrences(of: "/", with: "\(String.wordJoiner)/\(String.wordJoiner)") : unit
+        
         return String(
-            format: format,
+            format: NSLocalizedString("%1$@%2$@%3$@", comment: "String format for value with units (1: value, 2: separator, 3: units)"),
             stringValue,
+            separator,
             unit
         )
     }
 }
+
+public extension String {
+    static let nonBreakingSpace = "\u{00a0}"
+    static let wordJoiner = "\u{2060}"
+}

+ 1 - 1
Dependencies/LoopKit/LoopKit Example/Extensions/InsulinDeliveryTableViewController.swift

@@ -1,5 +1,5 @@
 //
-//  InsulinDeliveryTableViewController.swift
+//  LegacyInsulinDeliveryTableViewController.swift
 //  LoopKit
 //
 //  Created by Nate Racklyeft on 7/13/16.

+ 2 - 19
Dependencies/LoopKit/LoopKit Example/Managers/DeviceDataManager.swift

@@ -27,17 +27,11 @@ class DeviceDataManager {
             insulinSensitivitySchedule: insulinSensitivitySchedule,
             provenanceIdentifier: HKSource.default().bundleIdentifier
         )
-        let insulinModelSetting: InsulinModelSettings?
-        if let actionDuration = insulinActionDuration {
-            let insulinModel = WalshInsulinModel(actionDuration: actionDuration)
-            insulinModelSetting = InsulinModelSettings(model: insulinModel)
-        } else {
-            insulinModelSetting = nil
-        }
         doseStore = DoseStore(
             healthStore: healthStore,
             cacheStore: cacheStore,
-            pumpInsulinModelSetting: insulinModelSetting,
+            insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: ExponentialInsulinModelPreset.rapidActingAdult),
+            longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration,
             basalProfile: basalRateSchedule,
             insulinSensitivitySchedule: insulinSensitivitySchedule,
             provenanceIdentifier: HKSource.default().bundleIdentifier
@@ -73,17 +67,6 @@ class DeviceDataManager {
         }
     }
 
-    var insulinActionDuration = UserDefaults.standard.insulinActionDuration {
-        didSet {
-            UserDefaults.standard.insulinActionDuration = insulinActionDuration
-
-            if let duration = insulinActionDuration {
-                let model = WalshInsulinModel(actionDuration: duration)
-                doseStore.insulinModelSettings = InsulinModelSettings(model: model)
-            }
-        }
-    }
-
     var insulinSensitivitySchedule = UserDefaults.standard.insulinSensitivitySchedule {
         didSet {
             UserDefaults.standard.insulinSensitivitySchedule = insulinSensitivitySchedule

+ 37 - 80
Dependencies/LoopKit/LoopKit Example/MasterViewController.swift

@@ -126,31 +126,31 @@ class MasterViewController: UITableViewController {
         case .configuration:
             let row = ConfigurationRow(rawValue: indexPath.row)!
             switch row {
-            case .basalRate:
-
-                // x22 with max basal rate of 5U/hr
-                let pulsesPerUnit = 20
-                let basalRates = (1...100).map { Double($0) / Double(pulsesPerUnit) }
-
-                // full x23 rates
+//            case .basalRate:
+//
+//                // x22 with max basal rate of 5U/hr
+//                let pulsesPerUnit = 20
+//                let basalRates = (1...100).map { Double($0) / Double(pulsesPerUnit) }
+//
+//                // full x23 rates
 //                let rateGroup1 = ((1...39).map { Double($0) / Double(40) })
 //                let rateGroup2 = ((20...199).map { Double($0) / Double(20) })
 //                let rateGroup3 = ((100...350).map { Double($0) / Double(10) })
 //                let basalRates = rateGroup1 + rateGroup2 + rateGroup3
 
-                let scheduleVC = BasalScheduleTableViewController(allowedBasalRates: basalRates, maximumScheduleItemCount: 5, minimumTimeInterval: .minutes(30))
-
-                if let profile = dataManager?.basalRateSchedule {
-                    scheduleVC.timeZone = profile.timeZone
-
-
-                    scheduleVC.scheduleItems = profile.items
-                }
-                scheduleVC.delegate = self
-                scheduleVC.title = sender?.textLabel?.text
-                scheduleVC.syncSource = self
-
-                show(scheduleVC, sender: sender)
+//                let scheduleVC = BasalScheduleTableViewController(allowedBasalRates: basalRates, maximumScheduleItemCount: 5, minimumTimeInterval: .minutes(30))
+//
+//                if let profile = dataManager?.basalRateSchedule {
+//                    scheduleVC.timeZone = profile.timeZone
+//
+//
+//                    scheduleVC.scheduleItems = profile.items
+//                }
+//                scheduleVC.delegate = self
+//                scheduleVC.title = sender?.textLabel?.text
+//                scheduleVC.syncSource = self
+//
+//                show(scheduleVC, sender: sender)
             case .carbRatio:
                 let scheduleVC = DailyQuantityScheduleTableViewController()
 
@@ -166,21 +166,17 @@ class MasterViewController: UITableViewController {
 
                 show(scheduleVC, sender: sender)
             case .correctionRange:
+                var therapySettings = TherapySettings()
+                therapySettings.glucoseTargetRangeSchedule = self.dataManager?.glucoseTargetRangeSchedule
+                let therapySettingsViewModel = TherapySettingsViewModel(therapySettings: therapySettings)
+                
+                let view = CorrectionRangeScheduleEditor(mode: .settings, therapySettingsViewModel: therapySettingsViewModel, didSave: {
+                    self.dataManager?.glucoseTargetRangeSchedule = therapySettingsViewModel.therapySettings.glucoseTargetRangeSchedule
+                    self.navigationController?.popToViewController(self, animated: true)
+                })
+                    .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .milligramsPerDeciliter))
 
-                let unit = dataManager?.glucoseTargetRangeSchedule?.unit ?? dataManager?.glucoseStore.preferredUnit ?? HKUnit.milligramsPerDeciliter
-
-                let scheduleVC = GlucoseRangeScheduleTableViewController(allowedValues: unit.allowedCorrectionRangeValues, unit: unit)
-
-                scheduleVC.delegate = self
-                scheduleVC.title = sender?.textLabel?.text
-
-                if let schedule = dataManager?.glucoseTargetRangeSchedule {
-                    var overrides: [TemporaryScheduleOverride.Context: DoubleRange] = [:]
-                    overrides[.preMeal] = dataManager?.preMealTargetRange
-                    overrides[.legacyWorkout] = dataManager?.legacyWorkoutTargetRange
-                    scheduleVC.setSchedule(schedule, withOverrideRanges: overrides)
-                }
-
+                let scheduleVC = DismissibleHostingController(rootView: view, dismissalMode: .pop(to:  type(of: self)), isModalInPresentation: false)
                 show(scheduleVC, sender: sender)
             case .insulinSensitivity:
                 let unit = dataManager?.insulinSensitivitySchedule?.unit ?? dataManager?.glucoseStore.preferredUnit ?? HKUnit.milligramsPerDeciliter
@@ -204,6 +200,8 @@ class MasterViewController: UITableViewController {
                 textFieldVC.contextHelp = LocalizedString("The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).", comment: "Instructions on where to find the pump ID on a Minimed pump")
 
                 show(textFieldVC, sender: sender)
+            default:
+                break
             }
         case .data:
             switch DataRow(rawValue: indexPath.row)! {
@@ -281,7 +279,7 @@ class MasterViewController: UITableViewController {
                     }
 
                     group.enter()
-                    dataManager.glucoseStore.addGlucoseSamples([NewGlucoseSample(date: Date(), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 101), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: UUID().uuidString)], completion: { (result) in
+                    dataManager.glucoseStore.addGlucoseSamples([NewGlucoseSample(date: Date(), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 101), condition: nil, trend: nil, trendRate: nil, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: UUID().uuidString)], completion: { (result) in
                         group.leave()
                     })
 
@@ -335,10 +333,10 @@ extension MasterViewController: DailyValueScheduleTableViewControllerDelegate {
             switch Section(rawValue: indexPath.section)! {
             case .configuration:
                 switch ConfigurationRow(rawValue: indexPath.row)! {
-                case .basalRate:
-                    if let controller = controller as? BasalScheduleTableViewController {
-                        dataManager?.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
-                    }
+//                case .basalRate:
+//                    if let controller = controller as? BasalScheduleTableViewController {
+//                        dataManager?.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
+//                    }
                 default:
                     break
                 }
@@ -352,30 +350,6 @@ extension MasterViewController: DailyValueScheduleTableViewControllerDelegate {
 }
 
 
-extension MasterViewController: BasalScheduleTableViewControllerSyncSource {
-    func basalScheduleTableViewControllerIsReadOnly(_ viewController: BasalScheduleTableViewController) -> Bool {
-        return false
-    }
-
-    func syncButtonDetailText(for viewController: BasalScheduleTableViewController) -> String? {
-        return nil
-    }
-
-    func syncScheduleValues(for viewController: BasalScheduleTableViewController, completion: @escaping (SyncBasalScheduleResult<Double>) -> Void) {
-        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
-            let scheduleItems = viewController.scheduleItems
-            let timezone = self.dataManager?.basalRateSchedule?.timeZone ?? .currentFixed
-            let schedule = BasalRateSchedule(dailyItems: scheduleItems, timeZone: timezone)
-            self.dataManager?.basalRateSchedule = schedule
-            completion(.success(scheduleItems: scheduleItems, timeZone: .currentFixed))
-        }
-    }
-
-    func syncButtonTitle(for viewController: BasalScheduleTableViewController) -> String {
-        return LocalizedString("Sync With Pump", comment: "Title of button to sync basal profile from pump")
-    }
-}
-
 extension MasterViewController: InsulinSensitivityScheduleStorageDelegate {
     func saveSchedule(_ schedule: InsulinSensitivitySchedule, for viewController: InsulinSensitivityScheduleViewController, completion: @escaping (SaveInsulinSensitivityScheduleResult) -> Void) {
         self.dataManager?.insulinSensitivitySchedule = schedule
@@ -383,23 +357,6 @@ extension MasterViewController: InsulinSensitivityScheduleStorageDelegate {
     }
 }
 
-extension MasterViewController: GlucoseRangeScheduleStorageDelegate {
-    func saveSchedule(for viewController: GlucoseRangeScheduleTableViewController, completion: @escaping (SaveGlucoseRangeScheduleResult) -> Void) {
-        self.dataManager?.glucoseTargetRangeSchedule = viewController.schedule
-        for (context, range) in viewController.overrideRanges {
-            switch context {
-            case .preMeal:
-                self.dataManager?.preMealTargetRange = range
-            case .legacyWorkout:
-                self.dataManager?.legacyWorkoutTargetRange = range
-            default:
-                break
-            }
-        }
-        completion(.success)
-    }
-}
-
 private extension HKUnit {
     var allowedSensitivityValues: [Double] {
         if self == HKUnit.milligramsPerDeciliter {

+ 0 - 40
Dependencies/LoopKit/LoopKit Example/ar.lproj/Localizable.strings

@@ -1,40 +0,0 @@
-/* The title text for the basal rate schedule */
-"Basal Rates" = "Basal Rates";
-
-/* The title of the carb ratios schedule screen */
-"Carb Ratios" = "Carb Ratios";
-
-/* The title for the cell navigating to the carbs screen */
-"Carbs" = "Carbs";
-
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Correction Range";
-
-/* The title for the cell displaying diagnostic data */
-"Diagnostic" = "Diagnostic";
-
-/* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "Enter the 6-digit pump ID";
-
-/* The title for the cell displaying data generation */
-"Generate Data" = "Generate Data";
-
-/* The title of the insulin sensitivity schedule screen
-   The title text for the insulin sensitivity schedule */
-"Insulin Sensitivity" = "Insulin Sensitivity";
-
-/* The title text for the pump ID */
-"Pump ID" = "Pump ID";
-
-/* The title for the cell navigating to the reservoir screen */
-"Reservoir" = "Reservoir";
-
-/* Title for the cell resetting the data manager */
-"Reset" = "Reset";
-
-/* Title of button to sync basal profile from pump */
-"Sync With Pump" = "Sync With Pump";
-
-/* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/ar.lproj/Main.strings

@@ -1,9 +0,0 @@
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";
-

+ 0 - 40
Dependencies/LoopKit/LoopKit Example/ca.lproj/Localizable.strings

@@ -1,40 +0,0 @@
-/* The title text for the basal rate schedule */
-"Basal Rates" = "Basal Rates";
-
-/* The title of the carb ratios schedule screen */
-"Carb Ratios" = "Carb Ratios";
-
-/* The title for the cell navigating to the carbs screen */
-"Carbs" = "Carbs";
-
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Correction Range";
-
-/* The title for the cell displaying diagnostic data */
-"Diagnostic" = "Diagnostic";
-
-/* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "Enter the 6-digit pump ID";
-
-/* The title for the cell displaying data generation */
-"Generate Data" = "Generate Data";
-
-/* The title of the insulin sensitivity schedule screen
-   The title text for the insulin sensitivity schedule */
-"Insulin Sensitivity" = "Insulin Sensitivity";
-
-/* The title text for the pump ID */
-"Pump ID" = "Pump ID";
-
-/* The title for the cell navigating to the reservoir screen */
-"Reservoir" = "Reservoir";
-
-/* Title for the cell resetting the data manager */
-"Reset" = "Reset";
-
-/* Title of button to sync basal profile from pump */
-"Sync With Pump" = "Sync With Pump";
-
-/* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
-

+ 2 - 2
Dependencies/LoopKit/LoopKit Example/de.lproj/Localizable.strings

@@ -5,7 +5,7 @@
 "Carb Ratios" = "Kohlenhydratfaktoren";
 
 /* The title for the cell navigating to the carbs screen */
-"Carbs" = "KH";
+"Carbs" = "Carbs";
 
 /* The title text for the glucose correction range schedule */
 "Correction Range" = "Korrekturbereich";
@@ -30,7 +30,7 @@
 "Reservoir" = "Reservoir";
 
 /* Title for the cell resetting the data manager */
-"Reset" = "Zurücksetzen";
+"Reset" = "Reset";
 
 /* Title of button to sync basal profile from pump */
 "Sync With Pump" = "Mit der Pumpe synchronisieren";

+ 0 - 1
Dependencies/LoopKit/LoopKit Example/es.lproj/LaunchScreen.strings

@@ -1 +0,0 @@
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/es.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 1 - 1
Dependencies/LoopKit/LoopKit Example/fr.lproj/Localizable.strings

@@ -36,5 +36,5 @@
 "Sync With Pump" = "Synchroniser avec la pompe";
 
 /* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "L’identifiant de la pompe peut se trouver à l’arrière, ou vers le bas de l’écran STATUS/Esc. Il s’agit strictement de la partie numérique du numéro de série (indiqué comme SN ou S/N).";
+"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
 

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/fr.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 1 - 1
Dependencies/LoopKit/LoopKit Example/he.lproj/Main.strings

@@ -1,3 +1,4 @@
+
 /* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
 "7bK-jq-Zjz.title" = "UI Tests";
 
@@ -6,4 +7,3 @@
 
 /* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
 "RMx-3f-FxP.title" = "UI Tests";
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/it.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

Dependencies/LoopKit/LoopKit Example/ar.lproj/LaunchScreen.strings → Dependencies/LoopKit/LoopKit Example/ja.lproj/LaunchScreen.strings


Dependencies/LoopKit/LoopKit Example/ca.lproj/Main.strings → Dependencies/LoopKit/LoopKit Example/ja.lproj/Main.strings


+ 4 - 4
Dependencies/LoopKit/LoopKit Example/nb.lproj/Localizable.strings

@@ -1,5 +1,5 @@
 /* The title text for the basal rate schedule */
-"Basal Rates" = "Basal-program";
+"Basal Rates" = "Basalsatser";
 
 /* The title of the carb ratios schedule screen */
 "Carb Ratios" = "Karbohydratforhold";
@@ -14,7 +14,7 @@
 "Diagnostic" = "Diagnostikk";
 
 /* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "Skriv 6-siffret pumpe-ID";
+"Enter the 6-digit pump ID" = "Skriv 6-siffret pumpe ID";
 
 /* The title for the cell displaying data generation */
 "Generate Data" = "Generer data";
@@ -24,7 +24,7 @@
 "Insulin Sensitivity" = "Insulinfølsomhet";
 
 /* The title text for the pump ID */
-"Pump ID" = "Pumpe-ID";
+"Pump ID" = "Pumpe ID";
 
 /* The title for the cell navigating to the reservoir screen */
 "Reservoir" = "Reservoar";
@@ -36,5 +36,5 @@
 "Sync With Pump" = "Synkroniser med pumpe";
 
 /* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "Pumpens ID kan finnes på baksiden, eller i nærheten av bunnen av STATUS/Esc-skjermen. Det er kun den numeriske delen av serienummeret (vist som SN eller S/N).";
+"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
 

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/nb.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 3 - 3
Dependencies/LoopKit/LoopKit Example/nl.lproj/Localizable.strings

@@ -7,9 +7,6 @@
 /* The title for the cell navigating to the carbs screen */
 "Carbs" = "Koolhydraten";
 
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Gewenst glucose doelbereik";
-
 /* The title for the cell displaying diagnostic data */
 "Diagnostic" = "Diagnose";
 
@@ -23,6 +20,9 @@
    The title text for the insulin sensitivity schedule */
 "Insulin Sensitivity" = "Correctie bereik";
 
+/* The title text for the glucose target range schedule */
+"Correction Range" = "Gewenst glucose doelbereik";
+
 /* The title text for the pump ID */
 "Pump ID" = "Pomp ID";
 

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/nl.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/pl.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 0 - 1
Dependencies/LoopKit/LoopKit Example/pt-BR.lproj/Localizable.strings

@@ -37,4 +37,3 @@
 
 /* Instructions on where to find the pump ID on a Minimed pump */
 "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "O ID da bomba pode ser encontrado impresso na parte traseira ou na parte inferior da tela STATUS/Esc. É a parte estritamente numérica do número de série (mostrado como SN ou S/N).";
-

Dependencies/LoopKit/LoopKit Example/ca.lproj/LaunchScreen.strings → Dependencies/LoopKit/LoopKit Example/ro.lproj/LaunchScreen.strings


Dependencies/LoopKit/LoopKit Example/de.lproj/Main.strings → Dependencies/LoopKit/LoopKit Example/ro.lproj/Main.strings


+ 3 - 3
Dependencies/LoopKit/LoopKit Example/ru.lproj/Localizable.strings

@@ -7,9 +7,6 @@
 /* The title for the cell navigating to the carbs screen */
 "Carbs" = "Углеводы";
 
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Диапазон коррекции";
-
 /* The title for the cell displaying diagnostic data */
 "Diagnostic" = "Диагностически";
 
@@ -23,6 +20,9 @@
    The title text for the insulin sensitivity schedule */
 "Insulin Sensitivity" = "Чувствительность";
 
+/* The title text for the glucose target range schedule */
+"Correction Range" = "Диапазон коррекции";
+
 /* The title text for the pump ID */
 "Pump ID" = "Инд номер помпы";
 

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/ru.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

+ 0 - 1
Dependencies/LoopKit/LoopKit Example/sk.lproj/LaunchScreen.strings

@@ -1 +0,0 @@
-

+ 0 - 40
Dependencies/LoopKit/LoopKit Example/sk.lproj/Localizable.strings

@@ -1,40 +0,0 @@
-/* The title text for the basal rate schedule */
-"Basal Rates" = "Bazálne Hodnoty";
-
-/* The title of the carb ratios schedule screen */
-"Carb Ratios" = "Sacharidový Pomer";
-
-/* The title for the cell navigating to the carbs screen */
-"Carbs" = "Sach";
-
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Korekčný Rozsah";
-
-/* The title for the cell displaying diagnostic data */
-"Diagnostic" = "Diagnostika";
-
-/* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "Zadajte 6-miestny ID pumpy";
-
-/* The title for the cell displaying data generation */
-"Generate Data" = "Generovať Údaje";
-
-/* The title of the insulin sensitivity schedule screen
-   The title text for the insulin sensitivity schedule */
-"Insulin Sensitivity" = "Inzulínová Citlivosť";
-
-/* The title text for the pump ID */
-"Pump ID" = "ID Pumpy";
-
-/* The title for the cell navigating to the reservoir screen */
-"Reservoir" = "Rezervoár";
-
-/* Title for the cell resetting the data manager */
-"Reset" = "Resetovať";
-
-/* Title of button to sync basal profile from pump */
-"Sync With Pump" = "Synchronizuj s Pumpou";
-
-/* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "ID Pumpy môžete nájsť na etikete vzadu, alebo blízko spodnej časti obrazovky STATUS/Esc. Ide o výhradne číselný údaj sériového čísla (zobrazený ako SN alebo S/N).";
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/sk.lproj/Main.strings

@@ -1,9 +0,0 @@
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";
-

+ 1 - 2
Dependencies/LoopKit/LoopKit Example/sv.lproj/Localizable.strings

@@ -24,7 +24,7 @@
 "Insulin Sensitivity" = "Insulinkänslighet";
 
 /* The title text for the pump ID */
-"Pump ID" = "Pump-ID";
+"Pump ID" = "Pump ID";
 
 /* The title for the cell navigating to the reservoir screen */
 "Reservoir" = "Reservoar";
@@ -37,4 +37,3 @@
 
 /* Instructions on where to find the pump ID on a Minimed pump */
 "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "Ditt pump-ID står tryckt på baksidan, eller nästan längst ner på status/Esc-menyn. Det är den numeriska delen av serienumret (visad som SN eller S/N). ";
-

+ 13 - 13
Dependencies/LoopKit/LoopKit Example/tr.lproj/Localizable.strings

@@ -1,40 +1,40 @@
 /* The title text for the basal rate schedule */
-"Basal Rates" = "Bazal Oranları";
+"Basal Rates" = "Basal Rates";
 
 /* The title of the carb ratios schedule screen */
-"Carb Ratios" = "Karbonhidrat Oranları";
+"Carb Ratios" = "Carb Ratios";
 
 /* The title for the cell navigating to the carbs screen */
-"Carbs" = "Karbonhidrat";
+"Carbs" = "Carbs";
 
 /* The title text for the glucose correction range schedule */
-"Correction Range" = "Düzeltme Aralığı";
+"Correction Range" = "Correction Range";
 
 /* The title for the cell displaying diagnostic data */
-"Diagnostic" = "Tanılama";
+"Diagnostic" = "Diagnostic";
 
 /* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "6 haneli pompa ID kimliğini girin";
+"Enter the 6-digit pump ID" = "Enter the 6-digit pump ID";
 
 /* The title for the cell displaying data generation */
-"Generate Data" = "Veri Oluştur";
+"Generate Data" = "Generate Data";
 
 /* The title of the insulin sensitivity schedule screen
    The title text for the insulin sensitivity schedule */
-"Insulin Sensitivity" = "İnsülin Duyarlılığı";
+"Insulin Sensitivity" = "Insulin Sensitivity";
 
 /* The title text for the pump ID */
-"Pump ID" = "Pompa ID";
+"Pump ID" = "Pump ID";
 
 /* The title for the cell navigating to the reservoir screen */
-"Reservoir" = "Rezervuar";
+"Reservoir" = "Reservoir";
 
 /* Title for the cell resetting the data manager */
-"Reset" = "Sıfırla";
+"Reset" = "Reset";
 
 /* Title of button to sync basal profile from pump */
-"Sync With Pump" = "Pompa'yla eşitle";
+"Sync With Pump" = "Sync With Pump";
 
 /* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "Pompa ID kimliği, Pompanızın arka tarafında yazılı olarak bulunur. Veya DURUM/Esc tuşuna basıp ekranın alt kısmına giderek öğrenebilirsiniz. Seri numarası (SN veya S/N olarak gösterilir) ve sadece rakam kısmı yazılır. (6 haneli)";
+"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
 

+ 1 - 1
Dependencies/LoopKit/LoopKit Example/tr.lproj/Main.strings

@@ -1,3 +1,4 @@
+
 /* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
 "7bK-jq-Zjz.title" = "UI Tests";
 
@@ -6,4 +7,3 @@
 
 /* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
 "RMx-3f-FxP.title" = "UI Tests";
-

+ 0 - 1
Dependencies/LoopKit/LoopKit Example/uk.lproj/LaunchScreen.strings

@@ -1 +0,0 @@
-

+ 0 - 40
Dependencies/LoopKit/LoopKit Example/uk.lproj/Localizable.strings

@@ -1,40 +0,0 @@
-/* The title text for the basal rate schedule */
-"Basal Rates" = "Basal Rates";
-
-/* The title of the carb ratios schedule screen */
-"Carb Ratios" = "Carb Ratios";
-
-/* The title for the cell navigating to the carbs screen */
-"Carbs" = "Carbs";
-
-/* The title text for the glucose correction range schedule */
-"Correction Range" = "Correction Range";
-
-/* The title for the cell displaying diagnostic data */
-"Diagnostic" = "Diagnostic";
-
-/* The placeholder text instructing users how to enter a pump ID */
-"Enter the 6-digit pump ID" = "Enter the 6-digit pump ID";
-
-/* The title for the cell displaying data generation */
-"Generate Data" = "Generate Data";
-
-/* The title of the insulin sensitivity schedule screen
-   The title text for the insulin sensitivity schedule */
-"Insulin Sensitivity" = "Insulin Sensitivity";
-
-/* The title text for the pump ID */
-"Pump ID" = "Pump ID";
-
-/* The title for the cell navigating to the reservoir screen */
-"Reservoir" = "Reservoir";
-
-/* Title for the cell resetting the data manager */
-"Reset" = "Reset";
-
-/* Title of button to sync basal profile from pump */
-"Sync With Pump" = "Sync With Pump";
-
-/* Instructions on where to find the pump ID on a Minimed pump */
-"The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N)." = "The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).";
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/uk.lproj/Main.strings

@@ -1,9 +0,0 @@
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";
-

+ 0 - 1
Dependencies/LoopKit/LoopKit Example/zh-Hans.lproj/LaunchScreen.strings

@@ -1 +0,0 @@
-

+ 0 - 9
Dependencies/LoopKit/LoopKit Example/zh-Hans.lproj/Main.strings

@@ -1,9 +0,0 @@
-
-/* Class = "UITableViewController"; title = "UI Tests"; ObjectID = "7bK-jq-Zjz"; */
-"7bK-jq-Zjz.title" = "UI Tests";
-
-/* Class = "UILabel"; text = "Title"; ObjectID = "Arm-wq-HPj"; */
-"Arm-wq-HPj.text" = "Title";
-
-/* Class = "UINavigationController"; title = "UI Tests"; ObjectID = "RMx-3f-FxP"; */
-"RMx-3f-FxP.title" = "UI Tests";

文件差异内容过多而无法显示
+ 742 - 593
Dependencies/LoopKit/LoopKit.xcodeproj/project.pbxproj


+ 1 - 1
Dependencies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -2,6 +2,6 @@
 <Workspace
    version = "1.0">
    <FileRef
-      location = "self:LoopKit.xcodeproj">
+      location = "self:">
    </FileRef>
 </Workspace>

+ 14 - 0
Dependencies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -0,0 +1,14 @@
+{
+  "pins" : [
+    {
+      "identity" : "swiftcharts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "state" : {
+        "branch" : "master",
+        "revision" : "3d011f67eccb1ffa622fbfccb1348eed80309ae8"
+      }
+    }
+  ],
+  "version" : 2
+}

+ 87 - 0
Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1330"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "430157F61C7EC03B00B64B63"
+               BuildableName = "LoopKit Example.app"
+               BlueprintName = "LoopKit Example"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "430157F61C7EC03B00B64B63"
+            BuildableName = "LoopKit Example.app"
+            BlueprintName = "LoopKit Example"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "430157F61C7EC03B00B64B63"
+            BuildableName = "LoopKit Example.app"
+            BlueprintName = "LoopKit Example"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "430157F61C7EC03B00B64B63"
+            BuildableName = "LoopKit Example.app"
+            BlueprintName = "LoopKit Example"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 76 - 0
Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1330"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "NO">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "A9E6758022713F4700E25293"
+               BuildableName = "LoopKit.framework"
+               BlueprintName = "LoopKit-watchOS"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "A9E6758022713F4700E25293"
+            BuildableName = "LoopKit.framework"
+            BlueprintName = "LoopKit-watchOS"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "A9E6758022713F4700E25293"
+            BuildableName = "LoopKit.framework"
+            BlueprintName = "LoopKit-watchOS"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 161 - 0
Dependencies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme

@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1330"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "NO">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "43D8FDCA1C728FDF0073BE78"
+               BuildableName = "LoopKit.framework"
+               BlueprintName = "LoopKit"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "43BA7153201E484D0058961E"
+               BuildableName = "LoopKitUI.framework"
+               BlueprintName = "LoopKitUI"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "892A5D33222F03CB008961AB"
+               BuildableName = "LoopTestingKit.framework"
+               BlueprintName = "LoopTestingKit"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "89D2047121CC7BD7001238CC"
+               BuildableName = "MockKit.framework"
+               BlueprintName = "MockKit"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "89D2048E21CC7C12001238CC"
+               BuildableName = "MockKitUI.framework"
+               BlueprintName = "MockKitUI"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "43D8FDCA1C728FDF0073BE78"
+            BuildableName = "LoopKit.framework"
+            BlueprintName = "LoopKit"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
+               BuildableName = "LoopKitTests.xctest"
+               BlueprintName = "LoopKitTests"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "1DEE226824A676A300693C32"
+               BuildableName = "LoopKitHostedTests.xctest"
+               BlueprintName = "LoopKitHostedTests"
+               ReferencedContainer = "container:LoopKit.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "43D8FDCA1C728FDF0073BE78"
+            BuildableName = "LoopKit.framework"
+            BlueprintName = "LoopKit"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "43D8FDCA1C728FDF0073BE78"
+            BuildableName = "LoopKit.framework"
+            BlueprintName = "LoopKit"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 63 - 9
Dependencies/LoopKit/LoopKit/Alert.swift

@@ -8,8 +8,8 @@
 
 import Foundation
 
-/// Protocol that describes any class that presents Alerts.
-public protocol AlertPresenter: AnyObject {
+/// Protocol that describes any class that issues and retract Alerts.
+public protocol AlertIssuer: AnyObject {
     /// Issue (post) the given alert, according to its trigger schedule.
     func issueAlert(_ alert: Alert)
     /// Retract any alerts with the given identifier.  This includes both pending and delivered alerts.
@@ -18,8 +18,38 @@ public protocol AlertPresenter: AnyObject {
 
 /// Protocol that describes something that can deal with a user's response to an alert.
 public protocol AlertResponder: AnyObject {
-    /// Acknowledge alerts with a given type identifier
-    func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier) -> Void
+    /// Acknowledge alerts with a given type identifier. If the alert fails to clear, an error should be passed to the completion handler, indicating the cause of failure.
+    func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) -> Void
+}
+
+public struct PersistedAlert: Equatable {
+    public let alert: Alert
+    public let issuedDate: Date
+    public let retractedDate: Date?
+    public let acknowledgedDate: Date?
+    public init(alert: Alert, issuedDate: Date, retractedDate: Date?, acknowledgedDate: Date?) {
+        self.alert = alert
+        self.issuedDate = issuedDate
+        self.retractedDate = retractedDate
+        self.acknowledgedDate = acknowledgedDate
+    }
+}
+
+/// Protocol for recording and looking up alerts persisted in storage
+public protocol PersistedAlertStore {
+    /// Determine if an alert is already issued for a given `Alert.Identifier`.
+    func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Swift.Result<Bool, Error>) -> Void)
+
+    /// Look up all issued, but unretracted, alerts for a given `managerIdentifier`.  This is useful for an Alert issuer to see what alerts are extant (outstanding).
+    /// NOTE: the completion function may be called on a different queue than the caller.  Callers must be prepared for this.
+    func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void)
+
+    /// Look up all issued, but unretracted, and unacknowledged, alerts for a given `managerIdentifier`.  This is useful for an Alert issuer to see what alerts are extant (outstanding).
+    /// NOTE: the completion function may be called on a different queue than the caller.  Callers must be prepared for this.
+    func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void)
+
+    /// Records an alert that occurred (likely in the past) but is already retracted. This alert will never be presented to the user by an AlertPresenter. Such a retracted alert has the same date for issued and retracted dates, and there is no acknowledged date
+    func recordRetractedAlert(_ alert: Alert, at date: Date)
 }
 
 /// Structure that represents an Alert that is issued from a Device.
@@ -33,20 +63,27 @@ public struct Alert: Equatable {
         /// Delay triggering the alert by `repeatInterval`, and repeat at that interval until cancelled or unscheduled.
         case repeating(repeatInterval: TimeInterval)
     }
+    /// The interruption level of the alert.  Note that these follow the same definitions as defined by https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel
+    /// Handlers will determine how that is manifested.
+    public enum InterruptionLevel: String {
+        /// The system presents the notification immediately, lights up the screen, and can play a sound.  These alerts may be deferred if the user chooses.
+        case active
+        /// The system presents the notification immediately, lights up the screen, and can play a sound.  These alerts may not be deferred.
+        case timeSensitive
+        /// The system makes every attempt at alerting the user, including (possibly) ignoring the mute switch, or the user's notification settings.
+        case critical
+    }
     /// Content of the alert, either for foreground or background alerts
     public struct Content: Equatable  {
         public let title: String
         public let body: String
-        /// Should this alert be deemed "critical" for the User?  Handlers will determine how that is manifested.
-        public let isCritical: Bool
         // TODO: when we have more complicated actions.  For now, all we have is "acknowledge".
 //        let actions: [UserAlertAction]
         public let acknowledgeActionButtonLabel: String
-        public init(title: String, body: String, acknowledgeActionButtonLabel: String, isCritical: Bool = false) {
+        public init(title: String, body: String, acknowledgeActionButtonLabel: String) {
             self.title = title
             self.body = body
             self.acknowledgeActionButtonLabel = acknowledgeActionButtonLabel
-            self.isCritical = isCritical
         }
     }
     public struct Identifier: Equatable, Hashable {
@@ -73,6 +110,8 @@ public struct Alert: Equatable {
     public let backgroundContent: Content?
     /// Trigger for the alert.
     public let trigger: Trigger
+    /// Interruption level for the alert.  See `InterruptionLevel` above.
+    public let interruptionLevel: InterruptionLevel
 
     /// An alert's "identifier" is a tuple of `managerIdentifier` and `alertIdentifier`.  It's purpose is to uniquely identify an alert so we can
     /// find which device issued it, and send acknowledgment of that alert to the proper device manager.
@@ -85,13 +124,21 @@ public struct Alert: Equatable {
         case sound(name: String)
     }
     public let sound: Sound?
+
+    /// Any metadata for the alert used to customize the alert content
+    public typealias MetadataValue = AnyCodableEquatable
+    public typealias Metadata = [String: MetadataValue]
+    public let metadata: Metadata?
     
-    public init(identifier: Identifier, foregroundContent: Content?, backgroundContent: Content?, trigger: Trigger, sound: Sound? = nil) {
+    public init(identifier: Identifier, foregroundContent: Content?, backgroundContent: Content?, trigger: Trigger,
+                interruptionLevel: InterruptionLevel = .timeSensitive, sound: Sound? = nil, metadata: Metadata? = nil) {
         self.identifier = identifier
         self.foregroundContent = foregroundContent
         self.backgroundContent = backgroundContent
         self.trigger = trigger
+        self.interruptionLevel = interruptionLevel
         self.sound = sound
+        self.metadata = metadata
     }
 }
 
@@ -117,6 +164,7 @@ public protocol AlertSoundVendor {
 extension Alert: Codable { }
 extension Alert.Content: Codable { }
 extension Alert.Identifier: Codable { }
+extension Alert.InterruptionLevel: Codable { }
 // These Codable implementations of enums with associated values cannot be synthesized (yet) in Swift.
 // The code below follows a pattern described by https://medium.com/@hllmandel/codable-enum-with-associated-values-swift-4-e7d75d6f4370
 extension Alert.Trigger: Codable {
@@ -206,6 +254,12 @@ extension Alert.Sound: Codable {
     }
 }
 
+public extension Alert.Metadata {
+    init<E: Codable & Equatable>(dict: [String: E]) {
+        self = dict.mapValues { Alert.MetadataValue($0) }
+    }
+}
+
 extension Decoder {
     var enumDecodingError: DecodingError {
         return DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "invalid enumeration"))

+ 46 - 0
Dependencies/LoopKit/LoopKit/AnyCodableEquatable.swift

@@ -0,0 +1,46 @@
+//
+//  AnyCodableEquatable.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 2/8/22.
+//  Copyright © 2022 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public struct AnyCodableEquatable: Codable, Equatable {
+    public enum Error: Swift.Error {
+        case unknownType
+    }
+
+    public let wrapped: Any
+    private let equals: (Self) -> Bool
+
+    public init<T: Codable & Equatable>(_ wrapped: T) {
+        self.wrapped = wrapped
+        self.equals = { $0.wrapped as? T == wrapped }
+    }
+
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.singleValueContainer()
+        if let value = try? container.decode(String.self) {
+            self.init(value)
+        } else if let value = try? container.decode(Int.self) {
+            self.init(value)
+        } else if let value = try? container.decode(Double.self) {
+            self.init(value)
+        } else if let value = try? container.decode(Bool.self) {
+            self.init(value)
+        } else {
+            throw Error.unknownType
+        }
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        try (wrapped as? Encodable)?.encode(to: encoder)
+    }
+
+    public static func ==(lhs: AnyCodableEquatable, rhs: AnyCodableEquatable) -> Bool {
+        return lhs.equals(rhs)
+    }
+}

+ 15 - 0
Dependencies/LoopKit/LoopKit/AutomaticDosingStrategy.swift

@@ -0,0 +1,15 @@
+//
+//  DosingStrategy.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 6/27/22.
+//  Copyright © 2022 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+
+public enum AutomaticDosingStrategy: Int, CaseIterable, Codable {
+    case tempBasalOnly
+    case automaticBolus
+}

+ 76 - 0
Dependencies/LoopKit/LoopKit/BluetoothProvider.swift

@@ -0,0 +1,76 @@
+//
+//  BluetoothProvider.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 3/1/21.
+//  Copyright © 2021 LoopKit Authors. All rights reserved.
+//
+
+public enum BluetoothAuthorization: Int {
+    /// User has not yet made a choice regarding whether the application may use Bluetooth.
+    case notDetermined
+
+    /// This application is not authorized to use Bluetooth. The user cannot change this application’s status,
+    case restricted
+
+    /// User has explicitly denied this application from using Bluetooth.
+    case denied
+
+    /// User has authorized this application to use Bluetooth.
+    case authorized
+}
+
+public enum BluetoothState: Int {
+    /// State unknown, update imminent.
+    case unknown
+
+    /// The connection with the system service was momentarily lost, update imminent.
+    case resetting
+
+    /// The platform doesn't support the Bluetooth Low Energy Central/Client role.
+    case unsupported
+
+    /// The application is not authorized to use the Bluetooth Low Energy role.
+    case unauthorized
+
+    /// Bluetooth is currently powered off.
+    case poweredOff
+
+    /// Bluetooth is currently powered on and available to use.
+    case poweredOn
+}
+
+public protocol BluetoothObserver: AnyObject {
+    /// Informs the observer that the Bluetooth state has changed to the given value.
+    ///
+    /// - Parameters:
+    ///     - state: The latest Bluetooth state.
+    func bluetoothDidUpdateState(_ state: BluetoothState)
+}
+
+public protocol BluetoothProvider: AnyObject {
+    /// The current Bluetooth authorization.
+    var bluetoothAuthorization: BluetoothAuthorization { get }
+
+    /// The current Bluetooth state. If Bluetooth has not been authorized then returns .unknown.
+    var bluetoothState: BluetoothState { get }
+
+    /// Authorize Bluetooth. Should only be invoked if bluetoothAuthorization is .notDetermined.
+    ///
+    /// - Parameters:
+    ///     - completion: Invoked when Bluetooth authorization is complete along with the resulting authorization.
+    func authorizeBluetooth(_ completion: @escaping (BluetoothAuthorization) -> Void)
+
+    /// Start observing Bluetooth changes.
+    ///
+    /// - Parameters:
+    ///     - observer: The observer observing Bluetooth changes.
+    ///     - queue: The Dispatch queue upon which to notify the observer of Bluetooth changes.
+    func addBluetoothObserver(_ observer: BluetoothObserver, queue: DispatchQueue)
+
+    /// Stop observing Bluetooth changes.
+    ///
+    /// - Parameters:
+    ///     - observer: The observer observing Bluetooth changes.
+    func removeBluetoothObserver(_ observer: BluetoothObserver)
+}

+ 1 - 3
Dependencies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataClass.swift

@@ -91,8 +91,6 @@ extension CachedCarbObject {
 
     // HealthKit
     func create(from sample: HKQuantitySample, on date: Date = Date()) {
-        precondition(!sample.createdByCurrentApp)
-
         self.absorptionTime = sample.absorptionTime
         self.createdByCurrentApp = sample.createdByCurrentApp
         self.foodType = sample.foodType
@@ -222,7 +220,7 @@ extension CachedCarbObject {
         var metadata = [String: Any]()
 
         metadata[HKMetadataKeyFoodType] = foodType
-        metadata[MetadataKeyAbsorptionTimeMinutes] = absorptionTime?.minutes
+        metadata[MetadataKeyAbsorptionTime] = absorptionTime
 
         metadata[HKMetadataKeySyncIdentifier] = syncIdentifier
         metadata[HKMetadataKeySyncVersion] = syncVersion

+ 2 - 2
Dependencies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataProperties.swift

@@ -22,7 +22,7 @@ extension CachedCarbObject {
     @NSManaged public var grams: Double
     @NSManaged public var startDate: Date
     @NSManaged public var uuid: UUID?
-    @NSManaged public var provenanceIdentifier: String?
+    @NSManaged public var provenanceIdentifier: String
     @NSManaged public var syncIdentifier: String?
     @NSManaged public var primitiveSyncVersion: NSNumber?
     @NSManaged public var userCreatedDate: Date?
@@ -44,7 +44,7 @@ extension CachedCarbObject: Encodable {
         try container.encode(grams, forKey: .grams)
         try container.encode(startDate, forKey: .startDate)
         try container.encodeIfPresent(uuid, forKey: .uuid)
-        try container.encodeIfPresent(provenanceIdentifier, forKey: .provenanceIdentifier)
+        try container.encode(provenanceIdentifier, forKey: .provenanceIdentifier)
         try container.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
         try container.encodeIfPresent(syncVersion, forKey: .syncVersion)
         try container.encodeIfPresent(userCreatedDate, forKey: .userCreatedDate)

+ 28 - 16
Dependencies/LoopKit/LoopKit/CarbKit/CarbStore.swift

@@ -165,6 +165,8 @@ public final class CarbStore: HealthKitSampleStore {
     /// The interval to observe HealthKit data to populate the cache
     public let observationInterval: TimeInterval
 
+    private let storeEntriesToHealthKit: Bool
+
     private let cacheStore: PersistenceController
 
     /// The sync version used for new samples written to HealthKit
@@ -191,6 +193,7 @@ public final class CarbStore: HealthKitSampleStore {
     public init(
         healthStore: HKHealthStore,
         observeHealthKitSamplesFromOtherApps: Bool = true,
+        storeEntriesToHealthKit: Bool = true,
         cacheStore: PersistenceController,
         cacheLength: TimeInterval,
         defaultAbsorptionTimes: DefaultAbsorptionTimes,
@@ -205,6 +208,7 @@ public final class CarbStore: HealthKitSampleStore {
         carbAbsorptionModel: CarbAbsorptionModel = .nonlinear,
         provenanceIdentifier: String
     ) {
+        self.storeEntriesToHealthKit = storeEntriesToHealthKit
         self.cacheStore = cacheStore
         self.defaultAbsorptionTimes = defaultAbsorptionTimes
         self.lockedCarbRatioSchedule = Locked(carbRatioSchedule)
@@ -222,7 +226,7 @@ public final class CarbStore: HealthKitSampleStore {
         let observationEnabled = observationInterval > 0
 
         super.init(healthStore: healthStore,
-                   observeHealthKitSamplesFromCurrentApp: false,
+                   observeHealthKitSamplesFromCurrentApp: true,
                    observeHealthKitSamplesFromOtherApps: observeHealthKitSamplesFromOtherApps,
                    type: carbType,
                    observationStart: Date(timeIntervalSinceNow: -self.observationInterval),
@@ -248,7 +252,11 @@ public final class CarbStore: HealthKitSampleStore {
             cacheStore.fetchAnchor(key: CarbStore.healthKitQueryAnchorMetadataKey) { (anchor) in
                 self.queue.async {
                     self.queryAnchor = anchor
-            
+
+                    if !self.authorizationRequired {
+                        self.createQuery()
+                    }
+
                     self.migrateLegacyCarbEntryKeys()
                     
                     semaphore.signal()
@@ -343,7 +351,7 @@ public final class CarbStore: HealthKitSampleStore {
                 return
             }
 
-            self.handleUpdatedCarbData(updateSource: .queriedByHealthKit)
+            self.handleUpdatedCarbData()
             completion(true)
         }
     }
@@ -494,7 +502,7 @@ extension CarbStore {
 
             completion(.success(storedEntry!))
 
-            self.handleUpdatedCarbData(updateSource: .changedInApp)
+            self.handleUpdatedCarbData()
         }
     }
 
@@ -543,13 +551,17 @@ extension CarbStore {
 
             completion(.success(storedEntry!))
 
-            self.handleUpdatedCarbData(updateSource: .changedInApp)
+            self.handleUpdatedCarbData()
         }
     }
 
     private func saveEntryToHealthKit(_ object: CachedCarbObject) {
         dispatchPrecondition(condition: .onQueue(queue))
 
+        guard storeEntriesToHealthKit else {
+            return
+        }
+
         let quantitySample = object.quantitySample
         var error: Error?
 
@@ -617,7 +629,7 @@ extension CarbStore {
 
             completion(.success(true))
 
-            self.handleUpdatedCarbData(updateSource: .changedInApp)
+            self.handleUpdatedCarbData()
         }
     }
 
@@ -782,7 +794,7 @@ extension CarbStore {
 
             completion(error)
 
-            self.handleUpdatedCarbData(updateSource: .changedInApp)
+            self.handleUpdatedCarbData()
         }
     }
 }
@@ -847,7 +859,7 @@ extension CarbStore {
                 return
             }
 
-            self.handleUpdatedCarbData(updateSource: .changedInApp)
+            self.handleUpdatedCarbData()
             completion(nil)
         }
     }
@@ -874,26 +886,26 @@ extension CarbStore {
         return error
     }
 
-    private func handleUpdatedCarbData(updateSource: UpdateSource) {
+    private func handleUpdatedCarbData() {
         dispatchPrecondition(condition: .onQueue(queue))
 
         purgeExpiredCachedCarbObjects()
 
-        NotificationCenter.default.post(name: CarbStore.carbEntriesDidChange, object: self, userInfo: [CarbStore.notificationUpdateSourceKey: updateSource.rawValue])
+        NotificationCenter.default.post(name: CarbStore.carbEntriesDidChange, object: self)
         delegate?.carbStoreHasUpdatedCarbData(self)
     }
 
     private func areAllRelatedObjectsPurgable(to object: CachedCarbObject, before date: Date) throws -> Bool {
         dispatchPrecondition(condition: .onQueue(queue))
 
-        // If no provenance identifier nor sync identifier, then there are no related objects
-        guard let provenanceIdentifier = object.provenanceIdentifier, let syncIdentifier = object.syncIdentifier else {
+        // If no sync identifier, then there are no related objects
+        guard let syncIdentifier = object.syncIdentifier else {
             return true
         }
 
         // Count any that are NOT purgable
         let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
-        request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "provenanceIdentifier == %@", provenanceIdentifier),
+        request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "provenanceIdentifier == %@", object.provenanceIdentifier),
                                                                                 NSPredicate(format: "syncIdentifier == %@", syncIdentifier),
                                                                                 NSPredicate(format: "startDate >= %@", date as NSDate)])
         request.fetchLimit = 1
@@ -1146,7 +1158,7 @@ extension CarbStore {
 // MARK: - Remote Data Service Query
 
 extension CarbStore {
-    public struct QueryAnchor: RawRepresentable {
+    public struct QueryAnchor: Equatable, RawRepresentable {
         public typealias RawValue = [String: Any]
 
         internal var anchorKey: Int64
@@ -1183,7 +1195,7 @@ extension CarbStore {
             var queryError: Error?
 
             guard limit > 0 else {
-                completion(.success(queryAnchor, queryCreatedResult, queryUpdatedResult, queryDeletedResult))
+                completion(.success(queryAnchor, [], [], []))
                 return
             }
 
@@ -1399,7 +1411,7 @@ extension CarbStore {
                     return [
                         "\t",
                         entry.uuid?.uuidString ?? "",
-                        entry.provenanceIdentifier ?? "",
+                        entry.provenanceIdentifier,
                         entry.syncIdentifier ?? "",
                         entry.syncVersion != nil ? String(describing: entry.syncVersion) : "",
                         String(describing: entry.startDate),

+ 4 - 5
Dependencies/LoopKit/LoopKit/CarbKit/HKQuantitySample+CarbKit.swift

@@ -9,7 +9,8 @@
 import HealthKit
 
 
-let MetadataKeyAbsorptionTimeMinutes = "com.loudnate.CarbKit.HKMetadataKey.AbsorptionTimeMinutes"
+let LegacyMetadataKeyAbsorptionTime = "com.loudnate.CarbKit.HKMetadataKey.AbsorptionTimeMinutes"
+let MetadataKeyAbsorptionTime = "com.loopkit.AbsorptionTime"
 let MetadataKeyUserCreatedDate = "com.loopkit.CarbKit.HKMetadataKey.UserCreatedDate"
 let MetadataKeyUserUpdatedDate = "com.loopkit.CarbKit.HKMetadataKey.UserUpdatedDate"
 
@@ -19,10 +20,8 @@ extension HKQuantitySample {
     }
 
     public var absorptionTime: TimeInterval? {
-        guard let absorptionTimeMinutes = metadata?[MetadataKeyAbsorptionTimeMinutes] as? Double else {
-            return nil
-        }
-        return TimeInterval(minutes: absorptionTimeMinutes)
+        return metadata?[MetadataKeyAbsorptionTime] as? TimeInterval
+            ?? metadata?[LegacyMetadataKeyAbsorptionTime] as? TimeInterval
     }
 
     public var createdByCurrentApp: Bool {

+ 5 - 10
Dependencies/LoopKit/LoopKit/CarbKit/StoredCarbEntry.swift

@@ -15,7 +15,7 @@ public struct StoredCarbEntry: CarbEntry, Equatable {
 
     // MARK: - HealthKit Sync Support
 
-    public let provenanceIdentifier: String?
+    public let provenanceIdentifier: String
     public let syncIdentifier: String?
     public let syncVersion: Int?
 
@@ -37,7 +37,7 @@ public struct StoredCarbEntry: CarbEntry, Equatable {
 
     public init(
         uuid: UUID?,
-        provenanceIdentifier: String?,
+        provenanceIdentifier: String,
         syncIdentifier: String?,
         syncVersion: Int?,
         startDate: Date,
@@ -84,7 +84,7 @@ extension StoredCarbEntry: Codable {
     public init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         self.init(uuid: try container.decodeIfPresent(UUID.self, forKey: .uuid),
-                  provenanceIdentifier: try container.decodeIfPresent(String.self, forKey: .provenanceIdentifier),
+                  provenanceIdentifier: try container.decode(String.self, forKey: .provenanceIdentifier),
                   syncIdentifier: try container.decodeIfPresent(String.self, forKey: .syncIdentifier),
                   syncVersion: try container.decodeIfPresent(Int.self, forKey: .syncVersion),
                   startDate: try container.decode(Date.self, forKey: .startDate),
@@ -100,7 +100,7 @@ extension StoredCarbEntry: Codable {
     public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: CodingKeys.self)
         try container.encodeIfPresent(uuid, forKey: .uuid)
-        try container.encodeIfPresent(provenanceIdentifier, forKey: .provenanceIdentifier)
+        try container.encode(provenanceIdentifier, forKey: .provenanceIdentifier)
         try container.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
         try container.encodeIfPresent(syncVersion, forKey: .syncVersion)
         try container.encode(startDate, forKey: .startDate)
@@ -144,14 +144,9 @@ extension StoredCarbEntry {
             return nil
         }
 
-        var provenanceIdentifier: String?
         var syncIdentifier: String?
         var syncVersion: Int?
 
-        if createdByCurrentApp {
-            provenanceIdentifier = HKSource.default().bundleIdentifier
-        }
-
         if let externalID = rawValue["externalId"] as? String {
             syncIdentifier = externalID
             syncVersion = 1
@@ -159,7 +154,7 @@ extension StoredCarbEntry {
 
         self.init(
             uuid: uuid,
-            provenanceIdentifier: provenanceIdentifier,
+            provenanceIdentifier: createdByCurrentApp ? HKSource.default().bundleIdentifier : "",
             syncIdentifier: syncIdentifier,
             syncVersion: syncVersion,
             startDate: startDate,

+ 2 - 2
Dependencies/LoopKit/LoopKit/CarbKit/SyncCarbObject.swift

@@ -22,7 +22,7 @@ public struct SyncCarbObject: Codable, Equatable {
     public let grams: Double
     public let startDate: Date
     public let uuid: UUID?
-    public let provenanceIdentifier: String?
+    public let provenanceIdentifier: String
     public let syncIdentifier: String?
     public let syncVersion: Int?
     public let userCreatedDate: Date?
@@ -38,7 +38,7 @@ public struct SyncCarbObject: Codable, Equatable {
                 grams: Double,
                 startDate: Date,
                 uuid: UUID?,
-                provenanceIdentifier: String?,
+                provenanceIdentifier: String,
                 syncIdentifier: String?,
                 syncVersion: Int?,
                 userCreatedDate: Date?,

+ 67 - 0
Dependencies/LoopKit/LoopKit/CorrectionRangeOverrides.swift

@@ -23,6 +23,18 @@ public struct CorrectionRangeOverrides: Equatable {
         ranges[.workout] = workout?.quantityRange(for: unit)
     }
 
+    public init(preMeal: GlucoseRange?, workout: GlucoseRange?) {
+        ranges = [:]
+        ranges[.preMeal] = preMeal?.quantityRange
+        ranges[.workout] = workout?.quantityRange
+    }
+
+    public init(preMeal: ClosedRange<HKQuantity>?, workout: ClosedRange<HKQuantity>?) {
+        ranges = [:]
+        ranges[.preMeal] = preMeal
+        ranges[.workout] = workout
+    }
+
     public var preMeal: ClosedRange<HKQuantity>? { ranges[.preMeal] }
     public var workout: ClosedRange<HKQuantity>? { ranges[.workout] }
 }
@@ -44,3 +56,58 @@ public extension CorrectionRangeOverrides.Preset {
         }
     }
 }
+
+extension CorrectionRangeOverrides: Codable {
+    fileprivate var codingGlucoseUnit: HKUnit {
+        return .milligramsPerDeciliter
+    }
+
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let preMealGlucoseRange = try container.decodeIfPresent(GlucoseRange.self, forKey: .preMealRange)
+        let workoutGlucoseRange = try container.decodeIfPresent(GlucoseRange.self, forKey: .workoutRange)
+
+        self.ranges = [:]
+        self.ranges[.preMeal] = preMealGlucoseRange?.quantityRange
+        self.ranges[.workout] = workoutGlucoseRange?.quantityRange
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        let preMealGlucoseRange = preMeal?.glucoseRange(for: codingGlucoseUnit)
+        let workoutGlucoseRange = workout?.glucoseRange(for: codingGlucoseUnit)
+        try container.encodeIfPresent(preMealGlucoseRange, forKey: .preMealRange)
+        try container.encodeIfPresent(workoutGlucoseRange, forKey: .workoutRange)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case preMealRange
+        case workoutRange
+        case bloodGlucoseUnit
+    }
+}
+
+extension CorrectionRangeOverrides: RawRepresentable {
+    public typealias RawValue = [String: Any]
+
+    public init?(rawValue: RawValue) {
+        ranges = [:]
+        if let rawPreMealTargetRange = rawValue["preMealTargetRange"] as? GlucoseRange.RawValue {
+            ranges[.preMeal] = GlucoseRange(rawValue: rawPreMealTargetRange)?.quantityRange
+        }
+
+        if let rawWorkoutTargetRange = rawValue["workoutTargetRange"] as? GlucoseRange.RawValue {
+            ranges[.workout] = GlucoseRange(rawValue: rawWorkoutTargetRange)?.quantityRange
+        }
+    }
+
+    public var rawValue: RawValue {
+        var raw: RawValue = [:]
+        let preMealTargetGlucoseRange = preMeal?.glucoseRange(for: codingGlucoseUnit)
+        let workoutTargetGlucoseRange = workout?.glucoseRange(for: codingGlucoseUnit)
+        raw["preMealTargetRange"] = preMealTargetGlucoseRange?.rawValue
+        raw["workoutTargetRange"] = workoutTargetGlucoseRange?.rawValue
+
+        return raw
+    }
+}

+ 49 - 0
Dependencies/LoopKit/LoopKit/DailyQuantitySchedule.swift

@@ -128,8 +128,57 @@ public extension DailyQuantitySchedule where T == Double {
     func lowestValue() -> Double? {
         return valueSchedule.items.min(by: { $0.value < $1.value } )?.value
     }
+
+    var quantities: [RepeatingScheduleValue<HKQuantity>] {
+        return self.items.map {
+            RepeatingScheduleValue<HKQuantity>(startTime: $0.startTime,
+                                               value: HKQuantity(unit: unit, doubleValue: $0.value))
+        }
+    }
+    
+    func quantities(using unit: HKUnit) -> [RepeatingScheduleValue<HKQuantity>] {
+        return self.items.map {
+            RepeatingScheduleValue<HKQuantity>(startTime: $0.startTime,
+                                               value: HKQuantity(unit: unit, doubleValue: $0.value))
+        }
+    }
+    
+    init?(unit: HKUnit,
+          dailyQuantities: [RepeatingScheduleValue<HKQuantity>],
+          timeZone: TimeZone? = nil)
+    {
+        guard let valueSchedule = DailyValueSchedule(
+                dailyItems: dailyQuantities.map {
+                    RepeatingScheduleValue(startTime: $0.startTime, value: $0.value.doubleValue(for: unit))
+                },
+                timeZone: timeZone) else
+        {
+            return nil
+        }
+        
+        self.unit = unit
+        self.valueSchedule = valueSchedule
+    }
 }
 
+public extension DailyQuantitySchedule where T == DoubleRange {
+    init?(unit: HKUnit,
+          dailyQuantities: [RepeatingScheduleValue<ClosedRange<HKQuantity>>],
+          timeZone: TimeZone? = nil)
+    {
+        guard let valueSchedule = DailyValueSchedule(
+                dailyItems: dailyQuantities.map {
+                    RepeatingScheduleValue(startTime: $0.startTime, value: $0.value.doubleRange(for: unit))
+                },
+                timeZone: timeZone) else
+        {
+            return nil
+        }
+
+        self.unit = unit
+        self.valueSchedule = valueSchedule
+    }
+}
 
 extension DailyQuantitySchedule: Equatable where T: Equatable {
     public static func == (lhs: DailyQuantitySchedule<T>, rhs: DailyQuantitySchedule<T>) -> Bool {

+ 2 - 1
Dependencies/LoopKit/LoopKit/DailyValueSchedule.swift

@@ -91,12 +91,13 @@ extension DailySchedule where T: Comparable {
 
 public struct DailyValueSchedule<T>: DailySchedule {
     let referenceTimeInterval: TimeInterval
-    var repeatInterval = TimeInterval(hours: 24)
+    let repeatInterval: TimeInterval
 
     public let items: [RepeatingScheduleValue<T>]
     public var timeZone: TimeZone
 
     public init?(dailyItems: [RepeatingScheduleValue<T>], timeZone: TimeZone? = nil) {
+        self.repeatInterval = TimeInterval(hours: 24)
         self.items = dailyItems.sorted { $0.startTime < $1.startTime }
         self.timeZone = timeZone ?? TimeZone.currentFixed
 

+ 1 - 1
Dependencies/LoopKit/LoopKit/DeliveryLimits.swift

@@ -47,7 +47,7 @@ public extension DeliveryLimits.Setting {
     func localizedDescriptiveText(appName: String) -> String {
         switch self {
         case .maximumBasalRate:
-            return String(format: LocalizedString("Maximum Basal Rate is the highest temporary basal rate %1$@ is allowed to set automatically.", comment: "Descriptive text for maximum basal rate (1: app name)"), appName)
+            return String(format: LocalizedString("Maximum Basal Rate is the highest temporary basal rate %1$@ is allowed to set.", comment: "Descriptive text for maximum basal rate (1: app name)"), appName)
         case .maximumBolus:
             return LocalizedString("Maximum Bolus is the highest bolus amount you can deliver at one time to cover carbs or bring down high glucose.", comment: "Descriptive text for maximum bolus")
         }

+ 10 - 0
Dependencies/LoopKit/LoopKit/DeviceManager/AlertSoundPlayer.swift

@@ -17,6 +17,7 @@ import os.log
 public protocol AlertSoundPlayer {
     func vibrate()
     func play(url: URL)
+    func stopAll()
 }
 
 public class DeviceAVSoundPlayer: AlertSoundPlayer {
@@ -66,6 +67,14 @@ public class DeviceAVSoundPlayer: AlertSoundPlayer {
             }
         }
     }
+    
+    public func stopAll() {
+        DispatchQueue.main.async {
+            for soundEffect in self.players {
+                soundEffect.stop()
+            }
+        }
+    }
 }
 
 public extension DeviceAVSoundPlayer {
@@ -80,6 +89,7 @@ public extension DeviceAVSoundPlayer {
         default:
             if let baseURL = baseURL {
                 if let name = sound.filename {
+                    self.stopAll()
                     self.play(url: baseURL.appendingPathComponent(name))
                 } else {
                     log.default("No file to play for %@", "\(sound)")

+ 96 - 17
Dependencies/LoopKit/LoopKit/DeviceManager/CGMManager.swift

@@ -7,28 +7,66 @@
 
 import HealthKit
 
-
 /// Describes the result of CGM manager operations to fetch and report sensor readings.
 ///
 /// - noData: No new data was available or retrieved
+/// - unreliableData: New glucose data was received, but is not reliable enough to use for therapy
 /// - newData: New glucose data was received and stored
 /// - error: An error occurred while receiving or store data
 public enum CGMReadingResult {
     case noData
+    case unreliableData
     case newData([NewGlucoseSample])
     case error(Error)
 }
 
-public struct CGMManagerStatus {
+public struct CGMManagerStatus: Equatable {
     // Return false if no sensor active, or in a state where no future data is expected without user intervention
     public var hasValidSensorSession: Bool
+
+    public var lastCommunicationDate: Date?
+
+    public var device: HKDevice?
     
-    public init(hasValidSensorSession: Bool) {
+    public init(hasValidSensorSession: Bool, lastCommunicationDate: Date? = nil, device: HKDevice?) {
         self.hasValidSensorSession = hasValidSensorSession
+        self.lastCommunicationDate = lastCommunicationDate
+        self.device = device
+    }
+}
+
+extension CGMManagerStatus: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.hasValidSensorSession = try container.decode(Bool.self, forKey: .hasValidSensorSession)
+        self.lastCommunicationDate = try container.decodeIfPresent(Date.self, forKey: .lastCommunicationDate)
+        self.device = try container.decodeIfPresent(CodableDevice.self, forKey: .device)?.device
     }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(hasValidSensorSession, forKey: .hasValidSensorSession)
+        try container.encodeIfPresent(lastCommunicationDate, forKey: .lastCommunicationDate)
+        try container.encodeIfPresent(device.map { CodableDevice($0) }, forKey: .device)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case hasValidSensorSession
+        case lastCommunicationDate
+        case device
+    }
+}
+
+public protocol CGMManagerStatusObserver: AnyObject {
+    /// Notifies observers of changes in CGMManagerStatus
+    ///
+    /// - Parameter manager: The manager instance
+    /// - Parameter status: The new, updated status. Status includes properties associated with the manager, transmitter, or sensor,
+    ///                     that are not part of an individual sensor reading.
+    func cgmManager(_ manager: CGMManager, didUpdate status: CGMManagerStatus)
 }
 
-public protocol CGMManagerDelegate: DeviceManagerDelegate {
+public protocol CGMManagerDelegate: DeviceManagerDelegate, CGMManagerStatusObserver {
     /// Asks the delegate for a date with which to filter incoming glucose data
     ///
     /// - Parameter manager: The manager instance
@@ -57,13 +95,6 @@ public protocol CGMManagerDelegate: DeviceManagerDelegate {
     /// - Parameter manager: The manager instance
     /// - Returns: The unique prefix for the credential store
     func credentialStoragePrefix(for manager: CGMManager) -> String
-    
-    /// Notifies the delegate of a change in status
-    ///
-    /// - Parameter manager: The manager instance
-    /// - Parameter status: The new, updated status. Status includes properties associated with the manager, transmitter, or sensor,
-    ///                     that are not part of an individual sensor reading.
-    func cgmManager(_ manager: CGMManager, didUpdate status: CGMManagerStatus)
 }
 
 
@@ -77,22 +108,54 @@ public protocol CGMManager: DeviceManager {
 
     /// The length of time to keep samples in HealthKit before removal. Return nil to never remove samples.
     var managedDataInterval: TimeInterval? { get }
+    
+    /// The length of time to delay until storing samples into HealthKit.  Return 0 for no delay.
+    static var healthKitStorageDelay: TimeInterval { get }
 
     var shouldSyncToRemoteService: Bool { get }
 
     var glucoseDisplay: GlucoseDisplayable? { get }
-    
-    /// The representation of the device for use in HealthKit
-    var device: HKDevice? { get }
 
-    /// The current status of the cgm
-    var cgmStatus: CGMManagerStatus { get }
+    /// The current status of the cgm manager
+    var cgmManagerStatus: CGMManagerStatus { get }
 
-    /// Performs a manual fetch of glucose data from the device, if necessary
+    /// The queue on which CGMManagerDelegate methods are called
+    /// Setting to nil resets to a default provided by the manager
+    var delegateQueue: DispatchQueue! { get set }
+
+
+    /// Implementations of this function must call the `completion` block, with the appropriate `CGMReadingResult`
+    /// according to the current available data.
+    /// - If there is new unreliable data, return `.unreliableData`
+    /// - If there is no new data and the current data is unreliable, return `.unreliableData`
+    /// - If there is new reliable data, return `.newData` with the data samples
+    /// - If there is no new data and the current data is reliable, return `.noData`
+    /// - If there is an error, return `.error` with the appropriate error.
     ///
     /// - Parameters:
     ///   - completion: A closure called when operation has completed
     func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) -> Void
+
+    /// Adds an observer of changes in CGMManagerStatus
+    ///
+    /// Observers are held by weak reference.
+    ///
+    /// - Parameters:
+    ///   - observer: The observing object
+    ///   - queue: The queue on which the observer methods should be called
+    func addStatusObserver(_ observer: CGMManagerStatusObserver, queue: DispatchQueue)
+
+    /// Removes an observer of changes in CGMManagerStatus
+    ///
+    /// Since observers are held weakly, calling this method is not required when the observer is deallocated
+    ///
+    /// - Parameter observer: The observing object
+    func removeStatusObserver(_ observer: CGMManagerStatusObserver)
+
+    /// Requests that the manager completes its deletion process
+    ///
+    /// - Parameter completion: Action to take after the CGM manager is deleted
+    func delete(completion: @escaping () -> Void)
 }
 
 
@@ -100,6 +163,9 @@ public extension CGMManager {
     var appURL: URL? {
         return nil
     }
+    
+    /// Default is no delay to store samples in HealthKit
+    static var healthKitStorageDelay: TimeInterval { 0 }
 
     /// Convenience wrapper for notifying the delegate of deletion on the delegate queue
     ///
@@ -111,4 +177,17 @@ public extension CGMManager {
             completion()
         }
     }
+    
+    func addStatusObserver(_ observer: CGMManagerStatusObserver, queue: DispatchQueue) {
+        // optional since a CGM manager may not support status observers
+    }
+
+    func removeStatusObserver(_ observer: CGMManagerStatusObserver) {
+        // optional since a CGM manager may not support status observers
+    }
+
+    /// Override this default behaviour if the CGM Manager needs to complete tasks before being deleted
+    func delete(completion: @escaping () -> Void) {
+        notifyDelegateOfDeletion(completion: completion)
+    }
 }

+ 43 - 0
Dependencies/LoopKit/LoopKit/DeviceManager/CodableDevice.swift

@@ -0,0 +1,43 @@
+//
+//  CodableDevice.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 10/11/21.
+//  Copyright © 2021 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+struct CodableDevice: Codable {
+    let name: String?
+    let manufacturer: String?
+    let model: String?
+    let hardwareVersion: String?
+    let firmwareVersion: String?
+    let softwareVersion: String?
+    let localIdentifier: String?
+    let udiDeviceIdentifier: String?
+
+    init(_ device: HKDevice) {
+        self.name = device.name
+        self.manufacturer = device.manufacturer
+        self.model = device.model
+        self.hardwareVersion = device.hardwareVersion
+        self.firmwareVersion = device.firmwareVersion
+        self.softwareVersion = device.softwareVersion
+        self.localIdentifier = device.localIdentifier
+        self.udiDeviceIdentifier = device.udiDeviceIdentifier
+    }
+
+    var device: HKDevice {
+        return HKDevice(name: name,
+                        manufacturer: manufacturer,
+                        model: model,
+                        hardwareVersion: hardwareVersion,
+                        firmwareVersion: firmwareVersion,
+                        softwareVersion: softwareVersion,
+                        localIdentifier: localIdentifier,
+                        udiDeviceIdentifier: udiDeviceIdentifier)
+    }
+}
+

+ 1 - 0
Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLifecycleProgress.swift

@@ -18,6 +18,7 @@ public protocol DeviceLifecycleProgress {
 
 public enum DeviceLifecycleProgressState: String, Codable {
     case critical
+    case dimmed
     case normalCGM
     case normalPump
     case warning

+ 5 - 2
Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLog.xcdatamodeld/DeviceCommsLog.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17511" systemVersion="20C69" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="Entry" representedClassName=".DeviceLogEntry" syncable="YES">
         <attribute name="deviceIdentifier" optional="YES" attributeType="String"/>
         <attribute name="managerIdentifier" attributeType="String"/>
@@ -10,8 +10,11 @@
         <fetchIndex name="byTimestampIndex">
             <fetchIndexElement property="timestamp" type="Binary" order="ascending"/>
         </fetchIndex>
+        <fetchIndex name="byModificationCounter">
+            <fetchIndexElement property="modificationCounter" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     <elements>
-        <element name="Entry" positionX="-36" positionY="9" width="128" height="133"/>
+        <element name="Entry" positionX="-36" positionY="9" width="128" height="119"/>
     </elements>
 </model>

+ 9 - 2
Dependencies/LoopKit/LoopKit/DeviceManager/DeviceLog/PersistentDeviceLog.swift

@@ -50,16 +50,23 @@ public class PersistentDeviceLog {
     }
     
     public func log(managerIdentifier: String, deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)? = nil) {
+        // Grab timestamp at time of log, in case managedObjectContext is busy
+        let timestamp = Date()
+
         managedObjectContext.perform {
             let entry = DeviceLogEntry(context: self.managedObjectContext)
             entry.managerIdentifier = managerIdentifier
             entry.deviceIdentifier = deviceIdentifier
             entry.type = type
             entry.message = message
-            entry.timestamp = Date()
+            entry.timestamp = timestamp
             do {
                 try self.managedObjectContext.save()
-                self.log.default("Logged: %{public}@ (%{public}@) %{public}@", String(describing: type), deviceIdentifier ?? "", message)
+                if type == .error {
+                    self.log.error("%{public}@ (%{public}@) %{public}@", String(describing: type), deviceIdentifier ?? "", message)
+                } else {
+                    self.log.default("%{public}@ (%{public}@) %{public}@", String(describing: type), deviceIdentifier ?? "", message)
+                }
                 completion?(nil)
             } catch let error {
                 self.log.error("Could not store device log entry %{public}@", String(describing: error))

+ 7 - 34
Dependencies/LoopKit/LoopKit/DeviceManager/DeviceManager.swift

@@ -8,38 +8,20 @@
 import Foundation
 import UserNotifications
 
-public protocol DeviceManagerDelegate: AlertPresenter {
-    // Begin obsolescent code
-    // Note: once all plugins are updated to use the new alert system instead of Notifications, this can be removed.
-    func scheduleNotification(for manager: DeviceManager,
-                              identifier: String,
-                              content: UNNotificationContent,
-                              trigger: UNNotificationTrigger?)
-
-    func clearNotification(for manager: DeviceManager, identifier: String)
-    
-    func removeNotificationRequests(for manager: DeviceManager, identifiers: [String])
-    // End obsolescent code
-    
+public protocol DeviceManagerDelegate: AlertIssuer, PersistedAlertStore {
+    // This will be called from an unspecified queue
     func deviceManager(_ manager: DeviceManager, logEventForDeviceIdentifier deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)?)
 }
 
 public protocol DeviceManager: CustomDebugStringConvertible, AlertResponder, AlertSoundVendor {
     typealias RawStateValue = [String: Any]
 
-    /// The identifier of the manager. This should be unique
-    static var managerIdentifier: String { get }
-
-    /// A title describing this type of manager
-    static var localizedTitle: String { get }
-
+    /// A unique identifier for this manager
+    var managerIdentifier: String { get }
+    
     /// A title describing this manager
     var localizedTitle: String { get }
 
-    /// The queue on which delegate methods are called
-    /// Setting to nil resets to a default provided by the manager
-    var delegateQueue: DispatchQueue! { get set }
-    
     /// Initializes the manager with its previously-saved state
     ///
     /// Return nil if the saved state is invalid to prevent restoration
@@ -49,16 +31,7 @@ public protocol DeviceManager: CustomDebugStringConvertible, AlertResponder, Ale
 
     /// The current, serializable state of the manager
     var rawState: RawStateValue { get }
-}
 
-
-public extension DeviceManager {
-    var localizedTitle: String {
-        return type(of: self).localizedTitle
-    }
-    
-    /// Represents a per-device-manager-Type identifier that can uniquely identify a class of this type.
-    var managerIdentifier: String {
-        return Self.managerIdentifier
-    }
+    /// Is the device manager onboarded and ready for use?
+    var isOnboarded: Bool { get }
 }

+ 4 - 5
Dependencies/LoopKit/LoopKit/DeviceManager/DeviceStatusHighlight.swift

@@ -6,9 +6,7 @@
 //  Copyright © 2020 LoopKit Authors. All rights reserved.
 //
 
-import UIKit
-
-public protocol DeviceStatusHighlight {
+public protocol DeviceStatusHighlight: Codable {
     /// a localized message from the device
     var localizedMessage: String { get }
 
@@ -19,10 +17,11 @@ public protocol DeviceStatusHighlight {
     var state: DeviceStatusHighlightState { get }
 }
 
-public enum DeviceStatusHighlightState: String, Codable {
+public typealias DeviceStatusHighlightState = DeviceStatusElementState
+
+public enum DeviceStatusElementState: String, Codable {
     case critical
     case normalCGM
     case normalPump
     case warning
 }
-

+ 60 - 23
Dependencies/LoopKit/LoopKit/DeviceManager/PumpManager.swift

@@ -8,7 +8,6 @@
 import Foundation
 import HealthKit
 
-
 public enum PumpManagerResult<T> {
     case success(T)
     case failure(PumpManagerError)
@@ -18,6 +17,22 @@ public protocol PumpManagerStatusObserver: AnyObject {
     func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus)
 }
 
+public enum BolusActivationType: String, Codable {
+    case manualNoRecommendation
+    case manualRecommendationAccepted
+    case manualRecommendationChanged
+    case automatic
+
+    public var isAutomatic: Bool {
+        self == .automatic
+    }
+
+    static public func activationTypeFor(recommendedAmount: Double?, bolusAmount: Double) -> BolusActivationType {
+        guard let recommendedAmount = recommendedAmount else { return .manualNoRecommendation }
+        return recommendedAmount =~ bolusAmount ? .manualRecommendationAccepted : .manualRecommendationChanged
+    }
+}
+
 public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObserver {
     func pumpManagerBLEHeartbeatDidFire(_ pumpManager: PumpManager)
 
@@ -29,6 +44,9 @@ public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObs
     /// Informs the delegate that the manager is deactivating and should be deleted
     func pumpManagerWillDeactivate(_ pumpManager: PumpManager)
 
+    /// Informs the delegate that the hardware this PumpManager has been reporting for has been replaced.
+    func pumpManagerPumpWasReplaced(_ pumpManager: PumpManager)
+
     /// Triggered when pump model changes. With a more formalized setup flow (which requires a successful model fetch),
     /// this delegate method could go away.
     func pumpManager(_ pumpManager: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents pumpRecordsBasalProfileStartEvents: Bool)
@@ -36,7 +54,8 @@ public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObs
     /// Reports an error that should be surfaced to the user
     func pumpManager(_ pumpManager: PumpManager, didError error: PumpManagerError)
 
-    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void)
+    /// This should be called any time the PumpManager synchronizes with the pump, even if there are no new events in the log.
+    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastSync: Date?, completion: @escaping (_ error: Error?) -> Void)
 
     func pumpManager(_ pumpManager: PumpManager, didReadReservoirValue units: Double, at date: Date, completion: @escaping (_ result: Result<(newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error>) -> Void)
 
@@ -44,13 +63,30 @@ public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObs
 
     func pumpManagerDidUpdateState(_ pumpManager: PumpManager)
 
-    func pumpManagerRecommendsLoop(_ pumpManager: PumpManager)
-
     func startDateToFilterNewPumpEvents(for manager: PumpManager) -> Date
+
+    /// Indicates the system time offset from a trusted time source. If the return value is added to the system time, the result is the trusted time source value. If the trusted time source is earlier than the system time, the return value is negative.
+    var detectedSystemTimeOffset: TimeInterval { get }
 }
 
 
 public protocol PumpManager: DeviceManager {
+    /// The maximum number of scheduled basal rates in a single day supported by the pump. Used during onboarding by therapy settings.
+    static var onboardingMaximumBasalScheduleEntryCount: Int { get }
+
+    /// All user-selectable basal rates, in Units per Hour. Must be non-empty. Used during onboarding by therapy settings.
+    static var onboardingSupportedBasalRates: [Double] { get }
+
+    /// All user-selectable bolus volumes, in Units. Must be non-empty. Used during onboarding by therapy settings.
+    static var onboardingSupportedBolusVolumes: [Double] { get }
+
+    /// All user-selectable maximum bolus volumes, in Units. Must be non-empty. Used during onboarding by therapy settings.
+    static var onboardingSupportedMaximumBolusVolumes: [Double] { get }
+
+    /// The queue on which PumpManagerDelegate methods are called
+    /// Setting to nil resets to a default provided by the manager
+    var delegateQueue: DispatchQueue! { get set }
+
     /// Rounds a basal rate in U/hr to a rate supported by this pump.
     ///
     /// - Parameters:
@@ -71,6 +107,9 @@ public protocol PumpManager: DeviceManager {
     /// All user-selectable bolus volumes, in Units. Must be non-empty.
     var supportedBolusVolumes: [Double] { get }
 
+    /// All user-selectable bolus volumes for setting the maximum allowed bolus, in Units. Must be non-empty.
+    var supportedMaximumBolusVolumes: [Double] { get }
+
     /// The maximum number of scheduled basal rates in a single day supported by the pump
     var maximumBasalScheduleEntryCount: Int { get }
 
@@ -90,8 +129,8 @@ public protocol PumpManager: DeviceManager {
     /// The maximum reservoir volume of the pump
     var pumpReservoirCapacity: Double { get }
 
-    /// The time of the last reconciliation with the pump's event history
-    var lastReconciliation: Date? { get }
+    /// The time of the last sync with the pump's event history, or last status check if pump does not provide history.
+    var lastSync: Date? { get }
     
     /// The most-recent status
     var status: PumpManagerStatus { get }
@@ -113,10 +152,8 @@ public protocol PumpManager: DeviceManager {
     func removeStatusObserver(_ observer: PumpManagerStatusObserver)
     
     /// Ensure that the pump's data (reservoir/events) is up to date.  If not, fetch it.
-    /// After a successful fetch, the PumpManager should call the completion block.
-    /// Then, it must call the delegate method `pumpManagerRecommendsLoop(_:)` if it has an accurate and up to date understanding of insulin delivery
-    /// and has reported it via the appropriate status observer and delegate calls.
-    func ensureCurrentPumpData(completion: (() -> Void)?)
+    /// The PumpManager should call the completion block with the date of last sync with the pump, nil if no sync has occurred
+    func ensureCurrentPumpData(completion: ((_ lastSync: Date?) -> Void)?)
     
     /// Loop calls this method when the current environment requires the pump to provide its own periodic
     /// scheduling via BLE.
@@ -132,8 +169,8 @@ public protocol PumpManager: DeviceManager {
     ///   - units: The number of units to deliver
     ///   - automatic: Whether the dose was triggered automatically as opposed to commanded by user
     ///   - completion: A closure called after the command is complete
-    ///   - result: A DoseEntry or an error describing why the command failed
-    func enactBolus(units: Double, automatic: Bool, completion: @escaping (_ result: PumpManagerResult<DoseEntry>) -> Void)
+    ///   - error: An optional error describing why the command failed
+    func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: PumpManagerError?) -> Void)
 
     /// Cancels the current, in progress, bolus.
     ///
@@ -146,10 +183,10 @@ public protocol PumpManager: DeviceManager {
     ///
     /// - Parameters:
     ///   - unitsPerHour: The temporary basal rate to set
-    ///   - duration: The duration of the temporary basal rate.
+    ///   - duration: The duration of the temporary basal rate.  If you pass in a duration of 0, that cancels any currently running Temp Basal
     ///   - completion: A closure called after the command is complete
-    ///   - result: A DoseEntry or an error describing why the command failed
-    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (_ result: PumpManagerResult<DoseEntry>) -> Void)
+    ///   - error: An optional error describing why the command failed
+    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (_ error: PumpManagerError?) -> Void)
 
     /// Send a command to the pump to suspend delivery
     ///
@@ -165,14 +202,6 @@ public protocol PumpManager: DeviceManager {
     ///   - error: An error describing why the command failed
     func resumeDelivery(completion: @escaping (_ error: Error?) -> Void)
     
-    /// Notifies the PumpManager of a change in the user's preference for maximum basal rate.
-    ///
-    /// - Parameters:
-    ///   - rate: The maximum rate the pumpmanager should expect to receive in an enactTempBasal command.
-    func setMaximumTempBasalRate(_ rate: Double)
-
-    typealias SyncSchedule = (_ items: [RepeatingScheduleValue<Double>], _ completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) -> Void
-
     /// Sync the schedule of basal rates to the pump, annotating the result with the proper time zone.
     ///
     /// - Precondition:
@@ -183,6 +212,14 @@ public protocol PumpManager: DeviceManager {
     ///   - completion: A closure called after the command is complete
     ///   - result: A BasalRateSchedule or an error describing why the command failed
     func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (_ result: Result<BasalRateSchedule, Error>) -> Void)
+
+    /// Sync the delivery limits for basal rate and bolus. If the pump does not support setting max bolus or max basal rates, the completion should be called with success including the provided delivery limits.
+    ///
+    /// - Parameters:
+    ///   - deliveryLimits: The delivery limits
+    ///   - completion: A closure called after the command is complete
+    ///   - result: The delivery limits set or an error describing why the command failed
+    func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (_ result: Result<DeliveryLimits, Error>) -> Void)
 }
 
 

+ 30 - 80
Dependencies/LoopKit/LoopKit/DeviceManager/PumpManagerStatus.swift

@@ -6,36 +6,35 @@
 //
 
 import Foundation
-import UIKit
 import HealthKit
 
-public struct PumpManagerStatus: Equatable {
-    
-    public struct PumpStatusHighlight: DeviceStatusHighlight, Equatable {
-        public var localizedMessage: String
-        
-        public var imageName: String
-        
-        public var state: DeviceStatusHighlightState
-        
-        public init(localizedMessage: String, imageName: String, state: DeviceStatusHighlightState) {
-            self.localizedMessage = localizedMessage
-            self.imageName = imageName
-            self.state = state
-        }
+public struct PumpStatusHighlight: DeviceStatusHighlight, Equatable {
+    public var localizedMessage: String
+
+    public var imageName: String
+
+    public var state: DeviceStatusHighlightState
+
+    public init(localizedMessage: String, imageName: String, state: DeviceStatusHighlightState) {
+        self.localizedMessage = localizedMessage
+        self.imageName = imageName
+        self.state = state
     }
-    
-    public struct PumpLifecycleProgress: DeviceLifecycleProgress, Equatable {
-        public var percentComplete: Double
-        
-        public var progressState: DeviceLifecycleProgressState
-        
-        public init(percentComplete: Double, progressState: DeviceLifecycleProgressState) {
-            self.percentComplete = percentComplete
-            self.progressState = progressState
-        }
+}
+
+public struct PumpLifecycleProgress: DeviceLifecycleProgress, Equatable {
+    public var percentComplete: Double
+
+    public var progressState: DeviceLifecycleProgressState
+
+    public init(percentComplete: Double, progressState: DeviceLifecycleProgressState) {
+        self.percentComplete = percentComplete
+        self.progressState = progressState
     }
-    
+}
+
+public struct PumpManagerStatus: Equatable {
+
     public enum BasalDeliveryState: Equatable {
         case active(_ at: Date)
         case initiatingTempBasal
@@ -69,12 +68,8 @@ public struct PumpManagerStatus: Equatable {
     /// The type of insulin this pump is delivering, nil if pump is in a state where insulin type is unknown; i.e. between reservoirs, or pod changes
     public var insulinType: InsulinType?
 
-    public var pumpStatusHighlight: PumpStatusHighlight?
-    public var pumpLifecycleProgress: PumpLifecycleProgress?
     public var deliveryIsUncertain: Bool
 
-
-
     public init(
         timeZone: TimeZone,
         device: HKDevice,
@@ -82,8 +77,6 @@ public struct PumpManagerStatus: Equatable {
         basalDeliveryState: BasalDeliveryState?,
         bolusState: BolusState,
         insulinType: InsulinType?,
-        pumpStatusHighlight: PumpStatusHighlight? = nil,
-        pumpLifecycleProgress: PumpLifecycleProgress? = nil,
         deliveryIsUncertain: Bool = false
     ) {
         self.timeZone = timeZone
@@ -92,8 +85,6 @@ public struct PumpManagerStatus: Equatable {
         self.basalDeliveryState = basalDeliveryState
         self.bolusState = bolusState
         self.insulinType = insulinType
-        self.pumpStatusHighlight = pumpStatusHighlight
-        self.pumpLifecycleProgress = pumpLifecycleProgress
         self.deliveryIsUncertain = deliveryIsUncertain
     }
 }
@@ -102,13 +93,11 @@ extension PumpManagerStatus: Codable {
     public init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         self.timeZone = try container.decode(TimeZone.self, forKey: .timeZone)
-        self.device = (try container.decode(CodableDevice.self, forKey: .device)).device
+        self.device = try container.decode(CodableDevice.self, forKey: .device).device
         self.pumpBatteryChargeRemaining = try container.decodeIfPresent(Double.self, forKey: .pumpBatteryChargeRemaining)
         self.basalDeliveryState = try container.decodeIfPresent(BasalDeliveryState.self, forKey: .basalDeliveryState)
         self.bolusState = try container.decode(BolusState.self, forKey: .bolusState)
-        self.insulinType = try container.decode(InsulinType.self, forKey: .insulinType)
-        self.pumpStatusHighlight = try container.decodeIfPresent(PumpStatusHighlight.self, forKey: .pumpStatusHighlight)
-        self.pumpLifecycleProgress = try container.decodeIfPresent(PumpLifecycleProgress.self, forKey: .pumpLifecycleProgress)
+        self.insulinType = try container.decodeIfPresent(InsulinType.self, forKey: .insulinType)
         self.deliveryIsUncertain = try container.decode(Bool.self, forKey: .deliveryIsUncertain)
     }
 
@@ -119,45 +108,10 @@ extension PumpManagerStatus: Codable {
         try container.encodeIfPresent(pumpBatteryChargeRemaining, forKey: .pumpBatteryChargeRemaining)
         try container.encodeIfPresent(basalDeliveryState, forKey: .basalDeliveryState)
         try container.encode(bolusState, forKey: .bolusState)
-        try container.encode(insulinType, forKey: .insulinType)
-        try container.encodeIfPresent(pumpStatusHighlight, forKey: .pumpStatusHighlight)
-        try container.encodeIfPresent(pumpLifecycleProgress, forKey: .pumpLifecycleProgress)
+        try container.encodeIfPresent(insulinType, forKey: .insulinType)
         try container.encode(deliveryIsUncertain, forKey: .deliveryIsUncertain)
     }
 
-    private struct CodableDevice: Codable {
-        let name: String?
-        let manufacturer: String?
-        let model: String?
-        let hardwareVersion: String?
-        let firmwareVersion: String?
-        let softwareVersion: String?
-        let localIdentifier: String?
-        let udiDeviceIdentifier: String?
-
-        init(_ device: HKDevice) {
-            self.name = device.name
-            self.manufacturer = device.manufacturer
-            self.model = device.model
-            self.hardwareVersion = device.hardwareVersion
-            self.firmwareVersion = device.firmwareVersion
-            self.softwareVersion = device.softwareVersion
-            self.localIdentifier = device.localIdentifier
-            self.udiDeviceIdentifier = device.udiDeviceIdentifier
-        }
-
-        var device: HKDevice {
-            return HKDevice(name: name,
-                            manufacturer: manufacturer,
-                            model: model,
-                            hardwareVersion: hardwareVersion,
-                            firmwareVersion: firmwareVersion,
-                            softwareVersion: softwareVersion,
-                            localIdentifier: localIdentifier,
-                            udiDeviceIdentifier: udiDeviceIdentifier)
-        }
-    }
-
     private enum CodingKeys: String, CodingKey {
         case timeZone
         case device
@@ -165,8 +119,6 @@ extension PumpManagerStatus: Codable {
         case basalDeliveryState
         case bolusState
         case insulinType
-        case pumpStatusHighlight
-        case pumpLifecycleProgress
         case deliveryIsUncertain
     }
 }
@@ -301,9 +253,9 @@ extension PumpManagerStatus.BolusState: Codable {
     }
 }
 
-extension PumpManagerStatus.PumpStatusHighlight: Codable { }
+extension PumpStatusHighlight: Codable { }
 
-extension PumpManagerStatus.PumpLifecycleProgress: Codable { }
+extension PumpLifecycleProgress: Codable { }
 
 extension PumpManagerStatus: CustomDebugStringConvertible {
     public var debugDescription: String {
@@ -315,8 +267,6 @@ extension PumpManagerStatus: CustomDebugStringConvertible {
         * basalDeliveryState: \(basalDeliveryState as Any)
         * bolusState: \(bolusState)
         * insulinType: \(insulinType as Any)
-        * pumpStatusHighlight: \(pumpStatusHighlight as Any)
-        * pumpLifecycleProgress: \(pumpLifecycleProgress as Any)
         * deliveryIsUncertain: \(deliveryIsUncertain)
         """
     }

+ 0 - 65
Dependencies/LoopKit/LoopKit/DiagnosticLog.swift

@@ -1,65 +0,0 @@
-//
-//  DiagnosticLog.swift
-//  LoopKit
-//
-//  Created by Darin Krauss on 6/12/19.
-//  Copyright © 2019 LoopKit Authors. All rights reserved.
-//
-
-import os.log
-
-public class DiagnosticLog {
-
-    private let subsystem: String
-
-    private let category: String
-
-    private let log: OSLog
-
-    public init(subsystem: String, category: String) {
-        self.subsystem = subsystem
-        self.category = category
-        self.log = OSLog(subsystem: subsystem, category: category)
-    }
-
-    public func debug(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .debug, args)
-    }
-
-    public func info(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .info, args)
-    }
-
-    public func `default`(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .default, args)
-    }
-
-    public func error(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .error, args)
-    }
-
-    private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) {
-        switch args.count {
-        case 0:
-            os_log(message, log: log, type: type)
-        case 1:
-            os_log(message, log: log, type: type, args[0])
-        case 2:
-            os_log(message, log: log, type: type, args[0], args[1])
-        case 3:
-            os_log(message, log: log, type: type, args[0], args[1], args[2])
-        case 4:
-            os_log(message, log: log, type: type, args[0], args[1], args[2], args[3])
-        case 5:
-            os_log(message, log: log, type: type, args[0], args[1], args[2], args[3], args[4])
-        default:
-            os_log(message, log: log, type: type, args)
-        }
-
-        guard let sharedLogging = SharedLogging.instance else {
-            return
-        }
-        sharedLogging.log(message, subsystem: subsystem, category: category, type: type, args)
-    }
-
-}

+ 1 - 1
Dependencies/LoopKit/LoopKit/DosingDecisionObject+CoreDataClass.swift

@@ -8,7 +8,7 @@
 
 import CoreData
 
-public class DosingDecisionObject: NSManagedObject {
+class DosingDecisionObject: NSManagedObject {
     var hasUpdatedModificationCounter: Bool { changedValues().keys.contains("modificationCounter") }
 
     func updateModificationCounter() { setPrimitiveValue(managedObjectContext!.modificationCounter!, forKey: "modificationCounter") }

+ 18 - 0
Dependencies/LoopKit/LoopKit/DosingDecisionObject+CoreDataProperties.swift

@@ -18,3 +18,21 @@ extension DosingDecisionObject {
     @NSManaged public var date: Date
     @NSManaged public var modificationCounter: Int64
 }
+
+extension DosingDecisionObject: Encodable {
+    func encode(to encoder: Encoder) throws {
+        try EncodableDosingDecisionObject(self).encode(to: encoder)
+    }
+}
+
+fileprivate struct EncodableDosingDecisionObject: Encodable {
+    var data: StoredDosingDecision
+    var date: Date
+    var modificationCounter: Int64
+
+    init(_ object: DosingDecisionObject) throws {
+        self.data = try PropertyListDecoder().decode(StoredDosingDecision.self, from: object.data)
+        self.date = object.date
+        self.modificationCounter = object.modificationCounter
+    }
+}

+ 255 - 104
Dependencies/LoopKit/LoopKit/DosingDecisionStore.swift

@@ -32,13 +32,15 @@ public class DosingDecisionStore {
         self.expireAfter = expireAfter
     }
 
-    public func storeDosingDecisionData(_ dosingDecisionData: StoredDosingDecisionData, completion: @escaping () -> Void) {
+    public func storeDosingDecision(_ dosingDecision: StoredDosingDecision, completion: @escaping () -> Void) {
         dataAccessQueue.async {
-            self.store.managedObjectContext.performAndWait {
-                let object = DosingDecisionObject(context: self.store.managedObjectContext)
-                object.date = dosingDecisionData.date
-                object.data = dosingDecisionData.data
-                self.store.save()
+            if let data = self.encodeDosingDecision(dosingDecision) {
+                self.store.managedObjectContext.performAndWait {
+                    let object = DosingDecisionObject(context: self.store.managedObjectContext)
+                    object.data = data
+                    object.date = dosingDecision.date
+                    self.store.save()
+                }
             }
 
             self.purgeExpiredDosingDecisions()
@@ -83,10 +85,36 @@ public class DosingDecisionStore {
         delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
         completion?(nil)
     }
+    
+    private static var encoder: PropertyListEncoder = {
+        let encoder = PropertyListEncoder()
+        encoder.outputFormat = .binary
+        return encoder
+    }()
+
+    private func encodeDosingDecision(_ dosingDecision: StoredDosingDecision) -> Data? {
+        do {
+            return try Self.encoder.encode(dosingDecision)
+        } catch let error {
+            self.log.error("Error encoding StoredDosingDecision: %@", String(describing: error))
+            return nil
+        }
+    }
+
+    private static var decoder = PropertyListDecoder()
+
+    private func decodeDosingDecision(fromData data: Data) -> StoredDosingDecision? {
+        do {
+            return try Self.decoder.decode(StoredDosingDecision.self, from: data)
+        } catch let error {
+            self.log.error("Error decoding StoredDosingDecision: %@", String(describing: error))
+            return nil
+        }
+    }
 }
 
 extension DosingDecisionStore {
-    public struct QueryAnchor: RawRepresentable {
+    public struct QueryAnchor: Equatable, RawRepresentable {
         public typealias RawValue = [String: Any]
         
         internal var modificationCounter: Int64
@@ -109,19 +137,19 @@ extension DosingDecisionStore {
         }
     }
     
-    public enum DosingDecisionDataQueryResult {
-        case success(QueryAnchor, [StoredDosingDecisionData])
+    public enum DosingDecisionQueryResult {
+        case success(QueryAnchor, [StoredDosingDecision])
         case failure(Error)
     }
     
-    public func executeDosingDecisionDataQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (DosingDecisionDataQueryResult) -> Void) {
+    public func executeDosingDecisionQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (DosingDecisionQueryResult) -> Void) {
         dataAccessQueue.async {
             var queryAnchor = queryAnchor ?? QueryAnchor()
             var queryResult = [StoredDosingDecisionData]()
             var queryError: Error?
 
             guard limit > 0 else {
-                completion(.success(queryAnchor, queryResult))
+                completion(.success(queryAnchor, []))
                 return
             }
 
@@ -149,7 +177,11 @@ extension DosingDecisionStore {
                 return
             }
 
-            completion(.success(queryAnchor, queryResult))
+            // Decoding a large number of dosing decision can be very CPU intensive and may take considerable wall clock time.
+            // Do not block DosingDecisionStore dataAccessQueue. Perform work and callback in global utility queue.
+            DispatchQueue.global(qos: .utility).async {
+                completion(.success(queryAnchor, queryResult.compactMap { self.decodeDosingDecision(fromData: $0.data) }))
+            }
         }
     }
 }
@@ -164,70 +196,109 @@ public struct StoredDosingDecisionData {
     }
 }
 
+public typealias HistoricalGlucoseValue = PredictedGlucoseValue
+
 public struct StoredDosingDecision {
-    public let date: Date
-    public let insulinOnBoard: InsulinValue?
-    public let carbsOnBoard: CarbValue?
-    public let scheduleOverride: TemporaryScheduleOverride?
-    public let glucoseTargetRangeSchedule: GlucoseRangeSchedule?
-    public let effectiveGlucoseTargetRangeSchedule: GlucoseRangeSchedule?
-    public let predictedGlucose: [PredictedGlucoseValue]?
-    public let predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]?
-    public let lastReservoirValue: LastReservoirValue?
-    public let manualGlucose: SimpleGlucoseValue?
-    public let originalCarbEntry: StoredCarbEntry?
-    public let carbEntry: StoredCarbEntry?
-    public let automaticDoseRecommendation: AutomaticDoseRecommendationWithDate?
-    public let recommendedBolus: BolusRecommendationWithDate?
-    public let requestedBolus: Double?
-    public let pumpManagerStatus: PumpManagerStatus?
-    public let notificationSettings: NotificationSettings?
-    public let deviceSettings: DeviceSettings?
-    public let errors: [Error]?
-    public let syncIdentifier: String
+    public var date: Date
+    public var controllerTimeZone: TimeZone
+    public var reason: String
+    public var settings: Settings?
+    public var scheduleOverride: TemporaryScheduleOverride?
+    public var controllerStatus: ControllerStatus?
+    public var pumpManagerStatus: PumpManagerStatus?
+    public var pumpStatusHighlight: StoredDeviceHighlight?
+    public var cgmManagerStatus: CGMManagerStatus?
+    public var lastReservoirValue: LastReservoirValue?
+    public var historicalGlucose: [HistoricalGlucoseValue]?
+    public var originalCarbEntry: StoredCarbEntry?
+    public var carbEntry: StoredCarbEntry?
+    public var manualGlucoseSample: StoredGlucoseSample?
+    public var carbsOnBoard: CarbValue?
+    public var insulinOnBoard: InsulinValue?
+    public var glucoseTargetRangeSchedule: GlucoseRangeSchedule?
+    public var predictedGlucose: [PredictedGlucoseValue]?
+    public var automaticDoseRecommendation: AutomaticDoseRecommendation?
+    public var manualBolusRecommendation: ManualBolusRecommendationWithDate?
+    public var manualBolusRequested: Double?
+    public var warnings: [Issue]
+    public var errors: [Issue]
+    public var syncIdentifier: UUID
 
     public init(date: Date = Date(),
-                insulinOnBoard: InsulinValue? = nil,
-                carbsOnBoard: CarbValue? = nil,
+                controllerTimeZone: TimeZone = TimeZone.current,
+                reason: String,
+                settings: Settings? = nil,
                 scheduleOverride: TemporaryScheduleOverride? = nil,
-                glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
-                effectiveGlucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
-                predictedGlucose: [PredictedGlucoseValue]? = nil,
-                predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]? = nil,
+                controllerStatus: ControllerStatus? = nil,
+                pumpManagerStatus: PumpManagerStatus? = nil,
+                pumpStatusHighlight: StoredDeviceHighlight? = nil,
+                cgmManagerStatus: CGMManagerStatus? = nil,
                 lastReservoirValue: LastReservoirValue? = nil,
-                manualGlucose: SimpleGlucoseValue? = nil,
+                historicalGlucose: [HistoricalGlucoseValue]? = nil,
                 originalCarbEntry: StoredCarbEntry? = nil,
                 carbEntry: StoredCarbEntry? = nil,
-                automaticDoseRecommendation: AutomaticDoseRecommendationWithDate? = nil,
-                recommendedBolus: BolusRecommendationWithDate? = nil,
-                requestedBolus: Double? = nil,
-                pumpManagerStatus: PumpManagerStatus? = nil,
-                notificationSettings: NotificationSettings? = nil,
-                deviceSettings: DeviceSettings? = nil,
-                errors: [Error]? = nil,
-                syncIdentifier: String = UUID().uuidString) {
+                manualGlucoseSample: StoredGlucoseSample? = nil,
+                carbsOnBoard: CarbValue? = nil,
+                insulinOnBoard: InsulinValue? = nil,
+                glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
+                predictedGlucose: [PredictedGlucoseValue]? = nil,
+                automaticDoseRecommendation: AutomaticDoseRecommendation? = nil,
+                manualBolusRecommendation: ManualBolusRecommendationWithDate? = nil,
+                manualBolusRequested: Double? = nil,
+                warnings: [Issue] = [],
+                errors: [Issue] = [],
+                syncIdentifier: UUID = UUID()) {
         self.date = date
-        self.insulinOnBoard = insulinOnBoard
-        self.carbsOnBoard = carbsOnBoard
+        self.controllerTimeZone = controllerTimeZone
+        self.reason = reason
+        self.settings = settings
         self.scheduleOverride = scheduleOverride
-        self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
-        self.effectiveGlucoseTargetRangeSchedule = effectiveGlucoseTargetRangeSchedule
-        self.predictedGlucose = predictedGlucose
-        self.predictedGlucoseIncludingPendingInsulin = predictedGlucoseIncludingPendingInsulin
+        self.controllerStatus = controllerStatus
+        self.pumpManagerStatus = pumpManagerStatus
+        self.pumpStatusHighlight = pumpStatusHighlight
+        self.cgmManagerStatus = cgmManagerStatus
         self.lastReservoirValue = lastReservoirValue
-        self.manualGlucose = manualGlucose
+        self.historicalGlucose = historicalGlucose
         self.originalCarbEntry = originalCarbEntry
         self.carbEntry = carbEntry
+        self.manualGlucoseSample = manualGlucoseSample
+        self.carbsOnBoard = carbsOnBoard
+        self.insulinOnBoard = insulinOnBoard
+        self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
+        self.predictedGlucose = predictedGlucose
         self.automaticDoseRecommendation = automaticDoseRecommendation
-        self.recommendedBolus = recommendedBolus
-        self.requestedBolus = requestedBolus
-        self.pumpManagerStatus = pumpManagerStatus
-        self.notificationSettings = notificationSettings
-        self.deviceSettings = deviceSettings
+        self.manualBolusRecommendation = manualBolusRecommendation
+        self.manualBolusRequested = manualBolusRequested
+        self.warnings = warnings
         self.errors = errors
         self.syncIdentifier = syncIdentifier
     }
 
+    public struct Settings: Codable, Equatable {
+        public let syncIdentifier: UUID
+
+        public init(syncIdentifier: UUID) {
+            self.syncIdentifier = syncIdentifier
+        }
+    }
+
+    public struct ControllerStatus: Codable, Equatable {
+        public enum BatteryState: String, Codable {
+            case unknown
+            case unplugged
+            case charging
+            case full
+        }
+
+        public let batteryState: BatteryState?
+        public let batteryLevel: Float?
+
+        public init(batteryState: BatteryState? = nil, batteryLevel: Float? = nil) {
+            self.batteryState = batteryState
+            self.batteryLevel = batteryLevel
+        }
+    }
+
     public struct LastReservoirValue: Codable {
         public let startDate: Date
         public let unitVolume: Double
@@ -238,60 +309,132 @@ public struct StoredDosingDecision {
         }
     }
 
-    public struct AutomaticDoseRecommendationWithDate: Codable {
-        public let recommendation: AutomaticDoseRecommendation
-        public let date: Date
+    public struct Issue: Codable, Equatable {
+        public let id: String
+        public let details: [String: String]?
 
-        public init(recommendation: AutomaticDoseRecommendation, date: Date) {
-            self.recommendation = recommendation
-            self.date = date
+        public init(id: String, details: [String: String]? = nil) {
+            self.id = id
+            self.details = details?.isEmpty == false ? details : nil
         }
     }
 
-    public struct BolusRecommendationWithDate: Codable {
-        public let recommendation: ManualBolusRecommendation
-        public let date: Date
+    public struct StoredDeviceHighlight: Codable, Equatable, DeviceStatusHighlight {
+        public var localizedMessage: String
+        public var imageName: String
+        public var state: DeviceStatusHighlightState
 
-        public init(recommendation: ManualBolusRecommendation, date: Date) {
-            self.recommendation = recommendation
-            self.date = date
+        public init(localizedMessage: String, imageName: String, state: DeviceStatusHighlightState) {
+            self.localizedMessage = localizedMessage
+            self.imageName = imageName
+            self.state = state
         }
     }
+}
 
-    public struct DeviceSettings: Codable, Equatable {
-        let name: String
-        let systemName: String
-        let systemVersion: String
-        let model: String
-        let modelIdentifier: String
-        let batteryLevel: Float?
-        let batteryState: BatteryState?
-
-        public init(name: String, systemName: String, systemVersion: String, model: String, modelIdentifier: String, batteryLevel: Float? = nil, batteryState: BatteryState? = nil) {
-            self.name = name
-            self.systemName = systemName
-            self.systemVersion = systemVersion
-            self.model = model
-            self.modelIdentifier = modelIdentifier
-            self.batteryLevel = batteryLevel
-            self.batteryState = batteryState
-        }
+public struct ManualBolusRecommendationWithDate: Codable {
+    public let recommendation: ManualBolusRecommendation
+    public let date: Date
 
-        public enum BatteryState: String, Codable {
-            case unknown
-            case unplugged
-            case charging
-            case full
-        }
+    public init(recommendation: ManualBolusRecommendation, date: Date) {
+        self.recommendation = recommendation
+        self.date = date
+    }
+}
+
+extension StoredDosingDecision: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.init(date: try container.decode(Date.self, forKey: .date),
+                  controllerTimeZone: try container.decode(TimeZone.self, forKey: .controllerTimeZone),
+                  reason: try container.decode(String.self, forKey: .reason),
+                  settings: try container.decodeIfPresent(Settings.self, forKey: .settings),
+                  scheduleOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .scheduleOverride),
+                  controllerStatus: try container.decodeIfPresent(ControllerStatus.self, forKey: .controllerStatus),
+                  pumpManagerStatus: try container.decodeIfPresent(PumpManagerStatus.self, forKey: .pumpManagerStatus),
+                  pumpStatusHighlight: try container.decodeIfPresent(StoredDeviceHighlight.self, forKey: .pumpStatusHighlight),
+                  cgmManagerStatus: try container.decodeIfPresent(CGMManagerStatus.self, forKey: .cgmManagerStatus),
+                  lastReservoirValue: try container.decodeIfPresent(LastReservoirValue.self, forKey: .lastReservoirValue),
+                  historicalGlucose: try container.decodeIfPresent([HistoricalGlucoseValue].self, forKey: .historicalGlucose),
+                  originalCarbEntry: try container.decodeIfPresent(StoredCarbEntry.self, forKey: .originalCarbEntry),
+                  carbEntry: try container.decodeIfPresent(StoredCarbEntry.self, forKey: .carbEntry),
+                  manualGlucoseSample: try container.decodeIfPresent(StoredGlucoseSample.self, forKey: .manualGlucoseSample),
+                  carbsOnBoard: try container.decodeIfPresent(CarbValue.self, forKey: .carbsOnBoard),
+                  insulinOnBoard: try container.decodeIfPresent(InsulinValue.self, forKey: .insulinOnBoard),
+                  glucoseTargetRangeSchedule: try container.decodeIfPresent(GlucoseRangeSchedule.self, forKey: .glucoseTargetRangeSchedule),
+                  predictedGlucose: try container.decodeIfPresent([PredictedGlucoseValue].self, forKey: .predictedGlucose),
+                  automaticDoseRecommendation: try container.decodeIfPresent(AutomaticDoseRecommendation.self, forKey: .automaticDoseRecommendation),
+                  manualBolusRecommendation: try container.decodeIfPresent(ManualBolusRecommendationWithDate.self, forKey: .manualBolusRecommendation),
+                  manualBolusRequested: try container.decodeIfPresent(Double.self, forKey: .manualBolusRequested),
+                  warnings: try container.decodeIfPresent([Issue].self, forKey: .warnings) ?? [],
+                  errors: try container.decodeIfPresent([Issue].self, forKey: .errors) ?? [],
+                  syncIdentifier: try container.decode(UUID.self, forKey: .syncIdentifier))
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(date, forKey: .date)
+        try container.encode(controllerTimeZone, forKey: .controllerTimeZone)
+        try container.encode(reason, forKey: .reason)
+        try container.encodeIfPresent(settings, forKey: .settings)
+        try container.encodeIfPresent(scheduleOverride, forKey: .scheduleOverride)
+        try container.encodeIfPresent(controllerStatus, forKey: .controllerStatus)
+        try container.encodeIfPresent(pumpManagerStatus, forKey: .pumpManagerStatus)
+        try container.encodeIfPresent(pumpStatusHighlight, forKey: .pumpStatusHighlight)
+        try container.encodeIfPresent(cgmManagerStatus, forKey: .cgmManagerStatus)
+        try container.encodeIfPresent(lastReservoirValue, forKey: .lastReservoirValue)
+        try container.encodeIfPresent(historicalGlucose, forKey: .historicalGlucose)
+        try container.encodeIfPresent(originalCarbEntry, forKey: .originalCarbEntry)
+        try container.encodeIfPresent(carbEntry, forKey: .carbEntry)
+        try container.encodeIfPresent(manualGlucoseSample, forKey: .manualGlucoseSample)
+        try container.encodeIfPresent(carbsOnBoard, forKey: .carbsOnBoard)
+        try container.encodeIfPresent(insulinOnBoard, forKey: .insulinOnBoard)
+        try container.encodeIfPresent(glucoseTargetRangeSchedule, forKey: .glucoseTargetRangeSchedule)
+        try container.encodeIfPresent(predictedGlucose, forKey: .predictedGlucose)
+        try container.encodeIfPresent(automaticDoseRecommendation, forKey: .automaticDoseRecommendation)
+        try container.encodeIfPresent(manualBolusRecommendation, forKey: .manualBolusRecommendation)
+        try container.encodeIfPresent(manualBolusRequested, forKey: .manualBolusRequested)
+        try container.encodeIfPresent(!warnings.isEmpty ? warnings : nil, forKey: .warnings)
+        try container.encodeIfPresent(!errors.isEmpty ? errors : nil, forKey: .errors)
+        try container.encode(syncIdentifier, forKey: .syncIdentifier)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case date
+        case controllerTimeZone
+        case reason
+        case settings
+        case scheduleOverride
+        case controllerStatus
+        case pumpManagerStatus
+        case pumpStatusHighlight
+        case cgmManagerStatus
+        case lastReservoirValue
+        case historicalGlucose
+        case originalCarbEntry
+        case carbEntry
+        case manualGlucoseSample
+        case carbsOnBoard
+        case insulinOnBoard
+        case glucoseTargetRangeSchedule
+        case predictedGlucose
+        case automaticDoseRecommendation
+        case manualBolusRecommendation
+        case manualBolusRequested
+        case warnings
+        case errors
+        case syncIdentifier
     }
 }
 
 // MARK: - Critical Event Log Export
 
-extension DosingDecisionStore {
+extension DosingDecisionStore: CriticalEventLog {
     private var exportProgressUnitCountPerObject: Int64 { 33 }
     private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
 
+    public var exportName: String { "DosingDecisions.json" }
+
     public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
         var result: Result<Int64, Error>?
 
@@ -310,7 +453,8 @@ extension DosingDecisionStore {
         return result!
     }
 
-    public func export(startDate: Date, endDate: Date, using encoder: @escaping ([DosingDecisionObject]) throws -> Void, progress: Progress) -> Error? {
+    public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
+        let encoder = JSONStreamEncoder(stream: stream)
         var modificationCounter: Int64 = 0
         var fetching = true
         var error: Error?
@@ -334,7 +478,7 @@ extension DosingDecisionStore {
                         return
                     }
 
-                    try encoder(objects)
+                    try encoder.encode(objects)
 
                     modificationCounter = objects.last!.modificationCounter
 
@@ -345,6 +489,10 @@ extension DosingDecisionStore {
             }
         }
 
+        if let closeError = encoder.close(), error == nil {
+            error = closeError
+        }
+
         return error
     }
 
@@ -360,8 +508,8 @@ extension DosingDecisionStore {
 // MARK: - Core Data (Bulk) - TEST ONLY
 
 extension DosingDecisionStore {
-    public func addStoredDosingDecisionDatas(dosingDecisionDatas: [StoredDosingDecisionData], completion: @escaping (Error?) -> Void) {
-        guard !dosingDecisionDatas.isEmpty else {
+    public func addStoredDosingDecisions(dosingDecisions: [StoredDosingDecision], completion: @escaping (Error?) -> Void) {
+        guard !dosingDecisions.isEmpty else {
             completion(nil)
             return
         }
@@ -370,10 +518,13 @@ extension DosingDecisionStore {
             var error: Error?
 
             self.store.managedObjectContext.performAndWait {
-                for dosingDecisionData in dosingDecisionDatas {
+                for dosingDecision in dosingDecisions {
+                    guard let data = self.encodeDosingDecision(dosingDecision) else {
+                        continue
+                    }
                     let object = DosingDecisionObject(context: self.store.managedObjectContext)
-                    object.date = dosingDecisionData.date
-                    object.data = dosingDecisionData.data
+                    object.data = data
+                    object.date = dosingDecision.date
                 }
                 error = self.store.save()
             }
@@ -383,7 +534,7 @@ extension DosingDecisionStore {
                 return
             }
 
-            self.log.info("Added %d DosingDecisionObjects", dosingDecisionDatas.count)
+            self.log.info("Added %d DosingDecisionObjects", dosingDecisions.count)
             self.delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
             completion(nil)
         }

+ 8 - 0
Dependencies/LoopKit/LoopKit/Extensions/Double.swift

@@ -18,3 +18,11 @@ extension Double: RawRepresentable {
         return self
     }
 }
+
+infix operator =~ : ComparisonPrecedence
+
+ extension Double {
+     static func =~ (lhs: Double, rhs: Double) -> Bool {
+         return fabs(lhs - rhs) < Double.ulpOfOne
+     }
+ }

+ 108 - 5
Dependencies/LoopKit/LoopKit/GlucoseKit/CachedGlucoseObject+CoreDataClass.swift

@@ -21,7 +21,46 @@ class CachedGlucoseObject: NSManagedObject {
         set {
             willChangeValue(forKey: "syncVersion")
             defer { didChangeValue(forKey: "syncVersion") }
-            primitiveSyncVersion = newValue != nil ? NSNumber(value: newValue!) : nil
+            primitiveSyncVersion = newValue.map { NSNumber(value: $0) }
+        }
+    }
+    
+    var device: HKDevice? {
+        get {
+            willAccessValue(forKey: "device")
+            defer { didAccessValue(forKey: "device") }
+            return primitiveDevice.flatMap { try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKDevice.self, from: $0) }
+        }
+        set {
+            willChangeValue(forKey: "device")
+            defer { didChangeValue(forKey: "device") }
+            primitiveDevice = newValue.flatMap { try? NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
+        }
+    }
+    
+    var condition: GlucoseCondition? {
+        get {
+            willAccessValue(forKey: "condition")
+            defer { didAccessValue(forKey: "condition") }
+            return primitiveCondition.flatMap { GlucoseCondition(rawValue: $0) }
+        }
+        set {
+            willChangeValue(forKey: "condition")
+            defer { didChangeValue(forKey: "condition") }
+            primitiveCondition = newValue.map { $0.rawValue }
+        }
+    }
+
+    var trend: GlucoseTrend? {
+        get {
+            willAccessValue(forKey: "trend")
+            defer { didAccessValue(forKey: "trend") }
+            return primitiveTrend.flatMap { GlucoseTrend(rawValue: $0.intValue) }
+        }
+        set {
+            willChangeValue(forKey: "trend")
+            defer { didChangeValue(forKey: "trend") }
+            primitiveTrend = newValue.map { NSNumber(value: $0.rawValue) }
         }
     }
 
@@ -46,14 +85,63 @@ class CachedGlucoseObject: NSManagedObject {
 
 extension CachedGlucoseObject {
     var quantity: HKQuantity { HKQuantity(unit: HKUnit(from: unitString), doubleValue: value) }
+
+    var quantitySample: HKQuantitySample {
+        var metadata: [String: Any] = [:]
+        metadata[HKMetadataKeySyncIdentifier] = syncIdentifier
+        metadata[HKMetadataKeySyncVersion] = syncVersion
+        if isDisplayOnly {
+            metadata[MetadataKeyGlucoseIsDisplayOnly] = true
+        }
+        if wasUserEntered {
+            metadata[HKMetadataKeyWasUserEntered] = true
+        }
+        metadata[MetadataKeyGlucoseCondition] = condition?.rawValue
+        metadata[MetadataKeyGlucoseTrend] = trend?.symbol
+        metadata[MetadataKeyGlucoseTrendRateUnit] = trendRateUnit
+        metadata[MetadataKeyGlucoseTrendRateValue] = trendRateValue
+
+        return HKQuantitySample(
+            type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!,
+            quantity: quantity,
+            start: startDate,
+            end: startDate,
+            device: device,
+            metadata: metadata
+        )
+    }
+
+    var trendRate: HKQuantity? {
+        get {
+            guard let trendRateUnit = trendRateUnit, let trendRateValue = trendRateValue else {
+                return nil
+            }
+            return HKQuantity(unit: HKUnit(from: trendRateUnit), doubleValue: trendRateValue.doubleValue)
+        }
+
+        set {
+            if let newValue = newValue {
+                let unit = HKUnit(from: unitString).unitDivided(by: .minute())
+                trendRateUnit = unit.unitString
+                trendRateValue = NSNumber(value: newValue.doubleValue(for: unit))
+            } else {
+                trendRateUnit = nil
+                trendRateValue = nil
+            }
+        }
+    }
 }
 
 // MARK: - Operations
 
 extension CachedGlucoseObject {
 
-    // Loop
-    func create(from sample: NewGlucoseSample, provenanceIdentifier: String) {
+    /// Creates (initializes) a `CachedGlucoseObject` from a new CGM sample from Loop.
+    /// - parameters:
+    ///   - sample: A new glucose (CGM) sample to copy data from.
+    ///   - provenanceIdentifier: A string uniquely identifying the provenance (origin) of the sample.
+    ///   - healthKitStorageDelay: The amount of time (seconds) to delay writing this sample to HealthKit.  A `nil` here means this sample is not eligible (i.e. authorized) to be written to HealthKit.
+    func create(from sample: NewGlucoseSample, provenanceIdentifier: String, healthKitStorageDelay: TimeInterval?) {
         self.uuid = nil
         self.provenanceIdentifier = provenanceIdentifier
         self.syncIdentifier = sample.syncIdentifier
@@ -63,12 +151,15 @@ extension CachedGlucoseObject {
         self.startDate = sample.date
         self.isDisplayOnly = sample.isDisplayOnly
         self.wasUserEntered = sample.wasUserEntered
+        self.device = sample.device
+        self.condition = sample.condition
+        self.trend = sample.trend
+        self.trendRate = sample.trendRate
+        self.healthKitEligibleDate = healthKitStorageDelay.map { sample.date.addingTimeInterval($0) }
     }
 
     // HealthKit
     func create(from sample: HKQuantitySample) {
-        precondition(!sample.createdByCurrentApp)
-
         self.uuid = sample.uuid
         self.provenanceIdentifier = sample.provenanceIdentifier
         self.syncIdentifier = sample.syncIdentifier
@@ -78,6 +169,13 @@ extension CachedGlucoseObject {
         self.startDate = sample.startDate
         self.isDisplayOnly = sample.isDisplayOnly
         self.wasUserEntered = sample.wasUserEntered
+        self.device = sample.device
+        self.condition = sample.condition
+        self.trend = sample.trend
+        self.trendRate = sample.trendRate
+        // The assumption here is that if this is created from a HKQuantitySample, it is coming out of HealthKit, and
+        // therefore does not need to be written to HealthKit.
+        self.healthKitEligibleDate = nil
     }
 }
 
@@ -94,5 +192,10 @@ extension CachedGlucoseObject {
         self.startDate = sample.startDate
         self.isDisplayOnly = sample.isDisplayOnly
         self.wasUserEntered = sample.wasUserEntered
+        self.device = sample.device
+        self.condition = sample.condition
+        self.trend = sample.trend
+        self.trendRate = sample.trendRate
+        self.healthKitEligibleDate = sample.healthKitEligibleDate
     }
 }

+ 26 - 1
Dependencies/LoopKit/LoopKit/GlucoseKit/CachedGlucoseObject+CoreDataProperties.swift

@@ -8,6 +8,7 @@
 
 import Foundation
 import CoreData
+import HealthKit
 
 
 extension CachedGlucoseObject {
@@ -16,6 +17,7 @@ extension CachedGlucoseObject {
         return NSFetchRequest<CachedGlucoseObject>(entityName: "CachedGlucoseObject")
     }
 
+    /// This is the UUID provided from HealthKit.  Nil if not (yet) stored in HealthKit.  Note: it is _not_ a unique identifier for this object.
     @NSManaged public var uuid: UUID?
     @NSManaged public var provenanceIdentifier: String
     @NSManaged public var syncIdentifier: String?
@@ -26,7 +28,16 @@ extension CachedGlucoseObject {
     @NSManaged public var isDisplayOnly: Bool
     @NSManaged public var wasUserEntered: Bool
     @NSManaged public var modificationCounter: Int64
-
+    @NSManaged public var primitiveDevice: Data?
+    @NSManaged public var primitiveCondition: String?
+    @NSManaged public var primitiveTrend: NSNumber?
+    @NSManaged public var trendRateUnit: String?
+    @NSManaged public var trendRateValue: NSNumber?
+    /// This is the date when this object is eligible for writing to HealthKit.  For example, if it is required to delay writing
+    /// data to HealthKit, this date will be in the future.  If the date is in the past, then it is written to HealthKit as soon as possible,
+    /// and this value is set to `nil`.  A `nil` value either means that this object has already been written to HealthKit, or it is
+    /// not eligible for HealthKit in the first place (for example, if a user has denied permissions at the time the sample was taken).
+    @NSManaged public var healthKitEligibleDate: Date?
 }
 
 extension CachedGlucoseObject: Encodable {
@@ -42,6 +53,12 @@ extension CachedGlucoseObject: Encodable {
         try container.encode(isDisplayOnly, forKey: .isDisplayOnly)
         try container.encode(wasUserEntered, forKey: .wasUserEntered)
         try container.encode(modificationCounter, forKey: .modificationCounter)
+        try container.encodeIfPresent(device, forKey: .device)
+        try container.encodeIfPresent(condition, forKey: .condition)
+        try container.encodeIfPresent(trend, forKey: .trend)
+        try container.encodeIfPresent(trendRateUnit, forKey: .trendRateUnit)
+        try container.encodeIfPresent(trendRateValue?.doubleValue, forKey: .trendRateValue)
+        try container.encodeIfPresent(healthKitEligibleDate, forKey: .healthKitEligibleDate)
     }
 
     private enum CodingKeys: String, CodingKey {
@@ -55,5 +72,13 @@ extension CachedGlucoseObject: Encodable {
         case isDisplayOnly
         case wasUserEntered
         case modificationCounter
+        case device
+        case condition
+        case trend
+        case trendRateUnit
+        case trendRateValue
+        case healthKitEligibleDate
     }
 }
+
+extension GlucoseTrend: Codable {}

+ 12 - 0
Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseCondition.swift

@@ -0,0 +1,12 @@
+//
+//  GlucoseCondition.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 9/3/21.
+//  Copyright © 2021 LoopKit Authors. All rights reserved.
+//
+
+public enum GlucoseCondition: String, Codable {
+    case belowRange
+    case aboveRange
+}

+ 4 - 1
Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseDisplayable.swift

@@ -7,7 +7,7 @@
 //
 
 import Foundation
-
+import HealthKit
 
 public protocol GlucoseDisplayable {
     /// Returns whether the current state is valid
@@ -19,6 +19,9 @@ public protocol GlucoseDisplayable {
     /// Enumerates the trend of the sensor values
     var trendType: GlucoseTrend? { get }
 
+    /// The trend rate of the sensor values, if available
+    var trendRate: HKQuantity? { get }
+
     /// Returns whether the data is from a locally-connected device
     var isLocal: Bool { get }
     

+ 8 - 0
Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseSampleValue.swift

@@ -6,6 +6,8 @@
 //  Copyright © 2016 Nathan Racklyeft. All rights reserved.
 //
 
+import HealthKit
+
 public protocol GlucoseSampleValue: GlucoseValue {
     /// Uniquely identifies the source of the sample.
     var provenanceIdentifier: String { get }
@@ -15,4 +17,10 @@ public protocol GlucoseSampleValue: GlucoseValue {
 
     /// Whether the glucose value was entered by the user.
     var wasUserEntered: Bool { get }
+
+    /// Any condition applied to the sample.
+    var condition: GlucoseCondition? { get }
+
+    /// The trend rate of the sample.
+    var trendRate: HKQuantity? { get }
 }

+ 82 - 41
Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseStore.swift

@@ -91,15 +91,23 @@ public final class GlucoseStore: HealthKitSampleStore {
     }
     private let lockedLatestGlucose = Locked<GlucoseSampleValue?>(nil)
 
+    private let storeSamplesToHealthKit: Bool
+
     private let cacheStore: PersistenceController
 
     private let provenanceIdentifier: String
 
+    public var healthKitStorageDelay: TimeInterval = 0
+    
+    // If HealthKit sharing is not authorized, `nil` will prevent later storage
+    var healthKitStorageDelayIfAllowed: TimeInterval? { storeSamplesToHealthKit && sharingAuthorized ? healthKitStorageDelay : nil }
+    
     static let healthKitQueryAnchorMetadataKey = "com.loopkit.GlucoseStore.hkQueryAnchor"
 
     public init(
         healthStore: HKHealthStore,
         observeHealthKitSamplesFromOtherApps: Bool = true,
+        storeSamplesToHealthKit: Bool = true,
         cacheStore: PersistenceController,
         observationEnabled: Bool = true,
         cacheLength: TimeInterval = 60 /* minutes */ * 60 /* seconds */,
@@ -112,12 +120,13 @@ public final class GlucoseStore: HealthKitSampleStore {
         self.cacheStore = cacheStore
         self.momentumDataInterval = momentumDataInterval
 
+        self.storeSamplesToHealthKit = storeSamplesToHealthKit
         self.cacheLength = cacheLength
         self.observationInterval = observationInterval ?? cacheLength
         self.provenanceIdentifier = provenanceIdentifier
 
         super.init(healthStore: healthStore,
-                   observeHealthKitSamplesFromCurrentApp: false,
+                   observeHealthKitSamplesFromCurrentApp: true,
                    observeHealthKitSamplesFromOtherApps: observeHealthKitSamplesFromOtherApps,
                    type: glucoseType,
                    observationStart: Date(timeIntervalSinceNow: -self.observationInterval),
@@ -134,6 +143,10 @@ public final class GlucoseStore: HealthKitSampleStore {
                 self.queue.async {
                     self.queryAnchor = anchor
 
+                    if !self.authorizationRequired {
+                        self.createQuery()
+                    }
+
                     self.updateLatestGlucose()
 
                     semaphore.signal()
@@ -191,7 +204,7 @@ public final class GlucoseStore: HealthKitSampleStore {
                 }
             }
 
-            if error != nil {
+            guard error == nil else {
                 completion(false)
                 return
             }
@@ -206,7 +219,7 @@ public final class GlucoseStore: HealthKitSampleStore {
                 self.purgeExpiredManagedDataFromHealthKit(before: newestStartDate)
             }
 
-            self.handleUpdatedGlucoseData(updateSource: .queriedByHealthKit)
+            self.handleUpdatedGlucoseData()
             completion(true)
         }
     }
@@ -336,17 +349,17 @@ extension GlucoseStore {
 
                     let objects: [CachedGlucoseObject] = samples.map { sample in
                         let object = CachedGlucoseObject(context: self.cacheStore.managedObjectContext)
-                        object.create(from: sample, provenanceIdentifier: self.provenanceIdentifier)
+                        object.create(from: sample,
+                                      provenanceIdentifier: self.provenanceIdentifier,
+                                      healthKitStorageDelay: self.healthKitStorageDelayIfAllowed)
                         return object
                     }
 
                     error = self.cacheStore.save()
-                    if error != nil {
+                    guard error == nil else {
                         return
                     }
 
-                    self.saveSamplesToHealthKit(samples, objects: objects)
-
                     storedSamples = objects.map { StoredGlucoseSample(managedObject: $0) }
                 } catch let coreDataError {
                     error = coreDataError
@@ -358,42 +371,69 @@ extension GlucoseStore {
                 return
             }
 
-            self.handleUpdatedGlucoseData(updateSource: .changedInApp)
+            self.handleUpdatedGlucoseData()
             completion(.success(storedSamples))
         }
     }
-
-    private func saveSamplesToHealthKit(_ samples: [NewGlucoseSample], objects: [CachedGlucoseObject]) {
+    
+    private func saveSamplesToHealthKit() {
         dispatchPrecondition(condition: .onQueue(queue))
-
-        guard !samples.isEmpty else {
+        var error: Error?
+        
+        guard storeSamplesToHealthKit else {
             return
         }
 
-        let quantitySamples = samples.map { $0.quantitySample }
-        var error: Error?
+        cacheStore.managedObjectContext.performAndWait {
+            do {
+                let request: NSFetchRequest<CachedGlucoseObject> = CachedGlucoseObject.fetchRequest()
+                request.predicate = NSPredicate(format: "healthKitEligibleDate <= %@", Date() as NSDate)
+                request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]   // Maintains modificationCounter order
 
-        // Save objects to HealthKit, log any errors, but do not fail
-        let dispatchGroup = DispatchGroup()
-        dispatchGroup.enter()
-        self.healthStore.save(quantitySamples) { (_, healthKitError) in
-            error = healthKitError
-            dispatchGroup.leave()
-        }
-        dispatchGroup.wait()
+                let objects = try self.cacheStore.managedObjectContext.fetch(request)
+                guard !objects.isEmpty else {
+                    return
+                }
+                
+                if objects.contains(where: { $0.uuid != nil }) {
+                    self.log.error("Found CachedGlucoseObjects with non-nil uuid. Should never happen, but HealthKit should be able to resolve it.")
+                    // Note: UUIDs will be overwritten below, but since the syncIdentifiers will match then HealthKit can resolve correctly via replacement
+                }
+                    
+                let quantitySamples = objects.map { $0.quantitySample }
+                
+                let dispatchGroup = DispatchGroup()
+                dispatchGroup.enter()
+                self.healthStore.save(quantitySamples) { (_, healthKitError) in
+                    error = healthKitError
+                    dispatchGroup.leave()
+                }
+                dispatchGroup.wait()
 
-        if let error = error {
-            self.log.error("Error saving HealthKit objects: %@", String(describing: error))
-            return
-        }
+                // If there is an error writing to HealthKit, then do not persist uuids and retry later
+                guard error == nil else {
+                    return
+                }
+
+                for (object, quantitySample) in zip(objects, quantitySamples) {
+                    object.uuid = quantitySample.uuid
+                    object.healthKitEligibleDate = nil
+                    object.updateModificationCounter()  // Maintains modificationCounter order
+                }
 
-        // Update Core Data with the changes, log any errors, but do not fail
-        for (object, quantitySample) in zip(objects, quantitySamples) {
-            object.uuid = quantitySample.uuid
+                error = self.cacheStore.save()
+                guard error == nil else {
+                    return
+                }
+                
+                self.log.default("Stored %d eligible glucose samples to HealthKit", objects.count)
+            } catch let coreDataError {
+                error = coreDataError
+            }
         }
-        if let error = self.cacheStore.save() {
-            self.log.error("Error updating CachedGlucoseObjects after saving HealthKit objects: %@", String(describing: error))
-            objects.forEach { $0.uuid = nil }
+
+        if let error = error {
+            self.log.error("Error saving samples to HealthKit: %{public}@", String(describing: error))
         }
     }
 
@@ -503,7 +543,7 @@ extension GlucoseStore {
                 return
             }
 
-            self.handleUpdatedGlucoseData(updateSource: .changedInApp)
+            self.handleUpdatedGlucoseData()
             completion(nil)
         }
     }
@@ -531,7 +571,7 @@ extension GlucoseStore {
                         return
                     }
 
-                    self.handleUpdatedGlucoseData(updateSource: .changedInApp)
+                    self.handleUpdatedGlucoseData()
                     completion(nil)
                 }
             }
@@ -553,7 +593,7 @@ extension GlucoseStore {
                 completion(error)
                 return
             }
-            self.handleUpdatedGlucoseData(updateSource: .changedInApp)
+            self.handleUpdatedGlucoseData()
             completion(nil)
         }
     }
@@ -598,13 +638,14 @@ extension GlucoseStore {
         }
     }
 
-    private func handleUpdatedGlucoseData(updateSource: UpdateSource) {
+    private func handleUpdatedGlucoseData() {
         dispatchPrecondition(condition: .onQueue(queue))
 
         self.purgeExpiredCachedGlucoseObjects()
         self.updateLatestGlucose()
+        self.saveSamplesToHealthKit()
 
-        NotificationCenter.default.post(name: GlucoseStore.glucoseSamplesDidChange, object: self, userInfo: [GlucoseStore.notificationUpdateSourceKey: updateSource.rawValue])
+        NotificationCenter.default.post(name: GlucoseStore.glucoseSamplesDidChange, object: self)
         delegate?.glucoseStoreHasUpdatedGlucoseData(self)
     }
 }
@@ -668,7 +709,7 @@ extension GlucoseStore {
 // MARK: - Remote Data Service Query
 
 extension GlucoseStore {
-    public struct QueryAnchor: RawRepresentable {
+    public struct QueryAnchor: Equatable, RawRepresentable {
         public typealias RawValue = [String: Any]
 
         internal var modificationCounter: Int64
@@ -703,7 +744,7 @@ extension GlucoseStore {
             var queryError: Error?
 
             guard limit > 0 else {
-                completion(.success(queryAnchor, queryResult))
+                completion(.success(queryAnchor, []))
                 return
             }
 
@@ -742,7 +783,7 @@ extension GlucoseStore: CriticalEventLog {
     private var exportProgressUnitCountPerObject: Int64 { 1 }
     private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
 
-    public var exportName: String { "Glucoses.json" }
+    public var exportName: String { "Glucose.json" }
 
     public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
         var result: Result<Int64, Error>?
@@ -829,7 +870,7 @@ extension GlucoseStore {
             self.cacheStore.managedObjectContext.performAndWait {
                 for sample in samples {
                     let object = CachedGlucoseObject(context: self.cacheStore.managedObjectContext)
-                    object.create(from: sample, provenanceIdentifier: self.provenanceIdentifier)
+                    object.create(from: sample, provenanceIdentifier: self.provenanceIdentifier, healthKitStorageDelay: self.healthKitStorageDelayIfAllowed)
                 }
                 error = self.cacheStore.save()
             }

+ 23 - 0
Dependencies/LoopKit/LoopKit/GlucoseKit/GlucoseTrend.swift

@@ -75,3 +75,26 @@ public enum GlucoseTrend: Int, CaseIterable {
         }
     }
 }
+
+extension GlucoseTrend {
+    public init?(symbol: String) {
+        switch symbol {
+        case "↑↑":
+            self = .upUpUp
+        case "↑":
+            self = .upUp
+        case "↗︎":
+            self = .up
+        case "→":
+            self = .flat
+        case "↘︎":
+            self = .down
+        case "↓":
+            self = .downDown
+        case "↓↓":
+            self = .downDownDown
+        default:
+            return nil
+        }
+    }
+}

+ 54 - 0
Dependencies/LoopKit/LoopKit/GlucoseKit/HKDevice+Encodable.swift

@@ -0,0 +1,54 @@
+//
+//  HKDevice+Encodable.swift
+//  LoopKit
+//
+//  Created by Rick Pasetto on 8/2/21.
+//  Copyright © 2021 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+extension HKDevice: Encodable {
+    private enum CodingKeys: String, CodingKey {
+       case name, manufacturer, model, hardwareVersion, firmwareVersion, softwareVersion, localIdentifier, udiDeviceIdentifier
+    }
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(name, forKey: .name)
+        try container.encodeIfPresent(manufacturer, forKey: .manufacturer)
+        try container.encodeIfPresent(model, forKey: .model)
+        try container.encodeIfPresent(hardwareVersion, forKey: .hardwareVersion)
+        try container.encodeIfPresent(firmwareVersion, forKey: .firmwareVersion)
+        try container.encodeIfPresent(softwareVersion, forKey: .softwareVersion)
+        try container.encodeIfPresent(localIdentifier, forKey: .localIdentifier)
+        try container.encodeIfPresent(udiDeviceIdentifier, forKey: .udiDeviceIdentifier)
+    }
+        
+    // Swift won't let us implement Decodable for HKDevice, but we can at least implement deserialization "by hand"
+    // by trying both Plist and JSON -- not very efficient, but gets the job done.
+    public convenience init(from data: Data) throws {
+        var props = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
+        if props == nil {
+            props = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
+        }
+        guard let props = props else {
+            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid data"))
+        }
+        self.init(from: props)
+    }
+
+    convenience init(from props: [String: Any]) {
+        self.init(name: props[CodingKeys.name.rawValue] as! String?,
+                  manufacturer: props[CodingKeys.manufacturer.rawValue] as! String?,
+                  model: props[CodingKeys.model.rawValue] as! String?,
+                  hardwareVersion: props[CodingKeys.hardwareVersion.rawValue] as! String?,
+                  firmwareVersion: props[CodingKeys.firmwareVersion.rawValue] as! String?,
+                  softwareVersion: props[CodingKeys.softwareVersion.rawValue] as! String?,
+                  localIdentifier: props[CodingKeys.localIdentifier.rawValue] as! String?,
+                  udiDeviceIdentifier: props[CodingKeys.udiDeviceIdentifier.rawValue] as! String?)
+    }
+
+}
+
+

+ 26 - 0
Dependencies/LoopKit/LoopKit/GlucoseKit/HKQuantitySample+GlucoseKit.swift

@@ -10,6 +10,10 @@ import HealthKit
 
 
 let MetadataKeyGlucoseIsDisplayOnly = "com.loudnate.GlucoseKit.HKMetadataKey.GlucoseIsDisplayOnly"
+let MetadataKeyGlucoseCondition = "com.LoopKit.GlucoseKit.HKMetadataKey.GlucoseCondition"
+let MetadataKeyGlucoseTrend = "com.LoopKit.GlucoseKit.HKMetadataKey.GlucoseTrend"
+let MetadataKeyGlucoseTrendRateUnit = "com.LoopKit.GlucoseKit.HKMetadataKey.GlucoseTrendRateUnit"
+let MetadataKeyGlucoseTrendRateValue = "com.LoopKit.GlucoseKit.HKMetadataKey.GlucoseTrendRateValue"
 
 
 extension HKQuantitySample: GlucoseSampleValue {
@@ -24,4 +28,26 @@ extension HKQuantitySample: GlucoseSampleValue {
     public var wasUserEntered: Bool {
         return metadata?[HKMetadataKeyWasUserEntered] as? Bool ?? false
     }
+
+    public var condition: GlucoseCondition? {
+        guard let rawCondition = metadata?[MetadataKeyGlucoseCondition] as? String else {
+            return nil
+        }
+        return GlucoseCondition(rawValue: rawCondition)
+    }
+
+    public var trend: GlucoseTrend? {
+        guard let symbol = metadata?[MetadataKeyGlucoseTrend] as? String else {
+            return nil
+        }
+        return GlucoseTrend(symbol: symbol)
+    }
+
+    public var trendRate: HKQuantity? {
+        guard let unit = metadata?[MetadataKeyGlucoseTrendRateUnit] as? String,
+              let value = metadata?[MetadataKeyGlucoseTrendRateValue] as? Double else {
+            return nil
+        }
+        return HKQuantity(unit: HKUnit(from: unit), doubleValue: value)
+    }
 }

+ 24 - 2
Dependencies/LoopKit/LoopKit/GlucoseKit/NewGlucoseSample.swift

@@ -11,23 +11,39 @@ import HealthKit
 public struct NewGlucoseSample: Equatable {
     public let date: Date
     public let quantity: HKQuantity
+    public let condition: GlucoseCondition?
+    public let trend: GlucoseTrend?
+    public let trendRate: HKQuantity?
     public let isDisplayOnly: Bool
     public let wasUserEntered: Bool
     public let syncIdentifier: String
     public var syncVersion: Int
-    public var device: HKDevice?
+    public let device: HKDevice?
 
     /// - Parameters:
     ///   - date: The date the sample was collected
     ///   - quantity: The glucose sample quantity
+    ///   - trend: The glucose sample's trend.  A value of `nil` means no trend is available.
     ///   - isDisplayOnly: Whether the reading was shifted for visual consistency after calibration
     ///   - wasUserEntered: Whether the reading was entered by the user (manual) or not (device)
     ///   - syncIdentifier: A unique identifier representing the sample, used for de-duplication
     ///   - syncVersion: A version number for determining resolution in de-duplication
     ///   - device: The description of the device the collected the sample
-    public init(date: Date, quantity: HKQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, syncIdentifier: String, syncVersion: Int = 1, device: HKDevice? = nil) {
+    public init(date: Date,
+                quantity: HKQuantity,
+                condition: GlucoseCondition?,
+                trend: GlucoseTrend?,
+                trendRate: HKQuantity?,
+                isDisplayOnly: Bool,
+                wasUserEntered: Bool,
+                syncIdentifier: String,
+                syncVersion: Int = 1,
+                device: HKDevice? = nil) {
         self.date = date
         self.quantity = quantity
+        self.condition = condition
+        self.trend = trend
+        self.trendRate = trendRate
         self.isDisplayOnly = isDisplayOnly
         self.wasUserEntered = wasUserEntered
         self.syncIdentifier = syncIdentifier
@@ -44,6 +60,12 @@ extension NewGlucoseSample {
             HKMetadataKeySyncVersion: syncVersion,
         ]
 
+        metadata[MetadataKeyGlucoseCondition] = condition?.rawValue
+        metadata[MetadataKeyGlucoseTrend] = trend?.symbol
+        if let trendRate = trendRate {
+            metadata[MetadataKeyGlucoseTrendRateUnit] = HKUnit.milligramsPerDeciliterPerMinute.unitString
+            metadata[MetadataKeyGlucoseTrendRateValue] = trendRate.doubleValue(for: .milligramsPerDeciliterPerMinute)
+        }
         if isDisplayOnly {
             metadata[MetadataKeyGlucoseIsDisplayOnly] = true
         }

+ 81 - 4
Dependencies/LoopKit/LoopKit/GlucoseKit/StoredGlucoseSample.swift

@@ -8,13 +8,15 @@
 import HealthKit
 
 public struct StoredGlucoseSample: GlucoseSampleValue, Equatable {
-    public let uuid: UUID?
+    public let uuid: UUID?  // Note this is the UUID from HealthKit.  Nil if not (yet) stored in HealthKit.
 
     // MARK: - HealthKit Sync Support
 
     public let provenanceIdentifier: String
     public let syncIdentifier: String?
     public let syncVersion: Int?
+    public let device: HKDevice?
+    public let healthKitEligibleDate: Date?
 
     // MARK: - SampleValue
 
@@ -25,6 +27,9 @@ public struct StoredGlucoseSample: GlucoseSampleValue, Equatable {
 
     public let isDisplayOnly: Bool
     public let wasUserEntered: Bool
+    public let condition: GlucoseCondition?
+    public let trend: GlucoseTrend?
+    public let trendRate: HKQuantity?
 
     public init(sample: HKQuantitySample) {
         self.init(
@@ -34,8 +39,13 @@ public struct StoredGlucoseSample: GlucoseSampleValue, Equatable {
             syncVersion: sample.syncVersion,
             startDate: sample.startDate,
             quantity: sample.quantity,
+            condition: sample.condition,
+            trend: sample.trend,
+            trendRate: sample.trendRate,
             isDisplayOnly: sample.isDisplayOnly,
-            wasUserEntered: sample.wasUserEntered)
+            wasUserEntered: sample.wasUserEntered,
+            device: sample.device,
+            healthKitEligibleDate: nil)
     }
 
     public init(
@@ -45,16 +55,26 @@ public struct StoredGlucoseSample: GlucoseSampleValue, Equatable {
         syncVersion: Int?,
         startDate: Date,
         quantity: HKQuantity,
+        condition: GlucoseCondition?,
+        trend: GlucoseTrend?,
+        trendRate: HKQuantity?,
         isDisplayOnly: Bool,
-        wasUserEntered: Bool) {
+        wasUserEntered: Bool,
+        device: HKDevice?,
+        healthKitEligibleDate: Date?) {
         self.uuid = uuid
         self.provenanceIdentifier = provenanceIdentifier
         self.syncIdentifier = syncIdentifier
         self.syncVersion = syncVersion
         self.startDate = startDate
         self.quantity = quantity
+        self.condition = condition
+        self.trend = trend
+        self.trendRate = trendRate
         self.isDisplayOnly = isDisplayOnly
         self.wasUserEntered = wasUserEntered
+        self.device = device
+        self.healthKitEligibleDate = healthKitEligibleDate
     }
 }
 
@@ -67,7 +87,64 @@ extension StoredGlucoseSample {
             syncVersion: managedObject.syncVersion,
             startDate: managedObject.startDate,
             quantity: managedObject.quantity,
+            condition: managedObject.condition,
+            trend: managedObject.trend,
+            trendRate: managedObject.trendRate,
             isDisplayOnly: managedObject.isDisplayOnly,
-            wasUserEntered: managedObject.wasUserEntered)
+            wasUserEntered: managedObject.wasUserEntered,
+            device: managedObject.device,
+            healthKitEligibleDate: managedObject.healthKitEligibleDate)
+    }
+}
+
+extension StoredGlucoseSample: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.init(uuid: try container.decodeIfPresent(UUID.self, forKey: .uuid),
+                  provenanceIdentifier: try container.decode(String.self, forKey: .provenanceIdentifier),
+                  syncIdentifier: try container.decodeIfPresent(String.self, forKey: .syncIdentifier),
+                  syncVersion: try container.decodeIfPresent(Int.self, forKey: .syncVersion),
+                  startDate: try container.decode(Date.self, forKey: .startDate),
+                  quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: try container.decode(Double.self, forKey: .quantity)),
+                  condition: try container.decodeIfPresent(GlucoseCondition.self, forKey: .condition),
+                  trend: try container.decodeIfPresent(GlucoseTrend.self, forKey: .trend),
+                  trendRate: try container.decodeIfPresent(Double.self, forKey: .trendRate).map { HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: $0) },
+                  isDisplayOnly: try container.decode(Bool.self, forKey: .isDisplayOnly),
+                  wasUserEntered: try container.decode(Bool.self, forKey: .wasUserEntered),
+                  device: try container.decodeIfPresent(CodableDevice.self, forKey: .device).map { $0.device },
+                  healthKitEligibleDate: try container.decodeIfPresent(Date.self, forKey: .healthKitEligibleDate))
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(uuid, forKey: .uuid)
+        try container.encode(provenanceIdentifier, forKey: .provenanceIdentifier)
+        try container.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
+        try container.encodeIfPresent(syncVersion, forKey: .syncVersion)
+        try container.encode(startDate, forKey: .startDate)
+        try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity)
+        try container.encodeIfPresent(condition, forKey: .condition)
+        try container.encodeIfPresent(trend, forKey: .trend)
+        try container.encodeIfPresent(trendRate?.doubleValue(for: .milligramsPerDeciliterPerMinute), forKey: .trendRate)
+        try container.encode(isDisplayOnly, forKey: .isDisplayOnly)
+        try container.encode(wasUserEntered, forKey: .wasUserEntered)
+        try container.encodeIfPresent(device.map { CodableDevice($0) }, forKey: .device)
+        try container.encodeIfPresent(healthKitEligibleDate, forKey: .healthKitEligibleDate)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case uuid
+        case provenanceIdentifier
+        case syncIdentifier
+        case syncVersion
+        case startDate
+        case quantity
+        case condition
+        case trend
+        case trendRate
+        case isDisplayOnly
+        case wasUserEntered
+        case device
+        case healthKitEligibleDate
     }
 }

+ 78 - 0
Dependencies/LoopKit/LoopKit/GlucoseRange.swift

@@ -0,0 +1,78 @@
+//
+//  GlucoseRange.swift
+//  LoopKit
+//
+//  Created by Nathaniel Hamming on 2021-03-16.
+//  Copyright © 2021 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+public struct GlucoseRange {
+    public let range: DoubleRange
+    public let unit: HKUnit
+
+    public init(minValue: Double, maxValue: Double, unit: HKUnit) {
+        self.init(range: DoubleRange(minValue: minValue, maxValue: maxValue), unit: unit)
+    }
+
+    public init(range: DoubleRange, unit: HKUnit) {
+        precondition(unit == .milligramsPerDeciliter || unit == .millimolesPerLiter)
+        self.range = range
+        self.unit = unit
+    }
+
+    public var isZero: Bool {
+        return abs(range.minValue) < .ulpOfOne && abs(range.maxValue) < .ulpOfOne
+    }
+
+    public var quantityRange: ClosedRange<HKQuantity> {
+        range.quantityRange(for: unit)
+    }
+}
+
+extension GlucoseRange: Hashable {}
+
+extension GlucoseRange: Equatable {}
+
+extension GlucoseRange: RawRepresentable {
+    public typealias RawValue = [String:Any]
+
+    public init?(rawValue: RawValue) {
+        guard let rawRange = rawValue["range"] as? DoubleRange.RawValue,
+              let range = DoubleRange(rawValue: rawRange),
+              let bloodGlucoseUnit = rawValue["bloodGlucoseUnit"] as? String else
+        {
+            return nil
+        }
+        self.range = range
+        self.unit = HKUnit(from: bloodGlucoseUnit)
+    }
+
+    public var rawValue: RawValue {
+        return [
+            "range": range.rawValue,
+            "bloodGlucoseUnit": unit.unitString
+        ]
+    }
+}
+
+extension GlucoseRange: Codable  {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        unit = HKUnit(from: try container.decode(String.self, forKey: .bloodGlucoseUnit))
+        range = try container.decode(DoubleRange.self, forKey: .range)
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(range, forKey: .range)
+        try container.encode(unit.unitString, forKey: .bloodGlucoseUnit)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case bloodGlucoseUnit
+        case range
+    }
+}

+ 32 - 1
Dependencies/LoopKit/LoopKit/GlucoseRangeSchedule.swift

@@ -42,7 +42,6 @@ extension DoubleRange: RawRepresentable {
     }
 }
 
-
 extension DoubleRange: Equatable {
     public static func ==(lhs: DoubleRange, rhs: DoubleRange) -> Bool {
         return abs(lhs.minValue - rhs.minValue) < .ulpOfOne &&
@@ -149,6 +148,13 @@ public struct GlucoseRangeSchedule: DailySchedule, Equatable {
         return rangeSchedule.items
     }
 
+    public var quantityRanges: [RepeatingScheduleValue<ClosedRange<HKQuantity>>] {
+        return self.items.map {
+            RepeatingScheduleValue<ClosedRange<HKQuantity>>(startTime: $0.startTime,
+                                                            value: $0.value.quantityRange(for: unit))
+        }
+    }
+
     public var timeZone: TimeZone {
         get {
             return rangeSchedule.timeZone
@@ -180,6 +186,27 @@ public struct GlucoseRangeSchedule: DailySchedule, Equatable {
 
         return lowerBound...upperBound
     }
+
+    private func convertTo(unit: HKUnit) -> GlucoseRangeSchedule? {
+        guard unit != self.unit else {
+            return self
+        }
+
+        let convertedDailyItems: [RepeatingScheduleValue<DoubleRange>] = rangeSchedule.items.map {
+            RepeatingScheduleValue(startTime: $0.startTime,
+                                   value: $0.value.quantityRange(for: self.unit).doubleRange(for: unit)
+            )
+        }
+
+        return GlucoseRangeSchedule(unit: unit,
+                                    dailyItems: convertedDailyItems,
+                                    timeZone: timeZone)
+    }
+
+    public func schedule(for glucoseUnit: HKUnit) -> GlucoseRangeSchedule? {
+        precondition(glucoseUnit == .millimolesPerLiter || glucoseUnit == .milligramsPerDeciliter)
+        return self.convertTo(unit: glucoseUnit)
+    }
 }
 
 extension GlucoseRangeSchedule: Codable {}
@@ -198,6 +225,10 @@ extension ClosedRange where Bound == HKQuantity {
     public func doubleRange(for unit: HKUnit) -> DoubleRange {
         return DoubleRange(minValue: lowerBound.doubleValue(for: unit), maxValue: upperBound.doubleValue(for: unit))
     }
+
+    public func glucoseRange(for unit: HKUnit) -> GlucoseRange {
+        GlucoseRange(range: self.doubleRange(for: unit), unit: unit)
+    }
 }
 
 public extension DoubleRange {

+ 25 - 1
Dependencies/LoopKit/LoopKit/GlucoseSchedule.swift

@@ -9,6 +9,30 @@
 import Foundation
 import HealthKit
 
-
 public typealias GlucoseSchedule = SingleQuantitySchedule
+
 public typealias InsulinSensitivitySchedule = GlucoseSchedule
+
+public extension InsulinSensitivitySchedule {
+    private func convertTo(unit: HKUnit) -> InsulinSensitivitySchedule? {
+        guard unit != self.unit else {
+            return self
+        }
+
+        let convertedDailyItems: [RepeatingScheduleValue<Double>] = self.items.map {
+            RepeatingScheduleValue(startTime: $0.startTime,
+                                   value: HKQuantity(unit: self.unit, doubleValue: $0.value).doubleValue(for: unit)
+            )
+        }
+
+        return InsulinSensitivitySchedule(unit: unit,
+                                          dailyItems: convertedDailyItems,
+                                          timeZone: timeZone)
+    }
+
+    func schedule(for glucoseUnit: HKUnit) -> InsulinSensitivitySchedule? {
+        // InsulinSensitivitySchedule stores only the glucose unit.
+        precondition(glucoseUnit == .millimolesPerLiter || glucoseUnit == .milligramsPerDeciliter)
+        return self.convertTo(unit: glucoseUnit)
+    }
+}

+ 11 - 0
Dependencies/LoopKit/LoopKit/GlucoseThreshold.swift

@@ -38,6 +38,17 @@ public struct GlucoseThreshold: Equatable, RawRepresentable {
             "units": unit.unitString
         ]
     }
+
+    public func convertTo(unit: HKUnit) -> GlucoseThreshold {
+        guard unit != self.unit else {
+            return self
+        }
+
+        let convertedValue = self.quantity.doubleValue(for: unit)
+
+        return GlucoseThreshold(unit: unit,
+                                value: convertedValue)
+    }
 }
 
 extension GlucoseThreshold: Codable {

+ 3 - 3
Dependencies/LoopKit/LoopKit/Guardrail.swift

@@ -81,9 +81,9 @@ extension Guardrail where Value == HKQuantity {
 
     public func allValues(stridingBy increment: HKQuantity, unit: HKUnit) -> [Double] {
         Array(stride(
-            from: absoluteBounds.lowerBound.doubleValue(for: unit),
-            through: absoluteBounds.upperBound.doubleValue(for: unit),
-            by: increment.doubleValue(for: unit)
+            from: absoluteBounds.lowerBound.doubleValue(for: unit, withRounding: true),
+            through: absoluteBounds.upperBound.doubleValue(for: unit, withRounding: true),
+            by: increment.doubleValue(for: unit, withRounding: true)
         ))
     }
 }

+ 0 - 0
Dependencies/LoopKit/LoopKit/HealthKitSampleStore.swift


部分文件因为文件数量过多而无法显示