瀏覽代碼

Dependencies

Ivan Valkou 5 年之前
父節點
當前提交
d48e349366
共有 100 個文件被更改,包括 13393 次插入0 次删除
  1. 93 0
      Dependecies/LoopKit/.circleci/config.yml
  2. 69 0
      Dependecies/LoopKit/.gitignore
  3. 12 0
      Dependecies/LoopKit/.travis.yml
  4. 72 0
      Dependecies/LoopKit/CODE_OF_CONDUCT.md
  5. 1 0
      Dependecies/LoopKit/Cartfile
  6. 1 0
      Dependecies/LoopKit/Cartfile.resolved
  7. 21 0
      Dependecies/LoopKit/Common/LocalizedString.swift
  8. 19 0
      Dependecies/LoopKit/Extensions/ClosedRange.swift
  9. 98 0
      Dependecies/LoopKit/Extensions/Collection.swift
  10. 23 0
      Dependecies/LoopKit/Extensions/Comparable.swift
  11. 174 0
      Dependecies/LoopKit/Extensions/Guardrail+Settings.swift
  12. 53 0
      Dependecies/LoopKit/Extensions/HKUnit.swift
  13. 21 0
      Dependecies/LoopKit/Extensions/IdentifiableClass.swift
  14. 24 0
      Dependecies/LoopKit/Extensions/Math.swift
  15. 17 0
      Dependecies/LoopKit/Extensions/MutableCollection.swift
  16. 56 0
      Dependecies/LoopKit/Extensions/NSData.swift
  17. 33 0
      Dependecies/LoopKit/Extensions/NSDateFormatter.swift
  18. 36 0
      Dependecies/LoopKit/Extensions/NSTimeInterval.swift
  19. 21 0
      Dependecies/LoopKit/Extensions/NibLoadable.swift
  20. 47 0
      Dependecies/LoopKit/Extensions/NumberFormatter.swift
  21. 50 0
      Dependecies/LoopKit/Extensions/OSLog.swift
  22. 19 0
      Dependecies/LoopKit/Extensions/Sequence.swift
  23. 16 0
      Dependecies/LoopKit/Extensions/TimeZone.swift
  24. 22 0
      Dependecies/LoopKit/Extensions/UIColor.swift
  25. 13 0
      Dependecies/LoopKit/Extensions/UITableViewCell.swift
  26. 29 0
      Dependecies/LoopKit/Extensions/UITextField.swift
  27. 17 0
      Dependecies/LoopKit/Extensions/UUID.swift
  28. 22 0
      Dependecies/LoopKit/LICENSE
  29. 43 0
      Dependecies/LoopKit/LoopKit Example/AppDelegate.swift
  30. 98 0
      Dependecies/LoopKit/LoopKit Example/Assets.xcassets/AppIcon.appiconset/Contents.json
  31. 6 0
      Dependecies/LoopKit/LoopKit Example/Assets.xcassets/Contents.json
  32. 31 0
      Dependecies/LoopKit/LoopKit Example/Base.lproj/LaunchScreen.storyboard
  33. 85 0
      Dependecies/LoopKit/LoopKit Example/Base.lproj/Main.storyboard
  34. 12 0
      Dependecies/LoopKit/LoopKit Example/Extensions/CarbEntryTableViewController.swift
  35. 12 0
      Dependecies/LoopKit/LoopKit Example/Extensions/InsulinDeliveryTableViewController.swift
  36. 205 0
      Dependecies/LoopKit/LoopKit Example/Extensions/NSUserDefaults.swift
  37. 62 0
      Dependecies/LoopKit/LoopKit Example/Info.plist
  38. 8 0
      Dependecies/LoopKit/LoopKit Example/LoopKitExample.entitlements
  39. 131 0
      Dependecies/LoopKit/LoopKit Example/Managers/DeviceDataManager.swift
  40. 427 0
      Dependecies/LoopKit/LoopKit Example/MasterViewController.swift
  41. 40 0
      Dependecies/LoopKit/LoopKit Example/da.lproj/Localizable.strings
  42. 40 0
      Dependecies/LoopKit/LoopKit Example/de.lproj/Localizable.strings
  43. 40 0
      Dependecies/LoopKit/LoopKit Example/en.lproj/Localizable.strings
  44. 40 0
      Dependecies/LoopKit/LoopKit Example/es.lproj/Localizable.strings
  45. 6 0
      Dependecies/LoopKit/LoopKit Example/fi.lproj/InfoPlist.strings
  46. 40 0
      Dependecies/LoopKit/LoopKit Example/fi.lproj/Localizable.strings
  47. 40 0
      Dependecies/LoopKit/LoopKit Example/fr.lproj/Localizable.strings
  48. 40 0
      Dependecies/LoopKit/LoopKit Example/it.lproj/Localizable.strings
  49. 40 0
      Dependecies/LoopKit/LoopKit Example/ja.lproj/Localizable.strings
  50. 40 0
      Dependecies/LoopKit/LoopKit Example/nb.lproj/Localizable.strings
  51. 40 0
      Dependecies/LoopKit/LoopKit Example/nl.lproj/Localizable.strings
  52. 40 0
      Dependecies/LoopKit/LoopKit Example/pl.lproj/Localizable.strings
  53. 39 0
      Dependecies/LoopKit/LoopKit Example/pt-BR.lproj/Localizable.strings
  54. 40 0
      Dependecies/LoopKit/LoopKit Example/ro.lproj/Localizable.strings
  55. 40 0
      Dependecies/LoopKit/LoopKit Example/ru.lproj/Localizable.strings
  56. 39 0
      Dependecies/LoopKit/LoopKit Example/sv.lproj/Localizable.strings
  57. 40 0
      Dependecies/LoopKit/LoopKit Example/vi.lproj/Localizable.strings
  58. 40 0
      Dependecies/LoopKit/LoopKit Example/zh-Hans.lproj/Localizable.strings
  59. 4968 0
      Dependecies/LoopKit/LoopKit.xcodeproj/project.pbxproj
  60. 7 0
      Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  61. 8 0
      Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  62. 8 0
      Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  63. 91 0
      Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme
  64. 76 0
      Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme
  65. 161 0
      Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme
  66. 214 0
      Dependecies/LoopKit/LoopKit/Alert.swift
  67. 15 0
      Dependecies/LoopKit/LoopKit/AnalyticsService.swift
  68. 41 0
      Dependecies/LoopKit/LoopKit/BasalRateSchedule.swift
  69. 98 0
      Dependecies/LoopKit/LoopKit/Base.lproj/Localizable.strings
  70. 80 0
      Dependecies/LoopKit/LoopKit/CarbKit/AbsorbedCarbValue.swift
  71. 262 0
      Dependecies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataClass.swift
  72. 77 0
      Dependecies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataProperties.swift
  73. 14 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbEntry.swift
  74. 902 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbMath.swift
  75. 130 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbStatus.swift
  76. 1483 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbStore.swift
  77. 39 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbStoreError.swift
  78. 49 0
      Dependecies/LoopKit/LoopKit/CarbKit/CarbValue.swift
  79. 39 0
      Dependecies/LoopKit/LoopKit/CarbKit/HKQuantitySample+CarbKit.swift
  80. 40 0
      Dependecies/LoopKit/LoopKit/CarbKit/NSUserDefaults.swift
  81. 60 0
      Dependecies/LoopKit/LoopKit/CarbKit/NewCarbEntry.swift
  82. 174 0
      Dependecies/LoopKit/LoopKit/CarbKit/StoredCarbEntry.swift
  83. 88 0
      Dependecies/LoopKit/LoopKit/CarbKit/SyncCarbObject.swift
  84. 13 0
      Dependecies/LoopKit/LoopKit/CarbRatioSchedule.swift
  85. 15 0
      Dependecies/LoopKit/LoopKit/CarbSensitivitySchedule.swift
  86. 46 0
      Dependecies/LoopKit/LoopKit/CorrectionRangeOverrides.swift
  87. 41 0
      Dependecies/LoopKit/LoopKit/CriticalEventLog.swift
  88. 180 0
      Dependecies/LoopKit/LoopKit/DailyQuantitySchedule+Override.swift
  89. 154 0
      Dependecies/LoopKit/LoopKit/DailyQuantitySchedule.swift
  90. 274 0
      Dependecies/LoopKit/LoopKit/DailyValueSchedule.swift
  91. 56 0
      Dependecies/LoopKit/LoopKit/DeliveryLimits.swift
  92. 92 0
      Dependecies/LoopKit/LoopKit/DeviceManager/AlertSoundPlayer.swift
  93. 114 0
      Dependecies/LoopKit/LoopKit/DeviceManager/CGMManager.swift
  94. 24 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLifecycleProgress.swift
  95. 17 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLog.xcdatamodeld/DeviceCommsLog.xcdatamodel/contents
  96. 46 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntry+CoreDataClass.swift
  97. 58 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntry+CoreDataProperties.swift
  98. 24 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntryType.swift
  99. 232 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/PersistentDeviceLog.swift
  100. 0 0
      Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/StoredDeviceLogEntry.swift

+ 93 - 0
Dependecies/LoopKit/.circleci/config.yml

@@ -0,0 +1,93 @@
+version: 2.1
+
+#
+# Variables
+#
+
+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
+#
+
+jobs:
+  test:
+    working_directory: *project_directory
+    macos:
+      xcode: 12.2.0
+    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
+#
+
+workflows:
+  version: 2.1
+  build_and_test:
+    jobs:
+      - test
+      - build-example

+ 69 - 0
Dependecies/LoopKit/.gitignore

@@ -0,0 +1,69 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+
+## Other
+*.xccheckout
+*.moved-aside
+*.xcuserstate
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+Carthage/Checkouts
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/screenshots
+
+# Sensitive details about remote backup
+RemoteSettings.plist
+
+.DS_Store
+
+Carthage/

+ 12 - 0
Dependecies/LoopKit/.travis.yml

@@ -0,0 +1,12 @@
+language: objective-c
+osx_image: xcode12.2
+
+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

+ 72 - 0
Dependecies/LoopKit/CODE_OF_CONDUCT.md

@@ -0,0 +1,72 @@
+# Code of Conduct
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the maintaner [via email](mailto:loudnate@gmail.com). All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/

+ 1 - 0
Dependecies/LoopKit/Cartfile

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

+ 1 - 0
Dependecies/LoopKit/Cartfile.resolved

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

+ 21 - 0
Dependecies/LoopKit/Common/LocalizedString.swift

@@ -0,0 +1,21 @@
+//
+//  LocalizedString.swift
+//  LoopKit
+//
+//  Created by Retina15 on 8/6/18.
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+private class FrameworkBundle {
+    static let main = Bundle(for: FrameworkBundle.self)
+}
+
+func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String {
+    if let value = value {
+        return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment)
+    } else {
+        return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment)
+    }
+}

+ 19 - 0
Dependecies/LoopKit/Extensions/ClosedRange.swift

@@ -0,0 +1,19 @@
+//
+//  ClosedRange.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 6/23/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+extension ClosedRange {
+    func expandedToInclude(_ value: Bound) -> ClosedRange {
+        if value < lowerBound {
+            return value...upperBound
+        } else if value > upperBound {
+            return lowerBound...value
+        } else {
+            return self
+        }
+    }
+}

+ 98 - 0
Dependecies/LoopKit/Extensions/Collection.swift

@@ -0,0 +1,98 @@
+//
+//  Collection.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 2/14/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+/// Returns the cartesian product of a sequence and a collection.
+///
+/// O(1), but O(_n_*_m_) on iteration.
+/// - Note: Don't mind the scary return type; it's just a lazy sequence.
+func product<S: Sequence, C: Collection>(_ s: S, _ c: C) -> LazySequence<FlattenSequence<LazyMapSequence<S, LazyMapSequence<C, (S.Element, C.Element)>>>> {
+    return s.lazy.flatMap { first in
+        c.lazy.map { second in
+            (first, second)
+        }
+    }
+}
+
+extension Collection {
+    /// Returns a sequence containing adjacent pairs of elements in the ordered collection.
+    func adjacentPairs() -> Zip2Sequence<Self, SubSequence> {
+        return zip(self, dropFirst())
+    }
+}
+
+extension Collection {
+    func chunked(into size: Int) -> [SubSequence] {
+        precondition(size > 0, "Chunk size must be greater than zero")
+        var start = startIndex
+        return stride(from: 0, to: count, by: size).map {_ in
+            let end = index(start, offsetBy: size, limitedBy: endIndex) ?? endIndex
+            defer { start = end }
+            return self[start..<end]
+        }
+    }
+}
+
+extension RandomAccessCollection {
+    /// Returns all unique pair combinations of elements in the collection.
+    ///
+    /// O(1), but O(*n*²) on iteration.
+    /// - Note: Don't mind the scary return type; it's just a lazy sequence.
+    func allPairs() -> LazyMapSequence<LazyFilterSequence<FlattenSequence<LazyMapSequence<Indices, LazyMapSequence<Indices, (Index, Index)>>>>, (Element, Element)> {
+        return product(indices, indices).filter(<).map {
+            (self[$0], self[$1])
+        }
+    }
+}
+
+extension RangeReplaceableCollection where Index: Hashable {
+    /// Removes the elements at all of the given indices.
+    ///
+    /// O(_n_*_m_)
+    mutating func removeAll<S: Sequence>(at indices: S) where S.Element == Index {
+        let arranged = Set(indices).sorted(by: >)
+        for index in arranged {
+            remove(at: index)
+        }
+    }
+}
+
+// Source:  https://github.com/apple/swift/blob/master/stdlib/public/core/CollectionAlgorithms.swift#L476
+extension Collection {
+    /// Returns the index of the first element in the collection that matches
+    /// the predicate.
+    ///
+    /// The collection must already be partitioned according to the predicate.
+    /// That is, there should be an index `i` where for every element in
+    /// `collection[..<i]` the predicate is `false`, and for every element
+    /// in `collection[i...]` the predicate is `true`.
+    ///
+    /// - Parameter predicate: A predicate that partitions the collection.
+    /// - Returns: The index of the first element in the collection for which
+    ///   `predicate` returns `true`.
+    ///
+    /// - Complexity: O(log *n*), where *n* is the length of this collection if
+    ///   the collection conforms to `RandomAccessCollection`, otherwise O(*n*).
+    func partitioningIndex(
+        where predicate: (Element) throws -> Bool
+    ) rethrows -> Index {
+        var n = count
+        var l = startIndex
+
+        while n > 0 {
+            let half = n / 2
+            let mid = index(l, offsetBy: half)
+            if try predicate(self[mid]) {
+                n = half
+            } else {
+                l = index(after: mid)
+                n -= half + 1
+            }
+        }
+        return l
+    }
+}

+ 23 - 0
Dependecies/LoopKit/Extensions/Comparable.swift

@@ -0,0 +1,23 @@
+//
+//  Comparable.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 11/20/18.
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+extension Comparable {
+    func clamped(to range: ClosedRange<Self>) -> Self {
+        if self < range.lowerBound {
+            return range.lowerBound
+        } else if self > range.upperBound {
+            return range.upperBound
+        } else {
+            return self
+        }
+    }
+
+    mutating func clamp(to range: ClosedRange<Self>) {
+        self = clamped(to: range)
+    }
+}

+ 174 - 0
Dependecies/LoopKit/Extensions/Guardrail+Settings.swift

@@ -0,0 +1,174 @@
+//
+//  Guardrail+Settings.swift
+//  LoopKit
+//
+//  Created by Rick Pasetto on 7/14/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+public extension Guardrail where Value == HKQuantity {
+    static let suspendThreshold = Guardrail(absoluteBounds: 67...110, recommendedBounds: 74...80, unit: .milligramsPerDeciliter, startingSuggestion: 80)
+
+    static func maxSuspendThresholdValue(correctionRangeSchedule: GlucoseRangeSchedule?, preMealTargetRange: ClosedRange<HKQuantity>?, workoutTargetRange: ClosedRange<HKQuantity>?) -> HKQuantity {
+
+        return [
+            suspendThreshold.absoluteBounds.upperBound,
+            correctionRangeSchedule?.minLowerBound(),
+            preMealTargetRange?.lowerBound,
+            workoutTargetRange?.lowerBound
+        ]
+        .compactMap { $0 }
+        .min()!
+    }
+
+    static let correctionRange = Guardrail(absoluteBounds: 87...180, recommendedBounds: 100...115, unit: .milligramsPerDeciliter, startingSuggestion: 100)
+
+    static func minCorrectionRangeValue(suspendThreshold: GlucoseThreshold?) -> HKQuantity {
+        return [
+            correctionRange.absoluteBounds.lowerBound,
+            suspendThreshold?.quantity
+        ]
+        .compactMap { $0 }
+        .max()!
+    }
+    
+    // Static "unconstrained" constant values before applying constraints
+    static let unconstrainedWorkoutCorrectionRange = Guardrail(absoluteBounds: 85...250,
+                                                               recommendedBounds: correctionRange.recommendedBounds.lowerBound.doubleValue(for: .milligramsPerDeciliter)...180,
+                                                               unit: .milligramsPerDeciliter)
+    
+    fileprivate static func workoutCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
+                                                   suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
+        let absoluteLowerBound = [
+            unconstrainedWorkoutCorrectionRange.absoluteBounds.lowerBound,
+            suspendThreshold?.quantity
+        ]
+        .compactMap { $0 }
+        .max()!
+        let recommmendedLowerBound = max(absoluteLowerBound, correctionRangeScheduleRange.upperBound)
+        return Guardrail(
+            absoluteBounds: absoluteLowerBound...unconstrainedWorkoutCorrectionRange.absoluteBounds.upperBound,
+            recommendedBounds: recommmendedLowerBound...unconstrainedWorkoutCorrectionRange.recommendedBounds.upperBound
+        )
+    }
+
+    static let premealCorrectionRangeMaximum = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 130.0)
+
+    fileprivate static func preMealCorrectionRange(correctionRangeScheduleRange: ClosedRange<HKQuantity>,
+                                                   suspendThreshold: GlucoseThreshold?) -> Guardrail<HKQuantity> {
+        let absoluteLowerBound = suspendThreshold?.quantity ?? Guardrail.suspendThreshold.absoluteBounds.lowerBound
+        return Guardrail(
+            absoluteBounds: absoluteLowerBound...premealCorrectionRangeMaximum,
+            recommendedBounds: absoluteLowerBound...min(max(absoluteLowerBound, correctionRangeScheduleRange.lowerBound), premealCorrectionRangeMaximum)
+        )
+    }
+    
+    static func correctionRangeOverride(for preset: CorrectionRangeOverrides.Preset,
+                                        correctionRangeScheduleRange: ClosedRange<HKQuantity>,
+                                        suspendThreshold: GlucoseThreshold?) -> Guardrail {
+        
+        switch preset {
+        case .workout:
+            return workoutCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
+        case .preMeal:
+            return preMealCorrectionRange(correctionRangeScheduleRange: correctionRangeScheduleRange, suspendThreshold: suspendThreshold)
+        }
+    }
+    
+    static let insulinSensitivity = Guardrail(
+        absoluteBounds: 10...500,
+        recommendedBounds: 16...399,
+        unit: HKUnit.milligramsPerDeciliter.unitDivided(by: .internationalUnit()),
+        startingSuggestion: 50
+    )
+ 
+    static let carbRatio = Guardrail(
+        absoluteBounds: 2...150,
+        recommendedBounds: 4...28,
+        unit: .gramsPerUnit,
+        startingSuggestion: 15
+    )
+
+    static func basalRate(supportedBasalRates: [Double]) -> Guardrail {
+        let scheduledBasalRateAbsoluteRange = 0.05...30.0
+        let allowedBasalRates = supportedBasalRates.filter { scheduledBasalRateAbsoluteRange.contains($0) }
+        return Guardrail(
+            absoluteBounds: allowedBasalRates.first!...allowedBasalRates.last!,
+            recommendedBounds: allowedBasalRates.first!...allowedBasalRates.last!,
+            unit: .internationalUnitsPerHour,
+            startingSuggestion: allowedBasalRates.first!
+        )
+    }
+
+    static func maximumBasalRate(
+        supportedBasalRates: [Double],
+        scheduledBasalRange: ClosedRange<Double>?,
+        lowestCarbRatio: Double?,
+        maximumBasalRatePrecision decimalPlaces: Int = 3
+    ) -> Guardrail {
+        
+        let maximumUpperBound = 70.0 / (lowestCarbRatio ?? carbRatio.absoluteBounds.lowerBound.doubleValue(for: .gramsPerUnit))
+        let absoluteUpperBound = maximumUpperBound.matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
+
+        let recommendedHighScheduledBasalScaleFactor = 6.4
+        let recommendedLowScheduledBasalScaleFactor = 2.1
+
+        let recommendedLowerBound: Double
+        let recommendedUpperBound: Double
+        if let highestScheduledBasalRate = scheduledBasalRange?.upperBound {
+            recommendedLowerBound = (recommendedLowScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
+            recommendedUpperBound = (recommendedHighScheduledBasalScaleFactor * highestScheduledBasalRate).matchingOrTruncatedValue(from: supportedBasalRates, withinDecimalPlaces: decimalPlaces)
+            
+            let absoluteBounds = highestScheduledBasalRate...max(absoluteUpperBound, recommendedUpperBound)
+            let recommendedBounds = (recommendedLowerBound...recommendedUpperBound).clamped(to: absoluteBounds)
+            return Guardrail(
+                absoluteBounds: absoluteBounds,
+                recommendedBounds: recommendedBounds,
+                unit: .internationalUnitsPerHour
+            )
+
+        } else {
+            let bounds = supportedBasalRates.drop { $0 <= 0 }.first!...absoluteUpperBound
+            return Guardrail(
+                absoluteBounds: bounds,
+                recommendedBounds: bounds,
+                unit: .internationalUnitsPerHour,
+                startingSuggestion: 3.clamped(to: bounds)
+            )
+        }
+    }
+    
+    static func selectableMaxBasalRates(supportedBasalRates: [Double],
+                                     scheduledBasalRange: ClosedRange<Double>?,
+                                     lowestCarbRatio: Double?,
+                                     maximumBasalRatePrecision decimalPlaces: Int = 3) -> [Double] {
+        let basalGuardrail = Guardrail.maximumBasalRate(supportedBasalRates: supportedBasalRates, scheduledBasalRange: scheduledBasalRange, lowestCarbRatio: lowestCarbRatio)
+        let maximumScheduledBasalRate = scheduledBasalRange?.upperBound ?? -Double.infinity
+        return supportedBasalRates
+            .drop { $0 < maximumScheduledBasalRate }
+            .filter { basalGuardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0)) }
+    }
+
+    static func maximumBolus(supportedBolusVolumes: [Double]) -> Guardrail {
+        let maxBolusThresholdUnits: Double = 30
+        let maxBolusWarningThresholdUnits: Double = 20
+        let supportedBolusVolumes = supportedBolusVolumes.filter { $0 > 0 && $0 <= maxBolusThresholdUnits }
+        let recommendedUpperBound = supportedBolusVolumes.last { $0 < maxBolusWarningThresholdUnits }
+        let recommendedBounds = supportedBolusVolumes.dropFirst().first!...recommendedUpperBound!
+        return Guardrail(
+            absoluteBounds: supportedBolusVolumes.first!...supportedBolusVolumes.last!,
+            recommendedBounds: recommendedBounds,
+            unit: .internationalUnit(),
+            startingSuggestion: 5.clamped(to: recommendedBounds)
+        )
+    }
+    
+    static func selectableBolusVolumes(supportedBolusVolumes: [Double]) -> [Double] {
+        let guardrail = Guardrail.maximumBolus(supportedBolusVolumes: supportedBolusVolumes)
+        return supportedBolusVolumes.filter {
+            guardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnit(), doubleValue: $0))
+        }
+    }
+}

+ 53 - 0
Dependecies/LoopKit/Extensions/HKUnit.swift

@@ -0,0 +1,53 @@
+//
+//  HKUnit.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/17/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import HealthKit
+
+
+extension HKUnit {
+    static let milligramsPerDeciliter: HKUnit = {
+        return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    static let millimolesPerLiter: HKUnit = {
+        return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+
+    static let internationalUnitsPerHour: HKUnit = {
+        return HKUnit.internationalUnit().unitDivided(by: .hour())
+    }()
+
+    static let gramsPerUnit: HKUnit = {
+        return HKUnit.gram().unitDivided(by: .internationalUnit())
+    }()
+    
+    var foundationUnit: Unit? {
+        if self == HKUnit.milligramsPerDeciliter {
+            return UnitConcentrationMass.milligramsPerDeciliter
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            return UnitConcentrationMass.millimolesPerLiter(withGramsPerMole: HKUnitMolarMassBloodGlucose)
+        }
+
+        if self == HKUnit.gram() {
+            return UnitMass.grams
+        }
+
+        return nil
+    }
+    
+    /// The smallest value expected to be visible on a chart
+    var chartableIncrement: Double {
+        if self == .milligramsPerDeciliter {
+            return 1
+        } else {
+            return 1 / 25
+        }
+    }
+}

+ 21 - 0
Dependecies/LoopKit/Extensions/IdentifiableClass.swift

@@ -0,0 +1,21 @@
+//
+//  IdentifiableClass.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 2/9/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+protocol IdentifiableClass: class {
+    static var className: String { get }
+}
+
+
+extension IdentifiableClass {
+    static var className: String {
+        return NSStringFromClass(self).components(separatedBy: ".").last!
+    }
+}

+ 24 - 0
Dependecies/LoopKit/Extensions/Math.swift

@@ -0,0 +1,24 @@
+//
+//  Math.swift
+//  LoopKitUI
+//
+//  Created by Michael Pangburn on 3/23/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+
+func fractionThrough<Metric: FloatingPoint>(
+    _ value: Metric,
+    in range: ClosedRange<Metric>,
+    using transform: (Metric) -> Metric = { $0 }
+) -> Metric {
+    let transformedLowerBound = transform(range.lowerBound)
+    return (transform(value) - transformedLowerBound) / (transform(range.upperBound) - transformedLowerBound)
+}
+
+func interpolatedValue<Metric: FloatingPoint>(
+    at fraction: Metric,
+    through range: ClosedRange<Metric>
+) -> Metric {
+    fraction * (range.upperBound - range.lowerBound) + range.lowerBound
+}

+ 17 - 0
Dependecies/LoopKit/Extensions/MutableCollection.swift

@@ -0,0 +1,17 @@
+//
+//  MutableCollection.swift
+//  LoopKit Example
+//
+//  Created by Michael Pangburn on 4/21/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+extension MutableCollection {
+    public mutating func mutateEach(_ body: (inout Element) throws -> Void) rethrows {
+        var index = startIndex
+        while index != endIndex {
+            try body(&self[index])
+            formIndex(after: &index)
+        }
+    }
+}

+ 56 - 0
Dependecies/LoopKit/Extensions/NSData.swift

@@ -0,0 +1,56 @@
+//
+//  NSData.swift
+//  LoopKit
+//
+//  Created by Nate Racklyeft on 8/26/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+// String conversion methods, adapted from https://stackoverflow.com/questions/40276322/hex-binary-string-conversion-in-swift/40278391#40278391
+extension Data {
+    init?(hexadecimalString: String) {
+        self.init(capacity: hexadecimalString.utf16.count / 2)
+
+        // Convert 0 ... 9, a ... f, A ...F to their decimal value,
+        // return nil for all other input characters
+        func decodeNibble(u: UInt16) -> UInt8? {
+            switch u {
+            case 0x30 ... 0x39:  // '0'-'9'
+                return UInt8(u - 0x30)
+            case 0x41 ... 0x46:  // 'A'-'F'
+                return UInt8(u - 0x41 + 10)  // 10 since 'A' is 10, not 0
+            case 0x61 ... 0x66:  // 'a'-'f'
+                return UInt8(u - 0x61 + 10)  // 10 since 'a' is 10, not 0
+            default:
+                return nil
+            }
+        }
+
+        var even = true
+        var byte: UInt8 = 0
+        for c in hexadecimalString.utf16 {
+            guard let val = decodeNibble(u: c) else { return nil }
+            if even {
+                byte = val << 4
+            } else {
+                byte += val
+                self.append(byte)
+            }
+            even = !even
+        }
+        guard even else { return nil }
+    }
+
+    var hexadecimalString: String {
+        return map { String(format: "%02hhx", $0) }.joined()
+    }
+}
+
+extension Data {
+    static func newPumpEventIdentifier() -> Data {
+        return Data(UUID().uuidString.utf8)
+    }
+}

+ 33 - 0
Dependecies/LoopKit/Extensions/NSDateFormatter.swift

@@ -0,0 +1,33 @@
+//
+//  NSDateFormatter.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 11/25/15.
+//  Copyright © 2015 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+// MARK: - Extensions useful in parsing fixture dates
+extension ISO8601DateFormatter {
+    static func localTimeDate(timeZone: TimeZone = .currentFixed) -> Self {
+        let formatter = self.init()
+
+        formatter.formatOptions = .withInternetDateTime
+        formatter.formatOptions.subtract(.withTimeZone)
+        formatter.timeZone = timeZone
+
+        return formatter
+    }
+}
+
+
+extension DateFormatter {
+    static var descriptionFormatter: DateFormatter {
+        let formatter = self.init()
+        formatter.dateFormat = "yyyy-MM-dd HH:mm:ssZZZZZ"
+
+        return formatter
+    }
+}

+ 36 - 0
Dependecies/LoopKit/Extensions/NSTimeInterval.swift

@@ -0,0 +1,36 @@
+//
+//  NSTimeInterval.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/9/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+extension TimeInterval {
+    static func minutes(_ minutes: Double) -> TimeInterval {
+        return self.init(minutes: minutes)
+    }
+
+    static func hours(_ hours: Double) -> TimeInterval {
+        return self.init(hours: hours)
+    }
+
+    init(minutes: Double) {
+        self.init(minutes * 60)
+    }
+
+    init(hours: Double) {
+        self.init(minutes: hours * 60)
+    }
+
+    var minutes: Double {
+        return self / 60.0
+    }
+
+    var hours: Double {
+        return minutes / 60.0
+    }
+}

+ 21 - 0
Dependecies/LoopKit/Extensions/NibLoadable.swift

@@ -0,0 +1,21 @@
+//
+//  NibLoadable.swift
+//  LoopKit
+//
+//  Created by Nate Racklyeft on 7/2/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+
+protocol NibLoadable: IdentifiableClass {
+    static func nib() -> UINib
+}
+
+
+extension NibLoadable {
+    static func nib() -> UINib {
+        return UINib(nibName: className, bundle: Bundle(for: self))
+    }
+}

+ 47 - 0
Dependecies/LoopKit/Extensions/NumberFormatter.swift

@@ -0,0 +1,47 @@
+//
+//  NSNumberFormatter.swift
+//  Loop
+//
+//  Created by Nate Racklyeft on 9/5/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+extension NumberFormatter {
+    func string(from number: Double) -> String? {
+        return string(from: NSNumber(value: number))
+    }
+
+    func string(from number: Double, unit: String, style: Formatter.UnitStyle = .medium) -> String? {
+        guard let stringValue = string(from: number) else {
+            return nil
+        }
+
+        let format: 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 .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)"
+            )
+        }
+
+        return String(
+            format: format,
+            stringValue,
+            unit
+        )
+    }
+}

+ 50 - 0
Dependecies/LoopKit/Extensions/OSLog.swift

@@ -0,0 +1,50 @@
+//
+//  OSLog.swift
+//  Loop
+//
+//  Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import os.log
+
+
+extension OSLog {
+    convenience init(category: String) {
+        self.init(subsystem: "com.loopkit.LoopKit", category: category)
+    }
+
+    func debug(_ message: StaticString, _ args: CVarArg...) {
+        log(message, type: .debug, args)
+    }
+
+    func info(_ message: StaticString, _ args: CVarArg...) {
+        log(message, type: .info, args)
+    }
+
+    func `default`(_ message: StaticString, _ args: CVarArg...) {
+        log(message, type: .default, args)
+    }
+
+    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: self, type: type)
+        case 1:
+            os_log(message, log: self, type: type, args[0])
+        case 2:
+            os_log(message, log: self, type: type, args[0], args[1])
+        case 3:
+            os_log(message, log: self, type: type, args[0], args[1], args[2])
+        case 4:
+            os_log(message, log: self, type: type, args[0], args[1], args[2], args[3])
+        case 5:
+            os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4])
+        default:
+            os_log(message, log: self, type: type, args)
+        }
+    }
+}

+ 19 - 0
Dependecies/LoopKit/Extensions/Sequence.swift

@@ -0,0 +1,19 @@
+//
+//  Sequence.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 6/23/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+extension Sequence {
+    func range<Metric: Comparable>(of metricForElement: (Element) throws -> Metric) rethrows -> ClosedRange<Metric>? {
+        try lazy.map(metricForElement).reduce(nil) { range, metric in
+            if let range = range {
+                return range.expandedToInclude(metric)
+            } else {
+                return metric...metric
+            }
+        }
+    }
+}

+ 16 - 0
Dependecies/LoopKit/Extensions/TimeZone.swift

@@ -0,0 +1,16 @@
+//
+//  TimeZone.swift
+//  LoopKit
+//
+//  Created by Nate Racklyeft on 10/2/16.
+//  Copyright © 2016 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+
+extension TimeZone {
+    static var currentFixed: TimeZone {
+        return TimeZone(secondsFromGMT: TimeZone.current.secondsFromGMT())!
+    }
+}

+ 22 - 0
Dependecies/LoopKit/Extensions/UIColor.swift

@@ -0,0 +1,22 @@
+//
+//  UIColor.swift
+//  LoopKitUI
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import UIKit
+
+
+extension UIColor {
+    static let delete = UIColor.higRed()
+}
+
+
+// MARK: - HIG colors
+// See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/
+extension UIColor {
+    private static func higRed() -> UIColor {
+        return UIColor(red: 1, green: 59 / 255, blue: 48 / 255, alpha: 1)
+    }
+}

+ 13 - 0
Dependecies/LoopKit/Extensions/UITableViewCell.swift

@@ -0,0 +1,13 @@
+//
+//  UITableViewCell.swift
+//  CarbKit
+//
+//  Created by Nathan Racklyeft on 1/15/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+
+extension UITableViewCell: IdentifiableClass, NibLoadable {
+}

+ 29 - 0
Dependecies/LoopKit/Extensions/UITextField.swift

@@ -0,0 +1,29 @@
+//
+//  UITextField.swift
+//  LoopKitUI
+//
+//  Created by Rick Pasetto on 9/17/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import UIKit
+
+extension UITextField {
+    
+    func selectAll() {
+        dispatchPrecondition(condition: .onQueue(.main))
+        self.selectedTextRange = self.textRange(from: self.beginningOfDocument, to: self.endOfDocument)
+    }
+
+    func moveCursorToEnd() {
+        dispatchPrecondition(condition: .onQueue(.main))
+        let newPosition = self.endOfDocument
+        self.selectedTextRange = self.textRange(from: newPosition, to: newPosition)
+    }
+    
+    func moveCursorToBeginning() {
+        dispatchPrecondition(condition: .onQueue(.main))
+        let newPosition = self.beginningOfDocument
+        self.selectedTextRange = self.textRange(from: newPosition, to: newPosition)
+    }
+}

+ 17 - 0
Dependecies/LoopKit/Extensions/UUID.swift

@@ -0,0 +1,17 @@
+//
+//  UUID.swift
+//  LoopKitTests
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+
+extension UUID {
+    var data: Data {
+        return withUnsafePointer(to: uuid) {
+            return Data(bytes: $0, count: MemoryLayout.size(ofValue: uuid))
+        }
+    }
+}

+ 22 - 0
Dependecies/LoopKit/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Nathan Racklyeft
+Copyright (c) 2016 LoopKit Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 43 - 0
Dependecies/LoopKit/LoopKit Example/AppDelegate.swift

@@ -0,0 +1,43 @@
+//
+//  AppDelegate.swift
+//  LoopKit Example
+//
+//  Created by Nathan Racklyeft on 2/24/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
+
+    var window: UIWindow?
+
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+        return true
+    }
+
+    func applicationWillResignActive(_ application: UIApplication) {
+        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+    }
+
+    func applicationDidEnterBackground(_ application: UIApplication) {
+        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+    }
+
+    func applicationWillEnterForeground(_ application: UIApplication) {
+        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+    }
+
+    func applicationDidBecomeActive(_ application: UIApplication) {
+        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+    }
+
+    func applicationWillTerminate(_ application: UIApplication) {
+        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+    }
+
+}
+

+ 98 - 0
Dependecies/LoopKit/LoopKit Example/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,98 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "83.5x83.5",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "size" : "1024x1024",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 6 - 0
Dependecies/LoopKit/LoopKit Example/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 31 - 0
Dependecies/LoopKit/LoopKit Example/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 85 - 0
Dependecies/LoopKit/LoopKit Example/Base.lproj/Main.storyboard

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="RMx-3f-FxP">
+    <device id="retina4_7" orientation="portrait" appearance="dark"/>
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--UI Tests-->
+        <scene sceneID="pY4-Hu-kfo">
+            <objects>
+                <navigationController title="UI Tests" id="RMx-3f-FxP" sceneMemberID="viewController">
+                    <navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="Pmd-2v-anx">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <connections>
+                        <segue destination="7bK-jq-Zjz" kind="relationship" relationship="rootViewController" id="tsl-Nk-0bq"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="8fS-aE-onr" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-38" y="-630"/>
+        </scene>
+        <!--UI Tests-->
+        <scene sceneID="smW-Zh-WAh">
+            <objects>
+                <tableViewController title="UI Tests" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterViewController" customModule="LoopKit_Example" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="r7i-6Z-zg0">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <prototypes>
+                            <tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" textLabel="Arm-wq-HPj" style="IBUITableViewCellStyleDefault" id="WCw-Qf-5nD">
+                                <rect key="frame" x="0.0" y="28" width="375" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WCw-Qf-5nD" id="37f-cq-3Eg">
+                                    <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <label opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Arm-wq-HPj">
+                                            <rect key="frame" x="16" y="0.0" width="343" height="44"/>
+                                            <autoresizingMask key="autoresizingMask"/>
+                                            <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
+                                            <nil key="textColor"/>
+                                            <color key="highlightedColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                        </label>
+                                    </subviews>
+                                </tableViewCellContentView>
+                            </tableViewCell>
+                        </prototypes>
+                        <sections/>
+                        <connections>
+                            <outlet property="dataSource" destination="7bK-jq-Zjz" id="Gho-Na-rnu"/>
+                            <outlet property="delegate" destination="7bK-jq-Zjz" id="RA6-mI-bju"/>
+                        </connections>
+                    </tableView>
+                    <navigationItem key="navigationItem" id="Zdf-7t-Un8"/>
+                    <connections>
+                        <segue destination="P3Z-xa-jYH" kind="show" identifier="LegacyInsulinDeliveryTableViewController" id="tq6-Gh-gXl"/>
+                        <segue destination="4EV-cN-zR6" kind="show" identifier="CarbEntryTableViewController" id="yH4-bu-swj"/>
+                    </connections>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="709" y="-630"/>
+        </scene>
+        <!--CarbKit-->
+        <scene sceneID="RUH-Hm-Veg">
+            <objects>
+                <viewControllerPlaceholder storyboardName="CarbKit" bundleIdentifier="com.loopkit.LoopKitUI" id="4EV-cN-zR6" sceneMemberID="viewController"/>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="5oW-ge-vQQ" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1239" y="-888"/>
+        </scene>
+        <!--LegacyInsulinDeliveryTableViewController-->
+        <scene sceneID="gKw-ni-nQG">
+            <objects>
+                <viewControllerPlaceholder storyboardName="LegacyInsulinDeliveryTableViewController" bundleIdentifier="com.loopkit.LoopKitUI" id="P3Z-xa-jYH" sceneMemberID="viewController"/>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="OWP-Ht-6Nv" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1445" y="-713"/>
+        </scene>
+    </scenes>
+</document>

+ 12 - 0
Dependecies/LoopKit/LoopKit Example/Extensions/CarbEntryTableViewController.swift

@@ -0,0 +1,12 @@
+//
+//  CarbEntryTableViewController.swift
+//  LoopKit
+//
+//  Created by Nate Racklyeft on 7/13/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import LoopKitUI
+
+
+extension CarbEntryTableViewController: IdentifiableClass { }

+ 12 - 0
Dependecies/LoopKit/LoopKit Example/Extensions/InsulinDeliveryTableViewController.swift

@@ -0,0 +1,12 @@
+//
+//  InsulinDeliveryTableViewController.swift
+//  LoopKit
+//
+//  Created by Nate Racklyeft on 7/13/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import LoopKitUI
+
+
+extension LegacyInsulinDeliveryTableViewController: IdentifiableClass { }

+ 205 - 0
Dependecies/LoopKit/LoopKit Example/Extensions/NSUserDefaults.swift

@@ -0,0 +1,205 @@
+//
+//  NSUserDefaults.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 3/18/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import LoopKit
+
+
+extension UserDefaults {
+    private enum Key: String {
+        case BasalRateSchedule = "com.LoopKitExample.BasalRateSchedule"
+        case CarbRatioSchedule = "com.LoopKitExample.CarbRatioSchedule"
+        case InsulinActionDuration = "com.LoopKitExample.InsulinActionDuration"
+        case InsulinSensitivitySchedule = "com.LoopKitExample.InsulinSensitivitySchedule"
+        case GlucoseTargetRangeSchedule = "com.LoopKitExample.GlucoseTargetRangeSchedule"
+        case PreMealTargetRange = "com.LoopKitExample.PreMealTargetRange"
+        case LegacyWorkoutTargetRange = "com.LoopKitExample.LegacyWorkoutTargetRange"
+        case MaximumBasalRatePerHour = "com.LoopKitExample.MaximumBasalRatePerHour"
+        case MaximumBolus = "com.LoopKitExample.MaximumBolus"
+        case PumpID = "com.LoopKitExample.PumpID"
+        case PumpTimeZone = "com.LoopKitExample.PumpTimeZone"
+        case TransmitterID = "com.LoopKitExample.TransmitterID"
+        case TransmitterStartTime = "com.LoopKitExample.TransmitterStartTime"
+    }
+
+    var basalRateSchedule: BasalRateSchedule? {
+        get {
+            if let rawValue = dictionary(forKey: Key.BasalRateSchedule.rawValue) {
+                return BasalRateSchedule(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+        set {
+            set(newValue?.rawValue, forKey: Key.BasalRateSchedule.rawValue)
+        }
+    }
+
+    var carbRatioSchedule: CarbRatioSchedule? {
+        get {
+            if let rawValue = dictionary(forKey: Key.CarbRatioSchedule.rawValue) {
+                return CarbRatioSchedule(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+        set {
+            set(newValue?.rawValue, forKey: Key.CarbRatioSchedule.rawValue)
+        }
+    }
+
+    var insulinActionDuration: TimeInterval? {
+        get {
+            let value = double(forKey: Key.InsulinActionDuration.rawValue)
+
+            return value > 0 ? value : TimeInterval(hours: 4)
+        }
+        set {
+            if let insulinActionDuration = newValue {
+                set(insulinActionDuration, forKey: Key.InsulinActionDuration.rawValue)
+            } else {
+                removeObject(forKey: Key.InsulinActionDuration.rawValue)
+            }
+        }
+    }
+
+    var insulinSensitivitySchedule: InsulinSensitivitySchedule? {
+        get {
+            if let rawValue = dictionary(forKey: Key.InsulinSensitivitySchedule.rawValue) {
+                return InsulinSensitivitySchedule(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+        set {
+            set(newValue?.rawValue, forKey: Key.InsulinSensitivitySchedule.rawValue)
+        }
+    }
+
+    var glucoseTargetRangeSchedule: GlucoseRangeSchedule? {
+        get {
+            if let rawValue = dictionary(forKey: Key.GlucoseTargetRangeSchedule.rawValue) {
+                return GlucoseRangeSchedule(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+        set {
+            set(newValue?.rawValue, forKey: Key.GlucoseTargetRangeSchedule.rawValue)
+        }
+    }
+
+    var preMealTargetRange: DoubleRange? {
+        get {
+            if let rawValue = array(forKey: Key.PreMealTargetRange.rawValue) as? DoubleRange.RawValue {
+                return DoubleRange(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+
+        set {
+            set(newValue?.rawValue, forKey: Key.PreMealTargetRange.rawValue)
+        }
+    }
+
+
+    var legacyWorkoutTargetRange: DoubleRange? {
+        get {
+            if let rawValue = array(forKey: Key.LegacyWorkoutTargetRange.rawValue) as? DoubleRange.RawValue {
+                return DoubleRange(rawValue: rawValue)
+            } else {
+                return nil
+            }
+        }
+
+        set {
+            set(newValue?.rawValue, forKey: Key.LegacyWorkoutTargetRange.rawValue)
+        }
+    }
+
+    var maximumBasalRatePerHour: Double? {
+        get {
+            let value = double(forKey: Key.MaximumBasalRatePerHour.rawValue)
+
+            return value > 0 ? value : nil
+        }
+        set {
+            if let maximumBasalRatePerHour = newValue {
+                set(maximumBasalRatePerHour, forKey: Key.MaximumBasalRatePerHour.rawValue)
+            } else {
+                removeObject(forKey: Key.MaximumBasalRatePerHour.rawValue)
+            }
+        }
+    }
+
+    var maximumBolus: Double? {
+        get {
+            let value = double(forKey: Key.MaximumBolus.rawValue)
+
+            return value > 0 ? value : nil
+        }
+        set {
+            if let maximumBolus = newValue {
+                set(maximumBolus, forKey: Key.MaximumBolus.rawValue)
+            } else {
+                removeObject(forKey: Key.MaximumBolus.rawValue)
+            }
+        }
+    }
+
+    var pumpID: String? {
+        get {
+            return string(forKey: Key.PumpID.rawValue) ?? "123456"
+        }
+        set {
+            set(newValue, forKey: Key.PumpID.rawValue)
+        }
+    }
+
+    var pumpTimeZone: TimeZone? {
+        get {
+            if let offset = object(forKey: Key.PumpTimeZone.rawValue) as? NSNumber {
+                return TimeZone(secondsFromGMT: offset.intValue)
+            } else {
+                return nil
+            }
+        } set {
+            if let value = newValue {
+                set(NSNumber(value: value.secondsFromGMT()), forKey: Key.PumpTimeZone.rawValue)
+            } else {
+                removeObject(forKey: Key.PumpTimeZone.rawValue)
+            }
+        }
+    }
+
+    var transmitterStartTime: TimeInterval? {
+        get {
+            let value = double(forKey: Key.TransmitterStartTime.rawValue)
+
+            return value > 0 ? value : nil
+        }
+        set {
+            if let value = newValue {
+                set(value, forKey: Key.TransmitterStartTime.rawValue)
+            } else {
+                removeObject(forKey: Key.TransmitterStartTime.rawValue)
+            }
+        }
+    }
+
+    var transmitterID: String? {
+        get {
+            return string(forKey: Key.TransmitterID.rawValue)
+        }
+        set {
+            set(newValue, forKey: Key.TransmitterID.rawValue)
+        }
+    }
+
+}

+ 62 - 0
Dependecies/LoopKit/LoopKit Example/Info.plist

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>3.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>2</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>NSHealthShareUsageDescription</key>
+	<string>Meal data from the Health database is used to determine glucose effects. Glucose data from the Health database is used for graphing and momentum calculation.</string>
+	<key>NSHealthUpdateUsageDescription</key>
+	<string>Carbohydrate meal data entered in the app is stored in the Health database. Glucose data is stored securely in HealthKit.</string>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+		<string>healthkit</string>
+	</array>
+	<key>UIStatusBarTintParameters</key>
+	<dict>
+		<key>UINavigationBar</key>
+		<dict>
+			<key>Style</key>
+			<string>UIBarStyleDefault</string>
+			<key>Translucent</key>
+			<false/>
+		</dict>
+	</dict>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 8 - 0
Dependecies/LoopKit/LoopKit Example/LoopKitExample.entitlements

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.healthkit</key>
+	<true/>
+</dict>
+</plist>

+ 131 - 0
Dependecies/LoopKit/LoopKit Example/Managers/DeviceDataManager.swift

@@ -0,0 +1,131 @@
+//
+//  DeviceDataManager.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 3/18/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+import LoopKit
+
+
+class DeviceDataManager {
+
+    init() {
+        let healthStore = HKHealthStore()
+        let cacheStore = PersistenceController(directoryURL: FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!)
+
+        carbStore = CarbStore(
+            healthStore: healthStore,
+            cacheStore: cacheStore,
+            cacheLength: .hours(24),
+            defaultAbsorptionTimes: (fast: .minutes(30), medium: .hours(3), slow: .hours(5)),
+            observationInterval: .hours(24),
+            carbRatioSchedule: carbRatioSchedule,
+            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,
+            basalProfile: basalRateSchedule,
+            insulinSensitivitySchedule: insulinSensitivitySchedule,
+            provenanceIdentifier: HKSource.default().bundleIdentifier
+        )
+        glucoseStore = GlucoseStore(healthStore: healthStore,
+                                    cacheStore: cacheStore,
+                                    provenanceIdentifier: HKSource.default().bundleIdentifier)
+    }
+
+    // Data stores
+
+    let carbStore: CarbStore!
+
+    let doseStore: DoseStore
+
+    let glucoseStore: GlucoseStore!
+
+    // Settings
+
+    var basalRateSchedule = UserDefaults.standard.basalRateSchedule {
+        didSet {
+            UserDefaults.standard.basalRateSchedule = basalRateSchedule
+
+            doseStore.basalProfile = basalRateSchedule
+        }
+    }
+
+    var carbRatioSchedule = UserDefaults.standard.carbRatioSchedule {
+        didSet {
+            UserDefaults.standard.carbRatioSchedule = carbRatioSchedule
+
+            carbStore?.carbRatioSchedule = carbRatioSchedule
+        }
+    }
+
+    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
+
+            carbStore?.insulinSensitivitySchedule = insulinSensitivitySchedule
+            doseStore.insulinSensitivitySchedule = insulinSensitivitySchedule
+        }
+    }
+
+    var glucoseTargetRangeSchedule = UserDefaults.standard.glucoseTargetRangeSchedule {
+        didSet {
+            UserDefaults.standard.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
+        }
+    }
+
+    public var preMealTargetRange: DoubleRange? = UserDefaults.standard.preMealTargetRange {
+        didSet {
+            UserDefaults.standard.preMealTargetRange = preMealTargetRange
+        }
+    }
+
+    public var legacyWorkoutTargetRange: DoubleRange? = UserDefaults.standard.legacyWorkoutTargetRange {
+        didSet {
+            UserDefaults.standard.legacyWorkoutTargetRange = legacyWorkoutTargetRange
+        }
+    }
+
+    var pumpID = UserDefaults.standard.pumpID {
+        didSet {
+            UserDefaults.standard.pumpID = pumpID
+
+            if pumpID != oldValue {
+                doseStore.resetPumpData()
+            }
+        }
+    }
+
+    // MARK: CarbStoreDelegate
+
+    func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) {}
+
+    func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {
+        print("carbstore error: \(error)")
+    }
+}

+ 427 - 0
Dependecies/LoopKit/LoopKit Example/MasterViewController.swift

@@ -0,0 +1,427 @@
+//
+//  MasterViewController.swift
+//  LoopKit Example
+//
+//  Created by Nathan Racklyeft on 2/24/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+import LoopKit
+import LoopKitUI
+import HealthKit
+
+
+class MasterViewController: UITableViewController {
+
+    private var dataManager: DeviceDataManager? = DeviceDataManager()
+
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+
+        guard let dataManager = dataManager else {
+            return
+        }
+
+        let sampleTypes = Set([
+            dataManager.glucoseStore.sampleType,
+            dataManager.carbStore.sampleType,
+            dataManager.doseStore.sampleType,
+        ].compactMap { $0 })
+
+        if dataManager.glucoseStore.authorizationRequired ||
+            dataManager.carbStore.authorizationRequired ||
+            dataManager.doseStore.authorizationRequired
+        {
+            dataManager.carbStore.healthStore.requestAuthorization(toShare: sampleTypes, read: sampleTypes) { (success, error) in
+                if success {
+                    // Call the individual authorization methods to trigger query creation
+                    dataManager.carbStore.authorize({ _ in })
+                    dataManager.doseStore.insulinDeliveryStore.authorize(toShare: true, { _ in })
+                    dataManager.glucoseStore.authorize({ _ in })
+                }
+            }
+        }
+    }
+
+    // MARK: - Data Source
+
+    private enum Section: Int, CaseIterable {
+        case data
+        case configuration
+    }
+
+    private enum DataRow: Int, CaseIterable {
+        case carbs = 0
+        case reservoir
+        case diagnostic
+        case generate
+        case reset
+    }
+
+    private enum ConfigurationRow: Int, CaseIterable {
+        case basalRate
+        case carbRatio
+        case correctionRange
+        case insulinSensitivity
+        case pumpID
+    }
+
+    // MARK: UITableViewDataSource
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return Section.allCases.count
+    }
+
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        switch Section(rawValue: section)! {
+        case .configuration:
+            return ConfigurationRow.allCases.count
+        case .data:
+            return DataRow.allCases.count
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
+
+        switch Section(rawValue: indexPath.section)! {
+        case .configuration:
+            switch ConfigurationRow(rawValue: indexPath.row)! {
+            case .basalRate:
+                cell.textLabel?.text = LocalizedString("Basal Rates", comment: "The title text for the basal rate schedule")
+            case .carbRatio:
+                cell.textLabel?.text = LocalizedString("Carb Ratios", comment: "The title of the carb ratios schedule screen")
+            case .correctionRange:
+                cell.textLabel?.text = LocalizedString("Correction Range", comment: "The title text for the glucose correction range schedule")
+            case .insulinSensitivity:
+                cell.textLabel?.text = LocalizedString("Insulin Sensitivity", comment: "The title text for the insulin sensitivity schedule")
+            case .pumpID:
+                cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title text for the pump ID")
+            }
+        case .data:
+            switch DataRow(rawValue: indexPath.row)! {
+            case .carbs:
+                cell.textLabel?.text = LocalizedString("Carbs", comment: "The title for the cell navigating to the carbs screen")
+            case .reservoir:
+                cell.textLabel?.text = LocalizedString("Reservoir", comment: "The title for the cell navigating to the reservoir screen")
+            case .diagnostic:
+                cell.textLabel?.text = LocalizedString("Diagnostic", comment: "The title for the cell displaying diagnostic data")
+            case .generate:
+                cell.textLabel?.text = LocalizedString("Generate Data", comment: "The title for the cell displaying data generation")
+            case .reset:
+                cell.textLabel?.text = LocalizedString("Reset", comment: "Title for the cell resetting the data manager")
+            }
+        }
+
+        return cell
+    }
+
+    // MARK: - UITableViewDelegate
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let sender = tableView.cellForRow(at: indexPath)
+
+        switch Section(rawValue: indexPath.section)! {
+        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
+//                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)
+            case .carbRatio:
+                let scheduleVC = DailyQuantityScheduleTableViewController()
+
+                scheduleVC.delegate = self
+                scheduleVC.title = NSLocalizedString("Carb Ratios", comment: "The title of the carb ratios schedule screen")
+                scheduleVC.unit = .gram()
+
+                if let schedule = dataManager?.carbRatioSchedule {
+                    scheduleVC.timeZone = schedule.timeZone
+                    scheduleVC.scheduleItems = schedule.items
+                    scheduleVC.unit = schedule.unit
+                }
+
+                show(scheduleVC, sender: sender)
+            case .correctionRange:
+
+                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)
+                }
+
+                show(scheduleVC, sender: sender)
+            case .insulinSensitivity:
+                let unit = dataManager?.insulinSensitivitySchedule?.unit ?? dataManager?.glucoseStore.preferredUnit ?? HKUnit.milligramsPerDeciliter
+                let scheduleVC = InsulinSensitivityScheduleViewController(allowedValues: unit.allowedSensitivityValues, unit: unit)
+
+                scheduleVC.unit = unit
+                scheduleVC.delegate = self
+                scheduleVC.insulinSensitivityScheduleStorageDelegate = self
+                scheduleVC.schedule = dataManager?.insulinSensitivitySchedule
+                scheduleVC.title = NSLocalizedString("Insulin Sensitivity", comment: "The title of the insulin sensitivity schedule screen")
+                show(scheduleVC, sender: sender)
+
+            case .pumpID:
+                let textFieldVC = TextFieldTableViewController()
+
+//                textFieldVC.delegate = self
+                textFieldVC.title = sender?.textLabel?.text
+                textFieldVC.placeholder = LocalizedString("Enter the 6-digit pump ID", comment: "The placeholder text instructing users how to enter a pump ID")
+                textFieldVC.value = dataManager?.pumpID
+                textFieldVC.keyboardType = .numberPad
+                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)
+            }
+        case .data:
+            switch DataRow(rawValue: indexPath.row)! {
+            case .carbs:
+                performSegue(withIdentifier: CarbEntryTableViewController.className, sender: sender)
+            case .reservoir:
+                performSegue(withIdentifier: LegacyInsulinDeliveryTableViewController.className, sender: sender)
+            case .diagnostic:
+                let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
+                    let group = DispatchGroup()
+
+                    guard let dataManager = self?.dataManager else {
+                        completionHandler("")
+                        return "nil"
+                    }
+
+                    var doseStoreResponse = ""
+                    group.enter()
+                    dataManager.doseStore.generateDiagnosticReport { (report) in
+                        doseStoreResponse = report
+                        group.leave()
+                    }
+
+                    var carbStoreResponse = ""
+                    if let carbStore = dataManager.carbStore {
+                        group.enter()
+                        carbStore.generateDiagnosticReport { (report) in
+                            carbStoreResponse = report
+                            group.leave()
+                        }
+                    }
+
+                    var glucoseStoreResponse = ""
+                    group.enter()
+                    dataManager.glucoseStore.generateDiagnosticReport { (report) in
+                        glucoseStoreResponse = report
+                        group.leave()
+                    }
+
+                    group.notify(queue: DispatchQueue.main) {
+                        completionHandler([
+                            doseStoreResponse,
+                            carbStoreResponse,
+                            glucoseStoreResponse
+                        ].joined(separator: "\n\n"))
+                    }
+
+                    return "…"
+                })
+                vc.title = "Diagnostic"
+
+                show(vc, sender: sender)
+            case .generate:
+                let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
+                    guard let dataManager = self?.dataManager else {
+                        completionHandler("")
+                        return "dataManager is nil"
+                    }
+
+                    let group = DispatchGroup()
+
+                    var unitVolume = 150.0
+
+                    reservoir: for index in sequence(first: TimeInterval(hours: -6), next: { $0 + .minutes(5) }) {
+                        guard index < 0 else {
+                            break reservoir
+                        }
+
+                        unitVolume -= (drand48() * 2.0)
+
+                        group.enter()
+                        dataManager.doseStore.addReservoirValue(unitVolume, at: Date(timeIntervalSinceNow: index)) { (_, _, _, error) in
+                            group.leave()
+                        }
+                    }
+
+                    group.enter()
+                    dataManager.glucoseStore.addGlucoseSamples([NewGlucoseSample(date: Date(), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 101), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: UUID().uuidString)], completion: { (result) in
+                        group.leave()
+                    })
+
+                    group.notify(queue: .main) {
+                        completionHandler("Completed")
+                    }
+
+                    return "Generating…"
+                })
+                vc.title = sender?.textLabel?.text
+
+                show(vc, sender: sender)
+            case .reset:
+                dataManager = nil
+                tableView.reloadData()
+            }
+        }
+    }
+
+    // MARK: - Segues
+
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        super.prepare(for: segue, sender: sender)
+
+        var targetViewController = segue.destination
+
+        if let navVC = targetViewController as? UINavigationController, let topViewController = navVC.topViewController {
+            targetViewController = topViewController
+        }
+
+        switch targetViewController {
+        case let vc as CarbEntryTableViewController:
+            vc.carbStore = dataManager?.carbStore
+        case let vc as CarbEntryEditViewController:
+            if let carbStore = dataManager?.carbStore {
+                vc.defaultAbsorptionTimes = carbStore.defaultAbsorptionTimes
+                vc.preferredUnit = carbStore.preferredUnit
+            }
+        case let vc as LegacyInsulinDeliveryTableViewController:
+            vc.doseStore = dataManager?.doseStore
+        default:
+            break
+        }
+    }
+}
+
+
+extension MasterViewController: DailyValueScheduleTableViewControllerDelegate {
+    func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController) {
+        if let indexPath = tableView.indexPathForSelectedRow {
+            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)
+                    }
+                default:
+                    break
+                }
+
+                tableView.reloadRows(at: [indexPath], with: .none)
+            default:
+                break
+            }
+        }
+    }
+}
+
+
+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
+        completion(.success)
+    }
+}
+
+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 {
+            return (10...500).map { Double($0) }
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            return (6...270).map { Double($0) / 10.0 }
+        }
+
+        return []
+    }
+
+    var allowedCorrectionRangeValues: [Double] {
+        if self == HKUnit.milligramsPerDeciliter {
+            return (60...180).map { Double($0) }
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            return (33...100).map { Double($0) / 10.0 }
+        }
+
+        return []
+    }
+}

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/da.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basal Rater";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Kulhydrat Ratioer";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Kulhydrater";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Korrektions Interval";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostisk";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Indtast det 6-cifrede pumpe ID";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Genererer Data";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insulin Følsomhed";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pumpe ID";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Reservoir";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Nulstil";
+
+/* Title of button to sync basal profile from pump */
+"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)." = "Pumpe ID kan findes trykt på bagsiden, eller nær bunden på STATUS/Esc skærmen. Det er den rent numeriske del af serienummeret (vist som SN eller S/N)";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/de.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basalraten";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Kohlenhydratfaktoren";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Carbs";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Korrekturbereich";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnosedaten";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Geben Sie die 6-stellige Pumpen-ID ein";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Erzeuge Daten";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insulinempfindlichkeit";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pumpen-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" = "Mit der Pumpe synchronisieren";
+
+/* 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)." = "Die Pumpen-ID finden Sie auf der Rückseite oder am unteren Rand des STATUS / Esc-Bildschirms. Dies ist der streng numerische Teil der Seriennummer (gezeigt als SN oder S/N).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/en.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* 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).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/es.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Perfil Basal";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Relaciones de hidratos";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Carbohidratos";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Rango Objetivo de Glucosa";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnóstico";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Ingrese ID de 6 dígitios de la microinfusora";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Generar Datos";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Sensibilidad a la insulina";
+
+/* The title text for the pump ID */
+"Pump ID" = "ID de microinfusora";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Reservorio";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Reiniciar";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Sincronizar con la Microinfusora";
+
+/* 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)." = "El ID de microinfusora puede encontrarse en la parte trasera o cerca de la parte inferior de la pantalla STATUS/Esc. Es la parte estrictamente numérica del número de serie mostrado como SN o S/N";
+

+ 6 - 0
Dependecies/LoopKit/LoopKit Example/fi.lproj/InfoPlist.strings

@@ -0,0 +1,6 @@
+/* Privacy - Health Share Usage Description */
+"NSHealthShareUsageDescription" = "Terveys-sovelluksen ateriatietoja käytetään glukoosivaikutusten määrittämiseen. Terveys-sovelluksen glukoositietoja käytetään graafeissa ja laskelmissa.";
+
+/* Privacy - Health Update Usage Description */
+"NSHealthUpdateUsageDescription" = "Sovelluksen kautta tallennetut hiilihydraattitiedot tallennetaan Terveys-tietokantaan. Glukoositieto  tallennetaan turvallisesti HealthKitiin.";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/fi.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basaalitasot";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Hiilihydraattisuhteet";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Hiilihydraatit";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Korjausalue";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostiikka";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Syötä 6-numeroinen pumpun tunniste";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Luo tiedot";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insuliiniherkkyys";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pumpun tunniste";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Säiliö";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Nollaa";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Synkronoi pumpun kanssa";
+
+/* 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)." = "Pumpun tunniste löytyy pumpun takaosasta tai STATUS/Esc-valikon loppuosasta. Se on pelkästään numeroita sisältävä osa sarjanumerosta (SN tai S/N).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/fr.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Débits de basale";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Ratios de glucides";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Glucides";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Plage de correction";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostique";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Entrez l’ID de la pompe, composé de 6 lettres et chiffres";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Générer les données";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Facteur de sensibilité à l’insuline";
+
+/* The title text for the pump ID */
+"Pump ID" = "ID de la pompe";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Réservoir";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Réinitialiser";
+
+/* Title of button to sync basal profile from pump */
+"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)." = "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).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/it.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Velocità basali";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Proporzioni carboidrati";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Carboidrati";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Intervallo di correzione";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Dati diagnostici";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Inserisci ID a 6 cifre della pompa";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Genera dati";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Sensibilità all’insulina";
+
+/* The title text for the pump ID */
+"Pump ID" = "ID pompa";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Serbatoio";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Ripristina";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Sincronizza con la pompa";
+
+/* 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).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/ja.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"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" = "カーボ";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "ターゲット範囲";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "診断";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "6桁のトランスミッターIDを入力";
+
+/* The title for the cell displaying data generation */
+"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" = "ポンプID";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "リザーバ";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "リセット";
+
+/* Title of button to sync basal profile from 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)." = "ポンプIDは、ポンプの裏面か、STATUS/Esc画面の下方に表示されています。シリアル番号の数字の部分です。(SNまたはS/Nで表示)";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/nb.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basalsatser";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Karbohydratforhold";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Karbohydrater";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Korreksjonsområde";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostikk";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Skriv 6-siffret pumpe ID";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Generer data";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insulinfølsomhet";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pumpe ID";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Reservoar";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Nullstill";
+
+/* Title of button to sync basal profile from pump */
+"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)." = "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).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/nl.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basaalsnelheden";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Koolhydraten absorptie snelheid";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Koolhydraten";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnose";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Vul de 6 cijferige pomp ID in";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Genereer data";
+
+/* The title of the insulin sensitivity schedule screen
+   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";
+
+/* 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" = "Synchroniseer met pomp";
+
+/* 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).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/pl.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Dawki podstawowe";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Współczynniki węglowodanowe";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Węglowodany";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Zakres korekty";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostyka";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Wprowadź 6-cyfrowy ID pompy";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Generuj dane";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insulinowrażliwość";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pompą ID";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Zbiornik";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Resetuj";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Synchronizuj z pompą";
+
+/* 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).";
+

+ 39 - 0
Dependecies/LoopKit/LoopKit Example/pt-BR.lproj/Localizable.strings

@@ -0,0 +1,39 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Taxas Basais";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Taxas de Carbs";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Carbs";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Faixa de Correção";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnóstico";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Entre com o ID de 6 dígitos da bomba";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Gerar Dados";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Sensibilidade a Insulina";
+
+/* The title text for the pump ID */
+"Pump ID" = "ID da Bomba";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Reservatório";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Restabelecer";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Sincronizar com a Bomba";
+
+/* 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).";

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/ro.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Rate bazale";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Raport carbohidrați";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Carbohidrați";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Interval corecție";
+
+/* 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" = "Introduceți ID-ul pompei din 6 cifre";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Generează date";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Factor de sensibilitate la insulină";
+
+/* The title text for the pump ID */
+"Pump ID" = "ID pompă";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Rezervor";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Resetează";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Sincronizează cu pompa";
+
+/* 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-ul pompei poate fi găsit pe spate sau în partea inferioară a ecranului STATUS/Esc. Este format strict din porțiunea numerică a numărului serial (afișat ca SN sau S/N).";
+

文件差異過大導致無法顯示
+ 40 - 0
Dependecies/LoopKit/LoopKit Example/ru.lproj/Localizable.strings


+ 39 - 0
Dependecies/LoopKit/LoopKit Example/sv.lproj/Localizable.strings

@@ -0,0 +1,39 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "Basaldoser";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "Insulinkvoter";
+
+/* The title for the cell navigating to the carbs screen */
+"Carbs" = "Kolhydrater";
+
+/* The title text for the glucose correction range schedule */
+"Correction Range" = "Målvärden";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Diagnostisk";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Ange ditt 6-siffriga pump-ID";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Generera data";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Insulinkänslighet";
+
+/* The title text for the pump ID */
+"Pump ID" = "Pump ID";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Reservoar";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Återställ";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Synka med 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)." = "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). ";

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/vi.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* 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" = "Phạm vi liều Bổ sung";
+
+/* The title for the cell displaying diagnostic data */
+"Diagnostic" = "Chuẩn đoán";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "Khai báo 6 số ID của bơm";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "Xuất bản dữ liệu";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "Độ nhạy Insulin";
+
+/* The title text for the pump ID */
+"Pump ID" = "Số ID của bơm";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "Ngăn chứa insulin";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "Khôi phục lại";
+
+/* Title of button to sync basal profile from pump */
+"Sync With Pump" = "Đồng hóa dữ liệu với bơm";
+
+/* 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)." = "Số ID của bơm có thể nhìn thấy phía sau của bơm hay gần mục STATUS/Esc. Đây là phần số của số seri (bắt đầu bằng SN hay S/N).";
+

+ 40 - 0
Dependecies/LoopKit/LoopKit Example/zh-Hans.lproj/Localizable.strings

@@ -0,0 +1,40 @@
+/* The title text for the basal rate schedule */
+"Basal Rates" = "基础率";
+
+/* The title of the carb ratios schedule screen */
+"Carb Ratios" = "碳水化合物系数";
+
+/* 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" = "诊断";
+
+/* The placeholder text instructing users how to enter a pump ID */
+"Enter the 6-digit pump ID" = "输入6位数字泵编号";
+
+/* The title for the cell displaying data generation */
+"Generate Data" = "创建日期";
+
+/* The title of the insulin sensitivity schedule screen
+   The title text for the insulin sensitivity schedule */
+"Insulin Sensitivity" = "胰岛素敏感系数";
+
+/* The title text for the pump ID */
+"Pump ID" = "泵编号";
+
+/* The title for the cell navigating to the reservoir screen */
+"Reservoir" = "储药器";
+
+/* Title for the cell resetting the data manager */
+"Reset" = "重置";
+
+/* Title of button to sync basal profile from 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).";
+

文件差異過大導致無法顯示
+ 4968 - 0
Dependecies/LoopKit/LoopKit.xcodeproj/project.pbxproj


+ 7 - 0
Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:LoopKit.xcodeproj">
+   </FileRef>
+</Workspace>

+ 8 - 0
Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 8 - 0
Dependecies/LoopKit/LoopKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
+	<false/>
+</dict>
+</plist>

+ 91 - 0
Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1100"
+   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">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "430157F61C7EC03B00B64B63"
+            BuildableName = "LoopKit Example.app"
+            BlueprintName = "LoopKit Example"
+            ReferencedContainer = "container:LoopKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </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>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </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
Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1100"
+   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
Dependecies/LoopKit/LoopKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme

@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1100"
+   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>

+ 214 - 0
Dependecies/LoopKit/LoopKit/Alert.swift

@@ -0,0 +1,214 @@
+//
+//  Alert.swift
+//  LoopKit
+//
+//  Created by Rick Pasetto on 4/8/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+/// Protocol that describes any class that presents Alerts.
+public protocol AlertPresenter: class {
+    /// 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.
+    func retractAlert(identifier: Alert.Identifier)
+}
+
+/// Protocol that describes something that can deal with a user's response to an alert.
+public protocol AlertResponder: class {
+    /// Acknowledge alerts with a given type identifier
+    func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier) -> Void
+}
+
+/// Structure that represents an Alert that is issued from a Device.
+public struct Alert: Equatable {
+    /// Representation of an alert Trigger
+    public enum Trigger: Equatable {
+        /// Trigger the alert immediately
+        case immediate
+        /// Delay triggering the alert by `interval`, but issue it only once.
+        case delayed(interval: TimeInterval)
+        /// Delay triggering the alert by `repeatInterval`, and repeat at that interval until cancelled or unscheduled.
+        case repeating(repeatInterval: TimeInterval)
+    }
+    /// 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) {
+            self.title = title
+            self.body = body
+            self.acknowledgeActionButtonLabel = acknowledgeActionButtonLabel
+            self.isCritical = isCritical
+        }
+    }
+    public struct Identifier: Equatable, Hashable {
+        /// Unique device manager identifier from whence the alert came, and to which alert acknowledgements should be directed.
+        public let managerIdentifier: String
+        /// Per-alert-type identifier, for instance to group alert types.  This is the identifier that will be used to acknowledge the alert.
+        public let alertIdentifier: AlertIdentifier
+        public init(managerIdentifier: String, alertIdentifier: AlertIdentifier) {
+            self.managerIdentifier = managerIdentifier
+            self.alertIdentifier = alertIdentifier
+        }
+        /// An opaque value for this tuple for unique identification of the alert across devices.
+        public var value: String {
+            return "\(managerIdentifier).\(alertIdentifier)"
+        }
+    }
+    /// This type represents a per-alert-type identifier, but not necessarily unique across devices.  Each device may have its own Swift type for this,
+    /// so conversion to String is the most convenient, but aliasing the type is helpful because it is not just "any String".
+    public typealias AlertIdentifier = String
+
+    /// Alert content to show while app is in the foreground.  If nil, there shall be no alert while app is in the foreground.
+    public let foregroundContent: Content?
+    /// Alert content to show while app is in the background.  If nil, there shall be no alert while app is in the background.
+    public let backgroundContent: Content?
+    /// Trigger for the alert.
+    public let trigger: Trigger
+
+    /// 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.
+    public let identifier: Identifier
+
+    /// Representation of a "sound" (or other sound-like action, like vibrate) to perform when the alert is issued.
+    public enum Sound: Equatable {
+        case vibrate
+        case silence
+        case sound(name: String)
+    }
+    public let sound: Sound?
+    
+    public init(identifier: Identifier, foregroundContent: Content?, backgroundContent: Content?, trigger: Trigger, sound: Sound? = nil) {
+        self.identifier = identifier
+        self.foregroundContent = foregroundContent
+        self.backgroundContent = backgroundContent
+        self.trigger = trigger
+        self.sound = sound
+    }
+}
+
+public extension Alert.Sound {
+    var filename: String? {
+        switch self {
+        case .sound(let name): return name
+        case .silence, .vibrate: return nil
+        }
+    }
+}
+
+public protocol AlertSoundVendor {
+    // Get the base URL for where to find all the vendor's sounds.  It is under here that all of the sound files should be.
+    // Returns nil if the vendor has no sounds.
+    func getSoundBaseURL() -> URL?
+    // Get all the sounds for this vendor.  Returns an empty array if the vendor has no sounds.
+    func getSounds() -> [Alert.Sound]
+}
+
+// MARK: Codable implementations
+
+extension Alert: Codable { }
+extension Alert.Content: Codable { }
+extension Alert.Identifier: 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 {
+    private enum CodingKeys: String, CodingKey {
+      case immediate, delayed, repeating
+    }
+    private struct Delayed: Codable {
+        let delayInterval: TimeInterval
+    }
+    private struct Repeating: Codable {
+        let repeatInterval: TimeInterval
+    }
+    public init(from decoder: Decoder) throws {
+        if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
+            switch singleValue {
+            case CodingKeys.immediate.rawValue:
+                self = .immediate
+            default:
+                throw decoder.enumDecodingError
+            }
+        } else {
+            let container = try decoder.container(keyedBy: CodingKeys.self)
+            if let delayInterval = try? container.decode(Delayed.self, forKey: .delayed) {
+                self = .delayed(interval: delayInterval.delayInterval)
+            } else if let repeatInterval = try? container.decode(Repeating.self, forKey: .repeating) {
+                self = .repeating(repeatInterval: repeatInterval.repeatInterval)
+            } else {
+                throw decoder.enumDecodingError
+            }
+        }
+    }
+    
+    public func encode(to encoder: Encoder) throws {
+        switch self {
+        case .immediate:
+            var container = encoder.singleValueContainer()
+            try container.encode(CodingKeys.immediate.rawValue)
+        case .delayed(let interval):
+            var container = encoder.container(keyedBy: CodingKeys.self)
+            try container.encode(Delayed(delayInterval: interval), forKey: .delayed)
+        case .repeating(let repeatInterval):
+            var container = encoder.container(keyedBy: CodingKeys.self)
+            try container.encode(Repeating(repeatInterval: repeatInterval), forKey: .repeating)
+        }
+    }
+}
+
+extension Alert.Sound: Codable {
+    private enum CodingKeys: String, CodingKey {
+      case silence, vibrate, sound
+    }
+    private struct SoundName: Codable {
+        let name: String
+    }
+    public init(from decoder: Decoder) throws {
+        if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
+            switch singleValue {
+            case CodingKeys.silence.rawValue:
+                self = .silence
+            case CodingKeys.vibrate.rawValue:
+                self = .vibrate
+            default:
+                throw decoder.enumDecodingError
+            }
+        } else {
+            let container = try decoder.container(keyedBy: CodingKeys.self)
+            if let name = try? container.decode(SoundName.self, forKey: .sound) {
+                self = .sound(name: name.name); return
+            } else {
+                throw decoder.enumDecodingError
+            }
+        }
+    }
+    
+    public func encode(to encoder: Encoder) throws {
+        switch self {
+        case .silence:
+            var container = encoder.singleValueContainer()
+            try container.encode(CodingKeys.silence.rawValue)
+        case .vibrate:
+            var container = encoder.singleValueContainer()
+            try container.encode(CodingKeys.vibrate.rawValue)
+        case .sound(let name):
+            var container = encoder.container(keyedBy: CodingKeys.self)
+            try container.encode(SoundName(name: name), forKey: .sound)
+        }
+    }
+}
+
+extension Decoder {
+    var enumDecodingError: DecodingError {
+        return DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "invalid enumeration"))
+    }
+}
+

+ 15 - 0
Dependecies/LoopKit/LoopKit/AnalyticsService.swift

@@ -0,0 +1,15 @@
+//
+//  AnalyticsService.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 5/11/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public protocol AnalyticsService: Service {
+
+    func recordAnalyticsEvent(_ name: String, withProperties properties: [AnyHashable: Any]?, outOfSession: Bool)
+
+}

+ 41 - 0
Dependecies/LoopKit/LoopKit/BasalRateSchedule.swift

@@ -0,0 +1,41 @@
+//
+//  BasalRateSchedule.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 2/12/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+public typealias BasalRateSchedule = DailyValueSchedule<Double>
+
+public struct BasalScheduleValidationResult {
+    let scheduleError: Error?
+    let itemErrors: [(index: Int, error: Error)]
+}
+
+
+public extension DailyValueSchedule where T == Double {
+    /**
+     Calculates the total basal delivery for a day
+
+     - returns: The total basal delivery
+     */
+    func total() -> Double {
+        var total: Double = 0
+
+        for (index, item) in items.enumerated() {
+            var endTime = maxTimeInterval
+
+            if index < items.endIndex - 1 {
+                endTime = items[index + 1].startTime
+            }
+
+            total += (endTime - item.startTime).hours * item.value
+        }
+        
+        return total
+    }
+}

+ 98 - 0
Dependecies/LoopKit/LoopKit/Base.lproj/Localizable.strings

@@ -0,0 +1,98 @@
+/* Describes a certain bolus failure (1: size of the bolus in units) */
+"%1$@ U bolus failed" = "%1$@ U bolus failed";
+
+/* Describes an uncertain bolus failure (1: size of the bolus in units) */
+"%1$@ U bolus may not have succeeded" = "%1$@ U bolus may not have succeeded";
+
+/* The error description describing when Health sharing was denied */
+"Authorization Denied" = "Authorization Denied";
+
+/* Recovery instruction for an uncertain bolus failure */
+"Check your pump before retrying" = "Check your pump before retrying";
+
+/* The description of an error returned when attempting to delete a sample not shared by the current app */
+"com.loudnate.CarbKit.deleteCarbEntryUnownedErrorDescription" = "Authorization Denied";
+
+/* The error recovery suggestion when attempting to delete a sample not shared by the current app */
+"com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion" = "This sample can be deleted from the Health app";
+
+/* Generic pump error description */
+"Communication Failure" = "Communication Failure";
+
+/* Generic pump error description */
+"Connection Failure" = "Connection Failure";
+
+/* Generic pump error description */
+"Device Refused" = "Device Refused";
+
+/* Recovery suggestion for a no data error */
+"Ensure carb data exists for the specified date" = "Ensure carb data exists for the specified date";
+
+/* Glucose trend down */
+"Falling" = "Falling";
+
+/* Glucose trend down-down */
+"Falling fast" = "Falling fast";
+
+/* Glucose trend down-down-down */
+"Falling very fast" = "Falling very fast";
+
+/* Glucose trend flat */
+"Flat" = "Flat";
+
+/* The short unit display string for grams per U */
+"g/U" = "g/U";
+
+/* Generic pump error description */
+"Invalid Configuration" = "Invalid Configuration";
+
+/* Recovery instruction for a certain bolus failure */
+"It is safe to retry" = "It is safe to retry";
+
+/* The short unit display string for milligrams per deciliter per U */
+"mg/dL/U" = "mg/dL/U";
+
+/* The short unit display string for millimoles per liter */
+"mmol/L" = "mmol/L";
+
+/* The short unit display string for millimoles per liter per U */
+"mmol/L/U" = "mmol/L/U";
+
+/* Sensor state description for the non-valid state */
+"Needs Attention" = "Needs Attention";
+
+/* Describes an error for no data found in a CarbStore request */
+"No values found" = "No values found";
+
+/* Sensor state description for the valid state */
+"OK" = "OK";
+
+/* The error recovery suggestion when Health sharing was denied */
+"Please re-enable sharing in Health" = "Please re-enable sharing in Health";
+
+/* Glucose trend up */
+"Rising" = "Rising";
+
+/* Glucose trend up-up */
+"Rising fast" = "Rising fast";
+
+/* Glucose trend up-up-up */
+"Rising very fast" = "Rising very fast";
+
+/* The short unit display string for international units of insulin */
+"U" = "U";
+
+/* The short unit display string for international units of insulin per hour */
+"U/hr" = "U/hr";
+
+/* The long unit display string for a singular international unit of insulin */
+"Unit" = "Unit";
+
+/* The long unit display string for a singular international unit of insulin per hour */
+"Unit/hour" = "Unit/hour";
+
+/* The long unit display string for international units of insulin */
+"Units" = "Units";
+
+/* The long unit display string for international units of insulin per hour */
+"Units/hour" = "Units/hour";

+ 80 - 0
Dependecies/LoopKit/LoopKit/CarbKit/AbsorbedCarbValue.swift

@@ -0,0 +1,80 @@
+//
+//  AbsorbedCarbValue.swift
+//  LoopKit
+//
+//  Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+/// A quantity of carbs absorbed over a given date interval
+public struct AbsorbedCarbValue: SampleValue {
+    /// The quantity of carbs absorbed
+    public let observed: HKQuantity
+    /// The quantity of carbs absorbed, clamped to the original prediction
+    public let clamped: HKQuantity
+    /// The quantity of carbs entered as eaten
+    public let total: HKQuantity
+    /// The quantity of carbs expected to still absorb
+    public let remaining: HKQuantity
+    /// The dates over which absorption was observed
+    public let observedDate: DateInterval
+
+    /// The predicted time for the remaining carbs to absorb
+    public let estimatedTimeRemaining: TimeInterval
+
+    // Total predicted absorption time for this carb entry
+    public var estimatedDate: DateInterval {
+        return DateInterval(start: observedDate.start, duration: observedDate.duration + estimatedTimeRemaining)
+    }
+    
+    /// The amount of time required to absorb observed carbs
+    public let timeToAbsorbObservedCarbs: TimeInterval
+
+    /// Whether absorption is still in-progress
+    public var isActive: Bool {
+        return estimatedTimeRemaining > 0
+    }
+
+    public var observedProgress: HKQuantity {
+        let gram = HKUnit.gram()
+        let totalGrams = total.doubleValue(for: gram)
+        let percent = HKUnit.percent()
+
+        guard totalGrams > 0 else {
+            return HKQuantity(unit: percent, doubleValue: 0)
+        }
+
+        return HKQuantity(
+            unit: percent,
+            doubleValue: observed.doubleValue(for: gram) / totalGrams
+        )
+    }
+
+    public var clampedProgress: HKQuantity {
+        let gram = HKUnit.gram()
+        let totalGrams = total.doubleValue(for: gram)
+        let percent = HKUnit.percent()
+
+        guard totalGrams > 0 else {
+            return HKQuantity(unit: percent, doubleValue: 0)
+        }
+
+        return HKQuantity(
+            unit: percent,
+            doubleValue: clamped.doubleValue(for: gram) / totalGrams
+        )
+    }
+
+    // MARK: SampleValue
+
+    public var quantity: HKQuantity {
+        return clamped
+    }
+
+    public var startDate: Date {
+        return estimatedDate.start
+    }
+}

+ 262 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataClass.swift

@@ -0,0 +1,262 @@
+//
+//  CachedCarbObject+CoreDataClass.swift
+//  LoopKit
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+//
+
+import Foundation
+import CoreData
+import HealthKit
+
+
+class CachedCarbObject: NSManagedObject {
+    var absorptionTime: TimeInterval? {
+        get {
+            willAccessValue(forKey: "absorptionTime")
+            defer { didAccessValue(forKey: "absorptionTime") }
+            return primitiveAbsorptionTime?.doubleValue
+        }
+        set {
+            willChangeValue(forKey: "absorptionTime")
+            defer { didChangeValue(forKey: "absorptionTime") }
+            primitiveAbsorptionTime = newValue != nil ? NSNumber(value: newValue!) : nil
+        }
+    }
+
+    var syncVersion: Int? {
+        get {
+            willAccessValue(forKey: "syncVersion")
+            defer { didAccessValue(forKey: "syncVersion") }
+            return primitiveSyncVersion?.intValue
+        }
+        set {
+            willChangeValue(forKey: "syncVersion")
+            defer { didChangeValue(forKey: "syncVersion") }
+            primitiveSyncVersion = newValue != nil ? NSNumber(value: newValue!) : nil
+        }
+    }
+
+    var operation: Operation {
+        get {
+            willAccessValue(forKey: "operation")
+            defer { didAccessValue(forKey: "operation") }
+            return Operation(rawValue: primitiveOperation.intValue)!
+        }
+        set {
+            willChangeValue(forKey: "operation")
+            defer { didChangeValue(forKey: "operation") }
+            primitiveOperation = NSNumber(value: newValue.rawValue)
+        }
+    }
+
+    override func awakeFromInsert() {
+        super.awakeFromInsert()
+        setPrimitiveValue(managedObjectContext!.anchorKey!, forKey: "anchorKey")
+    }
+}
+
+// MARK: - Helpers
+
+extension CachedCarbObject {
+    var quantity: HKQuantity { HKQuantity(unit: .gram(), doubleValue: grams) }
+}
+
+// MARK: - Operations
+
+extension CachedCarbObject {
+
+    // Loop
+    func create(from entry: NewCarbEntry, provenanceIdentifier: String, syncIdentifier: String, syncVersion: Int = 1, on date: Date = Date()) {
+        self.absorptionTime = entry.absorptionTime
+        self.createdByCurrentApp = true
+        self.foodType = entry.foodType
+        self.grams = entry.quantity.doubleValue(for: .gram())
+        self.startDate = entry.startDate
+        self.uuid = nil
+
+        self.provenanceIdentifier = provenanceIdentifier
+        self.syncIdentifier = syncIdentifier
+        self.syncVersion = syncVersion
+
+        self.userCreatedDate = entry.date
+        self.userUpdatedDate = nil
+        self.userDeletedDate = nil
+
+        self.operation = .create
+        self.addedDate = date
+        self.supercededDate = nil
+    }
+
+    // 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
+        self.grams = sample.quantity.doubleValue(for: .gram())
+        self.startDate = sample.startDate
+        self.uuid = sample.uuid
+
+        self.provenanceIdentifier = sample.provenanceIdentifier
+        self.syncIdentifier = sample.syncIdentifier
+        self.syncVersion = sample.syncVersion
+
+        self.userCreatedDate = sample.userCreatedDate
+        self.userUpdatedDate = nil
+        self.userDeletedDate = nil
+
+        self.operation = .create
+        self.addedDate = date
+        self.supercededDate = nil
+    }
+
+    // Loop
+    func update(from entry: NewCarbEntry, replacing object: CachedCarbObject, on date: Date = Date()) {
+        precondition(object.createdByCurrentApp)
+        precondition(object.syncIdentifier != nil)
+        precondition(object.syncVersion != nil)
+
+        self.absorptionTime = entry.absorptionTime
+        self.createdByCurrentApp = object.createdByCurrentApp
+        self.foodType = entry.foodType
+        self.grams = entry.quantity.doubleValue(for: .gram())
+        self.startDate = entry.startDate
+        self.uuid = nil
+
+        self.provenanceIdentifier = object.provenanceIdentifier
+        self.syncIdentifier = object.syncIdentifier
+        self.syncVersion = object.syncVersion.map { $0 + 1 }
+
+        self.userCreatedDate = object.userCreatedDate
+        self.userUpdatedDate = entry.date
+        self.userDeletedDate = nil
+
+        self.operation = .update
+        self.addedDate = date
+        self.supercededDate = nil
+    }
+
+    // HealthKit
+    func update(from sample: HKQuantitySample, replacing object: CachedCarbObject, on date: Date = Date()) {
+        precondition(!object.createdByCurrentApp)
+        precondition(sample.createdByCurrentApp == object.createdByCurrentApp)
+        precondition(sample.provenanceIdentifier == object.provenanceIdentifier)
+        precondition(object.syncIdentifier != nil)
+        precondition(sample.syncIdentifier == object.syncIdentifier)
+
+        self.absorptionTime = sample.absorptionTime
+        self.createdByCurrentApp = sample.createdByCurrentApp
+        self.foodType = sample.foodType
+        self.grams = sample.quantity.doubleValue(for: .gram())
+        self.startDate = sample.startDate
+        self.uuid = sample.uuid
+
+        self.provenanceIdentifier = sample.provenanceIdentifier
+        self.syncIdentifier = sample.syncIdentifier
+        self.syncVersion = sample.syncVersion
+
+        self.userCreatedDate = object.userCreatedDate
+        self.userUpdatedDate = sample.userUpdatedDate
+        self.userDeletedDate = nil
+
+        self.operation = .update
+        self.addedDate = date
+        self.supercededDate = nil
+    }
+
+    // Either
+    func delete(from object: CachedCarbObject, on date: Date = Date()) {
+        self.absorptionTime = object.absorptionTime
+        self.createdByCurrentApp = object.createdByCurrentApp
+        self.foodType = object.foodType
+        self.grams = object.grams
+        self.startDate = object.startDate
+        self.uuid = object.uuid
+
+        self.provenanceIdentifier = object.provenanceIdentifier
+        self.syncIdentifier = object.syncIdentifier
+        self.syncVersion = object.syncVersion
+
+        self.userCreatedDate = object.userCreatedDate
+        self.userUpdatedDate = object.userUpdatedDate
+        self.userDeletedDate = object.createdByCurrentApp ? date : nil  // Cannot know actual user deleted data from other app
+
+        self.operation = .delete
+        self.addedDate = date
+        self.supercededDate = nil
+    }
+}
+
+// MARK: - Watch Synchronization
+
+extension CachedCarbObject {
+    func update(from object: SyncCarbObject) {
+        self.absorptionTime = object.absorptionTime
+        self.createdByCurrentApp = object.createdByCurrentApp
+        self.foodType = object.foodType
+        self.grams = object.grams
+        self.startDate = object.startDate
+        self.uuid = object.uuid
+
+        self.provenanceIdentifier = object.provenanceIdentifier
+        self.syncIdentifier = object.syncIdentifier
+        self.syncVersion = object.syncVersion
+
+        self.userCreatedDate = object.userCreatedDate
+        self.userUpdatedDate = object.userUpdatedDate
+        self.userDeletedDate = object.userDeletedDate
+
+        self.operation = object.operation
+        self.addedDate = object.addedDate
+        self.supercededDate = object.supercededDate
+    }
+}
+
+// MARK: - HealthKit Synchronization
+
+extension CachedCarbObject {
+    var quantitySample: HKQuantitySample {
+        var metadata = [String: Any]()
+
+        metadata[HKMetadataKeyFoodType] = foodType
+        metadata[MetadataKeyAbsorptionTimeMinutes] = absorptionTime?.minutes
+
+        metadata[HKMetadataKeySyncIdentifier] = syncIdentifier
+        metadata[HKMetadataKeySyncVersion] = syncVersion
+
+        metadata[MetadataKeyUserCreatedDate] = userCreatedDate
+        metadata[MetadataKeyUserUpdatedDate] = userUpdatedDate
+
+        return HKQuantitySample(
+            type: HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)!,
+            quantity: quantity,
+            start: startDate,
+            end: startDate,
+            metadata: metadata
+        )
+    }
+}
+
+// MARK: - DEPRECATED - Used only for migration
+
+extension CachedCarbObject {
+    func create(from entry: StoredCarbEntry) {
+        self.absorptionTime = entry.absorptionTime
+        self.createdByCurrentApp = entry.createdByCurrentApp
+        self.foodType = entry.foodType
+        self.grams = entry.quantity.doubleValue(for: .gram())
+        self.startDate = entry.startDate
+        self.uuid = entry.uuid
+
+        self.provenanceIdentifier = entry.provenanceIdentifier
+        self.syncIdentifier = entry.syncIdentifier
+        self.syncVersion = entry.syncVersion
+
+        self.operation = .create
+        self.addedDate = nil
+        self.supercededDate = nil
+    }
+}

+ 77 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CachedCarbObject+CoreDataProperties.swift

@@ -0,0 +1,77 @@
+//
+//  CachedCarbObject+CoreDataProperties.swift
+//  LoopKit
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+//
+
+import Foundation
+import CoreData
+
+
+extension CachedCarbObject {
+
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<CachedCarbObject> {
+        return NSFetchRequest<CachedCarbObject>(entityName: "CachedCarbObject")
+    }
+
+    @NSManaged public var primitiveAbsorptionTime: NSNumber?
+    @NSManaged public var createdByCurrentApp: Bool
+    @NSManaged public var foodType: String?
+    @NSManaged public var grams: Double
+    @NSManaged public var startDate: Date
+    @NSManaged public var uuid: UUID?
+    @NSManaged public var provenanceIdentifier: String?
+    @NSManaged public var syncIdentifier: String?
+    @NSManaged public var primitiveSyncVersion: NSNumber?
+    @NSManaged public var userCreatedDate: Date?
+    @NSManaged public var userUpdatedDate: Date?
+    @NSManaged public var userDeletedDate: Date?
+    @NSManaged public var primitiveOperation: NSNumber
+    @NSManaged public var addedDate: Date?
+    @NSManaged public var supercededDate: Date?
+    @NSManaged public var anchorKey: Int64
+
+}
+
+extension CachedCarbObject: Encodable {
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(absorptionTime, forKey: .absorptionTime)
+        try container.encode(createdByCurrentApp, forKey: .createdByCurrentApp)
+        try container.encodeIfPresent(foodType, forKey: .foodType)
+        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.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
+        try container.encodeIfPresent(syncVersion, forKey: .syncVersion)
+        try container.encodeIfPresent(userCreatedDate, forKey: .userCreatedDate)
+        try container.encodeIfPresent(userUpdatedDate, forKey: .userUpdatedDate)
+        try container.encodeIfPresent(userDeletedDate, forKey: .userDeletedDate)
+        try container.encodeIfPresent(operation, forKey: .operation)
+        try container.encodeIfPresent(addedDate, forKey: .addedDate)
+        try container.encodeIfPresent(supercededDate, forKey: .supercededDate)
+        try container.encode(anchorKey, forKey: .anchorKey)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case absorptionTime
+        case createdByCurrentApp
+        case foodType
+        case grams
+        case startDate
+        case uuid
+        case provenanceIdentifier
+        case syncIdentifier
+        case syncVersion
+        case userCreatedDate
+        case userUpdatedDate
+        case userDeletedDate
+        case operation
+        case addedDate
+        case supercededDate
+        case anchorKey
+    }
+}

+ 14 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbEntry.swift

@@ -0,0 +1,14 @@
+//
+//  CarbEntry.swift
+//  CarbKit
+//
+//  Created by Nathan Racklyeft on 1/3/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+public protocol CarbEntry: SampleValue {
+    var absorptionTime: TimeInterval? { get }
+}

+ 902 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbMath.swift

@@ -0,0 +1,902 @@
+//
+//  CarbMath.swift
+//  CarbKit
+//
+//  Created by Nathan Racklyeft on 1/16/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+struct CarbModelSettings {
+    var absorptionModel: CarbAbsorptionComputable
+    var initialAbsorptionTimeOverrun: Double
+    var adaptiveAbsorptionRateEnabled: Bool
+    var adaptiveRateStandbyIntervalFraction: Double
+    
+    init(absorptionModel: CarbAbsorptionComputable, initialAbsorptionTimeOverrun: Double, adaptiveAbsorptionRateEnabled: Bool, adaptiveRateStandbyIntervalFraction: Double = 0.2) {
+        self.absorptionModel = absorptionModel
+        self.initialAbsorptionTimeOverrun = initialAbsorptionTimeOverrun
+        self.adaptiveAbsorptionRateEnabled = adaptiveAbsorptionRateEnabled
+        self.adaptiveRateStandbyIntervalFraction = adaptiveRateStandbyIntervalFraction
+    }
+}
+
+protocol CarbAbsorptionComputable {
+    /// Returns the percentage of total carbohydrates absorbed as blood glucose at a specified interval after eating.
+    ///
+    /// - Parameters:
+    ///   - percentTime: The percentage of the total absorption time
+    /// - Returns: The percentage of the total carbohydrates that have been absorbed as blood glucose
+    func percentAbsorptionAtPercentTime(_ percentTime: Double) -> Double
+    
+    /// Returns the percent of total absorption time for a percentage of total carbohydrates absorbed
+    ///
+    /// The is the inverse of perecentAbsorptionAtPercentTime( :percentTime: )
+    ///
+    /// - Parameters:
+    ///   - percentAbsorption: The percentage of the total carbohydrates that have been absorbed as blood glucose
+    /// - Returns: The percentage of the absorption time needed to absorb the percentage of the total carbohydrates
+    func percentTimeAtPercentAbsorption(_ percentAbsorption: Double) -> Double
+
+    /// Returns the total absorption time for a percentage of total carbohydrates absorbed as blood glucose at a specified interval after eating.
+    ///
+    /// - Parameters:
+    ///   - percentAbsorption: The percentage of the total carbohydrates that have been absorbed as blood glucose
+    ///   - time: The interval after the carbohydrates were eaten
+    /// - Returns: The total time of carbohydrates absorption
+    func absorptionTime(forPercentAbsorption percentAbsorption: Double, atTime time: TimeInterval) -> TimeInterval
+
+    /// Returns the number of total carbohydrates absorbed as blood glucose at a specified interval after eating
+    ///
+    /// - Parameters:
+    ///   - total: The total number of carbohydrates eaten
+    ///   - time: The interval after carbohydrates were eaten
+    ///   - absorptionTime: The total time of carbohydrates absorption
+    /// - Returns: The number of total carbohydrates that have been absorbed as blood glucose
+    func absorbedCarbs(of total: Double, atTime time: TimeInterval, absorptionTime: TimeInterval) -> Double
+
+    /// Returns the number of total carbohydrates not yet absorbed as blood glucose at a specified interval after eating
+    ///
+    /// - Parameters:
+    ///   - total: The total number of carbs eaten
+    ///   - time: The interval after carbohydrates were eaten
+    ///   - absorptionTime: The total time of carb absorption
+    /// - Returns: The number of total carbohydrates that have not yet been absorbed as blood glucose
+    func unabsorbedCarbs(of total: Double, atTime time: TimeInterval, absorptionTime: TimeInterval) -> Double
+    
+    /// Returns the normalized rate of carbohydrates absorption at a specified percentage of the absorption time
+    ///
+    /// - Parameters:
+    ///   - percentTime: The percentage of absorption time elapsed since the carbohydrates were eaten
+    /// - Returns: The percentage absorption rate at the percentage of absorption time
+    func percentRateAtPercentTime(_ percentTime: Double) -> Double
+}
+
+
+extension CarbAbsorptionComputable {
+    func absorbedCarbs(of total: Double, atTime time: TimeInterval, absorptionTime: TimeInterval) -> Double {
+        let percentTime = time / absorptionTime
+        return total * percentAbsorptionAtPercentTime(percentTime)
+    }
+
+    func unabsorbedCarbs(of total: Double, atTime time: TimeInterval, absorptionTime: TimeInterval) -> Double {
+        let percentTime = time / absorptionTime
+        return total * (1.0 - percentAbsorptionAtPercentTime(percentTime))
+    }
+    
+    func absorptionTime(forPercentAbsorption percentAbsorption: Double, atTime time: TimeInterval) -> TimeInterval {
+        let percentTime = max(percentTimeAtPercentAbsorption(percentAbsorption), .ulpOfOne)
+        return time / percentTime
+    }
+    
+    func timeToAbsorb(forPercentAbsorbed percentAbsorption: Double, totalAbsorptionTime: TimeInterval) -> TimeInterval {
+        let percentTime = percentTimeAtPercentAbsorption(percentAbsorption)
+        return percentTime * totalAbsorptionTime
+    }
+    
+}
+
+
+// MARK: - Parabolic absorption as described by Scheiner
+// This is the integral approximation of the Scheiner GI curve found in Think Like a Pancreas, Fig 7-8, which first appeared in [GlucoDyn](https://github.com/kenstack/GlucoDyn)
+struct ParabolicAbsorption: CarbAbsorptionComputable {
+    func percentAbsorptionAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t <= 0.0:
+            return 0.0
+        case let t where t <= 0.5:
+            return 2.0 * pow(t, 2)
+        case let t where t < 1.0:
+            return -1.0 + 2.0 * t * (2.0 - t)
+        default:
+            return 1.0
+        }
+    }
+
+    func percentTimeAtPercentAbsorption(_ percentAbsorption: Double) -> Double {
+        switch percentAbsorption {
+        case let a where a <= 0:
+            return 0.0
+        case let a where a <= 0.5:
+            return sqrt(0.5 * a)
+        case let a where a < 1.0:
+            return 1.0 - sqrt(0.5 * (1.0 - a))
+        default:
+            return 1.0
+        }
+    }
+    
+    func percentRateAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t > 0 && t <= 0.5:
+            return 4.0 * t
+        case let t where t > 0.5 && t < 1.0:
+            return 4.0 - 4.0 * t
+        default:
+            return 0.0
+        }
+    }
+}
+
+
+// MARK: - Linear absorption as a factor of reported duration
+struct LinearAbsorption: CarbAbsorptionComputable {
+    func percentAbsorptionAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t <= 0.0:
+            return 0.0
+        case let t where t < 1.0:
+            return t
+        default:
+            return 1.0
+        }
+    }
+
+    func percentTimeAtPercentAbsorption(_ percentAbsorption: Double) -> Double {
+        switch percentAbsorption {
+        case let a where a <= 0.0:
+            return 0.0
+        case let a where a < 1.0:
+            return a
+        default:
+            return 1.0
+        }
+    }
+    
+    func percentRateAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t > 0.0 && t <= 1.0:
+            return 1.0
+        default:
+            return 0.0
+        }
+    }
+}
+
+// MARK: - Piecewise linear absorption as a factor of reported duration
+/// Nonlinear  carb absorption model where absorption rate increases linearly from zero to a maximum value at a fraction of absorption time equal to percentEndOfRise, then remains constant until a fraction of absorption time equal to percentStartOfFall, and then decreases linearly to zero at the end of absorption time
+/// - Parameters:
+///   - percentEndOfRise: the percentage of absorption time when absorption rate reaches maximum, must be strictly between 0 and 1
+///   - percentStartOfFall: the percentage of absorption time when absorption rate starts to decay, must be stritctly between 0 and 1 and  greater than percentEndOfRise
+struct PiecewiseLinearAbsorption: CarbAbsorptionComputable {
+    
+    let percentEndOfRise = 0.15
+    let percentStartOfFall = 0.5
+    var scale: Double {
+        return 2.0 / (1.0 + percentStartOfFall - percentEndOfRise)
+    }
+    
+    func percentAbsorptionAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t <= 0.0:
+            return 0.0
+        case let t where t < percentEndOfRise:
+            return 0.5 * scale * pow(t, 2.0) / percentEndOfRise
+        case let t where t >= percentEndOfRise && t < percentStartOfFall:
+            return scale * (t - 0.5 * percentEndOfRise)
+        case let t where t >= percentStartOfFall && t < 1.0:
+            return scale * (percentStartOfFall - 0.5 * percentEndOfRise +
+            (t - percentStartOfFall) * (1.0 - 0.5 * (t - percentStartOfFall) / (1.0 - percentStartOfFall)))
+        default:
+            return 1.0
+        }
+    }
+    
+    func percentTimeAtPercentAbsorption(_ percentAbsorption: Double) -> Double {
+        switch percentAbsorption {
+        case let a where a <= 0:
+            return 0.0
+        case let a where a > 0.0 && a < 0.5 * scale * percentEndOfRise:
+            return sqrt(2.0 * percentEndOfRise * a / scale)
+        case let a where a >= 0.5 * scale * percentEndOfRise && a < scale * (percentStartOfFall - 0.5 * percentEndOfRise):
+            return 0.5 * percentEndOfRise + a / scale
+        case let a where a >= scale * (percentStartOfFall - 0.5 * percentEndOfRise) && a < 1.0:
+            return 1.0 - sqrt((1.0 - percentStartOfFall) *
+                (1.0 + percentStartOfFall - percentEndOfRise) * (1.0 - a))
+        default:
+            return 1.0
+        }
+    }
+    
+    func percentRateAtPercentTime(_ percentTime: Double) -> Double {
+        switch percentTime {
+        case let t where t > 0 && t < percentEndOfRise:
+            return scale * t / percentEndOfRise
+        case let t where t >= percentEndOfRise && t < percentStartOfFall:
+            return scale
+        case let t where t >= percentStartOfFall && t < 1.0:
+            return scale * ((1.0 - t) / (1.0 - percentStartOfFall))
+        default:
+            return 0.0
+        }
+    }
+}
+
+extension CarbEntry {
+    
+    func carbsOnBoard(at date: Date, defaultAbsorptionTime: TimeInterval, delay: TimeInterval, absorptionModel: CarbAbsorptionComputable) -> Double {
+        let time = date.timeIntervalSince(startDate)
+        let value: Double
+
+        if time >= 0 {
+            value = absorptionModel.unabsorbedCarbs(of: quantity.doubleValue(for: HKUnit.gram()), atTime: time - delay, absorptionTime: absorptionTime ?? defaultAbsorptionTime)
+        } else {
+            value = 0
+        }
+
+        return value
+    }
+
+    // g
+    func absorbedCarbs(
+        at date: Date,
+        absorptionTime: TimeInterval,
+        delay: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable
+    ) -> Double {
+        let time = date.timeIntervalSince(startDate)
+
+        return absorptionModel.absorbedCarbs(
+            of: quantity.doubleValue(for: .gram()),
+            atTime: time - delay,
+            absorptionTime: absorptionTime
+        )
+    }
+
+    // mg/dL / g * g
+    fileprivate func glucoseEffect(
+        at date: Date,
+        carbRatio: HKQuantity,
+        insulinSensitivity: HKQuantity,
+        defaultAbsorptionTime: TimeInterval,
+        delay: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable
+    ) -> Double {
+        return insulinSensitivity.doubleValue(for: HKUnit.milligramsPerDeciliter) / carbRatio.doubleValue(for: .gram()) * absorbedCarbs(at: date, absorptionTime: absorptionTime ?? defaultAbsorptionTime, delay: delay, absorptionModel: absorptionModel)
+    }
+
+    fileprivate func estimatedAbsorptionTime(forAbsorbedCarbs carbs: Double, at date: Date, absorptionModel: CarbAbsorptionComputable) -> TimeInterval {
+        let time = date.timeIntervalSince(startDate)
+
+        return max(time, absorptionModel.absorptionTime(forPercentAbsorption: carbs / quantity.doubleValue(for: .gram()), atTime: time))
+    }
+}
+
+extension Collection where Element: CarbEntry {
+    fileprivate func simulationDateRange(
+        from start: Date? = nil,
+        to end: Date? = nil,
+        defaultAbsorptionTime: TimeInterval,
+        delay: TimeInterval,
+        delta: TimeInterval
+    ) -> (start: Date, end: Date)? {
+        guard count > 0 else {
+            return nil
+        }
+
+        if let start = start, let end = end {
+            return (start: start.dateFlooredToTimeInterval(delta), end: end.dateCeiledToTimeInterval(delta))
+        } else {
+            var minDate = first!.startDate
+            var maxDate = minDate
+
+            for sample in self {
+                if sample.startDate < minDate {
+                    minDate = sample.startDate
+                }
+
+                let endDate = sample.endDate.addingTimeInterval(sample.absorptionTime ?? defaultAbsorptionTime).addingTimeInterval(delay)
+                if endDate > maxDate {
+                    maxDate = endDate
+                }
+            }
+
+            return (
+                start: (start ?? minDate).dateFlooredToTimeInterval(delta),
+                end: (end ?? maxDate).dateCeiledToTimeInterval(delta)
+            )
+        }
+    }
+
+    /// Creates groups of entries that have overlapping absorption date intervals
+    ///
+    /// - Parameters:
+    ///   - defaultAbsorptionTime: The default absorption time value, if not set on the entry
+    /// - Returns: An array of arrays representing groups of entries, in chronological order by entry startDate
+    func groupedByOverlappingAbsorptionTimes(
+        defaultAbsorptionTime: TimeInterval
+    ) -> [[Iterator.Element]] {
+        var batches: [[Iterator.Element]] = []
+
+        for entry in sorted(by: { $0.startDate < $1.startDate }) {
+            if let lastEntry = batches.last?.last,
+                lastEntry.startDate.addingTimeInterval(lastEntry.absorptionTime ?? defaultAbsorptionTime) > entry.startDate
+            {
+                batches[batches.count - 1].append(entry)
+            } else {
+                batches.append([entry])
+            }
+        }
+
+        return batches
+    }
+
+    func carbsOnBoard(
+        from start: Date? = nil,
+        to end: Date? = nil,
+        defaultAbsorptionTime: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable,
+        delay: TimeInterval = TimeInterval(minutes: 10),
+        delta: TimeInterval = TimeInterval(minutes: 5)
+    ) -> [CarbValue] {
+        guard let (startDate, endDate) = simulationDateRange(from: start, to: end, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, delta: delta) else {
+            return []
+        }
+
+        var date = startDate
+        var values = [CarbValue]()
+
+        repeat {
+            let value = reduce(0.0) { (value, entry) -> Double in
+                return value + entry.carbsOnBoard(at: date, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, absorptionModel: absorptionModel)
+            }
+
+            values.append(CarbValue(startDate: date, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: value)))
+            date = date.addingTimeInterval(delta)
+        } while date <= endDate
+
+        return values
+    }
+
+    func glucoseEffects(
+        from start: Date? = nil,
+        to end: Date? = nil,
+        carbRatios: CarbRatioSchedule,
+        insulinSensitivities: InsulinSensitivitySchedule,
+        defaultAbsorptionTime: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable,
+        delay: TimeInterval = TimeInterval(minutes: 10),
+        delta: TimeInterval = TimeInterval(minutes: 5)
+    ) -> [GlucoseEffect] {
+        guard let (startDate, endDate) = simulationDateRange(from: start, to: end, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, delta: delta) else {
+            return []
+        }
+
+        var date = startDate
+        var values = [GlucoseEffect]()
+        let unit = HKUnit.milligramsPerDeciliter
+
+        repeat {
+            let value = reduce(0.0) { (value, entry) -> Double in
+                return value + entry.glucoseEffect(
+                    at: date,
+                    carbRatio: carbRatios.quantity(at: entry.startDate),
+                    insulinSensitivity: insulinSensitivities.quantity(at: entry.startDate),
+                    defaultAbsorptionTime: defaultAbsorptionTime,
+                    delay: delay,
+                    absorptionModel: absorptionModel
+                )
+            }
+
+            values.append(GlucoseEffect(startDate: date, quantity: HKQuantity(unit: unit, doubleValue: value)))
+            date = date.addingTimeInterval(delta)
+        } while date <= endDate
+
+        return values
+    }
+
+    var totalCarbs: CarbValue? {
+        guard count > 0 else {
+            return nil
+        }
+
+        let unit = HKUnit.gram()
+        var startDate = Date.distantFuture
+        var totalGrams: Double = 0
+
+        for entry in self {
+            totalGrams += entry.quantity.doubleValue(for: unit)
+
+            if entry.startDate < startDate {
+                startDate = entry.startDate
+            }
+        }
+
+        return CarbValue(startDate: startDate, quantity: HKQuantity(unit: unit, doubleValue: totalGrams))
+    }
+}
+
+
+// MARK: - Dyanamic absorption overrides
+extension Collection {
+    func dynamicCarbsOnBoard<T>(
+        from start: Date? = nil,
+        to end: Date? = nil,
+        defaultAbsorptionTime: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable,
+        delay: TimeInterval = TimeInterval(minutes: 10),
+        delta: TimeInterval = TimeInterval(minutes: 5)
+    ) -> [CarbValue] where Element == CarbStatus<T> {
+        guard let (startDate, endDate) = simulationDateRange(from: start, to: end, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, delta: delta) else {
+            return []
+        }
+
+        var date = startDate
+        var values = [CarbValue]()
+
+        repeat {
+            let value = reduce(0.0) { (value, entry) -> Double in
+                return value + entry.dynamicCarbsOnBoard(
+                    at: date,
+                    defaultAbsorptionTime: defaultAbsorptionTime,
+                    delay: delay,
+                    delta: delta,
+                    absorptionModel: absorptionModel
+                )
+            }
+
+            values.append(CarbValue(startDate: date, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: value)))
+            date = date.addingTimeInterval(delta)
+        } while date <= endDate
+        
+        return values
+    }
+
+    func dynamicGlucoseEffects<T>(
+        from start: Date? = nil,
+        to end: Date? = nil,
+        carbRatios: CarbRatioSchedule,
+        insulinSensitivities: InsulinSensitivitySchedule,
+        defaultAbsorptionTime: TimeInterval,
+        absorptionModel: CarbAbsorptionComputable,
+        delay: TimeInterval = TimeInterval(minutes: 10),
+        delta: TimeInterval = TimeInterval(minutes: 5)
+    ) -> [GlucoseEffect] where Element == CarbStatus<T> {
+        guard let (startDate, endDate) = simulationDateRange(from: start, to: end, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, delta: delta) else {
+            return []
+        }
+
+        var date = startDate
+        var values = [GlucoseEffect]()
+        let mgdL = HKUnit.milligramsPerDeciliter
+        let gram = HKUnit.gram()
+
+        repeat {
+            let value = reduce(0.0) { (value, entry) -> Double in
+                let csf = insulinSensitivities.quantity(at: entry.startDate).doubleValue(for: mgdL) / carbRatios.quantity(at: entry.startDate).doubleValue(for: gram)
+
+                return value + csf * entry.dynamicAbsorbedCarbs(
+                    at: date,
+                    absorptionTime: entry.absorptionTime ?? defaultAbsorptionTime,
+                    delay: delay,
+                    delta: delta,
+                    absorptionModel: absorptionModel
+                )
+            }
+
+            values.append(GlucoseEffect(startDate: date, quantity: HKQuantity(unit: mgdL, doubleValue: value)))
+            date = date.addingTimeInterval(delta)
+        } while date <= endDate
+        
+        return values
+    }
+
+    /// The quantity of carbs expected to still absorb at the last date of absorption
+    public func getClampedCarbsOnBoard<T>() -> CarbValue? where Element == CarbStatus<T> {
+        guard let firstAbsorption = first?.absorption else {
+            return nil
+        }
+
+        let gram = HKUnit.gram()
+        var maxObservedEndDate = firstAbsorption.observedDate.end
+        var remainingTotalGrams: Double = 0
+
+        for entry in self {
+            guard let absorption = entry.absorption else {
+                continue
+            }
+
+            maxObservedEndDate = Swift.max(maxObservedEndDate, absorption.observedDate.end)
+            remainingTotalGrams += absorption.remaining.doubleValue(for: gram)
+        }
+
+        return CarbValue(startDate: maxObservedEndDate, quantity: HKQuantity(unit: gram, doubleValue: remainingTotalGrams))
+    }
+}
+
+
+/// Aggregates and computes data about the absorption of a CarbEntry to create a CarbStatus value.
+///
+/// There are three key components managed by this builder:
+///   - The entry data as reported by the user
+///   - The observed data as calculated from glucose changes relative to insulin curves
+///   - The minimum/maximum amounts of absorption used to clamp our observation data within reasonable bounds
+fileprivate class CarbStatusBuilder<T: CarbEntry> {
+
+    // MARK: Model settings
+    
+    private var absorptionModel: CarbAbsorptionComputable
+    
+    private var adaptiveAbsorptionRateEnabled: Bool
+   
+    private var adaptiveRateStandbyIntervalFraction: Double
+    
+    private var adaptiveRateStandbyInterval: TimeInterval {
+        return initialAbsorptionTime * adaptiveRateStandbyIntervalFraction
+    }
+    
+    // MARK: User-entered data
+
+    /// The carb entry input
+    let entry: T
+
+    /// The unit used for carb values
+    let carbUnit: HKUnit
+
+    /// The total grams entered for this entry
+    let entryGrams: Double
+
+    /// The total glucose effect expected for this entry, in glucose units
+    let entryEffect: Double
+
+    /// The carbohydrate-sensitivity factor for this entry, in glucose units per gram
+    let carbohydrateSensitivityFactor: Double
+
+    /// The absorption time for this entry before any absorption is observed
+    let initialAbsorptionTime: TimeInterval
+
+    // MARK: Minimum/maximum bounding factors
+
+    /// The maximum absorption time allowed for this entry, determining the minimum absorption rate
+    let maxAbsorptionTime: TimeInterval
+
+    /// An amount of time to wait after the entry date before minimum absorption is assumed to begin
+    let delay: TimeInterval
+
+    /// The maximum end date allowed for this entry's absorption
+    let maxEndDate: Date
+
+    /// The last date we have effects observed, or "now" in real-time analysis.
+    private let lastEffectDate: Date
+
+    /// The minimum-required carb absorption rate for this entry, in g/s
+    var minAbsorptionRate: Double {
+        return entryGrams / maxAbsorptionTime
+    }
+
+    /// The minimum amount of carbs we assume must have absorbed at the last observation date
+    private var minPredictedGrams: Double {
+        // We incorporate a delay when calculating minimum absorption values
+        let time = lastEffectDate.timeIntervalSince(entry.startDate) - delay
+        return absorptionModel.absorbedCarbs(of: entryGrams, atTime: time, absorptionTime: maxAbsorptionTime)
+    }
+
+    // MARK: Incremental observation
+
+    /// The date at which we observe all the carbs were absorbed. or nil if carb absorption has not finished
+    private var observedCompletionDate: Date?
+
+    /// The total observed effect for each entry, in glucose units
+    private(set) var observedEffect: Double = 0
+
+    /// The timeline of absorption amounts credited to this carb entry, in grams, for computation of historical COB and effect history
+    private(set) var observedTimeline: [CarbValue] = []
+
+    /// The amount of carbs we've observed absorbing
+    private var observedGrams: Double {
+        return observedEffect / carbohydrateSensitivityFactor
+    }
+
+    /// The amount of effect remaining until 100% of entry absorption is observed
+    var remainingEffect: Double {
+        return max(entryEffect - observedEffect, 0)
+    }
+
+    /// The dates over which we observed absorption, from start until 100% or last observed effect.
+    private var observedAbsorptionDates: DateInterval {
+        return DateInterval(start: entry.startDate, end: observedCompletionDate ?? lastEffectDate)
+    }
+
+
+    // MARK: Clamped results
+
+    /// The number of carbs absorbed, suitable for use in calculations.
+    /// This is bounded by minimumPredictedGrams and the entry total.
+    private var clampedGrams: Double {
+        let minPredictedGrams = self.minPredictedGrams
+
+        return min(entryGrams, max(minPredictedGrams, observedGrams))
+    }
+    
+    private var percentAbsorbed: Double {
+        return clampedGrams / entryGrams
+    }
+    
+    /// The amount of time needed to absorb observed grams
+    private var timeToAbsorbObservedCarbs: TimeInterval {
+        let time = lastEffectDate.timeIntervalSince(entry.startDate) - delay
+        guard time > 0 else {
+            return 0.0
+        }
+        var timeToAbsorb: TimeInterval
+        if adaptiveAbsorptionRateEnabled && time > adaptiveRateStandbyInterval {
+            // If adaptive absorption rate is enabled, and if the time since start of absorption is greater than the standby interval, the time to absorb observed carbs equals the obervation time
+            timeToAbsorb = time
+        } else {
+            // If adaptive absorption rate is disabled, or if the time since start of absorption is less than the standby interval, the time to absorb observed carbs is calculated based on the absorption model
+            timeToAbsorb = absorptionModel.timeToAbsorb(forPercentAbsorbed: percentAbsorbed, totalAbsorptionTime: initialAbsorptionTime)
+        }
+        return min(timeToAbsorb, maxAbsorptionTime)
+    }
+    
+    /// The amount of time needed for the remaining entry grams to absorb
+    private var estimatedTimeRemaining: TimeInterval {
+        let time = lastEffectDate.timeIntervalSince(entry.startDate) - delay
+        guard time > 0 else {
+            return initialAbsorptionTime
+        }
+        let notToExceedTimeRemaining = max(maxAbsorptionTime - time, 0.0)
+        guard notToExceedTimeRemaining > 0 else {
+            return 0.0
+        }
+        var dynamicTimeRemaining: TimeInterval
+        if adaptiveAbsorptionRateEnabled && time > adaptiveRateStandbyInterval {
+            // If adaptive absorption rate is enabled, and if the time since start of absorption is greater than the standby interval, the remaining time is estimated assuming the observed relative absorption rate persists for the remaining carbs
+            let dynamicAbsorptionTime = absorptionModel.absorptionTime(forPercentAbsorption: percentAbsorbed, atTime: time)
+            dynamicTimeRemaining = dynamicAbsorptionTime - time
+        } else {
+            // If adaptive absorption rate is disabled, or if the time since start of absorption is less than the standby interval, the remaining time is estimated assuming the modeled absorption rate
+            dynamicTimeRemaining = initialAbsorptionTime - timeToAbsorbObservedCarbs
+        }
+        // time remaining must not extend beyond the maximum absorption time
+        return min(dynamicTimeRemaining, notToExceedTimeRemaining)
+    }
+
+    /// The timeline of observed absorption, if greater than the minimum required absorption.
+    private var clampedTimeline: [CarbValue]? {
+        return observedGrams >= minPredictedGrams ? observedTimeline : nil
+    }
+
+    /// Configures a new builder
+    ///
+    /// - Parameters:
+    ///   - entry: The carb entry input
+    ///   - carbUnit: The unit used for carb values
+    ///   - carbohydrateSensitivityFactor: The carbohydrate-sensitivity factor for the entry, in glucose units per gram
+    ///   - initialAbsorptionTime: The absorption initially assigned to this entry before any absorption is observed
+    ///   - maxAbsorptionTime: The maximum absorption time allowed for this entry, determining the minimum absorption rate
+    ///   - delay: An amount of time to wait after the entry date before minimum absorption is assumed to begin
+    ///   - lastEffectDate: The last recorded date of effect observation, used to initialize absorption at model defined rate
+    ///   - initialObservedEffect: The initial amount of observed effect, in glucose units. Defaults to 0
+    ///   - absorptionModel: The absorption model to use when computing remaining absorption
+    ///   - adaptiveAbsorptionRateEnabled: Whether the remaining absorption rate changes based in observed absorption rate
+    ///   - adaptiveRateStandbyIntervalFraction: The delay, specified as a fraction of total absorption time, before the absorption rate will change based on observed absorption rate. Only used if adaptiveAbsorptionRateEnabled is true.
+    init(entry: T, carbUnit: HKUnit, carbohydrateSensitivityFactor: Double, initialAbsorptionTime: TimeInterval, maxAbsorptionTime: TimeInterval, delay: TimeInterval, lastEffectDate: Date?, absorptionModel: CarbAbsorptionComputable, adaptiveAbsorptionRateEnabled: Bool, adaptiveRateStandbyIntervalFraction: Double, initialObservedEffect: Double = 0) {
+        self.entry = entry
+        self.carbUnit = carbUnit
+        self.carbohydrateSensitivityFactor = carbohydrateSensitivityFactor
+        self.initialAbsorptionTime = initialAbsorptionTime
+        self.maxAbsorptionTime = maxAbsorptionTime
+        self.delay = delay
+        self.observedEffect = initialObservedEffect
+        self.absorptionModel = absorptionModel
+        self.adaptiveAbsorptionRateEnabled = adaptiveAbsorptionRateEnabled
+        self.adaptiveRateStandbyIntervalFraction = adaptiveRateStandbyIntervalFraction
+        self.entryGrams = entry.quantity.doubleValue(for: carbUnit)
+        self.entryEffect = entryGrams * carbohydrateSensitivityFactor
+        self.maxEndDate = entry.startDate.addingTimeInterval(maxAbsorptionTime + delay)
+        self.lastEffectDate = min(
+            maxEndDate,
+            Swift.max(lastEffectDate ?? entry.startDate, entry.startDate)
+        )
+
+    }
+
+    /// Increments the builder state with the next glucose effect.
+    /// 
+    /// This function should only be called with values in ascending date order.
+    ///
+    /// - Parameters:
+    ///   - effect: The effect value, in glucose units corresponding to `carbohydrateSensitivityFactor`
+    ///   - start: The start date of the effect
+    ///   - end: The end date of the effect
+    func addNextEffect(_ effect: Double, start: Date, end: Date) {
+        guard start >= entry.startDate else {
+            return
+        }
+
+        observedEffect += effect
+
+        if observedCompletionDate == nil {
+            // Continue recording the timeline until 100% of the carbs have been observed
+            observedTimeline.append(CarbValue(
+                startDate: start,
+                endDate: end,
+                quantity: HKQuantity(
+                    unit: carbUnit,
+                    doubleValue: effect / carbohydrateSensitivityFactor
+                )
+            ))
+
+            // Once 100% of the carbs are observed, track the endDate
+            if observedEffect + Double(Float.ulpOfOne) >= entryEffect {
+                observedCompletionDate = end
+            }
+        }
+    }
+
+    /// The resulting CarbStatus value
+    var result: CarbStatus<T> {
+        let absorption = AbsorbedCarbValue(
+            observed: HKQuantity(unit: carbUnit, doubleValue: observedGrams),
+            clamped: HKQuantity(unit: carbUnit, doubleValue: clampedGrams),
+            total: entry.quantity,
+            remaining: HKQuantity(unit: carbUnit, doubleValue: entryGrams - clampedGrams),
+            observedDate: observedAbsorptionDates,
+            estimatedTimeRemaining: estimatedTimeRemaining,
+            timeToAbsorbObservedCarbs: timeToAbsorbObservedCarbs
+        )
+
+        return CarbStatus(
+            entry: entry,
+            absorption: absorption,
+            observedTimeline: clampedTimeline
+        )
+    }
+    
+    func absorptionRateAtTime(t: TimeInterval) -> Double {
+        let dynamicAbsorptionTime = min(observedAbsorptionDates.duration + estimatedTimeRemaining, maxAbsorptionTime)
+        guard dynamicAbsorptionTime > 0 else {
+            return 0.0
+        }
+        // time t nomalized to absorption time
+        let percentTime = t / dynamicAbsorptionTime
+        let averageAbsorptionRate = entryGrams / dynamicAbsorptionTime
+        return averageAbsorptionRate * absorptionModel.percentRateAtPercentTime(percentTime)
+    }
+    
+}
+
+
+// MARK: - Sorted collections of CarbEntries
+extension Collection where Element: CarbEntry {
+    /// Maps a sorted timeline of carb entries to the observed absorbed carbohydrates for each, from a timeline of glucose effect velocities.
+    ///
+    /// This makes some important assumptions:
+    /// - insulin effects, used with glucose to calculate counteraction, are "correct"
+    /// - carbs are absorbed completely in the order they were eaten without mixing or overlapping effects
+    ///
+    /// - Parameters:
+    ///   - effectVelocities: A timeline of glucose effect velocities, ordered by start date
+    ///   - carbRatio: The schedule of carb ratios, in grams per unit
+    ///   - insulinSensitivity: The schedule of insulin sensitivities, in units of insulin per glucose-unit
+    ///   - absorptionTimeOverrun: A multiplier for determining the minimum absorption time from the specified absorption time
+    ///   - defaultAbsorptionTime: The absorption time to use for unspecified carb entries
+    ///   - delay: The time to delay the dose effect
+    /// - Returns: A new array of `CarbStatus` values describing the absorbed carb quantities
+    func map(
+        to effectVelocities: [GlucoseEffectVelocity],
+        carbRatio: CarbRatioSchedule?,
+        insulinSensitivity: InsulinSensitivitySchedule?,
+        absorptionTimeOverrun: Double,
+        defaultAbsorptionTime: TimeInterval,
+        delay: TimeInterval,
+        initialAbsorptionTimeOverrun: Double,
+        absorptionModel: CarbAbsorptionComputable,
+        adaptiveAbsorptionRateEnabled: Bool,
+        adaptiveRateStandbyIntervalFraction: Double
+    ) -> [CarbStatus<Element>] {
+        guard count > 0 else {
+            // TODO: Apply unmatched effects to meal prediction
+            return []
+        }
+
+        guard let carbRatios = carbRatio, let insulinSensitivities = insulinSensitivity else {
+            return map { (entry) in
+                CarbStatus(entry: entry, absorption: nil, observedTimeline: nil)
+            }
+        }
+        
+        // for computation
+        let glucoseUnit = HKUnit.milligramsPerDeciliter
+        let carbUnit = HKUnit.gram()
+
+        let builders: [CarbStatusBuilder<Element>] = map { (entry) in
+            let carbRatio = carbRatios.quantity(at: entry.startDate)
+            let insulinSensitivity = insulinSensitivities.quantity(at: entry.startDate)
+            let initialAbsorptionTimeOverrun = initialAbsorptionTimeOverrun
+
+            return CarbStatusBuilder(
+                entry: entry,
+                carbUnit: carbUnit,
+                carbohydrateSensitivityFactor: insulinSensitivity.doubleValue(for: glucoseUnit) / carbRatio.doubleValue(for: carbUnit),
+                initialAbsorptionTime: (entry.absorptionTime ?? defaultAbsorptionTime) * initialAbsorptionTimeOverrun,
+                maxAbsorptionTime: (entry.absorptionTime ?? defaultAbsorptionTime) * absorptionTimeOverrun,
+                delay: delay,
+                lastEffectDate: effectVelocities.last?.endDate,
+                absorptionModel: absorptionModel,
+                adaptiveAbsorptionRateEnabled: adaptiveAbsorptionRateEnabled,
+                adaptiveRateStandbyIntervalFraction: adaptiveRateStandbyIntervalFraction
+            )
+        }
+
+        for dxEffect in effectVelocities {
+            guard dxEffect.endDate > dxEffect.startDate else {
+                assertionFailure()
+                continue
+            }
+
+            // calculate instantanous absorption rate for all active entries
+            
+            // Apply effect to all active entries
+
+            // Select only the entries whose dates overlap the current date interval.
+            // These are not necessarily contiguous as maxEndDate varies between entries
+            let activeBuilders = builders.filter { (builder) -> Bool in
+                return dxEffect.startDate < builder.maxEndDate && dxEffect.startDate >= builder.entry.startDate
+            }
+
+            // Ignore velocities < 0 when estimating carb absorption.
+            // These are most likely the result of insulin absorption increases such as
+            // during activity
+            var effectValue = Swift.max(0, dxEffect.effect.quantity.doubleValue(for: glucoseUnit))
+
+            // Sum the current absorption rates of each active entry to determine how to split the active effects
+            var totalRate = activeBuilders.reduce(0) { (totalRate, builder) -> Double in
+                let effectTime = dxEffect.startDate.timeIntervalSince(builder.entry.startDate)
+                let absorptionRateAtEffectTime = builder.absorptionRateAtTime(t: effectTime)
+                return totalRate + absorptionRateAtEffectTime
+            }
+
+            for builder in activeBuilders {
+                // Apply a portion of the effect to this entry
+                let effectTime = dxEffect.startDate.timeIntervalSince(builder.entry.startDate)
+                let absorptionRateAtEffectTime = builder.absorptionRateAtTime(t: effectTime)
+                // If total rate is zero, assign zero to partial effect
+                var partialEffectValue: Double = 0.0
+                if totalRate > 0 {
+                    partialEffectValue = Swift.min(builder.remainingEffect, (absorptionRateAtEffectTime / totalRate) * effectValue)
+                    totalRate -= absorptionRateAtEffectTime
+                    effectValue -= partialEffectValue
+                }
+
+                builder.addNextEffect(partialEffectValue, start: dxEffect.startDate, end: dxEffect.endDate)
+
+                // If there's still remainder effects with no additional entries to account them to, count them as overrun on the final entry
+                if effectValue > Double(Float.ulpOfOne) && builder === activeBuilders.last! {
+                    builder.addNextEffect(effectValue, start: dxEffect.startDate, end: dxEffect.endDate)
+                }
+            }
+
+            // We have remaining effect and no activeBuilders (otherwise we would have applied the effect to the last one)
+            if effectValue > Double(Float.ulpOfOne) {
+                // TODO: Track "phantom meals"
+            }
+        }
+
+        return builders.map { $0.result }
+    }
+}

+ 130 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbStatus.swift

@@ -0,0 +1,130 @@
+//
+//  CarbStatus.swift
+//  LoopKit
+//
+//  Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public struct CarbStatus<T: CarbEntry> {
+    /// Details entered by the user
+    public let entry: T
+
+    /// The last-computed absorption of the carbs
+    public let absorption: AbsorbedCarbValue?
+
+    /// The timeline of observed carb absorption. Nil if observed absorption is less than the modeled minimum
+    public let observedTimeline: [CarbValue]?
+}
+
+
+// Masquerade as a carb entry, substituting AbsorbedCarbValue's interpretation of absorption time
+extension CarbStatus: SampleValue {
+    public var quantity: HKQuantity {
+        return entry.quantity
+    }
+
+    public var startDate: Date {
+        return entry.startDate
+    }
+}
+
+
+extension CarbStatus: CarbEntry {
+    public var absorptionTime: TimeInterval? {
+        return absorption?.estimatedDate.duration ?? entry.absorptionTime
+    }
+}
+
+
+extension CarbStatus {
+    
+    func dynamicCarbsOnBoard(at date: Date, defaultAbsorptionTime: TimeInterval, delay: TimeInterval, delta: TimeInterval, absorptionModel: CarbAbsorptionComputable) -> Double {
+        guard date >= startDate - delta,
+            let absorption = absorption
+        else {
+            // We have to have absorption info for dynamic calculation
+            return entry.carbsOnBoard(at: date, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, absorptionModel: absorptionModel)
+        }
+
+        let unit = HKUnit.gram()
+
+        guard let observedTimeline = observedTimeline, let observationEnd = observedTimeline.last?.endDate else {
+            // Less than minimum observed or observation not yet started; calc based on modeled absorption rate
+            let total = absorption.total.doubleValue(for: unit)
+            let time = date.timeIntervalSince(startDate) - delay
+            let absorptionTime = absorption.estimatedDate.duration
+            return absorptionModel.unabsorbedCarbs(of: total, atTime: time, absorptionTime: absorptionTime)
+        }
+
+        guard date <= observationEnd else {
+            // Predicted absorption for remaining carbs, post-observation
+            let effectiveTime = date.timeIntervalSince(observationEnd) + absorption.timeToAbsorbObservedCarbs
+            let effectiveAbsorptionTime = absorption.timeToAbsorbObservedCarbs + absorption.estimatedTimeRemaining
+            let total = absorption.total.doubleValue(for: unit)
+            let unabsorbedAtEffectiveTime = absorptionModel.unabsorbedCarbs(of: total, atTime: effectiveTime, absorptionTime: effectiveAbsorptionTime)
+            let unabsorbedCarbs = max(unabsorbedAtEffectiveTime, 0.0)
+            return unabsorbedCarbs
+        }
+
+        // Observed absorption
+        // TODO: This creates an O(n^2) situation for COB timelines
+        let total = entry.quantity.doubleValue(for: unit)
+        return max(observedTimeline.filter({ $0.endDate <= date }).reduce(total) { (total, value) -> Double in
+            return total - value.quantity.doubleValue(for: unit)
+        }, 0)
+    }
+
+    func dynamicAbsorbedCarbs(at date: Date, absorptionTime: TimeInterval, delay: TimeInterval, delta: TimeInterval, absorptionModel: CarbAbsorptionComputable) -> Double {
+        guard date >= startDate,
+            let absorption = absorption
+        else {
+            // We have to have absorption info for dynamic calculation
+            return entry.absorbedCarbs(at: date, absorptionTime: absorptionTime, delay: delay, absorptionModel: absorptionModel)
+        }
+
+        let unit = HKUnit.gram()
+
+        guard let observedTimeline = observedTimeline, let observationEnd = observedTimeline.last?.endDate else {
+            // Less than minimum observed or observation not yet started; calc based on modeled absorption rate
+            let total = absorption.total.doubleValue(for: unit)
+            let time = date.timeIntervalSince(startDate) - delay
+            let absorptionTime = absorption.estimatedDate.duration
+            return absorptionModel.absorbedCarbs(of: total, atTime: time, absorptionTime: absorptionTime)
+        }
+
+        guard date <= observationEnd else {
+            // Predicted absorption for remaining carbs, post-observation
+            let effectiveTime = date.timeIntervalSince(observationEnd) + absorption.timeToAbsorbObservedCarbs
+            let effectiveAbsorptionTime = absorption.timeToAbsorbObservedCarbs + absorption.estimatedTimeRemaining
+            let total = absorption.total.doubleValue(for: unit)
+            let absorbedAtEffectiveTime = absorptionModel.absorbedCarbs(of: total, atTime: effectiveTime, absorptionTime: effectiveAbsorptionTime)
+            let absorbedCarbs = min(absorbedAtEffectiveTime, total)
+            return absorbedCarbs
+        }
+
+        // Observed absorption
+        // TODO: This creates an O(n^2) situation for carb effect timelines
+        var sum: Double = 0
+        var beforeDate = observedTimeline.filter { (value) -> Bool in
+            value.startDate.addingTimeInterval(delta) <= date
+        }
+
+        // Apply only a portion of the value if it extends past the final value
+        if let last = beforeDate.popLast() {
+            let observationInterval = DateInterval(start: last.startDate, end: last.endDate)
+            if  observationInterval.duration > 0,
+                let calculationInterval = DateInterval(start: last.startDate, end: date).intersection(with: observationInterval)
+            {
+                sum += calculationInterval.duration / observationInterval.duration * last.quantity.doubleValue(for: unit)
+            }
+        }
+
+        return min(beforeDate.reduce(sum) { (sum, value) -> Double in
+            return sum + value.quantity.doubleValue(for: unit)
+        }, quantity.doubleValue(for: unit))
+    }
+}

文件差異過大導致無法顯示
+ 1483 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbStore.swift


+ 39 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbStoreError.swift

@@ -0,0 +1,39 @@
+//
+//  CarbStoreError.swift
+//  LoopKit
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+
+extension CarbStore.CarbStoreError: LocalizedError {
+    public var errorDescription: String? {
+        switch self {
+        case .unauthorized:
+            return LocalizedString("com.loudnate.CarbKit.deleteCarbEntryUnownedErrorDescription", value: "Authorization Denied", comment: "The description of an error returned when attempting to delete a sample not shared by the current app")
+        case .notConfigured:
+            return nil
+        case .healthStoreError(let error):
+            return error.localizedDescription
+        case .coreDataError(let error):
+            return error.localizedDescription
+        case .noData:
+            return LocalizedString("No values found", comment: "Describes an error for no data found in a CarbStore request")
+        }
+    }
+
+    public var recoverySuggestion: String? {
+        switch self {
+        case .unauthorized:
+            return LocalizedString("com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion", value: "This sample can be deleted from the Health app", comment: "The error recovery suggestion when attempting to delete a sample not shared by the current app")
+        case .notConfigured:
+            return nil
+        case .healthStoreError:
+            return nil
+        case .coreDataError:
+            return nil
+        case .noData:
+            return LocalizedString("Ensure carb data exists for the specified date", comment: "Recovery suggestion for a no data error")
+        }
+    }
+}

+ 49 - 0
Dependecies/LoopKit/LoopKit/CarbKit/CarbValue.swift

@@ -0,0 +1,49 @@
+//
+//  CarbValue.swift
+//  LoopKit
+//
+//  Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public struct CarbValue: SampleValue {
+    public let startDate: Date
+    public let endDate: Date
+    public var quantity: HKQuantity
+
+    public init(startDate: Date, endDate: Date? = nil, quantity: HKQuantity) {
+        self.startDate = startDate
+        self.endDate = endDate ?? startDate
+        self.quantity = quantity
+    }
+}
+
+extension CarbValue: Equatable {}
+
+extension CarbValue: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.startDate = try container.decode(Date.self, forKey: .startDate)
+        self.endDate = try container.decode(Date.self, forKey: .endDate)
+        self.quantity = HKQuantity(unit: HKUnit(from: try container.decode(String.self, forKey: .quantityUnit)),
+                                   doubleValue: try container.decode(Double.self, forKey: .quantity))
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(startDate, forKey: .startDate)
+        try container.encode(endDate, forKey: .endDate)
+        try container.encode(quantity.doubleValue(for: .gram()), forKey: .quantity)
+        try container.encode(HKUnit.gram().unitString, forKey: .quantityUnit)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case startDate
+        case endDate
+        case quantity
+        case quantityUnit
+    }
+}

+ 39 - 0
Dependecies/LoopKit/LoopKit/CarbKit/HKQuantitySample+CarbKit.swift

@@ -0,0 +1,39 @@
+//
+//  HKQuantitySample.swift
+//  CarbKit
+//
+//  Created by Nathan Racklyeft on 1/10/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import HealthKit
+
+
+let MetadataKeyAbsorptionTimeMinutes = "com.loudnate.CarbKit.HKMetadataKey.AbsorptionTimeMinutes"
+let MetadataKeyUserCreatedDate = "com.loopkit.CarbKit.HKMetadataKey.UserCreatedDate"
+let MetadataKeyUserUpdatedDate = "com.loopkit.CarbKit.HKMetadataKey.UserUpdatedDate"
+
+extension HKQuantitySample {
+    public var foodType: String? {
+        return metadata?[HKMetadataKeyFoodType] as? String
+    }
+
+    public var absorptionTime: TimeInterval? {
+        guard let absorptionTimeMinutes = metadata?[MetadataKeyAbsorptionTimeMinutes] as? Double else {
+            return nil
+        }
+        return TimeInterval(minutes: absorptionTimeMinutes)
+    }
+
+    public var createdByCurrentApp: Bool {
+        return sourceRevision.source == HKSource.default()
+    }
+
+    public var userCreatedDate: Date? {
+        return metadata?[MetadataKeyUserCreatedDate] as? Date
+    }
+
+    public var userUpdatedDate: Date? {
+        return metadata?[MetadataKeyUserUpdatedDate] as? Date
+    }
+}

+ 40 - 0
Dependecies/LoopKit/LoopKit/CarbKit/NSUserDefaults.swift

@@ -0,0 +1,40 @@
+//
+//  NSUserDefaults.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 2/20/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+extension UserDefaults {
+    private enum Key: String {
+        case CarbEntryCache = "com.loudnate.CarbKit.CarbEntryCache"
+        case ModifiedCarbEntries = "com.loudnate.CarbKit.ModifiedCarbEntries"
+        case DeletedCarbEntryIds = "com.loudnate.CarbKit.DeletedCarbEntryIds"
+    }
+
+    func purgeLegacyCarbEntryKeys() {
+        removeObject(forKey: Key.CarbEntryCache.rawValue)
+        removeObject(forKey: Key.ModifiedCarbEntries.rawValue)
+        removeObject(forKey: Key.DeletedCarbEntryIds.rawValue)
+    }
+
+    var modifiedCarbEntries: [StoredCarbEntry]? {
+        get {
+            if let rawValue = array(forKey: Key.ModifiedCarbEntries.rawValue) as? [StoredCarbEntry.RawValue] {
+                return rawValue.compactMap { StoredCarbEntry(rawValue: $0) }
+            } else {
+                return nil
+            }
+        }
+    }
+
+    var deletedCarbEntryIds: [String]? {
+        get {
+            return array(forKey: Key.DeletedCarbEntryIds.rawValue) as? [String]
+        }
+    }
+}

+ 60 - 0
Dependecies/LoopKit/LoopKit/CarbKit/NewCarbEntry.swift

@@ -0,0 +1,60 @@
+//
+//  NewCarbEntry.swift
+//  CarbKit
+//
+//  Created by Nathan Racklyeft on 1/15/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public struct NewCarbEntry: CarbEntry, Equatable, RawRepresentable {
+    public typealias RawValue = [String: Any]
+
+    public let date: Date
+    public let quantity: HKQuantity
+    public let startDate: Date
+    public let foodType: String?
+    public let absorptionTime: TimeInterval?
+
+    public init(date: Date = Date(), quantity: HKQuantity, startDate: Date, foodType: String?, absorptionTime: TimeInterval?) {
+        self.date = date
+        self.quantity = quantity
+        self.startDate = startDate
+        self.foodType = foodType
+        self.absorptionTime = absorptionTime
+    }
+
+    public init?(rawValue: RawValue) {
+        guard
+            let date = rawValue["date"] as? Date,
+            let grams = rawValue["grams"] as? Double,
+            let startDate = rawValue["startDate"] as? Date
+        else {
+            return nil
+        }
+
+        self.init(
+            date: date,
+            quantity: HKQuantity(unit: .gram(), doubleValue: grams),
+            startDate: startDate,
+            foodType: rawValue["foodType"] as? String,
+            absorptionTime: rawValue["absorptionTime"] as? TimeInterval
+        )
+    }
+
+    public var rawValue: RawValue {
+        var rawValue: RawValue = [
+            "date": date,
+            "grams": quantity.doubleValue(for: .gram()),
+            "startDate": startDate
+        ]
+
+        rawValue["foodType"] = foodType
+        rawValue["absorptionTime"] = absorptionTime
+
+        return rawValue
+    }
+}

+ 174 - 0
Dependecies/LoopKit/LoopKit/CarbKit/StoredCarbEntry.swift

@@ -0,0 +1,174 @@
+//
+//  StoredCarbEntry.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/22/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import HealthKit
+import CoreData
+
+public struct StoredCarbEntry: CarbEntry, Equatable {
+
+    public let uuid: UUID?
+
+    // MARK: - HealthKit Sync Support
+
+    public let provenanceIdentifier: String?
+    public let syncIdentifier: String?
+    public let syncVersion: Int?
+
+    // MARK: - SampleValue
+
+    public let startDate: Date
+    public let quantity: HKQuantity
+
+    // MARK: - CarbEntry
+
+    public let foodType: String?
+    public let absorptionTime: TimeInterval?
+    public let createdByCurrentApp: Bool
+
+    // MARK: - User dates
+
+    public let userCreatedDate: Date?
+    public let userUpdatedDate: Date?
+
+    public init(
+        uuid: UUID?,
+        provenanceIdentifier: String?,
+        syncIdentifier: String?,
+        syncVersion: Int?,
+        startDate: Date,
+        quantity: HKQuantity,
+        foodType: String?,
+        absorptionTime: TimeInterval?,
+        createdByCurrentApp: Bool,
+        userCreatedDate: Date?,
+        userUpdatedDate: Date?
+    ) {
+        self.uuid = uuid
+        self.provenanceIdentifier = provenanceIdentifier
+        self.syncIdentifier = syncIdentifier
+        self.syncVersion = syncVersion
+        self.startDate = startDate
+        self.quantity = quantity
+        self.foodType = foodType
+        self.absorptionTime = absorptionTime
+        self.createdByCurrentApp = createdByCurrentApp
+        self.userCreatedDate = userCreatedDate
+        self.userUpdatedDate = userUpdatedDate
+    }
+}
+
+extension StoredCarbEntry {
+    init(managedObject: CachedCarbObject) {
+        self.init(
+            uuid: managedObject.uuid,
+            provenanceIdentifier: managedObject.provenanceIdentifier,
+            syncIdentifier: managedObject.syncIdentifier,
+            syncVersion: managedObject.syncVersion,
+            startDate: managedObject.startDate,
+            quantity: managedObject.quantity,
+            foodType: managedObject.foodType,
+            absorptionTime: managedObject.absorptionTime,
+            createdByCurrentApp: managedObject.createdByCurrentApp,
+            userCreatedDate: managedObject.userCreatedDate,
+            userUpdatedDate: managedObject.userUpdatedDate
+        )
+    }
+}
+
+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),
+                  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: .gram(), doubleValue: try container.decode(Double.self, forKey: .quantity)),
+                  foodType: try container.decodeIfPresent(String.self, forKey: .foodType),
+                  absorptionTime: try container.decodeIfPresent(TimeInterval.self, forKey: .absorptionTime),
+                  createdByCurrentApp: try container.decode(Bool.self, forKey: .createdByCurrentApp),
+                  userCreatedDate: try container.decodeIfPresent(Date.self, forKey: .userCreatedDate),
+                  userUpdatedDate: try container.decodeIfPresent(Date.self, forKey: .userUpdatedDate)
+        )
+    }
+    
+    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.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
+        try container.encodeIfPresent(syncVersion, forKey: .syncVersion)
+        try container.encode(startDate, forKey: .startDate)
+        try container.encode(quantity.doubleValue(for: .gram()), forKey: .quantity)
+        try container.encodeIfPresent(foodType, forKey: .foodType)
+        try container.encodeIfPresent(absorptionTime, forKey: .absorptionTime)
+        try container.encode(createdByCurrentApp, forKey: .createdByCurrentApp)
+        try container.encodeIfPresent(userCreatedDate, forKey: .userCreatedDate)
+        try container.encodeIfPresent(userUpdatedDate, forKey: .userUpdatedDate)
+    }
+    
+    private enum CodingKeys: String, CodingKey {
+        case uuid
+        case provenanceIdentifier
+        case syncIdentifier
+        case syncVersion
+        case startDate
+        case quantity
+        case foodType
+        case absorptionTime
+        case createdByCurrentApp
+        case userCreatedDate
+        case userUpdatedDate
+    }
+}
+
+// MARK: - DEPRECATED - Used only for migration
+
+extension StoredCarbEntry {
+    typealias RawValue = [String: Any]
+
+    init?(rawValue: RawValue) {
+        guard let
+            sampleUUIDString = rawValue["sampleUUID"] as? String,
+            let uuid = UUID(uuidString: sampleUUIDString),
+            let startDate = rawValue["startDate"] as? Date,
+            let unitString = rawValue["unitString"] as? String,
+            let value = rawValue["value"] as? Double,
+            let createdByCurrentApp = rawValue["createdByCurrentApp"] as? Bool else
+        {
+            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
+        }
+
+        self.init(
+            uuid: uuid,
+            provenanceIdentifier: provenanceIdentifier,
+            syncIdentifier: syncIdentifier,
+            syncVersion: syncVersion,
+            startDate: startDate,
+            quantity: HKQuantity(unit: HKUnit(from: unitString), doubleValue: value),
+            foodType: rawValue["foodType"] as? String,
+            absorptionTime: rawValue["absorptionTime"] as? TimeInterval,
+            createdByCurrentApp: createdByCurrentApp,
+            userCreatedDate: nil,
+            userUpdatedDate: nil
+        )
+    }
+}

+ 88 - 0
Dependecies/LoopKit/LoopKit/CarbKit/SyncCarbObject.swift

@@ -0,0 +1,88 @@
+//
+//  SyncCarbObject.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 8/10/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+public enum Operation: Int, CaseIterable, Codable {
+    case create
+    case update
+    case delete
+}
+
+public struct SyncCarbObject: Codable, Equatable {
+    public let absorptionTime: TimeInterval?
+    public let createdByCurrentApp: Bool
+    public let foodType: String?
+    public let grams: Double
+    public let startDate: Date
+    public let uuid: UUID?
+    public let provenanceIdentifier: String?
+    public let syncIdentifier: String?
+    public let syncVersion: Int?
+    public let userCreatedDate: Date?
+    public let userUpdatedDate: Date?
+    public let userDeletedDate: Date?
+    public let operation: Operation
+    public let addedDate: Date?
+    public let supercededDate: Date?
+
+    public init(absorptionTime: TimeInterval?,
+                createdByCurrentApp: Bool,
+                foodType: String?,
+                grams: Double,
+                startDate: Date,
+                uuid: UUID?,
+                provenanceIdentifier: String?,
+                syncIdentifier: String?,
+                syncVersion: Int?,
+                userCreatedDate: Date?,
+                userUpdatedDate: Date?,
+                userDeletedDate: Date?,
+                operation: Operation,
+                addedDate: Date?,
+                supercededDate: Date?) {
+        self.absorptionTime = absorptionTime
+        self.createdByCurrentApp = createdByCurrentApp
+        self.foodType = foodType
+        self.grams = grams
+        self.startDate = startDate
+        self.uuid = uuid
+        self.provenanceIdentifier = provenanceIdentifier
+        self.syncIdentifier = syncIdentifier
+        self.syncVersion = syncVersion
+        self.userCreatedDate = userCreatedDate
+        self.userUpdatedDate = userUpdatedDate
+        self.userDeletedDate = userDeletedDate
+        self.operation = operation
+        self.addedDate = addedDate
+        self.supercededDate = supercededDate
+    }
+
+    public var quantity: HKQuantity { HKQuantity(unit: .gram(), doubleValue: grams) }
+}
+
+extension SyncCarbObject {
+    init(managedObject: CachedCarbObject) {
+        self.init(absorptionTime: managedObject.absorptionTime,
+                  createdByCurrentApp: managedObject.createdByCurrentApp,
+                  foodType: managedObject.foodType,
+                  grams: managedObject.grams,
+                  startDate: managedObject.startDate,
+                  uuid: managedObject.uuid,
+                  provenanceIdentifier: managedObject.provenanceIdentifier,
+                  syncIdentifier: managedObject.syncIdentifier,
+                  syncVersion: managedObject.syncVersion,
+                  userCreatedDate: managedObject.userCreatedDate,
+                  userUpdatedDate: managedObject.userUpdatedDate,
+                  userDeletedDate: managedObject.userDeletedDate,
+                  operation: managedObject.operation,
+                  addedDate: managedObject.addedDate,
+                  supercededDate: managedObject.supercededDate)
+    }
+}

+ 13 - 0
Dependecies/LoopKit/LoopKit/CarbRatioSchedule.swift

@@ -0,0 +1,13 @@
+//
+//  CarbRatioSchedule.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 2/12/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public typealias CarbRatioSchedule = SingleQuantitySchedule

+ 15 - 0
Dependecies/LoopKit/LoopKit/CarbSensitivitySchedule.swift

@@ -0,0 +1,15 @@
+//
+//  CarbSensitivitySchedule.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 3/27/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+public typealias CarbSensitivitySchedule = SingleQuantitySchedule
+
+extension /* CarbSensitivitySchedule */ DailyQuantitySchedule where T == Double {
+    public static func carbSensitivitySchedule(insulinSensitivitySchedule: InsulinSensitivitySchedule, carbRatioSchedule: CarbRatioSchedule) -> CarbSensitivitySchedule {
+        return insulinSensitivitySchedule / carbRatioSchedule
+    }
+}

+ 46 - 0
Dependecies/LoopKit/LoopKit/CorrectionRangeOverrides.swift

@@ -0,0 +1,46 @@
+//
+//  CorrectionRangeOverrides.swift
+//  LoopKit
+//
+//  Created by Rick Pasetto on 7/14/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+import Foundation
+
+public struct CorrectionRangeOverrides: Equatable {
+    public enum Preset: Hashable, CaseIterable {
+        case preMeal
+        case workout
+    }
+
+    public var ranges: [Preset: ClosedRange<HKQuantity>]
+
+    public init(preMeal: DoubleRange?, workout: DoubleRange?, unit: HKUnit) {
+        ranges = [:]
+        ranges[.preMeal] = preMeal?.quantityRange(for: unit)
+        ranges[.workout] = workout?.quantityRange(for: unit)
+    }
+
+    public var preMeal: ClosedRange<HKQuantity>? { ranges[.preMeal] }
+    public var workout: ClosedRange<HKQuantity>? { ranges[.workout] }
+}
+
+public extension CorrectionRangeOverrides.Preset {
+    var title: String {
+        switch self {
+        case .preMeal:
+            return LocalizedString("Pre-Meal", comment: "Title for pre-meal mode")
+        case .workout:
+            return LocalizedString("Workout", comment: "Title for workout mode")
+        }
+    }
+    
+    var therapySetting: TherapySetting {
+        switch self {
+        case .preMeal: return .preMealCorrectionRangeOverride
+        case .workout: return .workoutCorrectionRangeOverride
+        }
+    }
+}

+ 41 - 0
Dependecies/LoopKit/LoopKit/CriticalEventLog.swift

@@ -0,0 +1,41 @@
+//
+//  CriticalEventLog.swift
+//  LoopKit
+//
+//  Created by Darin Krauss on 7/15/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public protocol CriticalEventLog {
+
+    /// The name for the critical event log export.
+    var exportName: String { get }
+
+    /// Calculate the progress total unit count for the critical event log export for the specified date range.
+    ///
+    /// - Parameters:
+    ///   - startDate: The start date for the critical events to export.
+    ///   - endDate: The end date for the critical events to export. Optional. If not specified, default to now.
+    /// - Returns: An progress total unit count, or an error.
+    func exportProgressTotalUnitCount(startDate: Date, endDate: Date?) -> Result<Int64, Error>
+
+    /// Export the critical event log for the specified date range.
+    ///
+    /// - Parameters:
+    ///   - startDate: The start date for the critical events to export.
+    ///   - endDate: The end date for the critical events to export.
+    ///   - stream: The output stream to write the critical event log to. Typically writes JSON UTF-8 text.
+    ///   - progressor: The estimated duration progress to use to check if cancelled and report progress.
+    /// - Returns: Any error that occurs during the export, or nil if successful.
+    func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error?
+}
+
+public enum CriticalEventLogError: Error {
+
+    /// The export was cancelled either by the user or the OS.
+    case cancelled
+}
+
+public let criticalEventLogExportProgressUnitCountPerFetch: Int64 = 250

+ 180 - 0
Dependecies/LoopKit/LoopKit/DailyQuantitySchedule+Override.swift

@@ -0,0 +1,180 @@
+//
+//  DailyQuantitySchedule+Override.swift
+//  LoopKit
+//
+//  Created by Michael Pangburn on 3/26/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+
+extension GlucoseRangeSchedule {
+    public func applyingOverride(_ override: TemporaryScheduleOverride) -> GlucoseRangeSchedule {
+        guard let targetRange = override.settings.targetRange else {
+            return self
+        }
+
+        let doubleRange = targetRange.doubleRange(for: unit)
+        let rangeOverride = GlucoseRangeSchedule.Override(value: doubleRange, start: override.startDate, end: override.scheduledEndDate)
+        return GlucoseRangeSchedule(rangeSchedule: rangeSchedule, override: rangeOverride)
+    }
+}
+
+extension /* BasalRateSchedule */ DailyValueSchedule where T == Double {
+    func applyingBasalRateMultiplier(
+        from override: TemporaryScheduleOverride,
+        relativeTo date: Date = Date()
+    ) -> BasalRateSchedule {
+        return applyingOverride(override, relativeTo: date, multiplier: \.basalRateMultiplier)
+    }
+}
+
+extension /* InsulinSensitivitySchedule */ DailyQuantitySchedule where T == Double {
+    func applyingSensitivityMultiplier(
+        from override: TemporaryScheduleOverride,
+        relativeTo date: Date = Date()
+    ) -> InsulinSensitivitySchedule {
+        return DailyQuantitySchedule(
+            unit: unit,
+            valueSchedule: valueSchedule.applyingOverride(
+                override,
+                relativeTo: date,
+                multiplier: \.insulinSensitivityMultiplier
+            )
+        )
+    }
+}
+
+extension /* CarbRatioSchedule */ DailyQuantitySchedule where T == Double {
+    func applyingCarbRatioMultiplier(
+        from override: TemporaryScheduleOverride,
+        relativeTo date: Date = Date()
+    ) -> CarbRatioSchedule {
+        return DailyQuantitySchedule(
+            unit: unit,
+            valueSchedule: valueSchedule.applyingOverride(
+                override,
+                relativeTo: date,
+                multiplier: \.carbRatioMultiplier
+            )
+        )
+    }
+}
+
+extension DailyValueSchedule where T == Double {
+    fileprivate func applyingOverride(
+        _ override: TemporaryScheduleOverride,
+        relativeTo date: Date,
+        multiplier multiplierKeyPath: KeyPath<TemporaryScheduleOverrideSettings, Double?>
+    ) -> DailyValueSchedule {
+        guard let multiplier = override.settings[keyPath: multiplierKeyPath] else {
+            return self
+        }
+        return applyingOverride(
+            during: override.activeInterval,
+            relativeTo: date,
+            updatingOverridenValuesWith: { $0 * multiplier }
+        )
+    }
+}
+
+extension DailyValueSchedule {
+    fileprivate func applyingOverride(
+        during activeInterval: DateInterval,
+        relativeTo referenceDate: Date,
+        updatingOverridenValuesWith update: (T) -> T
+    ) -> DailyValueSchedule {
+        guard let activeInterval = clampingToAffectedInterval(activeInterval, relativeTo: referenceDate) else {
+            // Override has no effect relative to the reference date
+            return self
+        }
+
+        let overrideStartOffset = scheduleOffset(for: activeInterval.start)
+        let overrideEndOffset = scheduleOffset(for: activeInterval.end)
+        guard overrideStartOffset != overrideEndOffset else {
+            // Full schedule overridden
+            return DailyValueSchedule(
+                dailyItems: items.map { item in RepeatingScheduleValue(startTime: item.startTime, value: update(item.value)) },
+                timeZone: timeZone
+            )!
+        }
+
+        let overrideCrossesMidnight = overrideStartOffset > overrideEndOffset
+        let scheduleItemsIncludingOverride = scheduleItemsPaddedToClosedInterval
+            .adjacentPairs()
+            .flatMap { item, nextItem -> [RepeatingScheduleValue<T>] in
+                let overriddenItemValue = update(item.value)
+                let overriddenItem = RepeatingScheduleValue(startTime: item.startTime, value: overriddenItemValue)
+                let overrideStart = RepeatingScheduleValue(startTime: overrideStartOffset, value: overriddenItemValue)
+                let overrideEnd = RepeatingScheduleValue(startTime: overrideEndOffset, value: item.value)
+
+                let scheduleItemInterval = item.startTime..<nextItem.startTime
+                let overrideStartsInThisSegment = scheduleItemInterval.contains(overrideStartOffset)
+                let overrideEndsInThisSegment = scheduleItemInterval.contains(overrideEndOffset)
+
+                switch (overrideStartsInThisSegment, overrideEndsInThisSegment) {
+                case (true, true):
+                    if overrideCrossesMidnight {
+                        return item.startTime == overrideEndOffset
+                            ? [overrideEnd, overrideStart]
+                            : [overriddenItem, overrideEnd, overrideStart]
+                    } else {
+                        return item.startTime == overrideStartOffset
+                            ? [overrideStart, overrideEnd]
+                            : [item, overrideStart, overrideEnd]
+                    }
+                case (true, false):
+                    return item.startTime == overrideStartOffset
+                        ? [overrideStart]
+                        : [item, overrideStart]
+                case (false, true):
+                    return item.startTime == overrideEndOffset
+                        ? [overrideEnd]
+                        : [overriddenItem, overrideEnd]
+                case (false, false):
+                    let segmentIsDisjointWithOverride = overrideCrossesMidnight
+                        ? overrideEndOffset...overrideStartOffset ~= item.startTime
+                        : !(overrideStartOffset...overrideEndOffset ~= item.startTime)
+                    return segmentIsDisjointWithOverride
+                        ? [item]
+                        : [overriddenItem]
+                }
+        }
+
+        return DailyValueSchedule(
+            dailyItems: scheduleItemsIncludingOverride,
+            timeZone: timeZone
+        )!
+    }
+
+    /// Clamps the override date interval to the relevant period of effect given a reference date.
+    /// Returns `nil` if an override during the given interval has no effect relative to the reference date.
+    private func clampingToAffectedInterval(_ interval: DateInterval, relativeTo referenceDate: Date) -> DateInterval? {
+        let relevantPeriodStart = referenceDate.addingTimeInterval(-repeatInterval)
+        let relevantPeriodEnd = referenceDate.addingTimeInterval(repeatInterval)
+
+        guard
+            interval.end > relevantPeriodStart,
+            interval.start < relevantPeriodEnd
+        else {
+            return nil
+        }
+
+        let startDate = max(interval.start, relevantPeriodStart)
+        let endDate = min(interval.end, relevantPeriodEnd)
+        let affectedInterval = DateInterval(start: startDate, end: endDate)
+        return affectedInterval
+    }
+
+    /// Pads the schedule with an extra item to form a closed interval.
+    private var scheduleItemsPaddedToClosedInterval: [RepeatingScheduleValue<T>] {
+        guard let lastItem = items.last else {
+            assertionFailure("Schedule can never be empty")
+            return []
+        }
+        let lastItemStartingAtDayEnd = RepeatingScheduleValue(startTime: maxTimeInterval, value: lastItem.value)
+        return items + [lastItemStartingAtDayEnd]
+    }
+}
+

+ 154 - 0
Dependecies/LoopKit/LoopKit/DailyQuantitySchedule.swift

@@ -0,0 +1,154 @@
+//
+//  DailyQuantitySchedule.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 2/12/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public struct DailyQuantitySchedule<T: RawRepresentable>: DailySchedule {
+    public typealias RawValue = [String: Any]
+    public let unit: HKUnit
+    var valueSchedule: DailyValueSchedule<T>
+
+    public init?(unit: HKUnit, dailyItems: [RepeatingScheduleValue<T>], timeZone: TimeZone? = nil) {
+        guard let valueSchedule = DailyValueSchedule<T>(dailyItems: dailyItems, timeZone: timeZone) else {
+            return nil
+        }
+
+        self.unit = unit
+        self.valueSchedule = valueSchedule
+    }
+
+    init(unit: HKUnit, valueSchedule: DailyValueSchedule<T>) {
+        self.unit = unit
+        self.valueSchedule = valueSchedule
+    }
+
+    public init?(rawValue: RawValue) {
+        guard let rawUnit = rawValue["unit"] as? String,
+            let valueSchedule = DailyValueSchedule<T>(rawValue: rawValue)
+            else
+        {
+            return nil
+        }
+
+        self.unit = HKUnit(from: rawUnit)
+        self.valueSchedule = valueSchedule
+    }
+
+    public var items: [RepeatingScheduleValue<T>] {
+        return valueSchedule.items
+    }
+
+    public var timeZone: TimeZone {
+        get {
+            return valueSchedule.timeZone
+        }
+        set {
+            valueSchedule.timeZone = newValue
+        }
+    }
+
+    public var rawValue: RawValue {
+        var rawValue = valueSchedule.rawValue
+
+        rawValue["unit"] = unit.unitString
+
+        return rawValue
+    }
+
+    public func between(start startDate: Date, end endDate: Date) -> [AbsoluteScheduleValue<T>] {
+        return valueSchedule.between(start: startDate, end: endDate)
+    }
+
+    public func value(at time: Date) -> T {
+        return valueSchedule.value(at: time)
+    }
+}
+
+extension DailyQuantitySchedule: Codable where T: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.unit = HKUnit(from: try container.decode(String.self, forKey: .unit))
+        self.valueSchedule = try container.decode(DailyValueSchedule<T>.self, forKey: .valueSchedule)
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(unit.unitString, forKey: .unit)
+        try container.encode(valueSchedule, forKey: .valueSchedule)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case unit
+        case valueSchedule
+    }
+}
+
+extension DailyQuantitySchedule: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        return String(reflecting: rawValue)
+    }
+}
+
+
+public typealias SingleQuantitySchedule = DailyQuantitySchedule<Double>
+
+
+public extension DailyQuantitySchedule where T == Double {
+    func quantity(at time: Date) -> HKQuantity {
+        return HKQuantity(unit: unit, doubleValue: valueSchedule.value(at: time))
+    }
+
+    func averageValue() -> Double {
+        var total: Double = 0
+
+        for (index, item) in valueSchedule.items.enumerated() {
+            var endTime = valueSchedule.maxTimeInterval
+
+            if index < valueSchedule.items.endIndex - 1 {
+                endTime = valueSchedule.items[index + 1].startTime
+            }
+
+            total += (endTime - item.startTime) * item.value
+        }
+
+        return total / valueSchedule.repeatInterval
+    }
+
+    func averageQuantity() -> HKQuantity {
+        return HKQuantity(unit: unit, doubleValue: averageValue())
+    }
+    
+    func lowestValue() -> Double? {
+        return valueSchedule.items.min(by: { $0.value < $1.value } )?.value
+    }
+}
+
+
+extension DailyQuantitySchedule: Equatable where T: Equatable {
+    public static func == (lhs: DailyQuantitySchedule<T>, rhs: DailyQuantitySchedule<T>) -> Bool {
+        return lhs.valueSchedule == rhs.valueSchedule && lhs.unit.unitString == rhs.unit.unitString
+    }
+}
+
+extension DailyQuantitySchedule where T: Numeric {
+    public static func * (lhs: DailyQuantitySchedule, rhs: DailyQuantitySchedule) -> DailyQuantitySchedule {
+        let unit = lhs.unit.unitMultiplied(by: rhs.unit)
+        let schedule = DailyValueSchedule.zip(lhs.valueSchedule, rhs.valueSchedule).map(*)
+        return DailyQuantitySchedule(unit: unit, valueSchedule: schedule)
+    }
+}
+
+extension DailyQuantitySchedule where T: FloatingPoint {
+    public static func / (lhs: DailyQuantitySchedule, rhs: DailyQuantitySchedule) -> DailyQuantitySchedule {
+        let unit = lhs.unit.unitDivided(by: rhs.unit)
+        let schedule = DailyValueSchedule.zip(lhs.valueSchedule, rhs.valueSchedule).map(/)
+        return DailyQuantitySchedule(unit: unit, valueSchedule: schedule)
+    }
+}

+ 274 - 0
Dependecies/LoopKit/LoopKit/DailyValueSchedule.swift

@@ -0,0 +1,274 @@
+//
+//  QuantitySchedule.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/18/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public struct RepeatingScheduleValue<T> {
+    public var startTime: TimeInterval
+    public var value: T
+
+    public init(startTime: TimeInterval, value: T) {
+        self.startTime = startTime
+        self.value = value
+    }
+
+    public func map<U>(_ transform: (T) -> U) -> RepeatingScheduleValue<U> {
+        return RepeatingScheduleValue<U>(startTime: startTime, value: transform(value))
+    }
+}
+
+extension RepeatingScheduleValue: Equatable where T: Equatable {
+    public static func == (lhs: RepeatingScheduleValue, rhs: RepeatingScheduleValue) -> Bool {
+        return abs(lhs.startTime - rhs.startTime) < .ulpOfOne && lhs.value == rhs.value
+    }
+}
+
+extension RepeatingScheduleValue: Hashable where T: Hashable {}
+
+public struct AbsoluteScheduleValue<T>: TimelineValue {
+    public let startDate: Date
+    public let endDate: Date
+    public let value: T
+}
+
+extension AbsoluteScheduleValue: Equatable where T: Equatable {}
+
+extension RepeatingScheduleValue: RawRepresentable where T: RawRepresentable {
+    public typealias RawValue = [String: Any]
+
+    public init?(rawValue: RawValue) {
+        guard let startTime = rawValue["startTime"] as? Double,
+            let rawValue = rawValue["value"] as? T.RawValue,
+            let value = T(rawValue: rawValue) else
+        {
+            return nil
+        }
+
+        self.init(startTime: startTime, value: value)
+    }
+
+    public var rawValue: RawValue {
+        return [
+            "startTime": startTime,
+            "value": value.rawValue
+        ]
+    }
+}
+
+extension RepeatingScheduleValue: Codable where T: Codable {}
+
+public protocol DailySchedule {
+    associatedtype T
+
+    var items: [RepeatingScheduleValue<T>] { get }
+
+    var timeZone: TimeZone { get set }
+
+    func between(start startDate: Date, end endDate: Date) -> [AbsoluteScheduleValue<T>]
+
+    func value(at time: Date) -> T
+}
+
+
+public extension DailySchedule {
+    func value(at time: Date) -> T {
+        return between(start: time, end: time).first!.value
+    }
+}
+
+extension DailySchedule where T: Comparable {
+    public func valueRange() -> ClosedRange<T> {
+        items.range(of: { $0.value })!
+    }
+}
+
+public struct DailyValueSchedule<T>: DailySchedule {
+    let referenceTimeInterval: TimeInterval
+    let repeatInterval = TimeInterval(hours: 24)
+
+    public let items: [RepeatingScheduleValue<T>]
+    public var timeZone: TimeZone
+
+    public init?(dailyItems: [RepeatingScheduleValue<T>], timeZone: TimeZone? = nil) {
+        self.items = dailyItems.sorted { $0.startTime < $1.startTime }
+        self.timeZone = timeZone ?? TimeZone.currentFixed
+
+        guard let firstItem = self.items.first else {
+            return nil
+        }
+
+        referenceTimeInterval = firstItem.startTime
+    }
+
+    var maxTimeInterval: TimeInterval {
+        return referenceTimeInterval + repeatInterval
+    }
+
+    /**
+     Returns the time interval for a given date normalized to the span of the schedule items
+
+     - parameter date: The date to convert
+     */
+    func scheduleOffset(for date: Date) -> TimeInterval {
+        // The time interval since a reference date in the specified time zone
+        let interval = date.timeIntervalSinceReferenceDate + TimeInterval(timeZone.secondsFromGMT(for: date))
+
+        // The offset of the time interval since the last occurence of the reference time + n * repeatIntervals.
+        // If the repeat interval was 1 day, this is the fractional amount of time since the most recent repeat interval starting at the reference time
+        return ((interval - referenceTimeInterval).truncatingRemainder(dividingBy: repeatInterval)) + referenceTimeInterval
+    }
+
+    /**
+     Returns a slice of schedule items that occur between two dates
+
+     - parameter startDate: The start date of the range
+     - parameter endDate:   The end date of the range
+
+     - returns: A slice of `ScheduleItem` values
+     */
+    public func between(start startDate: Date, end endDate: Date) -> [AbsoluteScheduleValue<T>] {
+        guard startDate <= endDate else {
+            return []
+        }
+
+        let startOffset = scheduleOffset(for: startDate)
+        let endOffset = startOffset + endDate.timeIntervalSince(startDate)
+
+        guard endOffset <= maxTimeInterval else {
+            let boundaryDate = startDate.addingTimeInterval(maxTimeInterval - startOffset)
+
+            return between(start: startDate, end: boundaryDate) + between(start: boundaryDate, end: endDate)
+        }
+
+        var startIndex = 0
+        var endIndex = items.count
+
+        for (index, item) in items.enumerated() {
+            if startOffset >= item.startTime {
+                startIndex = index
+            }
+            if endOffset < item.startTime {
+                endIndex = index
+                break
+            }
+        }
+
+        let referenceDate = startDate.addingTimeInterval(-startOffset)
+
+        return (startIndex..<endIndex).map { (index) in
+            let item = items[index]
+            let endTime = index + 1 < items.count ? items[index + 1].startTime : maxTimeInterval
+
+            return AbsoluteScheduleValue(
+                startDate: referenceDate.addingTimeInterval(item.startTime),
+                endDate: referenceDate.addingTimeInterval(endTime),
+                value: item.value
+            )
+        }
+    }
+
+    public func map<U>(_ transform: (T) -> U) -> DailyValueSchedule<U> {
+        return DailyValueSchedule<U>(
+            dailyItems: items.map { $0.map(transform) },
+            timeZone: timeZone
+        )!
+    }
+
+    public static func zip<L, R>(_ lhs: DailyValueSchedule<L>, _ rhs: DailyValueSchedule<R>) -> DailyValueSchedule where T == (L, R) {
+        precondition(lhs.timeZone == rhs.timeZone)
+
+        var (leftCursor, rightCursor) = (lhs.items.startIndex, rhs.items.startIndex)
+        var alignedItems: [RepeatingScheduleValue<(L, R)>] = []
+        repeat {
+            let (leftItem, rightItem) = (lhs.items[leftCursor], rhs.items[rightCursor])
+            let alignedItem = RepeatingScheduleValue(
+                startTime: max(leftItem.startTime, rightItem.startTime),
+                value: (leftItem.value, rightItem.value)
+            )
+            alignedItems.append(alignedItem)
+
+            let nextLeftStartTime = leftCursor == lhs.items.endIndex - 1 ? nil : lhs.items[leftCursor + 1].startTime
+            let nextRightStartTime = rightCursor == rhs.items.endIndex - 1 ? nil : rhs.items[rightCursor + 1].startTime
+            switch (nextLeftStartTime, nextRightStartTime) {
+            case (.some(let leftStart), .some(let rightStart)):
+                if leftStart < rightStart {
+                    leftCursor += 1
+                } else if rightStart < leftStart {
+                    rightCursor += 1
+                } else {
+                    leftCursor += 1
+                    rightCursor += 1
+                }
+            case (.some, .none):
+                leftCursor += 1
+            case (.none, .some):
+                rightCursor += 1
+            case (.none, .none):
+                leftCursor += 1
+                rightCursor += 1
+            }
+        } while leftCursor < lhs.items.endIndex && rightCursor < rhs.items.endIndex
+
+        return DailyValueSchedule(dailyItems: alignedItems, timeZone: lhs.timeZone)!
+    }
+}
+
+
+extension DailyValueSchedule: RawRepresentable, CustomDebugStringConvertible where T: RawRepresentable {
+    public typealias RawValue = [String: Any]
+    public init?(rawValue: RawValue) {
+        guard let rawItems = rawValue["items"] as? [RepeatingScheduleValue<T>.RawValue] else {
+            return nil
+        }
+
+        var timeZone: TimeZone?
+
+        if let offset = rawValue["timeZone"] as? Int {
+            timeZone = TimeZone(secondsFromGMT: offset)
+        }
+
+        let validScheduleItems = rawItems.compactMap(RepeatingScheduleValue<T>.init(rawValue:))
+        guard validScheduleItems.count == rawItems.count else {
+            return nil
+        }
+        self.init(dailyItems: validScheduleItems, timeZone: timeZone)
+    }
+
+    public var rawValue: RawValue {
+        let rawItems = items.map { $0.rawValue }
+
+        return [
+            "timeZone": timeZone.secondsFromGMT(),
+            "items": rawItems
+        ]
+    }
+
+    public var debugDescription: String {
+        return String(reflecting: rawValue)
+    }
+}
+
+extension DailyValueSchedule: Codable where T: Codable {}
+
+extension DailyValueSchedule: Equatable where T: Equatable {}
+
+extension RepeatingScheduleValue {
+    public static func == <L: Equatable, R: Equatable> (lhs: RepeatingScheduleValue, rhs: RepeatingScheduleValue) -> Bool where T == (L, R) {
+        return lhs.startTime == rhs.startTime && lhs.value == rhs.value
+    }
+}
+
+extension DailyValueSchedule {
+    public static func == <L: Equatable, R: Equatable> (lhs: DailyValueSchedule, rhs: DailyValueSchedule) -> Bool where T == (L, R) {
+        return lhs.timeZone == rhs.timeZone
+            && lhs.items.count == rhs.items.count
+            && Swift.zip(lhs.items, rhs.items).allSatisfy(==)
+    }
+}

+ 56 - 0
Dependecies/LoopKit/LoopKit/DeliveryLimits.swift

@@ -0,0 +1,56 @@
+//
+//  DeliveryLimits.swift
+//  LoopKit
+//
+//  Created by Rick Pasetto on 7/15/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+public struct DeliveryLimits: Equatable {
+    public enum Setting: Equatable {
+        case maximumBasalRate
+        case maximumBolus
+    }
+
+    private var settings: [Setting: HKQuantity]
+
+    public init(maximumBasalRate: HKQuantity?, maximumBolus: HKQuantity?) {
+        settings = [:]
+        settings[.maximumBasalRate] = maximumBasalRate
+        settings[.maximumBolus] = maximumBolus
+    }
+
+    public var maximumBasalRate: HKQuantity? {
+        get { settings[.maximumBasalRate] }
+        set { settings[.maximumBasalRate] = newValue }
+    }
+
+    public var maximumBolus: HKQuantity? {
+        get { settings[.maximumBolus] }
+        set { settings[.maximumBolus] = newValue }
+    }
+}
+
+public extension DeliveryLimits.Setting {
+    // The following comes from https://tidepool.atlassian.net/browse/IFU-24
+    var title: String {
+        switch self {
+        case .maximumBasalRate:
+            return LocalizedString("Maximum Basal Rate", comment: "Title text for maximum basal rate configuration")
+        case .maximumBolus:
+            return LocalizedString("Maximum Bolus", comment: "Title text for maximum bolus configuration")
+        }
+    }
+    
+    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)
+        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")
+        }
+    }
+}
+

+ 92 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/AlertSoundPlayer.swift

@@ -0,0 +1,92 @@
+//
+//  AlertSoundPlayer.swift
+//  Loop
+//
+//  Created by Rick Pasetto on 4/27/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+#if os(watchOS)
+import WatchKit
+#else
+import AudioToolbox
+#endif
+import AVFoundation
+import os.log
+
+public protocol AlertSoundPlayer {
+    func vibrate()
+    func play(url: URL)
+}
+
+public class DeviceAVSoundPlayer: AlertSoundPlayer {
+    private let log = OSLog(category: "DeviceAVSoundPlayer")
+    private let baseURL: URL?
+    private var delegate: Delegate!
+    private var players = [AVAudioPlayer]()
+    
+    @objc class Delegate: NSObject, AVAudioPlayerDelegate {
+        weak var parent: DeviceAVSoundPlayer?
+        init(parent: DeviceAVSoundPlayer) { self.parent = parent }
+        func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+            parent?.players.removeAll { $0 == player }
+        }
+    }
+    
+    public init(baseURL: URL? = nil) {
+        self.baseURL = baseURL
+        self.delegate = Delegate(parent: self)
+    }
+    
+    enum Error: Swift.Error {
+        case playFailed
+    }
+    
+    public func vibrate() {
+        #if os(watchOS)
+        WKInterfaceDevice.current().play(.notification)
+        #else
+        AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
+        #endif
+    }
+    
+    public func play(url: URL) {
+        DispatchQueue.main.async {
+            do {
+                let soundEffect = try AVAudioPlayer(contentsOf: url)
+                soundEffect.delegate = self.delegate
+                // The AVAudioPlayer has to remain around until the sound completes playing, which is why we hold
+                // onto it until it completes.
+                self.players.append(soundEffect)
+                if !soundEffect.play() {
+                    self.log.default("couldn't play sound (app may be in the background): %@", url.absoluteString)
+                }
+            } catch {
+                self.log.error("couldn't play sound %@: %@", url.absoluteString, String(describing: error))
+            }
+        }
+    }
+}
+
+public extension DeviceAVSoundPlayer {
+
+    func playAlert(sound: Alert.Sound) {
+        switch sound {
+        case .silence:
+            // noop
+            break
+        case .vibrate:
+            vibrate()
+        default:
+            if let baseURL = baseURL {
+                if let name = sound.filename {
+                    self.play(url: baseURL.appendingPathComponent(name))
+                } else {
+                    log.default("No file to play for %@", "\(sound)")
+                }
+            } else {
+                log.error("No base URL, could not play %@", sound.filename ?? "")
+            }
+        }
+    }
+}

+ 114 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/CGMManager.swift

@@ -0,0 +1,114 @@
+//
+//  CGMManager.swift
+//  Loop
+//
+//  Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+
+/// Describes the result of CGM manager operations to fetch and report sensor readings.
+///
+/// - noData: No new data was available or retrieved
+/// - newData: New glucose data was received and stored
+/// - error: An error occurred while receiving or store data
+public enum CGMReadingResult {
+    case noData
+    case newData([NewGlucoseSample])
+    case error(Error)
+}
+
+public struct CGMManagerStatus {
+    // Return false if no sensor active, or in a state where no future data is expected without user intervention
+    public var hasValidSensorSession: Bool
+    
+    public init(hasValidSensorSession: Bool) {
+        self.hasValidSensorSession = hasValidSensorSession
+    }
+}
+
+public protocol CGMManagerDelegate: DeviceManagerDelegate {
+    /// Asks the delegate for a date with which to filter incoming glucose data
+    ///
+    /// - Parameter manager: The manager instance
+    /// - Returns: The date data occuring on or after which should be kept
+    func startDateToFilterNewData(for manager: CGMManager) -> Date?
+
+    /// Informs the delegate that the device has updated with a new result
+    ///
+    /// - Parameters:
+    ///   - manager: The manager instance
+    ///   - result: The result of the update
+    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) -> Void
+
+    /// Informs the delegate that the manager is deactivating and should be deleted
+    ///
+    /// - Parameter manager: The manager instance
+    func cgmManagerWantsDeletion(_ manager: CGMManager)
+
+    /// Informs the delegate that the manager has updated its state and should be persisted.
+    ///
+    /// - Parameter manager: The manager instance
+    func cgmManagerDidUpdateState(_ manager: CGMManager)
+    
+    /// Asks the delegate for credential store prefix to avoid namespace conflicts
+    ///
+    /// - 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)
+}
+
+
+public protocol CGMManager: DeviceManager {
+    var cgmManagerDelegate: CGMManagerDelegate? { get set }
+
+    var appURL: URL? { get }
+
+    /// Whether the device is capable of waking the app
+    var providesBLEHeartbeat: Bool { get }
+
+    /// The length of time to keep samples in HealthKit before removal. Return nil to never remove samples.
+    var managedDataInterval: 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 }
+
+    /// Performs a manual fetch of glucose data from the device, if necessary
+    ///
+    /// - Parameters:
+    ///   - completion: A closure called when operation has completed
+    func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) -> Void
+}
+
+
+public extension CGMManager {
+    var appURL: URL? {
+        return nil
+    }
+
+    /// Convenience wrapper for notifying the delegate of deletion on the delegate queue
+    ///
+    /// - Parameters:
+    ///   - completion: A closure called from the delegate queue after the delegate is called
+    func notifyDelegateOfDeletion(completion: @escaping () -> Void) {
+        delegateQueue.async {
+            self.cgmManagerDelegate?.cgmManagerWantsDeletion(self)
+            completion()
+        }
+    }
+}

+ 24 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLifecycleProgress.swift

@@ -0,0 +1,24 @@
+//
+//  DeviceLifecycleProgress.swift
+//  LoopKit
+//
+//  Created by Nathaniel Hamming on 2020-06-30.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public protocol DeviceLifecycleProgress {
+    /// the percent complete of the progress for this device status. Expects a value between 0.0 and 1.0
+    var percentComplete: Double { get }
+
+    /// the status of the progress to provide guidance as to how to present the progress
+    var progressState: DeviceLifecycleProgressState { get }
+}
+
+public enum DeviceLifecycleProgressState: String, Codable {
+    case critical
+    case normalCGM
+    case normalPump
+    case warning
+}

+ 17 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLog.xcdatamodeld/DeviceCommsLog.xcdatamodel/contents

@@ -0,0 +1,17 @@
+<?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="">
+    <entity name="Entry" representedClassName=".DeviceLogEntry" syncable="YES">
+        <attribute name="deviceIdentifier" optional="YES" attributeType="String"/>
+        <attribute name="managerIdentifier" attributeType="String"/>
+        <attribute name="message" attributeType="String"/>
+        <attribute name="modificationCounter" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="timestamp" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="type" attributeType="String"/>
+        <fetchIndex name="byTimestampIndex">
+            <fetchIndexElement property="timestamp" type="Binary" order="ascending"/>
+        </fetchIndex>
+    </entity>
+    <elements>
+        <element name="Entry" positionX="-36" positionY="9" width="128" height="133"/>
+    </elements>
+</model>

+ 46 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntry+CoreDataClass.swift

@@ -0,0 +1,46 @@
+//
+//  DeviceLogEntry+CoreDataClass.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 1/13/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+//
+
+import Foundation
+import CoreData
+
+class DeviceLogEntry: NSManagedObject {
+
+    var type: DeviceLogEntryType? {
+        get {
+            willAccessValue(forKey: "type")
+            defer { didAccessValue(forKey: "type") }
+            guard let primitiveType = primitiveType else {
+                return nil
+            }
+            return DeviceLogEntryType(rawValue: primitiveType)
+        }
+        set {
+            willChangeValue(forKey: "type")
+            defer { didChangeValue(forKey: "type") }
+            primitiveType = newValue?.rawValue
+        }
+    }
+
+    var hasUpdatedModificationCounter: Bool { changedValues().keys.contains("modificationCounter") }
+
+    func updateModificationCounter() { setPrimitiveValue(managedObjectContext!.modificationCounter!, forKey: "modificationCounter") }
+
+    override func awakeFromInsert() {
+        super.awakeFromInsert()
+        updateModificationCounter()
+    }
+
+    override func willSave() {
+        if isUpdated && !hasUpdatedModificationCounter {
+            updateModificationCounter()
+        }
+        super.willSave()
+    }
+}

+ 58 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntry+CoreDataProperties.swift

@@ -0,0 +1,58 @@
+//
+//  DeviceLogEntry+CoreDataProperties.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 1/13/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+//
+
+import Foundation
+import CoreData
+
+
+extension DeviceLogEntry {
+
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<DeviceLogEntry> {
+        return NSFetchRequest<DeviceLogEntry>(entityName: "Entry")
+    }
+
+    @NSManaged public var primitiveType: String?
+    @NSManaged public var managerIdentifier: String?
+    @NSManaged public var deviceIdentifier: String?
+    @NSManaged public var message: String?
+    @NSManaged public var timestamp: Date?
+    @NSManaged public var modificationCounter: Int64
+
+}
+
+extension DeviceLogEntry: Encodable {
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(type?.rawValue, forKey: .type)
+        try container.encodeIfPresent(managerIdentifier, forKey: .managerIdentifier)
+        try container.encodeIfPresent(deviceIdentifier, forKey: .deviceIdentifier)
+        try container.encodeIfPresent(message, forKey: .message)
+        try container.encodeIfPresent(timestamp, forKey: .timestamp)
+        try container.encode(modificationCounter, forKey: .modificationCounter)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case type
+        case managerIdentifier
+        case deviceIdentifier
+        case message
+        case timestamp
+        case modificationCounter
+    }
+}
+
+extension DeviceLogEntry {
+    func update(from entry: StoredDeviceLogEntry) {
+        type = entry.type
+        managerIdentifier = entry.managerIdentifier
+        deviceIdentifier = entry.deviceIdentifier
+        message = entry.message
+        timestamp = entry.timestamp
+    }
+}

+ 24 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/DeviceLogEntryType.swift

@@ -0,0 +1,24 @@
+//
+//  DeviceLogEntryType.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 1/13/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public enum DeviceLogEntryType: String {
+    /// Log entry related to data or commands sent to the device.
+    case send
+    /// Log entry related to data or events received from the device.
+    case receive
+    /// Log entry related to any errors from the device's SDK or the device itself.
+    case error
+    /// Log entry related to a delegate call from the device's SDK (for example, acknowledgement of receiving a command).
+    case delegate
+    /// Log entry related to a response from a delegate call (for example, acknowledgement of executing a command).
+    case delegateResponse
+    /// Log entry related to any device connection activities (e.g. scanning, connecting, disconnecting, reconnecting, etc.).
+    case connection
+}

+ 232 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/PersistentDeviceLog.swift

@@ -0,0 +1,232 @@
+//
+//  PersistentDeviceLog.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 1/13/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import CoreData
+import os.log
+
+
+// Using a framework specific class will search the framework's bundle for model files.
+class PersistentContainer: NSPersistentContainer { }
+
+public class PersistentDeviceLog {
+
+    private let storageFile: URL
+    
+    private let managedObjectContext: NSManagedObjectContext
+
+    private let persistentContainer: NSPersistentContainer
+    
+    private let maxEntryAge: TimeInterval
+    
+    public var earliestLogEntryDate: Date {
+        return Date(timeIntervalSinceNow: -maxEntryAge)
+    }
+    
+    private let log = OSLog(category: "PersistentDeviceLog")
+    
+    public init(storageFile: URL, maxEntryAge: TimeInterval = TimeInterval(7 * 24 * 60 * 60)) {
+        self.storageFile = storageFile
+        self.maxEntryAge = maxEntryAge
+
+        managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+        managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
+        managedObjectContext.automaticallyMergesChangesFromParent = true
+
+        let storeDescription = NSPersistentStoreDescription(url: storageFile)
+        persistentContainer = PersistentContainer(name: "DeviceLog")
+        persistentContainer.persistentStoreDescriptions = [storeDescription]
+        persistentContainer.loadPersistentStores { description, error in
+            if let error = error {
+                fatalError("Unable to load persistent stores: \(error)")
+            }
+        }
+        managedObjectContext.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
+    }
+    
+    public func log(managerIdentifier: String, deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)? = nil) {
+        managedObjectContext.perform {
+            let entry = DeviceLogEntry(context: self.managedObjectContext)
+            entry.managerIdentifier = managerIdentifier
+            entry.deviceIdentifier = deviceIdentifier
+            entry.type = type
+            entry.message = message
+            entry.timestamp = Date()
+            do {
+                try self.managedObjectContext.save()
+                self.log.default("Logged: %{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))
+                completion?(error)
+            }
+        }
+    }
+    
+    public func getLogEntries(startDate: Date, endDate: Date? = nil, completion: @escaping (_ result: Result<[StoredDeviceLogEntry], Error>) -> Void) {
+        
+        managedObjectContext.perform {
+            var predicate: NSPredicate = NSPredicate(format: "timestamp >= %@", startDate as NSDate)
+            if let endDate = endDate {
+                let endFilter = NSPredicate(format: "timestamp < %@", endDate as NSDate)
+                predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, endFilter])
+            }
+            
+            let request: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest()
+            request.predicate = predicate
+            request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
+            
+            do {
+                let entries = try self.managedObjectContext.fetch(request)
+                completion(.success(entries.map { StoredDeviceLogEntry(managedObject: $0) } ))
+                self.purgeExpiredLogEntries()
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+    
+    // Should only be called from managed object context queue
+    private func purgeExpiredLogEntries() {
+        let predicate = NSPredicate(format: "timestamp < %@", earliestLogEntryDate as NSDate)
+
+        do {
+            let fetchRequest: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest()
+            fetchRequest.predicate = predicate
+            let count = try managedObjectContext.deleteObjects(matching: fetchRequest)
+            log.info("Deleted %d DeviceLogEntries", count)
+        } catch let error {
+            log.error("Could not purge expired log entry %{public}@", String(describing: error))
+        }
+    }
+
+    public func purgeLogEntries(before date: Date, completion: ((Error?) -> Void)? = nil) {
+        var purgeError: Error?
+
+        managedObjectContext.performAndWait {
+            do {
+                let count = try managedObjectContext.purgeObjects(of: DeviceLogEntry.self, matching: NSPredicate(format: "timestamp < %@", date as NSDate))
+                log.info("Purged %d DeviceLogEntries", count)
+            } catch let error {
+                log.error("Unable to purge DeviceLogEntries: %{public}@", String(describing: error))
+                purgeError = error
+            }
+        }
+
+        completion?(purgeError)
+    }
+}
+
+// MARK: - Critical Event Log Export
+
+extension PersistentDeviceLog: CriticalEventLog {
+    private var exportProgressUnitCountPerObject: Int64 { 1 }
+    private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
+
+    public var exportName: String { "DeviceLog.json" }
+
+    public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
+        var result: Result<Int64, Error>?
+
+        self.managedObjectContext.performAndWait {
+            do {
+                let request: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest()
+                request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
+
+                let objectCount = try self.managedObjectContext.count(for: request)
+                result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
+            } catch let error {
+                result = .failure(error)
+            }
+        }
+
+        return result!
+    }
+
+    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?
+
+        while fetching && error == nil {
+            self.managedObjectContext.performAndWait {
+                do {
+                    guard !progress.isCancelled else {
+                        throw CriticalEventLogError.cancelled
+                    }
+
+                    let request: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
+                                                                                            self.exportDatePredicate(startDate: startDate, endDate: endDate)])
+                    request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
+                    request.fetchLimit = self.exportFetchLimit
+
+                    let objects = try self.managedObjectContext.fetch(request)
+                    if objects.isEmpty {
+                        fetching = false
+                        return
+                    }
+
+                    try encoder.encode(objects)
+
+                    modificationCounter = objects.last!.modificationCounter
+
+                    progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
+                } catch let fetchError {
+                    error = fetchError
+                }
+            }
+        }
+
+        if let closeError = encoder.close(), error == nil {
+            error = closeError
+        }
+
+        return error
+    }
+
+    private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
+        var predicate = NSPredicate(format: "timestamp >= %@", startDate as NSDate)
+        if let endDate = endDate {
+            predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "timestamp < %@", endDate as NSDate)])
+        }
+        return predicate
+    }
+}
+
+// MARK: - Core Data (Bulk) - TEST ONLY
+
+extension PersistentDeviceLog {
+    public func addStoredDeviceLogEntries(entries: [StoredDeviceLogEntry]) -> Error? {
+        guard !entries.isEmpty else {
+            return nil
+        }
+
+        var error: Error?
+
+        self.managedObjectContext.performAndWait {
+            for entry in entries {
+                let object = DeviceLogEntry(context: self.managedObjectContext)
+                object.update(from: entry)
+            }
+            do {
+                try self.managedObjectContext.save()
+            } catch let saveError {
+                error = saveError
+            }
+        }
+
+        guard error == nil else {
+            return error
+        }
+
+        self.log.info("Added %d StoredDeviceLogEntries", entries.count)
+        return nil
+    }
+}

+ 0 - 0
Dependecies/LoopKit/LoopKit/DeviceManager/DeviceLog/StoredDeviceLogEntry.swift


部分文件因文件數量過多而無法顯示